test(FN-4115): complete Step 1 — wrong-checkout completion rejection tests
Fusion-Task-Id: FN-4115 Fusion-Task-Lineage: 9e34cdc1-ac78-41f2-9587-269f50b30af4
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
FN-4115 invariant: fn_task_done is the only path from in-progress to done, and it must refuse completion unless (1) git toplevel is a valid task worktree under <repo>/.worktrees, (2) branch matches fusion/<task-id>, and (3) there is at least one commit beyond baseCommitSha. Violations must requeue via taskDoneRetryCount + moveTask("todo", { preserveProgress: true }) and must not log successful completion.
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import "./executor-test-helpers.js";
|
||||
import { TaskExecutor } from "../executor.js";
|
||||
import * as worktreePool from "../worktree-pool.js";
|
||||
import { createMockStore, mockedCreateFnAgent, mockedExecSync, resetExecutorMocks } from "./executor-test-helpers.js";
|
||||
|
||||
function makeTask(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: "FN-4115",
|
||||
title: "Invariant test",
|
||||
description: "",
|
||||
column: "in-progress",
|
||||
worktree: "/repo/.worktrees/swift-falcon",
|
||||
branch: "fusion/fn-4115",
|
||||
baseCommitSha: "abc123",
|
||||
taskDoneRetryCount: 0,
|
||||
paused: true,
|
||||
pausedByAgentId: "agent-x",
|
||||
steps: [{ name: "Step 1", status: "in-progress" as const }],
|
||||
currentStep: 0,
|
||||
dependencies: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function setup(overrides: Record<string, unknown> = {}) {
|
||||
const store = createMockStore();
|
||||
let task: any = makeTask(overrides);
|
||||
let tool: any;
|
||||
|
||||
store.getTask.mockImplementation(async () => ({ ...task, steps: task.steps.map((s: any) => ({ ...s })) }));
|
||||
store.updateTask.mockImplementation(async (_id: string, updates: any) => {
|
||||
task = { ...task, ...updates };
|
||||
return task;
|
||||
});
|
||||
store.moveTask.mockImplementation(async (id: string, column: string) => {
|
||||
task = { ...task, id, column, paused: false, pausedByAgentId: null, status: null, error: null };
|
||||
});
|
||||
|
||||
mockedCreateFnAgent.mockImplementation(async ({ customTools }: any) => {
|
||||
tool = customTools.find((t: any) => t.name === "fn_task_done");
|
||||
return { session: { prompt: vi.fn().mockResolvedValue(undefined), dispose: vi.fn() } } as any;
|
||||
});
|
||||
|
||||
const executor = new TaskExecutor(store as any, "/repo");
|
||||
await executor.execute(makeTask(overrides) as any);
|
||||
|
||||
return { store, tool, getTask: () => task };
|
||||
}
|
||||
|
||||
describe("FN-4115 wrong-checkout completion rejection", () => {
|
||||
beforeEach(() => {
|
||||
resetExecutorMocks();
|
||||
vi.spyOn(worktreePool, "isUsableTaskWorktree").mockResolvedValue(true);
|
||||
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-4115\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("FN-4115: fn_task_done refuses completion when git toplevel resolves to repo root", async () => {
|
||||
const { store, tool } = await setup();
|
||||
mockedExecSync.mockImplementation((cmd: string) => {
|
||||
if (cmd.includes("rev-parse --show-toplevel")) return Buffer.from("/repo\n");
|
||||
if (cmd.includes("rev-parse --abbrev-ref HEAD")) return Buffer.from("fusion/fn-4115\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("");
|
||||
});
|
||||
const result = await tool.execute("id", {});
|
||||
expect(result.content[0].text).toContain("fn_task_done refused: wrong_toplevel");
|
||||
expect(store.updateStep).not.toHaveBeenCalled();
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-4115", "todo", { preserveProgress: true });
|
||||
expect(store.logEntry).not.toHaveBeenCalledWith("FN-4115", expect.stringContaining("Task marked done by agent"));
|
||||
});
|
||||
|
||||
it("FN-4115: fn_task_done refuses completion when current branch is not fusion/<task-id>", async () => {
|
||||
const { store, tool } = await setup();
|
||||
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("main\n");
|
||||
if (cmd.includes("rev-parse HEAD")) return Buffer.from("def456\n");
|
||||
return Buffer.from("");
|
||||
});
|
||||
const result = await tool.execute("id", {});
|
||||
expect(result.content[0].text).toContain("fn_task_done refused: wrong_branch");
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-4115", "todo", { preserveProgress: true });
|
||||
});
|
||||
|
||||
it("FN-4115: fn_task_done refuses completion when there are zero commits beyond base", async () => {
|
||||
const { store, tool } = await setup();
|
||||
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-4115\n");
|
||||
if (cmd.includes("rev-list --count")) return Buffer.from("0\n");
|
||||
if (cmd.includes("rev-parse HEAD")) return Buffer.from("def456\n");
|
||||
return Buffer.from("");
|
||||
});
|
||||
const result = await tool.execute("id", {});
|
||||
expect(result.content[0].text).toContain("fn_task_done refused: no_commits");
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-4115", "todo", { preserveProgress: true });
|
||||
});
|
||||
|
||||
it("FN-4115: fn_task_done completes on valid worktree branch and commit state", async () => {
|
||||
const { store, tool } = await setup();
|
||||
const result = await tool.execute("id", {});
|
||||
expect(result.content[0].text).toContain("Task marked complete");
|
||||
expect(store.updateStep).toHaveBeenCalled();
|
||||
expect(store.moveTask).not.toHaveBeenCalledWith("FN-4115", "todo", { preserveProgress: true });
|
||||
});
|
||||
|
||||
it("FN-4115: pre-session liveness rejects missing worktree before createFnAgent", async () => {
|
||||
vi.spyOn(worktreePool, "isUsableTaskWorktree").mockResolvedValue(false);
|
||||
const store = createMockStore();
|
||||
store.getTask.mockResolvedValue(makeTask());
|
||||
const executor = new TaskExecutor(store as any, "/repo");
|
||||
await executor.execute(makeTask() as any);
|
||||
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-4115", "todo", { preserveProgress: true });
|
||||
});
|
||||
|
||||
it("FN-4115: pre-session liveness rejects paths outside repo .worktrees directory", async () => {
|
||||
vi.spyOn(worktreePool, "isUsableTaskWorktree").mockResolvedValue(true);
|
||||
const store = createMockStore();
|
||||
const escaped = makeTask({ worktree: "/repo/not-a-worktree" });
|
||||
store.getTask.mockResolvedValue(escaped);
|
||||
const executor = new TaskExecutor(store as any, "/repo");
|
||||
await executor.execute(escaped as any);
|
||||
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-4115", "todo", { preserveProgress: true });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user