Skip to content
564 changes: 564 additions & 0 deletions docs/solutions/test-failures/main-full-suite-census-2026-09-25.md

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion packages/cli/src/__tests__/cli-quiet-prompt-surfaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ describe("CLI quiet prompt and result source contracts", () => {
});

it("keeps all audited result writers attached to the output seam", () => {
for (const file of ["task.ts", "org-import.ts", "workflow.ts", "research.ts", "experiment-finalize.ts", "update.ts"]) {
// FN-9331 (74ffa19ef) removed the `fn research` CLI, so research.ts no longer exists and must
// not be audited here. Listing it made this test die with ENOENT on a removed source file.
for (const file of ["task.ts", "org-import.ts", "workflow.ts", "experiment-finalize.ts", "update.ts"]) {
const source = readFileSync(join(cliRoot, "commands", file), "utf8");
expect(source, file).toMatch(/import\s*\{[^}]*\bresult\b[^}]*\}\s*from\s*["']\.\.\/output\.js["']/);
expect(source, file).toMatch(/(?:result|outputResult)\(/);
Expand Down
9 changes: 7 additions & 2 deletions packages/cli/src/__tests__/docs-screenshot-links.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,15 @@ describe("docs screenshot links", () => {
}
}

// FN-9295: artifacts-doc-edit.png and artifacts-gallery.png are not referenced by any
// markdown files; removed from expected list to match actual docs.
// FN-9295 removed artifacts-doc-edit.png and artifacts-gallery.png from this list because no
// markdown referenced them at the time. docs/dashboard-guide.md has since referenced both
// again (Artifacts gallery, Artifact document viewer with edit mode), so the references are
// real and the expected list must carry them again — otherwise this assertion fails on
// origin/main with "expected [ ...(18) ] to deeply equal [ ...(16) ]".
expect(screenshotReferences.map(({ repoPath }) => repoPath).sort()).toEqual([
"docs/screenshots/agents-view.png",
"docs/screenshots/artifacts-doc-edit.png",
"docs/screenshots/artifacts-gallery.png",
"docs/screenshots/chat-view.png",
"docs/screenshots/dashboard-overview.png",
"docs/screenshots/dashboard-overview.png",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ vi.mock("@fusion/core", () => ({
formatRoleMismatchReason: vi.fn(() => ""),
resolveAgentProvisioningPolicy: vi.fn(() => ({ approvalMode: "auto" })),
TASK_PRIORITIES: ["low", "normal", "high", "urgent"],
// extension.ts reads MAX_TASK_MESSAGE_LENGTH while registering the refine tool's zod schema.
// A full-replacement mock that omits it fails at registration, before any assertion runs, so the
// mock has to carry the real constant or this whole file errors with
// 'No "MAX_TASK_MESSAGE_LENGTH" export is defined on the @fusion/core mock'.
MAX_TASK_MESSAGE_LENGTH: 100_000,
getProjectRootFromWorktree: vi.fn(() => null),
// FNXC:ToolPermissionGates 2026-07-26-14:55: fn_experiment_finalize is now withheld from agent
// principals; the guard resolves the caller principal via the session-identity registry.
Expand Down
65 changes: 63 additions & 2 deletions packages/cli/src/__tests__/task-retry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,25 @@ pgTest("runTaskRetry", () => {
await expect(runTaskRetry(task.id)).rejects.toThrow(/not in a retryable state/);
});

it("clears the deadlock auto-pause when retrying a failed task", async () => {
/*
* FNXC:MergeRetryAdmission CI drift fix (b4cddcbd9 lane):
* This fixture is a MERGE failure, not an execution failure: every step is done and
* mergeRetries has already been spent. FN-9317 ("recover stalled in-review merges")
* made that shape retry IN PLACE — clear status/error/auto-pause, reset mergeRetries,
* and keep the card in its review column so the approved work is not re-run.
*
* The `todo` assertion below is pre-FN-9317 drift from FN-6173, which briefly sent CLI
* merge retries back to todo; FN-9317 reverted that in commands/task.ts. The product is
* correct here, and every sibling surface already asserts the in-place behavior for this
* same shape: src/commands/__tests__/task.test.ts asserts `moveTask` is NOT called and the
* "in-review merge retry, mergeRetries reset" log; src/__tests__/extension.test.ts asserts
* `details.newColumn === "in-review"`; the dashboard route classifies it identically.
*
* The auto-pause clear this test exists to pin (FN-5937) is asserted unchanged below, and
* the deadlock auto-pause on a path that DOES re-queue is covered by the execution-failure
* case added directly after this one.
*/
it("clears the deadlock auto-pause on an in-review merge retry without re-queueing the card", async () => {
const store = h.store();
const task = await store.createTask({
title: "deadlock-paused task",
Expand All @@ -130,11 +148,54 @@ pgTest("runTaskRetry", () => {
await runTaskRetry(task.id);

const updated = await store.getTask(task.id);
expect(updated.column).toBe("todo");
// Merge retry restarts merge in review; it must not rebound a fully executed card.
expect(updated.column).toBe("in-review");
expect(updated.status).toBeFalsy();
expect(updated.error).toBeFalsy();
expect(updated.paused).toBeFalsy();
expect(updated.pausedReason).toBeFalsy();
expect(updated.mergeRetries).toBe(0);
});

/*
* FNXC:MergeRetryAdmission CI drift fix: the re-queue half of the deadlock auto-pause
* contract. An in-review card with unfinished steps is an EXECUTION failure, so retry
* re-queues it to the board's hold column with progress preserved. Mirrors the
* execution-failed deadlock case already asserted in src/__tests__/extension.test.ts.
*/
it("clears the deadlock auto-pause and re-queues an execution-failed in-review task", async () => {
const store = h.store();
const task = await store.createTask({
title: "deadlock-paused execution-failed task",
description: "test",
column: "todo",
});
await store.updateTask(task.id, {
steps: [
{ name: "implemented", status: "done" },
{ name: "fix", status: "pending" },
],
});
await store.moveTask(task.id, "in-progress");
await store.moveTask(task.id, "in-review");
await store.updateTask(task.id, {
status: "failed",
error: "executor stalled after deadlock pause",
paused: true,
pausedReason: "in-review-stall-deadlock",
mergeRetries: 0,
});

await runTaskRetry(task.id);

const updated = await store.getTask(task.id);
expect(updated.column).toBe("todo");
expect(updated.status).toBeFalsy();
expect(updated.error).toBeFalsy();
expect(updated.paused).toBeFalsy();
expect(updated.pausedReason).toBeFalsy();
// Execution retry preserves step progress rather than resetting merge bookkeeping.
expect(updated.steps?.[0]?.status).toBe("done");
expect(updated.steps?.[1]?.status).toBe("pending");
});
});
26 changes: 24 additions & 2 deletions packages/cli/src/commands/__tests__/skills-get.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,34 @@ describe("fn skills get", () => {
expect(branch).not.toMatch(/\b(?:readFile|readFileSync|fetch|spawn|exec)\s*\(/);
});

/*
* FNXC:ComputerUseSkill built-CLI budget (CI shard lane, b4cddcbd9):
* Every assertion below needs the BUILT binary, and each boot of `dist/bin.js` is a
* single ~19 MB ESM bundle: ~1.0 s per spawn on a warm developer box, 2-3 s on a
* shared GitHub runner. This test used to do three of those boots back to back inside
* one `it`, so it needed ~3.1 s locally against Vitest's 5000 ms default and blew that
* default on the shard runner (this lane's exact "Test timed out in 5000ms"). Nothing
* hung; three serial cold boots simply did not fit one test's budget.
*
* The cost is structural, so the seam changes rather than the budget: raising testTimeout
* is refused by scripts/check-no-test-timeout-appeasement.mjs and would hide a real
* regression, and no product seam is involved (the guide is rendered in-process by
* design). The two independent invocations are now awaited TOGETHER, so their boots
* overlap instead of summing, and the error-path boot moved to its own test. Wall clock
* is now one boot rather than three, and the assertions are unchanged: the guide must
* still come from the built entry point, and its embedded version must still be the
* version that same built binary reports for --version.
*/
it("prints a guide and version from the same built CLI entry point", async () => {
const guide = await execFile(process.execPath, [builtCli, "skills", "get", "computer-use"], { cwd: cliRoot });
const version = await execFile(process.execPath, [builtCli, "--version"], { cwd: cliRoot });
const [guide, version] = await Promise.all([
execFile(process.execPath, [builtCli, "skills", "get", "computer-use"], { cwd: cliRoot }),
execFile(process.execPath, [builtCli, "--version"], { cwd: cliRoot }),
]);
for (const heading of COMPUTER_USE_GUIDE_HEADINGS) expect(guide.stdout).toContain(heading);
expect(guide.stdout).toContain(`# Fusion computer-use guide (v${version.stdout.trim()})`);
});

it("rejects an unknown skill from the built CLI entry point", async () => {
await expect(execFile(process.execPath, [builtCli, "skills", "get", "definitely-not-a-skill"], { cwd: cliRoot }))
.rejects.toMatchObject({ code: 1, stderr: expect.stringContaining("computer-use") });
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,9 @@ describeIfGit("aiMergeTask finalize no-op unproven reproduction (real git)", ()

expect(result.merged).toBe(true);
expect(result.noOp).toBe(true);
expect(store.moveTask).toHaveBeenCalledWith("FN-NO-COMMITS-DONE", "done");
expect(store.moveTask).toHaveBeenCalledWith("FN-NO-COMMITS-DONE", "done", {
workflowMoveSource: "merger-complete-task",
});
}, 20_000);

it("FN-213: clears a removed worktree pointer while retaining an operator branch", async () => {
Expand Down Expand Up @@ -435,7 +437,9 @@ describeIfGit("aiMergeTask finalize no-op unproven reproduction (real git)", ()

expect(result.merged).toBe(true);
expect(result.noOp).toBe(true);
expect(store.moveTask).toHaveBeenCalledWith("FN-EMPTY-DONE", "done");
expect(store.moveTask).toHaveBeenCalledWith("FN-EMPTY-DONE", "done", {
workflowMoveSource: "merger-complete-task",
});
}, 20_000);

it("blocks FN-4653 shape: foreign start-point branch with no FN-owned commits", async () => {
Expand Down
24 changes: 21 additions & 3 deletions packages/engine/src/__tests__/merger-merge-details.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,21 @@ describe("aiMergeTask — agent log persistence", () => {

await aiMergeTask(store, "/tmp/root", "FN-050");

expect(store.appendAgentLog).toHaveBeenCalledWith("FN-050", "Bash", "tool", undefined, "merger");
/*
FNXC:MergerAgentLogProvenance 2026-09-25-09:40 (FUSI-020):
`appendAgentLog`'s 4th parameter is `summarizeToolArgs(name, args)`. This
agent emits one string `command` arg, so the summary is that command
verbatim. The old `undefined` assertion encoded the pre-provenance call
shape; assert the real value, not `expect.any(String)`, so a summarizer
change cannot silently alter what the agent log records.
*/
expect(store.appendAgentLog).toHaveBeenCalledWith(
"FN-050",
"Bash",
"tool",
"git status",
"merger",
);
});

it("still fires onAgentText callback alongside logging", async () => {
Expand Down Expand Up @@ -1129,8 +1143,12 @@ describe("aiMergeTask — merge details collection", () => {
);
expect(mergeDetailsCall).toBeUndefined();

// Task should still be moved to done
expect(store.moveTask).toHaveBeenCalledWith("FN-050", "done");
// Task should still be moved to done. `moveTask` now carries a third
// `{ workflowMoveSource }` provenance argument, so assert the audit marker
// rather than the pre-provenance 2-argument call shape.
expect(store.moveTask).toHaveBeenCalledWith("FN-050", "done", {
workflowMoveSource: "merger-complete-task",
});
});

it("handles missing shortstat gracefully when show --shortstat fails", async () => {
Expand Down
5 changes: 4 additions & 1 deletion packages/engine/src/__tests__/merger-skills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,10 @@ describe("aiMergeTask — skill selection non-fatal diagnostics (FN-1510/FN-1511
});

expect(result.merged).toBe(true);
expect(store.moveTask).toHaveBeenCalledWith("FN-050", "done");
// `moveTask` carries a third `{ workflowMoveSource }` provenance argument.
expect(store.moveTask).toHaveBeenCalledWith("FN-050", "done", {
workflowMoveSource: "merger-complete-task",
});
});

it("records skill source in context result for debugging", async () => {
Expand Down
39 changes: 32 additions & 7 deletions packages/engine/src/__tests__/merger-verification.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,9 @@ describe("aiMergeTask — build verification", () => {
const result = await aiMergeTask(store, "/tmp/root", "FN-050");

expect(result.merged).toBe(true);
expect(store.moveTask).toHaveBeenCalledWith("FN-050", "done");
expect(store.moveTask).toHaveBeenCalledWith("FN-050", "done", {
workflowMoveSource: "merger-complete-task",
});
});

it("merge aborts when build fails via fn_report_build_failure tool", async () => {
Expand Down Expand Up @@ -559,7 +561,9 @@ describe("aiMergeTask — build verification", () => {
const result = await aiMergeTask(store, "/tmp/root", "FN-050");

expect(result.merged).toBe(true);
expect(store.moveTask).toHaveBeenCalledWith("FN-050", "done");
expect(store.moveTask).toHaveBeenCalledWith("FN-050", "done", {
workflowMoveSource: "merger-complete-task",
});
});

it("merge proceeds when buildCommand is empty string (treated as undefined)", async () => {
Expand All @@ -583,7 +587,9 @@ describe("aiMergeTask — build verification", () => {
const result = await aiMergeTask(store, "/tmp/root", "FN-050");

expect(result.merged).toBe(true);
expect(store.moveTask).toHaveBeenCalledWith("FN-050", "done");
expect(store.moveTask).toHaveBeenCalledWith("FN-050", "done", {
workflowMoveSource: "merger-complete-task",
});
});

function setupDependencySyncVerificationScenario({
Expand Down Expand Up @@ -1108,7 +1114,9 @@ describe("aiMergeTask — deterministic merge verification", () => {
const result = await aiMergeTask(store, "/tmp/root", "FN-050");

expect(result.merged).toBe(true);
expect(store.moveTask).toHaveBeenCalledWith("FN-050", "done");
expect(store.moveTask).toHaveBeenCalledWith("FN-050", "done", {
workflowMoveSource: "merger-complete-task",
});
expect(store.logEntry).toHaveBeenCalledWith(
"FN-050",
expect.stringMatching(/^\[timing\] \[verification\] test command succeeded \(exit 0(?:, output exceeded buffer)?\) in \d+ms$/),
Expand Down Expand Up @@ -1909,7 +1917,9 @@ describe("aiMergeTask — inferred test command execution", () => {
await aiMergeTask(store, "/tmp/root", "FN-050");

expect(verificationCalls).toContain("pnpm test");
expect(store.moveTask).toHaveBeenCalledWith("FN-050", "done");
expect(store.moveTask).toHaveBeenCalledWith("FN-050", "done", {
workflowMoveSource: "merger-complete-task",
});
});

it("logs that test command was inferred from project files", async () => {
Expand Down Expand Up @@ -2086,7 +2096,9 @@ describe("aiMergeTask — inferred test command execution", () => {
expect(verificationCalls).toHaveLength(0);
// Merge should still succeed
expect(result.merged).toBe(true);
expect(store.moveTask).toHaveBeenCalledWith("FN-050", "done");
expect(store.moveTask).toHaveBeenCalledWith("FN-050", "done", {
workflowMoveSource: "merger-complete-task",
});
});
});

Expand Down Expand Up @@ -2367,7 +2379,20 @@ describe("aiMergeTask — in-merge verification fix", () => {
expect(capturedFixOptions.onToolStart).toBeTypeOf("function");
expect(capturedFixOptions.onToolEnd).toBeTypeOf("function");

expect(store.appendAgentLog).toHaveBeenCalledWith("FN-050", "Bash", "tool", undefined, "merger");
/*
FNXC:MergerAgentLogProvenance 2026-09-25-09:40 (FUSI-020):
`appendAgentLog`'s 4th argument is `summarizeToolArgs(name, args)`. The fix
agent emits a single string `command` arg, so the summary is that command
verbatim — assert the real value, not `expect.any(String)`, so a future
change to the summarizer cannot silently alter what the log records.
*/
expect(store.appendAgentLog).toHaveBeenCalledWith(
"FN-050",
"Bash",
"tool",
"vitest run",
"merger",
);

const logMessages = (store.logEntry as ReturnType<typeof vi.fn>).mock.calls
.map((call: any[]) => call[1])
Expand Down
Loading
Loading