test(FN-4946): add implicit completion refusal guard coverage

Fusion-Task-Id: FN-4946
Fusion-Task-Lineage: 9a48f72f-d2cc-4950-8a00-f69053dd1163
This commit is contained in:
Fusion (runfusion.ai)
2026-05-17 20:45:35 -07:00
committed by gsxdsm
parent 8a050be8f3
commit 3a17b85ed6
3 changed files with 171 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import "./executor-test-helpers.js";
import { TaskExecutor } from "../executor.js";
import { executorLog } from "../logger.js";
import { createMockStore, resetExecutorMocks } from "./executor-test-helpers.js";
function refusal() {
return {
ok: false as const,
refusalClass: "pending-code-review-revise" as const,
reason: "Step 1 has pending REVISE",
message: "fn_task_done refused (pending-code-review-revise): Step 1 has pending REVISE",
};
}
function task(retryCount: number) {
return {
id: "FN-4946-B",
title: "Budget",
description: "",
column: "in-progress",
worktree: "/repo/.worktrees/swift-falcon",
branch: "fusion/fn-4946-b",
baseCommitSha: "abc123",
taskDoneRetryCount: retryCount,
dependencies: [],
steps: [{ name: "Step 1", status: "in-progress" as const }],
currentStep: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as any;
}
describe("FN-4946 implicit refusal budget handling", () => {
beforeEach(() => {
resetExecutorMocks();
});
it("requeues to todo under budget", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store as any, "/repo");
await (executor as any).handleImplicitTaskDoneRefusal(task(2), "/repo/.worktrees/swift-falcon", refusal());
expect(store.updateTask).toHaveBeenCalledWith("FN-4946-B", expect.objectContaining({ taskDoneRetryCount: 3, status: "failed" }));
expect(store.moveTask).toHaveBeenCalledWith("FN-4946-B", "todo", { preserveProgress: true });
expect(executorLog.error).toHaveBeenCalledWith(expect.stringContaining("(implicit completion)"));
});
it("escalates to in-review at budget limit", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store as any, "/repo");
const persistSpy = vi.spyOn(executor as any, "persistTokenUsage").mockResolvedValue(undefined);
await (executor as any).handleImplicitTaskDoneRefusal(task(3), "/repo/.worktrees/swift-falcon", refusal());
expect(store.updateTask).toHaveBeenCalledWith("FN-4946-B", expect.objectContaining({ status: "failed" }));
expect(store.moveTask).toHaveBeenCalledWith("FN-4946-B", "in-review");
expect(persistSpy).toHaveBeenCalledWith("FN-4946-B");
});
});

View File

@@ -0,0 +1,72 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import "./executor-test-helpers.js";
import { TaskExecutor } from "../executor.js";
import { reviewStep } from "../reviewer.js";
import { createMockStore, mockedCreateFnAgent, mockedExecSync, resetExecutorMocks } from "./executor-test-helpers.js";
function makeTask(overrides: Record<string, unknown> = {}) {
return {
id: "FN-4946-R",
title: "Implicit revise guard",
description: "",
column: "in-progress",
worktree: "/repo/.worktrees/swift-falcon",
branch: "fusion/fn-4946-r",
baseCommitSha: "abc123",
taskDoneRetryCount: 0,
dependencies: [],
steps: [{ name: "Step 1", status: "in-progress" as const }],
currentStep: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
};
}
describe("FN-4946 implicit completion + REVISE verdict interaction", () => {
beforeEach(() => {
resetExecutorMocks();
mockedExecSync.mockImplementation((cmd: string) => {
if (cmd.includes("rev-parse --show-toplevel")) return Buffer.from("/repo/.worktrees/swift-falcon\n");
if (cmd.includes("rev-parse --abbrev-ref HEAD")) return Buffer.from("fusion/fn-4946-r\n");
if (cmd.includes("rev-list --count")) return Buffer.from("1\n");
if (cmd.includes("rev-parse HEAD")) return Buffer.from("def456\n");
return Buffer.from("");
});
});
it("does not refuse implicit completion when REVISE is on an already done step", async () => {
const store = createMockStore();
let task: any = makeTask();
store.getTask.mockImplementation(async () => ({ ...task, steps: task.steps.map((s: any) => ({ ...s })) }));
store.updateStep.mockImplementation(async (_id: string, step: number, patch: any) => {
task.steps[step] = { ...task.steps[step], ...patch };
});
vi.mocked(reviewStep).mockResolvedValue({ verdict: "REVISE", summary: "fix", review: "fix" } as any);
mockedCreateFnAgent.mockImplementation(async ({ customTools }: any) => {
const updateTool = customTools.find((t: any) => t.name === "fn_task_update");
const reviewTool = customTools.find((t: any) => t.name === "fn_review_step");
return {
session: {
prompt: vi.fn().mockImplementation(async () => {
await reviewTool.execute("review", { step: 0, type: "code", step_name: "Step 1", baseline: "abc123" });
await updateTool.execute("update", { step: 0, status: "done" });
}),
dispose: vi.fn(),
subscribe: vi.fn(),
on: vi.fn(),
state: {},
},
} as any;
});
const executor = new TaskExecutor(store as any, "/repo");
await executor.execute(task);
expect(store.moveTask).toHaveBeenCalledWith("FN-4946-R", "in-review");
expect(store.moveTask).not.toHaveBeenCalledWith("FN-4946-R", "todo", { preserveProgress: true });
});
});

View File

@@ -0,0 +1,38 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { evaluateTaskDoneRefusal } from "../executor.js";
describe("FN-4946 shared task_done refusal helper invariant", () => {
it("keeps a single helper implementation and routes explicit+implicit paths through it", () => {
const source = readFileSync(new URL("../executor.ts", import.meta.url), "utf8");
const invocations = source.match(/evaluateTaskDoneRefusal\(/g) ?? [];
const helperDecl = source.match(/\bfunction evaluateTaskDoneRefusal\b/g) ?? [];
const dissentDecl = source.match(/\bconst DISSENT_PATTERNS\b/g) ?? [];
expect(invocations.length).toBeGreaterThanOrEqual(3);
expect(helperDecl).toHaveLength(1);
expect(dissentDecl).toHaveLength(1);
});
it("returns pending-code-review-revise for a pending step with REVISE and no summary", () => {
const result = evaluateTaskDoneRefusal(
{
id: "FN-4946-H",
title: "t",
description: "",
column: "in-progress",
dependencies: [],
steps: [{ name: "Step 1", status: "in-progress" }],
currentStep: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as any,
{},
new Map([[0, "REVISE"]]),
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected refusal");
expect(result.refusalClass).toBe("pending-code-review-revise");
});
});