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
138 lines
5.5 KiB
TypeScript
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);
|
|
});
|
|
});
|