test(U7): rescue the orphan-triage regression test — main has the fix but not its test (#2663)

Main already carries **every other artifact** from #2593 — the
provenance fix, the `DELIBERATE-LITERAL` markers in
`TaskCard`/`TaskDetailModal`/`register-routes`, the audit doc. The one
thing missing is the test.

That is the same artifact class that vanished when #2645's branch was
force-pushed, so I rebased #2593 onto current main, found every commit
conflicting because the work had landed by other routes, and rescued the
one piece that had not.

**#2593 can now be closed** — it carries nothing else main lacks.
**#2654 needs rebasing onto main** rather than stacking on it.

## What makes this test worth rescuing

It took three attempts to write honestly, and the reason is pinned in
the test body: on a bare mock, `resolvePlannerLanes` reads
`resolveTaskWorkflowIrSync`, which the mock does not define, so it
returns `LEGACY_PLANNER_LANES` (`intake: "triage"`) and a `triage` card
matches the **first** arm — the orphan arm is never reached. Every
earlier fixture I wrote passed through that short-circuit and proved
nothing.

All three cases stub that reader with the merged default (`intake:
"todo"`), which is what production resolves, leaving the orphan arm as
the only thing deciding. They differ **only** in the workflow readers.

| case | role |
|---|---|
| **C** — workflow declares `triage` as a review lane | **the
discriminator.** Pre-fix, the sync reader ignores the selection, returns
the default IR declaring no `triage`, so the arm fires and a card is
finalized out of a custom workflow's code-review column |
| **B** — workflow resolves, declares no `triage` | positive control;
without it "returns false" is unfalsifiable |
| **A** — workflow unresolvable | **behavior pin, NOT a regression
test** — passes in both worlds |

I had A labelled "REGRESSION" until the mutation said otherwise. It is
relabelled with the null result documented, because a future edit making
it flip would mean the arm's scope changed.

## Verification, stated precisely

**231/231** against main's implementation.

The mutation that proved C discriminates was run on the branch where the
pre-fix code still compiled. **It cannot be re-run against main**: the
`WorkflowIr` type import was removed along with the fix, so a naive
revert no longer transforms. I am stating that rather than implying I
re-verified it here — the discrimination was demonstrated, just not on
this base.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-30 00:37:20 -07:00
committed by GitHub
parent efbbc45eb0
commit b6b2fdcdc6

View File

@@ -7225,3 +7225,116 @@ describe("TriageProcessor.sweepStalePlanningStatuses", () => {
expect(store.updateTask).not.toHaveBeenCalled();
});
});
/*
FNXC:RecoverApprovedIntakePostU11 2026-07-30-00:40 (the test PR #2593's review asked for):
WHY THIS TOOK THREE ATTEMPTS TO WRITE HONESTLY. The guard under test is the ORPHAN arm of
`inPlannerColumn` — `task.column === "triage" && !declaresLegacyTriage`. On a bare mock store the arm
is NEVER REACHED: `resolvePlannerLanes` reads `resolveTaskWorkflowIrSync`, which a bare mock does not
define, so it returns LEGACY_PLANNER_LANES (`intake: "triage"`) and a `triage` card matches the FIRST
arm. Every earlier fixture I wrote passed through that short-circuit and proved nothing.
So all three cases below stub `resolveTaskWorkflowIrSync` with the MERGED DEFAULT shape, which is what
production resolves: `intake` and `hold` both `todo`. That makes the first arm fail for a `triage` card
and leaves the orphan arm as the only thing deciding the outcome.
The three cases differ ONLY in the workflow readers, so the outcome difference can have no other cause.
Case B is the positive control: without it, "returns false" is unfalsifiable — every case would pass if
the arm were dead.
*/
describe("recoverApprovedTask — the orphan-`triage` arm, with the intake short-circuit disabled", () => {
const MERGED_DEFAULT = {
version: "v2", id: "builtin:coding", nodes: [], edges: [],
columns: [
{ id: "todo", name: "Planning", traits: [{ trait: "intake" }, { trait: "hold", config: { release: "capacity" } }] },
{ id: "in-progress", name: "in-progress", traits: [{ trait: "wip", config: { limitSetting: "maxConcurrent" } }] },
{ id: "done", name: "done", traits: [{ trait: "complete" }] },
],
} as never;
const customIr = (id: string, withTriage: boolean) => ({
version: "v2", id, nodes: [], edges: [],
columns: [
{ id: "todo", name: "Planning", traits: [{ trait: "intake" }, { trait: "hold", config: { release: "capacity" } }] },
...(withTriage ? [{ id: "triage", name: "Code review", traits: [{ trait: "review" }] }] : []),
{ id: "in-progress", name: "in-progress", traits: [{ trait: "wip", config: { limitSetting: "maxConcurrent" } }] },
{ id: "done", name: "done", traits: [{ trait: "complete" }] },
],
} as never);
const PLAN = "## Objective\nDo the thing.\n\n## Steps\n1. Step one\n";
let root = "";
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), "fusion-orphan-triage-"));
await mkdir(join(root, ".fusion", "tasks", "FN-ORPHAN"), { recursive: true });
await writeFile(join(root, ".fusion", "tasks", "FN-ORPHAN", "PROMPT.md"), PLAN);
});
afterEach(async () => { await rm(root, { recursive: true, force: true }); });
/** Identical in every case except the workflow readers passed in. */
const run = async (workflowReaders: Partial<TaskStore>): Promise<boolean> => {
const store = createMockStore({
getSettings: vi.fn().mockResolvedValue({
maxConcurrent: 2, maxWorktrees: 4, pollIntervalMs: 10000,
groupOverlappingFiles: false, autoMerge: true, requirePlanApproval: true,
} as Settings),
// Production resolves the MERGED default here, so `lanes.intake` is `todo`, not `triage`.
resolveTaskWorkflowIrSync: vi.fn(() => MERGED_DEFAULT),
...workflowReaders,
} as Partial<TaskStore>);
return new TriageProcessor(store, root).recoverApprovedTask({
id: "FN-ORPHAN",
description: "Orphaned triage row",
column: "triage",
status: "planning",
approvedPlanFingerprint: computePlanApprovalFingerprint(PLAN),
dependencies: [], steps: [], currentStep: 0,
log: [{ timestamp: "2026-01-01T00:00:00.000Z", action: "Spec review: APPROVE" }],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:02:00.000Z",
} as never);
};
it("B (POSITIVE CONTROL) recovers the orphan when the workflow RESOLVES and declares no `triage`", async () => {
// Proves the arm is reachable and returns true. Without this the two false cases below are
// unfalsifiable.
expect(await run({
getTaskWorkflowSelectionAsync: vi.fn(async () => ({ workflowId: "custom:no-triage", stepIds: [] })),
getWorkflowDefinition: vi.fn(async () => ({ ir: customIr("custom:no-triage", false) })),
} as Partial<TaskStore>)).toBe(true);
});
it("A (BEHAVIOR PIN — does NOT discriminate) declines when the workflow cannot be resolved", async () => {
/*
HONEST LABEL, from the mutation run. I wrote this as THE regression case and it is not: reverting
to the pre-fix sync read leaves it GREEN. It returns false in both worlds, so something other
than `declaresLegacyTriage` decides it — most likely a later gate that needs the selection this
case deliberately withholds. Kept as a fail-closed behavior pin, explicitly NOT as proof of the
fix; case C below is what actually discriminates.
Left in with the wrong-looking result documented rather than deleted, because a future edit that
makes this case flip would be informative — it would mean the orphan arm had become reachable
here, which is a real change in the guard's scope.
*/
expect(await run({
getTaskWorkflowSelectionAsync: undefined,
getTaskWorkflowSelection: vi.fn(() => undefined),
} as Partial<TaskStore>)).toBe(false);
});
it("C (THE REGRESSION) declines when the workflow DECLARES `triage` as a non-intake lane", async () => {
/*
THE DISCRIMINATOR, confirmed by mutation: reverting `recoverApprovedTask` to the pre-fix
`resolveTaskWorkflowIrSync` read makes THIS case fail (and only this one, of the three). The sync
reader ignores the selection and hands back the default IR, which declares no `triage`, so the
orphan arm fires and the card is finalized out of a custom workflow's CODE REVIEW column —
bypassing that column's transition. This is the greptile P1 case, and it is the one that proves
the provenance fix does work.
*/
expect(await run({
getTaskWorkflowSelectionAsync: vi.fn(async () => ({ workflowId: "custom:triage-review", stepIds: [] })),
getWorkflowDefinition: vi.fn(async () => ({ ir: customIr("custom:triage-review", true) })),
} as Partial<TaskStore>)).toBe(false);
});
});