Files
fusion/packages/engine/src/__tests__/executor-task-done-invariant.test.ts
gsxdsm c7fa02f370 FN-8597: restore executor task-done invariant coverage
Restore the quarantined executor graph-completion invariant suite with real foreach projections.

- Exercise complete and partial expanded workflow-step projections at the merge boundary.
- Remove the rescued invariant suite from Vitest quarantine and clear its ledger entry.
- Extend the shared executor logger mock with the debug method required by the integration tip.

Files changed:
 .../__tests__/executor-task-done-invariant.test.ts | 267 +++++++++++++++++++--
 .../engine/src/__tests__/executor-test-helpers.ts  |   7 +
 packages/engine/vitest.config.ts                   |   7 -
 scripts/lib/test-quarantine.json                   |   8 +-
 4 files changed, 254 insertions(+), 35 deletions(-)

Fusion-Task-Id: FN-8597

Fusion-Task-Lineage: 05a08e31-7da0-4c93-86a0-9baf8db7ce52

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 10:04:36 -07:00

1200 lines
56 KiB
TypeScript

import { beforeEach, afterEach, describe, expect, it, vi } from "vitest";
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import "./executor-test-helpers.js";
import { TaskExecutor } from "../executor.js";
import * as worktreePool from "../worktree-pool.js";
import { getBuiltinWorkflow, instanceNodeId, type TaskStore } from "@fusion/core";
import { createTaskStoreForTest, pgDescribe, type PgTestHarness } from "../../../core/src/__test-utils__/pg-test-harness.js";
import { captureNamedTool, createMockStore, mockedCreateFnAgent, mockedExec, mockedExecSync, resetExecutorMocks } from "./executor-test-helpers.js";
const fn416Prompt = `# Task: FN-416 - Assign ready implementation task to active owner
**Created:** 2026-06-12
**Size:** S
## Review Level: 1 (Plan Only)
**Assessment:** This is an operational routing task with no expected product-source changes.
## Mission
Assign or route exactly one ready implementation task to an eligible active owner, or record an intentional no-route state. No source files expected.
## File Scope
- FN-416 task document docs via fn_task_document_write
- .fusion/tasks/FN-416/ task log evidence only
## Steps
### Step 0: Preflight
- [x] Check board state
### Step 1: Route exactly one existing ready task or record no-route
- [x] Record evidence in task documents/logs
`;
const sourceChangingPlanOnlyPrompt = `# Task: FN-999 - Implement source fix
**Size:** S
## Review Level: 1 (Plan Only)
## Mission
Implement a source-changing bug-fix in the executor.
## File Scope
- packages/engine/src/executor.ts
## Steps
### Step 1: Implement
- [ ] Change source
`;
function baseTask(overrides: Record<string, unknown> = {}) {
return {
id: "FN-4114",
title: "Invariant test",
description: "",
column: "in-progress",
worktree: "/repo/.worktrees/swift-falcon",
branch: "fusion/fn-4114",
baseCommitSha: "abc123",
taskDoneRetryCount: 0,
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 = baseTask(overrides);
let tool: any;
store.getTask.mockImplementation(async () => ({ ...task, steps: task.steps.map((s: any) => ({ ...s })) }));
store.moveTask.mockImplementation(async (id: string, column: string) => {
task = { ...task, id, column, paused: false, pausedByAgentId: null, status: null, error: null };
});
store.handoffToReview.mockImplementation(async (id: string) => {
task = { ...task, id, column: "in-review", paused: false, pausedByAgentId: null };
return task;
});
mockedCreateFnAgent.mockImplementation(async ({ customTools }: any) => {
tool = captureNamedTool(customTools, "fn_task_done", tool);
return { session: { prompt: vi.fn().mockResolvedValue(undefined), dispose: vi.fn() } } as any;
});
const executor = new TaskExecutor(store as any, "/repo");
await executor.execute(task as any);
/*
FNXC:EngineTests 2026-07-19-16:10 (U10b):
`execute()` here is only a VEHICLE for capturing the live `fn_task_done` tool; the requirement
under test in this file is what that tool does when the agent calls it against a given row state.
Under graph ownership the vehicle is no longer inert: the harness session never calls
fn_task_done, so the graph spends the task-done budget and rebounds the row (column -> todo,
steps reset to the prompt-derived pending shape, taskDoneRetryCount bumped) and records its own
moveTask/updateStep calls. That vehicle noise is not the contract — it silently rewrote the
PRECONDITION each test declares (all-steps-done source-free delivery, two-pending-step bulk
guard, ...) and polluted the spies the assertions read.
Restoring the declared row through `_setRow` (patches win over the per-file `getTask` override,
so this is the only ordering that beats the executor's own writes) and clearing call history
re-establishes the exact precondition each test always intended, without weakening any assertion.
*/
const baseline = baseTask(overrides);
task = { ...baseline, column: "in-progress", paused: false, pausedByAgentId: null, status: null, error: null };
store._setRow(baseline.id as string, { ...task });
for (const spy of [
store.moveTask,
store.updateStep,
store.updateTask,
store.logEntry,
store.recordActivity,
store.handoffToReview,
mockedExecSync,
]) {
spy.mockClear();
}
return { store, tool, setTask: (next: any) => (task = { ...task, ...next }) };
}
describe("FN-4114 fn_task_done invariants", () => {
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-4114\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-4114 refuses fn_task_done when 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-4114\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-4114", "todo", { preserveProgress: true });
});
it("FN-4114 refuses fn_task_done when branch is wrong", 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-4114", "todo", { preserveProgress: true });
});
it("FN-4114 refuses fn_task_done when no commits exist 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-4114\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-4114", "todo", { preserveProgress: true });
});
it.each([
"NO-OP: existing tests already cover this",
"PREMISE STALE: targeted reproduction already passes unchanged on HEAD",
"DUPLICATE: FN-6239 existing QuickChatFAB tests already cover this",
])("FN-6275 allows verified no-op zero-commit completion with sentinel %s", async (summary) => {
const { store, tool } = await setup({
steps: [
{ name: "Preflight", status: "done" as const },
{ name: "Implement", status: "skipped" as const },
{ name: "Testing & Verification", status: "done" as const },
],
currentStep: 2,
});
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-4114\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", { summary });
expect(result.content[0].text).toContain("Task marked complete");
expect(result.content[0].text).not.toContain("fn_task_done refused: no_commits");
expect(store.moveTask).not.toHaveBeenCalledWith("FN-4114", "todo", { preserveProgress: true });
expect(store.updateTask).toHaveBeenCalledWith("FN-4114", { noCommitsExpected: true });
expect(store.logEntry).toHaveBeenCalledWith(
"FN-4114",
expect.stringContaining("completion sentinel accepted"),
expect.stringContaining(summary),
undefined,
);
expect(store.recordActivity).toHaveBeenCalledWith(expect.objectContaining({
type: "task:updated",
taskId: "FN-4114",
metadata: expect.objectContaining({ summary }),
}));
});
it("FN-6275 still refuses ordinary zero-commit completion summaries", async () => {
const { store, tool } = await setup({
steps: [{ name: "Implement", status: "done" as const }],
});
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-4114\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", { summary: "Verified existing behavior with targeted tests." });
expect(result.content[0].text).toContain("fn_task_done refused: no_commits");
expect(store.moveTask).toHaveBeenCalledWith("FN-4114", "todo", { preserveProgress: true });
expect(store.updateTask).not.toHaveBeenCalledWith("FN-4114", { noCommitsExpected: true });
});
it.each([
["wrong_toplevel", "/repo\n", "fusion/fn-4114\n"],
["wrong_branch", "/repo/.worktrees/swift-falcon\n", "main\n"],
] as const)("FN-6275 does not relax %s for sentinel summaries", async (reason, toplevel, branch) => {
const { store, tool } = await setup({ steps: [{ name: "Implement", status: "done" as const }] });
mockedExecSync.mockImplementation((cmd: string) => {
if (cmd.includes("rev-parse --show-toplevel")) return Buffer.from(toplevel);
if (cmd.includes("rev-parse --abbrev-ref HEAD")) return Buffer.from(branch);
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", { summary: "NO-OP: already covered" });
expect(result.content[0].text).toContain(`fn_task_done refused: ${reason}`);
expect(store.moveTask).toHaveBeenCalledWith("FN-4114", "todo", { preserveProgress: true });
expect(store.updateTask).not.toHaveBeenCalledWith("FN-4114", { noCommitsExpected: true });
});
it("FN-6275 sentinel summaries do not auto-complete multiple pending unreviewed steps", async () => {
const { store, tool } = await setup({
steps: [
{ name: "Implement", status: "in-progress" as const },
{ name: "Testing", status: "pending" as const },
],
});
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-4114\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", { summary: "NO-OP: already covered" });
expect(result.content[0].text).toContain("fn_task_done refused (bulk-step-completion-without-review)");
expect(store.moveTask).toHaveBeenCalledWith("FN-4114", "todo", { preserveProgress: true });
});
it("FN-350 allows Review Level 1 coordination completion with zero commits when no source files are scoped", async () => {
const fn350Prompt = `# Task: FN-350 - Route Ready Swift Tasks to Executor Owner
**Created:** 2026-06-12
**Size:** S
## Review Level: 1 (Plan Only)
**Assessment:** This is a coordination/routing task that should not change product source, but it can affect execution ordering and owner assignment for active Swift implementation work. Risk is low if the executor follows the existing coordinator handoff policy, routes at most one existing ready task, and records clear evidence instead of creating duplicate implementation work.
## Mission
Route exactly one existing ready Swift implementation task to the durable executor owner, or record the intentional block if no safe candidate exists. Do not change product source.
## File Scope
Atlas Notes task-board artifacts only:
- FN-350 task document \`docs\` via \`fn_task_document_write\`
- Board task metadata and logs via Fusion task tools
## Steps
### Step 0: Preflight
- [x] Required board records exist.
### Step 1: Re-check live candidate readiness
- [x] Candidate readiness inspected.
### Step 2: Select exactly one routing action
- [x] One routing action selected.
### Step 3: Perform safe routing or record intentional block
- [x] Routing evidence recorded.
### Step 4: Testing & Verification
- [x] Board-only verification recorded.
### Step 5: Documentation & Delivery
- [x] Final documentation saved.
## Do NOT
- Do not edit product source.
- Do not create duplicate implementation tasks.
`;
const { store, tool } = await setup({
id: "FN-350",
title: "Route Ready Swift Tasks to Executor Owner",
description: "Coordination/routing task with task-document evidence only.",
prompt: fn350Prompt,
branch: "fusion/fn-350",
noCommitsExpected: undefined,
steps: [
{ name: "Preflight", status: "done" as const },
{ name: "Re-check live candidate readiness", status: "done" as const },
{ name: "Select exactly one routing action", status: "done" as const },
{ name: "Perform safe routing or record intentional block", status: "done" as const },
{ name: "Testing & Verification", status: "done" as const },
{ name: "Documentation & Delivery", status: "in-progress" as const },
],
currentStep: 5,
});
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-350\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("");
});
store.moveTask.mockClear();
const result = await tool.execute("id", { summary: "Recorded routing evidence in task documents and logs." });
expect(result.content[0].text).toContain("Task marked complete");
expect(result.content[0].text).not.toContain("fn_task_done refused: no_commits");
/*
FNXC:EngineTests 2026-07-19-16:25 (U10b):
A clean completion performs NO column move of its own. The old expectation of a single
`moveTask(id,"in-progress")` was an artifact of the vehicle leaving the row parked in `todo`;
with the row restored to its declared `in-progress` state the requirement is exactly the
stricter one this test always meant — the completion neither rebounds to `todo` nor stages the
review handoff itself (the merge-node boundary owns the in-review move).
*/
expect(store.moveTask.mock.calls).toEqual([]);
expect(store.handoffToReview).not.toHaveBeenCalled();
});
it("FN-7487 allows source-free gitignored task-artifact delivery with zero commits", async () => {
const fn7487Prompt = `# Task: FN-7487 - Audit FN-6902 spec compliance
**Created:** 2026-07-03
**Size:** S
## Review Level: 0 (None)
## Mission
Deliver the requested source-free task-artifact audit. The deliverables are gitignored task artifacts only, not product source changes.
## File Scope
- .fusion/tasks/FN-6902/PROMPT.md
- .fusion/tasks/FN-7487/attachments/fn-6902-spec-compliance.test.mjs
## Steps
### Step 0: Preflight
- [x] Read the source task prompt.
### Step 1: Write source-free task artifact evidence
- [x] Save the audit artifact under .fusion/tasks/.
## Do NOT
- Do not force-add gitignored .fusion/ artifacts.
- Do not create empty commits or fabricate commits for this source-free delivery.
`;
const { store, tool } = await setup({
id: "FN-7487",
title: "Audit FN-6902 spec compliance",
description: "Source-free task-artifact delivery only.",
prompt: fn7487Prompt,
branch: "fusion/fn-7487",
noCommitsExpected: undefined,
reviewLevel: 0,
steps: [
{ name: "Preflight", status: "done" as const },
{ name: "Write source-free task artifact evidence", status: "done" as const },
],
currentStep: 1,
});
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-7487\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("");
});
store.moveTask.mockClear();
const result = await tool.execute("id", { summary: "Saved the source-free task-artifact audit and verified the artifact path." });
expect(result.content[0].text).toContain("Task marked complete");
expect(result.content[0].text).not.toContain("fn_task_done refused: no_commits");
expect(store.moveTask).not.toHaveBeenCalledWith("FN-7487", "todo", { preserveProgress: true });
expect(store.logEntry).toHaveBeenCalledWith(
"FN-7487",
expect.stringContaining("prompt-derived source-free task-artifact contract"),
undefined,
undefined,
);
const revListCalled = mockedExecSync.mock.calls.some(([cmd]) => String(cmd).includes("rev-list --count"));
expect(revListCalled).toBe(false);
});
it("FN-7487 refuses mixed task artifacts and tracked source scope with zero commits", async () => {
const mixedPrompt = `# Task: FN-7487 - Audit and update executor
## Review Level: 0 (None)
## Mission
Deliver source-free task artifacts if possible, but also update tracked documentation.
## File Scope
- .fusion/tasks/FN-6902/PROMPT.md
- .fusion/tasks/FN-7487/attachments/fn-6902-spec-compliance.test.mjs
- docs/testing.md
## Steps
### Step 1: Deliver
- [x] Work completed.
## Do NOT
- Do not force-add gitignored .fusion/ artifacts.
- Do not create empty commits or fabricate commits for this source-free delivery.
`;
const { store, tool } = await setup({
id: "FN-7487",
prompt: mixedPrompt,
branch: "fusion/fn-7487",
noCommitsExpected: undefined,
reviewLevel: 0,
steps: [{ name: "Deliver", status: "done" as const }],
});
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-7487\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", { summary: "Saved artifacts and documentation." });
expect(result.content[0].text).toContain("fn_task_done refused: no_commits");
expect(store.moveTask).toHaveBeenCalledWith("FN-7487", "todo", { preserveProgress: true });
expect(store.logEntry).not.toHaveBeenCalledWith(
"FN-7487",
expect.stringContaining("prompt-derived source-free task-artifact contract"),
undefined,
undefined,
);
});
it("FN-350 refuses contradictory implementation plus coordination fallback prompts", async () => {
const prompt = `# Task: FN-350 - Route Ready Swift Tasks to Executor Owner
## Review Level: 1 (Plan Only)
**Assessment:** This is a coordination/routing task that should not change product source.
## Mission
Implement the source fix if possible, or record the intentional block if no safe candidate exists. Do not change product source.
## File Scope
- FN-350 task document \`docs\` via \`fn_task_document_write\`
## Steps
### Step 1: Decide
- [x] Decision recorded.
`;
const { store, tool } = await setup({
id: "FN-350",
title: "Route Ready Swift Tasks to Executor Owner",
description: "Coordination/routing task with task-document evidence only.",
prompt,
branch: "fusion/fn-350",
noCommitsExpected: undefined,
steps: [{ name: "Decide", status: "done" as const }],
});
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-350\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-350", "todo", { preserveProgress: true });
});
it("FN-4114 still refuses source-changing implementation tasks with zero commits and no explicit no-commit contract", async () => {
const implementationPrompt = `# Task: FN-4114 - Implement source change
**Size:** M
## Review Level: 2 (Plan and Code)
## Mission
Implement a bug fix in the engine.
## File Scope
- packages/engine/src/executor.ts
- packages/engine/src/__tests__/executor-task-done-invariant.test.ts
## Steps
### Step 1: Implement
- [ ] Change source code and tests.
`;
const { store, tool } = await setup({ prompt: implementationPrompt, noCommitsExpected: undefined });
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-4114\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-4114", "todo", { preserveProgress: true });
});
it("FN-4114 allows no-commit completion when noCommitsExpected is true", async () => {
const { store, tool } = await setup({ noCommitsExpected: 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-4114\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("Task marked complete");
expect(store.updateStep).toHaveBeenCalled();
expect(store.logEntry).toHaveBeenCalledWith(
"FN-4114",
expect.stringContaining("noCommitsExpected=true"),
undefined,
undefined,
);
const revListCalled = mockedExecSync.mock.calls.some(([cmd]) => String(cmd).includes("rev-list --count"));
expect(revListCalled).toBe(false);
});
it("FN-4114 still refuses wrong_toplevel even when noCommitsExpected is true", async () => {
const { store, tool } = await setup({ noCommitsExpected: true });
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-4114\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.moveTask).toHaveBeenCalledWith("FN-4114", "todo", { preserveProgress: true });
});
it("FN-4114 still refuses wrong_branch even when noCommitsExpected is true", async () => {
const { store, tool } = await setup({ noCommitsExpected: 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("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-4114", "todo", { preserveProgress: true });
});
it("FN-4114 allows no-commit completion when noCommitsExpected audit logging fails", async () => {
const { store, tool } = await setup({ noCommitsExpected: 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-4114\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("");
});
store.logEntry.mockImplementation(async (_id: string, message: string) => {
if (message.includes("no_commits guard skipped")) throw new Error("audit unavailable");
});
const result = await tool.execute("id", {});
expect(result.content[0].text).toContain("Task marked complete");
expect(store.updateStep).toHaveBeenCalled();
});
it("FN-416 allows plan-only operational no-source completion with zero commits when the explicit flag is missing", async () => {
const { store, tool } = await setup({
id: "FN-416",
branch: "fusion/fn-416",
title: "Assign ready implementation task to active owner",
description: "Operational routing task with no expected product-source changes; record routing evidence or no-route state.",
reviewLevel: 1,
prompt: fn416Prompt,
sourceMetadata: { fileScope: ["FN-416 task document docs via fn_task_document_write"] },
log: [{ timestamp: new Date().toISOString(), action: "Routing evidence recorded", outcome: "No-route state documented in task docs" }],
steps: [
{ name: "Preflight", status: "done" as const },
{ name: "Route or record no-route", status: "done" as const },
],
});
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-416\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("Task marked complete");
expect(store.moveTask).not.toHaveBeenCalledWith("FN-416", "todo", { preserveProgress: true });
expect(store.handoffToReview).not.toHaveBeenCalledWith("FN-416", expect.objectContaining({
evidence: expect.objectContaining({ reason: "invariant-check-failed" }),
}));
expect(store.logEntry).toHaveBeenCalledWith(
"FN-416",
expect.stringContaining("prompt/source metadata derived operational no-commit contract"),
undefined,
undefined,
);
const revListCalled = mockedExecSync.mock.calls.some(([cmd]) => String(cmd).includes("rev-list --count"));
expect(revListCalled).toBe(false);
});
it("FN-416 refuses plan-only operational no-source completion when File Scope is missing", async () => {
const promptWithoutFileScope = `# Task: FN-417 - Assign ready implementation task to active owner
## Review Level: 1 (Plan Only)
**Assessment:** This is an operational routing task with no expected product-source changes.
## Mission
Assign or route exactly one ready implementation task to an eligible active owner, or record an intentional no-route state. No source files expected.
## Steps
### Step 1: Route exactly one existing ready task or record no-route
- [x] Record evidence in task documents/logs
`;
const { store, tool } = await setup({
id: "FN-417",
branch: "fusion/fn-417",
title: "Assign ready implementation task to active owner",
description: "Operational routing task with no expected product-source changes; record routing evidence or no-route state.",
reviewLevel: 1,
prompt: promptWithoutFileScope,
sourceMetadata: {},
log: [{ timestamp: new Date().toISOString(), action: "Routing evidence recorded", outcome: "No-route state documented in task docs" }],
steps: [{ name: "Route or record no-route", status: "done" as const }],
});
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-417\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-417", "todo", { preserveProgress: true });
});
it("FN-416 refuses prompt-only evidence text when steps are incomplete and logs are empty", async () => {
const { store, tool } = await setup({
id: "FN-418",
branch: "fusion/fn-418",
title: "Assign ready implementation task to active owner",
description: "Operational routing task with no expected product-source changes; record routing evidence or no-route state.",
reviewLevel: 1,
prompt: fn416Prompt.replace("# Task: FN-416", "# Task: FN-418"),
sourceMetadata: { fileScope: ["FN-418 task document docs via fn_task_document_write"] },
log: [],
steps: [{ name: "Route or record no-route", status: "in-progress" as const }],
});
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-418\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-418", "todo", { preserveProgress: true });
});
it("FN-416 refuses mixed no-source text with source-changing scope entries", async () => {
const mixedScopePrompt = fn416Prompt
.replace("# Task: FN-416", "# Task: FN-419")
.replace(
"- FN-416 task document docs via fn_task_document_write",
"- No source changes expected, but inspect packages/engine/src/executor.ts",
);
const { store, tool } = await setup({
id: "FN-419",
branch: "fusion/fn-419",
title: "Assign ready implementation task to active owner",
description: "Operational routing task with no expected product-source changes; record routing evidence or no-route state.",
reviewLevel: 1,
prompt: mixedScopePrompt,
sourceMetadata: { fileScope: ["No source changes expected, but inspect packages/engine/src/executor.ts"] },
log: [{ timestamp: new Date().toISOString(), action: "Routing evidence recorded", outcome: "No-route state documented in task docs" }],
steps: [{ name: "Route or record no-route", status: "done" as const }],
});
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-419\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-419", "todo", { preserveProgress: true });
});
it("FN-416 keeps the missing-commit guard for source-changing plan-only tasks without an explicit contract", async () => {
const { store, tool } = await setup({
title: "Implement executor fix",
description: "Plan Only but requires source-changing implementation work.",
reviewLevel: 1,
prompt: sourceChangingPlanOnlyPrompt,
sourceMetadata: { fileScope: ["packages/engine/src/executor.ts"] },
steps: [{ name: "Implement", status: "done" as const }],
});
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-4114\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-4114", "todo", { preserveProgress: true });
});
it("FN-4114 allows fn_task_done on valid worktree/branch/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();
});
});
/* FNXC:PgMigrationQuarantine 2026-07-18-01:20: FN-8258 runs handoff-audit invariants against the PostgreSQL TaskStore and its async audit boundary, replacing the removed SQLite constructor. */
pgDescribe("FN-5241 executor handoff auditing", () => {
let rootDir: string;
let harness: PgTestHarness;
let store: TaskStore;
beforeEach(async () => {
// Reset the executor subprocess mock before PG setup so the harness can pass psql through.
resetExecutorMocks();
harness = await createTaskStoreForTest({ prefix: "fusion_executor_handoff_audit" });
rootDir = harness.rootDir;
store = harness.store;
/*
FNXC:EngineTests 2026-07-26-08:18:
The PG harness deliberately has no global directory. FN-8597's graph test has no MCP
configuration, so use its no-MCP secret-reader seam instead of resolving a real global store.
This isolates the executor handoff invariant from environment-specific secrets initialization.
*/
vi.spyOn(store, "getSecretsStore").mockResolvedValue({
revealSecret: vi.fn(),
} as any);
vi.spyOn(worktreePool, "isUsableTaskWorktree").mockResolvedValue(true);
});
afterEach(async () => {
await harness?.teardown();
});
async function createExecutorTask(taskDoneRetryCount = 0, stepNames = ["Implement"]) {
const created = await store.createTask({ description: "Invariant test", priority: "high" });
await store.moveTask(created.id, "todo");
await store.moveTask(created.id, "in-progress");
const worktreePath = join(rootDir, ".worktrees", "swift-falcon");
mkdirSync(worktreePath, { recursive: true });
const branch = `fusion/${created.id.toLowerCase()}`;
/*
FNXC:EngineTests 2026-07-26-08:18:
FN-8597 exercises the real graph expansion from task-document step headings.
Derive the heading from the requested projection so graph-owned execution, rather than
manually seeded workflow-step results, determines the active foreach node ids.
*/
const prompt = `# Test\n## Steps\n${stepNames.map((name, index) => `### Step ${index}: ${name}\n- [ ] check`).join("\n")}\n`;
const taskDir = join(rootDir, ".fusion", "tasks", created.id);
mkdirSync(taskDir, { recursive: true });
writeFileSync(join(taskDir, "PROMPT.md"), prompt, "utf8");
await store.updateTask(created.id, {
worktree: worktreePath,
branch,
baseCommitSha: "abc123",
taskDoneRetryCount,
// Start pending so graph-owned fn_task_done completes the real foreach projection.
steps: stepNames.map((name) => ({ name, status: "pending" })),
currentStep: 0,
prompt,
} as any);
const task = (await store.getTask(created.id))!;
return {
task: {
...task,
prompt,
},
worktreePath,
};
}
/*
FNXC:EngineTests 2026-07-27-08:50:
FN-8597's merge-proof regression coverage must inspect the persisted foreach
instances, not `workflowStepResults`: that field belongs to optional-group
review gates, while `steps#N:step-execute` state is stored under the live
graph run id. Reading the rows derived by the runner proves the partial and
complete projections use the actual expanded node identities rather than
guessed synthetic result ids.
*/
async function loadForeachInstances(taskId: string) {
return store.loadWorkflowRunStepInstancesAsync(taskId, `${taskId}:builtin:coding`);
}
async function advanceMergeBoundary(executor: TaskExecutor, task: unknown, mergeBoundaryNodeId: string) {
return (executor as unknown as {
ensureWorkflowMergeBoundaryTask: (candidate: unknown, metadata: unknown) => Promise<unknown>;
}).ensureWorkflowMergeBoundaryTask(task, {
reason: "fn-8597-workflow-step-result-proof",
nodeId: mergeBoundaryNodeId,
workflowId: "builtin:coding",
runId: `${(task as { id: string }).id}:builtin:coding`,
});
}
type GraphCompletionProjection = {
parentIds: string[];
groupIds: string[];
leafIds: string[];
mergeBoundaryNodeId: string;
};
/*
FNXC:EngineTests 2026-07-27-11:05:
FN-8597 must seed the merge proof from the live foreach expansion, not guessed
`steps#N` strings. Derive the foreach parents, expanded groups, leaves, and
merge boundary from builtin:coding so a graph topology change cannot leave this
PG fixture proving an obsolete projection.
*/
async function deriveGraphCompletionProjection(taskId: string): Promise<GraphCompletionProjection> {
const instances = await loadForeachInstances(taskId);
const graph = getBuiltinWorkflow("builtin:coding")?.ir;
expect(graph).toBeDefined();
const groupIds = [...new Set(instances.map((instance) => instance.foreachNodeId))].sort();
const parentIds = [...new Set(
graph!.edges
.filter((edge) => groupIds.includes(edge.to))
.map((edge) => edge.from),
)].sort();
const leafIds = instances
.filter((instance) => instance.status === "completed")
.map((instance) => {
const foreach = graph!.nodes.find((node) => node.id === instance.foreachNodeId && node.kind === "foreach");
const template = foreach?.config?.template;
const stepExecute = template?.nodes.find((node) => node.config?.seam === "step-execute");
expect(stepExecute?.id).toBeTypeOf("string");
return instanceNodeId(instance.foreachNodeId, instance.stepIndex, stepExecute!.id);
})
.sort();
const mergeBoundary = graph!.nodes.find((node) => node.kind === "merge-gate");
expect(parentIds).not.toHaveLength(0);
expect(groupIds).not.toHaveLength(0);
expect(mergeBoundary?.id).toBeTypeOf("string");
expect(leafIds).toHaveLength(instances.length);
return { parentIds, groupIds, leafIds, mergeBoundaryNodeId: mergeBoundary!.id };
}
function completedProjectionResults(projection: GraphCompletionProjection) {
return [
...projection.parentIds,
...projection.groupIds,
...projection.leafIds,
].map((workflowStepId) => ({
workflowStepId,
workflowStepName: workflowStepId,
source: "node" as const,
phase: "pre-merge" as const,
status: "passed" as const,
}));
}
/*
FNXC:WorkflowLifecycle 2026-07-01-22:10:
workflowGraphExecutor is default-on, so the FN-5241 "atomic in-review handoff seam" auditing was
superseded by the graph lifecycle:
- SUCCESSFUL builtin:coding completion reaches in-review via the MERGE-NODE boundary moveTask
(executor.ts:6106, "handoff-invariant-violation-allowlist: workflow merge node owns the merge
lifecycle boundary"), NOT the review-seam handoffTaskToReview("workflow-graph-review"). There is no
longer a task:handoff("workflow-graph-review") event nor a review-seam merge-queue enqueue on this
path; the merge boundary records task:move audits instead.
- EXHAUSTION/no-fn_task_done budget now FAILS IN PLACE (executor.ts:2088 "in-review is reserved for
clean completion handoffs"); the "max-task-done-retries-exhausted" in-review reason no longer exists.
These tests are migrated to assert the CURRENT mechanisms; they still protect the FN-5241 intent (a clean
completion reaches in-review; an exhausted no-fn_task_done run is terminal), just via the graph seams.
*/
/*
FNXC:EngineTests 2026-07-26-08:18:
FN-8597 found the old `steps#0:step-execute` failure was an unconfigured PG-harness
secrets-store access, not a graph-completion defect. Do not seed guessed result ids:
completion must traverse the live single-instance foreach expansion and merge boundary.
*/
it("moves a cleanly completed task to in-review via the merge-node boundary", async () => {
const { task, worktreePath } = await createExecutorTask();
// Graph-native implementation proof: the mock agent signals completion via fn_task_done without running
// the foreach step-execute nodes, so the merge-boundary FN-7260/FN-7271 proof gate needs an explicit
// node-source pre-merge pass to model a genuinely-implemented task reaching the merge boundary.
mockedExec.mockImplementation(((cmd: string, _opts: unknown, cb?: (err: Error | null, stdout: string, stderr: string) => void) => {
if (!cb) return undefined as any;
if (cmd.includes("rev-parse --show-toplevel")) return cb(null, `${worktreePath}\n`, "");
if (cmd.includes("rev-parse --abbrev-ref HEAD")) return cb(null, `${task.branch}\n`, "");
if (cmd.includes("rev-list --count")) return cb(null, "1\n", "");
if (cmd.includes("rev-parse HEAD")) return cb(null, "def456\n", "");
return cb(null, "", "");
}) as any);
mockedCreateFnAgent.mockImplementation(async ({ customTools }: any) => ({
session: {
prompt: vi.fn().mockImplementation(async () => {
const tools = customTools ?? [];
const taskDoneTool = tools.find((tool: any) => tool.name === "fn_task_done");
if (taskDoneTool) {
await taskDoneTool.execute("tool-1", {});
return;
}
// FNXC:EngineTests 2026-07-26-08:18: graph step sessions without task_done use the native step-completion tool.
const stepDone = tools.find((tool: any) => tool.name === "fn_task_step_done" || tool.name === "fn_step_done");
if (stepDone) {
await stepDone.execute("tool-step", {});
}
}),
dispose: vi.fn(),
subscribe: vi.fn(),
on: vi.fn(),
sessionManager: { getLeafId: vi.fn().mockReturnValue("leaf-1") },
state: {},
},
}) as any);
const executor = new TaskExecutor(store as any, rootDir);
// The merge-node boundary requests merge via the injected requester before it inline-merges; returning
// a non-merged "queued" result keeps the task terminal in-review (autoMerge deferred) instead of
// finalizing it to done, so we observe the review-staging state under test.
executor.setMergeRequester(async () => ({ merged: false, noOp: false, reason: "queued" }) as any);
await executor.execute(task as any);
// CURRENT completion mechanism: task ends in-review via the merge-node boundary moveTask.
const latest = await store.getTask(task.id);
expect(latest?.column).toBe("in-review");
// No synthetic optional-group result can bypass the proof: the live foreach traversal
// completes the actual task-step projection before the merge boundary advances.
expect(latest?.steps.map((step) => step.status)).toEqual(["done"]);
const instances = await loadForeachInstances(task.id);
expect(instances).toEqual(expect.arrayContaining([
expect.objectContaining({ foreachNodeId: "steps", stepIndex: 0, status: "completed" }),
]));
// Forensic intent preserved via the mechanism that actually fires now: the merge boundary records a
// task:move into in-review (the review-seam task:handoff("workflow-graph-review") event and the
// review-seam merge-queue enqueue no longer occur on this path).
const moveToReview = (await store
.getRunAuditEventsAsync({ taskId: task.id, mutationType: "task:move", limit: 20 }))
.find((event) => (event.metadata as { to?: string })?.to === "in-review");
expect(moveToReview).toBeDefined();
expect(await store.getRunAuditEventsAsync({ taskId: task.id, mutationType: "task:handoff", limit: 10 })).toHaveLength(0);
});
/*
FNXC:EngineTests 2026-07-27-08:35:
FN-8597's rescue must protect the foreach projection rather than only its former
single-step reproduction. A partial multi-step run has no implementation proof for
the unvisited instance, so it must fail before the graph merge boundary can move the
task to in-review.
*/
it("does not move a partial foreach expansion to in-review", async () => {
const { task, worktreePath } = await createExecutorTask(0, ["Implement", "Verify"]);
mockedExec.mockImplementation(((cmd: string, _opts: unknown, cb?: (err: Error | null, stdout: string, stderr: string) => void) => {
if (!cb) return undefined as any;
if (cmd.includes("rev-parse --show-toplevel")) return cb(null, `${worktreePath}\n`, "");
if (cmd.includes("rev-parse --abbrev-ref HEAD")) return cb(null, `${task.branch}\n`, "");
if (cmd.includes("rev-list --count")) return cb(null, "1\n", "");
if (cmd.includes("rev-parse HEAD")) return cb(null, "def456\n", "");
return cb(null, "", "");
}) as any);
const executor = new TaskExecutor(store as any, rootDir);
// The graph's per-instance seam is the narrow deterministic boundary: complete
// only expanded instance 0 in the real PG projection, then reject instance 1.
// This makes the persisted result set partial without depending on agent timing.
vi.spyOn(executor as any, "runGraphTaskStep").mockImplementation(async (_task: unknown, stepIndex: number) => {
if (stepIndex !== 0) return { success: false, error: "intentionally partial projection" };
await store.updateStep(task.id, stepIndex, "done", { source: "graph" });
return { success: true };
});
await executor.execute(task as any);
const latest = await store.getTask(task.id);
expect(latest?.column).not.toBe("in-review");
// The failed second instance is left in-progress for graph-owned recovery; its
// non-terminal projection is the condition that must keep merge unavailable.
expect(latest?.steps.map((step) => step.status)).toEqual(["done", "in-progress"]);
// The graph persisted the actual first expanded node as complete but its sibling
// `steps#1:step-execute` as failed; this partial result set must not cross merge.
const instances = await loadForeachInstances(task.id);
expect(instances).toEqual(expect.arrayContaining([
expect.objectContaining({ foreachNodeId: "steps", stepIndex: 0, status: "completed" }),
expect.objectContaining({ foreachNodeId: "steps", stepIndex: 1, status: "failed" }),
]));
expect((await store.getRunAuditEventsAsync({ taskId: task.id, mutationType: "task:move", limit: 20 }))
.some((event) => (event.metadata as { to?: string })?.to === "in-review")).toBe(false);
expect(await store.getRunAuditEventsAsync({ taskId: task.id, mutationType: "task:handoff", limit: 10 })).toHaveLength(0);
expect(await store.peekMergeQueue()).toEqual([]);
});
it("requires every expanded foreach step before the merge boundary advances", async () => {
const { task, worktreePath } = await createExecutorTask(0, ["Implement", "Verify"]);
mockedExec.mockImplementation(((cmd: string, _opts: unknown, cb?: (err: Error | null, stdout: string, stderr: string) => void) => {
if (!cb) return undefined as any;
if (cmd.includes("rev-parse --show-toplevel")) return cb(null, `${worktreePath}\n`, "");
if (cmd.includes("rev-parse --abbrev-ref HEAD")) return cb(null, `${task.branch}\n`, "");
if (cmd.includes("rev-list --count")) return cb(null, "1\n", "");
if (cmd.includes("rev-parse HEAD")) return cb(null, "def456\n", "");
return cb(null, "", "");
}) as any);
const executor = new TaskExecutor(store as any, rootDir);
// Complete each runtime-expanded instance through the same projection seam.
// The runner, not this test, creates and persists `steps#0` and `steps#1`.
vi.spyOn(executor as any, "runGraphTaskStep").mockImplementation(async (_task: unknown, stepIndex: number) => {
await store.updateStep(task.id, stepIndex, "done", { source: "graph" });
return { success: true };
});
executor.setMergeRequester(async () => ({ merged: false, noOp: false, reason: "queued" }) as any);
await executor.execute(task as any);
const latest = await store.getTask(task.id);
// A graph run uses one shared implementation pass for sequential foreach instances.
// The proof is the complete live projection, paired with the partial case above:
// merge can advance only after both expanded task steps are terminal.
expect(latest?.steps.map((step) => step.status)).toEqual(["done", "done"]);
// Complete the same real expansion: both persisted `steps#N:step-execute`
// identities must be complete before this run can enter the merge boundary.
const instances = await loadForeachInstances(task.id);
expect(instances).toEqual(expect.arrayContaining([
expect.objectContaining({ foreachNodeId: "steps", stepIndex: 0, status: "completed" }),
expect.objectContaining({ foreachNodeId: "steps", stepIndex: 1, status: "completed" }),
]));
expect(instances).toHaveLength(2);
expect(latest?.column).toBe("in-review");
expect((await store.getRunAuditEventsAsync({ taskId: task.id, mutationType: "task:move", limit: 20 }))
.some((event) => (event.metadata as { to?: string })?.to === "in-review")).toBe(true);
expect(await store.getRunAuditEventsAsync({ taskId: task.id, mutationType: "task:handoff", limit: 10 })).toHaveLength(0);
});
/*
FNXC:EngineTests 2026-07-27-10:30:
FN-8597's rescue proves the merge boundary sees the complete live expansion:
its execution parent, foreach group, and every `steps#N:step-execute` leaf.
A partial leaf projection must not enter review even when parent/group results
and persisted PG instances are present.
*/
it("blocks partial workflowStepResults and advances a complete multi-instance merge proof", async () => {
const { task } = await createExecutorTask(0, ["Implement", "Verify"]);
const runId = `${task.id}:builtin:coding`;
for (const stepIndex of [0, 1]) {
await store.saveWorkflowRunStepInstanceAsync({
taskId: task.id,
runId,
foreachNodeId: "steps",
stepIndex,
pinnedStepCount: 2,
currentNodeId: "step-execute",
status: "completed",
reworkCount: 0,
});
}
const projection = await deriveGraphCompletionProjection(task.id);
const completeResults = completedProjectionResults(projection);
const partialResults = completeResults.filter((result) => result.workflowStepId !== projection.leafIds[1]);
await store.updateTask(task.id, { workflowStepResults: partialResults } as any);
const executor = new TaskExecutor(store as any, rootDir);
await advanceMergeBoundary(executor, task, projection.mergeBoundaryNodeId);
let latest = (await store.getTask(task.id))!;
expect(latest.column).not.toBe("in-review");
expect(latest.workflowStepResults.map((result) => result.workflowStepId).sort()).toEqual(
partialResults.map((result) => result.workflowStepId).sort(),
);
expect(latest.steps.map((step) => step.status)).toEqual(["pending", "pending"]);
await store.updateTask(task.id, { workflowStepResults: completeResults } as any);
latest = (await store.getTask(task.id))!;
await advanceMergeBoundary(executor, latest, projection.mergeBoundaryNodeId);
latest = (await store.getTask(task.id))!;
expect(latest.column).toBe("in-review");
expect(latest.workflowStepResults.map((result) => result.workflowStepId).sort()).toEqual(
completeResults.map((result) => result.workflowStepId).sort(),
);
expect(latest.steps.every((step) => step.status === "done")).toBe(true);
});
it("fails a no-fn_task_done retry-budget-exhausted run in place without moving to in-review", async () => {
const { task, worktreePath } = await createExecutorTask(3);
mockedExec.mockImplementation(((cmd: string, _opts: unknown, cb?: (err: Error | null, stdout: string, stderr: string) => void) => {
if (!cb) return undefined as any;
if (cmd.includes("rev-parse --show-toplevel")) return cb(null, `${worktreePath}\n`, "");
if (cmd.includes("rev-parse --abbrev-ref HEAD")) return cb(null, `${task.branch}\n`, "");
if (cmd.includes("rev-list --count")) return cb(null, "1\n", "");
if (cmd.includes("rev-parse HEAD")) return cb(null, "def456\n", "");
return cb(null, "", "");
}) as any);
mockedCreateFnAgent.mockResolvedValue({
session: {
prompt: vi.fn().mockResolvedValue(undefined),
dispose: vi.fn(),
subscribe: vi.fn(),
on: vi.fn(),
sessionManager: { getLeafId: vi.fn().mockReturnValue("leaf-1") },
state: {},
},
} as any);
const executor = new TaskExecutor(store as any, rootDir);
await executor.execute(task as any);
const latest = await store.getTask(task.id);
// A no-fn_task_done run that cannot cleanly complete is NOT a review handoff (executor.ts:2088:
// "in-review is reserved for clean completion handoffs"). It must never advance to in-review, and the
// FN-5241 review-seam handoff auditing (task:handoff "workflow-graph-review" /
// "max-task-done-retries-exhausted") + merge-queue enqueue are superseded — none of them fire here.
expect(latest?.column).not.toBe("in-review");
expect(await store.getRunAuditEventsAsync({ taskId: task.id, mutationType: "task:handoff", limit: 10 })).toHaveLength(0);
expect(await store.peekMergeQueue()).toEqual([]);
});
});