Files
fusion/packages/engine/src/__tests__/executor-implicit-task-done-budget.test.ts
gsxdsm eb113f3e33 FN-6068: fix engine executor test expectations for queued retries
Align engine executor tests with the current retry and worktree behavior.

- update implicit task-done refusal expectations to assert queued retry state resets
- update pause recovery coverage to expect requeued tasks instead of stuck-killed status
- stub the sandbox backend in worktree init tests and drop obsolete execSync assertions

Files changed:
 .../executor-implicit-task-done-budget.test.ts     | 11 ++++++-
 ...xecutor-implicit-task-done-revise-guard.test.ts | 11 ++++++-
 .../engine/src/__tests__/executor-pause.test.ts    |  5 +--
 .../engine/src/__tests__/executor-worktree.test.ts | 37 ++++++++++++++++------
 4 files changed, 50 insertions(+), 14 deletions(-)

Fusion-Task-Id: FN-6068

Fusion-Task-Lineage: b8ccabaa-7e5d-42dc-b522-6f4738b28db4
2026-06-09 02:27:43 -07:00

138 lines
5.5 KiB
TypeScript

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, mockedCreateFnAgent, 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), refusal());
expect(store.updateTask).toHaveBeenCalledWith("FN-4946-B", expect.objectContaining({
status: "queued",
error: null,
taskDoneRetryCount: 3,
worktree: null,
branch: null,
paused: false,
pausedByAgentId: null,
sessionFile: null,
}));
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), 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");
});
it("shares retry budget with explicit fn_task_done refusals", async () => {
const store = createMockStore();
let currentTask: any = { ...task(2), id: "FN-4946-B2", steps: [{ name: "Step 1", status: "in-progress" }] };
let doneTool: any;
store.getTask.mockImplementation(async () => ({ ...currentTask, steps: currentTask.steps.map((s: any) => ({ ...s })) }));
store.updateTask.mockImplementation(async (_id: string, patch: any) => {
currentTask = { ...currentTask, ...patch };
});
mockedCreateFnAgent.mockImplementation(async ({ customTools }: any) => {
doneTool = customTools.find((t: any) => t.name === "fn_task_done");
return { session: { prompt: vi.fn().mockResolvedValue(undefined), dispose: vi.fn(), subscribe: vi.fn(), on: vi.fn(), state: {} } } as any;
});
const executor = new TaskExecutor(store as any, "/repo");
await executor.execute(currentTask);
// Burn explicit-path refusal budget from 2 -> 3 (still todo), then implicit refusal should escalate immediately.
await doneTool.execute("d1", { summary: "I am not done yet." });
expect(currentTask.taskDoneRetryCount).toBe(3);
await (executor as any).handleImplicitTaskDoneRefusal(
{ ...currentTask, id: "FN-4946-B2", column: "todo" },
refusal(),
);
const inReviewMoves = store.moveTask.mock.calls.filter((call: any[]) => call[0] === "FN-4946-B2" && call[1] === "in-review");
expect(inReviewMoves.length).toBeGreaterThanOrEqual(1);
const implicitEscalationUpdate = store.updateTask.mock.calls.find(
([id, patch]: [string, Record<string, unknown>]) =>
id === "FN-4946-B2" && patch.status === "failed" && !("taskDoneRetryCount" in patch),
);
expect(implicitEscalationUpdate).toBeTruthy();
});
it("resets taskDoneRetryCount after later clean completion", async () => {
const store = createMockStore();
let currentTask: any = { ...task(1), id: "FN-4946-B3", steps: [{ name: "Step 1", status: "in-progress" }], executionMode: "fast" };
store.getTask.mockImplementation(async () => ({ ...currentTask, steps: currentTask.steps.map((s: any) => ({ ...s })) }));
store.updateTask.mockImplementation(async (_id: string, patch: any) => {
currentTask = { ...currentTask, ...patch };
});
mockedCreateFnAgent.mockImplementation(async ({ customTools }: any) => {
const doneTool = customTools.find((t: any) => t.name === "fn_task_done");
return {
session: {
prompt: vi.fn().mockImplementation(async () => {
await doneTool.execute("done", { summary: "complete" });
}),
dispose: vi.fn(),
subscribe: vi.fn(),
on: vi.fn(),
state: {},
},
} as any;
});
const executor = new TaskExecutor(store as any, "/repo");
await executor.execute(currentTask);
expect(store.moveTask).toHaveBeenCalledWith("FN-4946-B3", "in-review");
const retryBumpCalls = store.updateTask.mock.calls.filter(([, patch]: [string, Record<string, unknown>]) => typeof patch.taskDoneRetryCount === "number" && patch.taskDoneRetryCount > 1);
expect(retryBumpCalls).toHaveLength(0);
});
});