From d09a8569414f5e0d535dd2662d2b8ec72391b1cd Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 31 Jul 2026 05:49:59 -0700 Subject: [PATCH] test(engine): pin the FN-5256 liveness guard on a renamed board (the sweep that clears a live task's worktree) (#3132) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Top item from the verified coverage map on #3115. `reconcileTaskWorktreeMetadata` had **three** uncovered resolvers — the most of any sweep in the file — and it is the one that nulls `worktree`/`branch`/`sessionFile` on a live row. ## Why this sweep first Its own header names FN-5256: the incident where clearing worktree metadata yanked a checkout out from under a running shell. The guard that prevents it is `scopeOverrideMergeActiveSafe`, and that guard is exactly what the wip/review resolvers feed. The existing guard test uses `column: "in-progress"` — **the literal**. So blinding `worktreeReconcileWipColumns` back to `["in-progress"]` leaves all 825 self-healing tests green. The guard is converted; nothing in the suite could tell. On a renamed board the pre-conversion form matched nothing, `scopeOverrideMergeActiveSafe` became true for a card an executor was actively running, and the sweep cleared its metadata. ## The case The renamed twin of the existing FN-5256 test: a `scopeOverride` task live in a **renamed wip lane** keeps its metadata. Same shape, same assertions, different vocabulary — which is the whole point, since the original passes either way. **Measured:** 414 pass; blinding `worktreeReconcileWipColumns` to the legacy id fails **exactly this test**. ## Remaining from the map 25 uncovered resolvers left. Next by risk: `reclaimStaleActiveBranches` (deletes branches) and `reconcileInReviewBranchRebind` (rebinds branches of live cards) — both need a git-shelling harness, so they are slower to pin than this one was. Then the two `reconcileDependencyBlockingLeases` resolvers. I will keep working down that list. The map is on #3115 with verified names; anyone can pick an entry and check it the same way — blind one resolver, run `vitest run src/__tests__/self-healing`, and if it stays green that conversion has nothing behind it. ## Verification `self-healing.test.ts` **414 passed** · `pnpm test:gate` 13 + 161 + 487 + 71 · lint — green. --- .../engine/src/__tests__/self-healing.test.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/engine/src/__tests__/self-healing.test.ts b/packages/engine/src/__tests__/self-healing.test.ts index bd3015c99f..0cadf74bcf 100644 --- a/packages/engine/src/__tests__/self-healing.test.ts +++ b/packages/engine/src/__tests__/self-healing.test.ts @@ -4089,6 +4089,56 @@ describe("SelfHealingManager", () => { managerWithRecovery.stop(); }); + /* + FNXC:WorkflowResolvedColumns 2026-07-31-21:30: + THE FN-5256 GUARD HAD NO RENAMED-BOARD CASE, which is why blinding this sweep's wip resolver back + to `["in-progress"]` leaves all 825 self-healing tests green: the guard test directly above uses + that literal, so it cannot tell the conversion from the id it replaced. + + What that costs on a renamed board: the guard matches nothing, `scopeOverrideMergeActiveSafe` + becomes true for a card an executor is actively running, and this sweep nulls its + `worktree`/`branch`/`sessionFile` — yanking the checkout out from under a live shell. FN-5256 is + the incident that guard exists to prevent. + */ + it("does NOT clear worktree metadata for a scopeOverride task live in a RENAMED wip lane (FN-5256)", async () => { + const managerWithRecovery = new SelfHealingManager(store, { rootDir: "/tmp/test-project" }); + mockedExistsSync.mockReturnValue(false); + mockedGetRegisteredWorktreeBranchMap.mockResolvedValue(new Map()); + (store as unknown as { listWorkflowDefinitions: unknown }).listWorkflowDefinitions = vi.fn(async () => [{ + ir: { + version: "v2", + id: "custom:renamed", + nodes: [], + edges: [], + columns: [ + { id: "building", name: "building", traits: [{ trait: "wip", config: { limitSetting: "maxConcurrent" } }] }, + { id: "checking", name: "checking", traits: [{ trait: "merge" }] }, + ], + }, + }]); + (store.listTasks as ReturnType).mockResolvedValue([ + { + id: "FN-RENAMED-LIVE", + column: "building", + paused: false, + status: null, + scopeOverride: true, + worktree: "/tmp/project/.worktrees/fn-renamed-live", + branch: "fusion/FN-RENAMED-LIVE", + sessionFile: "/tmp/project/.fusion/sessions/fn-renamed-live.json", + steps: [{ status: "in-progress" }], + log: [], + }, + ]); + + const result = await managerWithRecovery.reconcileTaskWorktreeMetadata(); + + expect(result).toBe(0); + /* The live checkout survives: nothing nulled the worktree out from under the executor. */ + expect(store.updateTask).not.toHaveBeenCalled(); + managerWithRecovery.stop(); + }); + it("does NOT clear worktree metadata for a scopeOverride in-review task mid-step (status: null)", async () => { const managerWithRecovery = new SelfHealingManager(store, { rootDir: "/tmp/test-project" }); mockedExistsSync.mockReturnValue(false);