Files
fusion/packages/engine/src/__tests__/executor-review-step-indexing.test.ts
Fusion (runfusion.ai) cfe35325b4 feat(FN-4990): complete Step 5 — finalize verification and delivery
Fusion-Task-Id: FN-4990
Fusion-Task-Lineage: 167a6011-d246-45bd-ad7a-1f2fe8b99323
2026-05-18 02:43:33 -07:00

129 lines
4.6 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import "./executor-test-helpers.js";
import { TaskExecutor } from "../executor.js";
import { reviewStep as mockedReviewStepFn } from "../reviewer.js";
import {
createMockStore,
mockedCreateFnAgent,
mockedExistsSync,
resetExecutorMocks,
} from "./executor-test-helpers.js";
const mockedReviewStep = vi.mocked(mockedReviewStepFn);
async function captureTools() {
const store = createMockStore();
const stepStates = [
{ name: "Preflight", status: "done" },
{ name: "Implement", status: "pending" },
{ name: "Test", status: "pending" },
];
let checkpointLeafId = "leaf-1";
const navigateTree = vi.fn().mockResolvedValue({ cancelled: false });
store.getTask.mockImplementation(async () => ({
id: "FN-TEST",
title: "Test",
description: "Test",
column: "in-progress",
dependencies: [],
steps: stepStates.map((s) => ({ ...s })),
currentStep: 1,
log: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}));
store.updateStep.mockImplementation(async (_taskId: string, stepIndex: number, status: string) => {
stepStates[stepIndex].status = status;
return { steps: stepStates.map((s) => ({ ...s })) };
});
let customTools: any[] = [];
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
customTools = opts.customTools || [];
return {
session: {
prompt: vi.fn().mockResolvedValue(undefined),
dispose: vi.fn(),
navigateTree,
sessionManager: {
getLeafId: vi.fn(() => checkpointLeafId),
branchWithSummary: vi.fn(),
},
},
} as any;
});
mockedExistsSync.mockReturnValue(true);
const executor = new TaskExecutor(store, "/tmp/test");
await executor.execute({
id: "FN-TEST",
title: "Test",
description: "Test",
column: "in-progress",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
const tools: Record<string, any> = {};
for (const tool of customTools) tools[tool.name] = tool.execute;
return { tools, store, stepStates, navigateTree, setLeaf: (leaf: string) => { checkpointLeafId = leaf; } };
}
describe("fn_review_step indexing", () => {
beforeEach(() => {
resetExecutorMocks();
});
it("uses step=2 to update internal step index 1", async () => {
mockedReviewStep.mockResolvedValue({ verdict: "APPROVE", review: "ok", summary: "ok" } as any);
const { tools, store, stepStates } = await captureTools();
await tools.fn_review_step("call-1", { step: 2, type: "code", step_name: "Implement", baseline: "abc" });
expect(stepStates[1].status).toBe("done");
expect(store.updateStep).toHaveBeenCalledWith("FN-TEST", 1, "in-progress");
expect(store.updateStep).toHaveBeenCalledWith("FN-TEST", 1, "done");
});
it("RETHINK resets internal step index 1 and uses step-index checkpoint", async () => {
mockedReviewStep.mockResolvedValue({ verdict: "RETHINK", review: "redo", summary: "redo" } as any);
const { tools, store, navigateTree } = await captureTools();
await tools.fn_task_update("set-cp", { step: 2, status: "in-progress" });
await tools.fn_review_step("call-1", { step: 2, type: "code", step_name: "Implement", baseline: "abc" });
expect(store.updateStep).toHaveBeenCalledWith("FN-TEST", 1, "pending");
expect(navigateTree).toHaveBeenCalled();
});
it("REVISE verdict for step=2 blocks fn_task_update step=2 done", async () => {
mockedReviewStep.mockResolvedValue({ verdict: "REVISE", review: "fix", summary: "fix" } as any);
const { tools } = await captureTools();
await tools.fn_review_step("call-1", { step: 2, type: "code", step_name: "Implement", baseline: "abc" });
const result = await tools.fn_task_update("call-2", { step: 2, status: "done" });
expect(result.content[0].text).toContain("Cannot mark Step 2 as done");
});
it("rejects out-of-range steps without reviewer call", async () => {
mockedReviewStep.mockResolvedValue({ verdict: "APPROVE", review: "ok", summary: "ok" } as any);
const { tools, store } = await captureTools();
const invalids = [0, -1, 4];
for (const step of invalids) {
const result = await tools.fn_review_step("bad", { step, type: "code", step_name: "Implement", baseline: "abc" });
expect(result.details.error).toBe("invalid_step");
}
expect(mockedReviewStep).not.toHaveBeenCalled();
expect(store.logEntry).not.toHaveBeenCalledWith("FN-TEST", expect.stringContaining("review requested"));
});
});