From b6b2fdcdc6950830b39342f191241460bc150760 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 30 Jul 2026 00:37:20 -0700 Subject: [PATCH] =?UTF-8?q?test(U7):=20rescue=20the=20orphan-`triage`=20re?= =?UTF-8?q?gression=20test=20=E2=80=94=20main=20has=20the=20fix=20but=20no?= =?UTF-8?q?t=20its=20test=20(#2663)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- packages/engine/src/__tests__/triage.test.ts | 113 +++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/packages/engine/src/__tests__/triage.test.ts b/packages/engine/src/__tests__/triage.test.ts index b4f8e01dea..eefde72172 100644 --- a/packages/engine/src/__tests__/triage.test.ts +++ b/packages/engine/src/__tests__/triage.test.ts @@ -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): Promise => { + 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); + 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)).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)).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)).toBe(false); + }); +});