diff --git a/packages/engine/src/__tests__/self-healing-foreign-only-contamination.test.ts b/packages/engine/src/__tests__/self-healing-foreign-only-contamination.test.ts index 06d6626ef9..ce919acf1e 100644 --- a/packages/engine/src/__tests__/self-healing-foreign-only-contamination.test.ts +++ b/packages/engine/src/__tests__/self-healing-foreign-only-contamination.test.ts @@ -47,6 +47,52 @@ describe("SelfHealingManager.recoverForeignOnlyContaminatedInReviewTasks", () => vi.clearAllMocks(); }); + /* + FNXC:WorkflowResolvedColumns 2026-07-31-18:50: + `contaminationWipColumns` was UNCOVERED on the #3115 map. Every case in this file seeds the + candidate in `in-review`, so only the review bucket is exercised and blinding the WIP resolver back + to `["in-progress"]` leaves the file green. + + The WIP bucket is a real source of candidates: a card sent back for a fix re-enters execution while + its branch still carries the foreign commits, so contamination is discovered there as often as in + review. Keyed on the id that bucket read nothing on a renamed board, and the card kept a branch + built on someone else's work — which is what this sweep exists to re-anchor. + */ + it("recovers a foreign-only candidate resting in a RENAMED wip lane", async () => { + store.listTasks.mockImplementation(async ({ column }: { column: string }) => ( + column === "building" + ? [mkTask({ id: "FN-WIP", column: "building", paused: true, pausedReason: "branch-cross-contamination" })] + : [] + )); + const RENAMED_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" }] }, + ], + }; + /* + The per-task readers are REQUIRED, not incidental. This sweep is an ACTION site: it deliberately + SKIPS a card whose own board cannot be read rather than guessing from the project union, so a fake + with only `listWorkflowDefinitions` resolves the default IR, the card is reported unresolvable, and + the case would fail for a reason that has nothing to do with the resolver under test. + */ + store.listWorkflowDefinitions = vi.fn(async () => [{ id: "custom:renamed", ir: RENAMED_IR }]); + store.getTaskWorkflowSelection = vi.fn(() => ({ workflowId: "custom:renamed", stepIds: [] })); + store.getTaskWorkflowSelectionAsync = vi.fn(async () => ({ workflowId: "custom:renamed", stepIds: [] })); + store.getWorkflowDefinition = vi.fn(async () => ({ ir: RENAMED_IR })); + mocked.classifyForeignOnlyContamination.mockResolvedValue({ kind: "foreign-only-no-own-work" }); + mocked.recoverForeignOnlyContamination.mockResolvedValue({ recovered: true, subtype: "reanchor" }); + + const manager = new SelfHealingManager(store, { rootDir: process.cwd() }); + + expect(await manager.recoverForeignOnlyContaminatedInReviewTasks()).toBe(1); + expect(mocked.recoverForeignOnlyContamination).toHaveBeenCalledOnce(); + }); + it("recovers foreign-only in-review candidates", async () => { store.listTasks.mockImplementation(async ({ column }: { column: string }) => column === "in-review" ? [mkTask()] : []); mocked.classifyForeignOnlyContamination.mockResolvedValue({ kind: "foreign-only-no-own-work" });