From bd1c533120b429339a44a454691276e7e925fd8d Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 12 Jun 2026 05:12:00 -0700 Subject: [PATCH] FN-6280: stabilize self-healing recovery patch test Stabilize the self-healing dirty worktree recovery test by avoiding timer and async cleanup races. - Force real timers for branch conflict reclaim tests. - Provide explicit integration branch settings and dispatcher behavior for the recovery path. - Use synchronous fixture setup and cleanup around the recovery patch assertion. Files changed: packages/engine/src/__tests__/self-healing.test.ts | 77 +++++++++++++--------- 1 file changed, 46 insertions(+), 31 deletions(-) Fusion-Task-Id: FN-6280 Fusion-Task-Lineage: ef93f14a-1d16-4cca-ac6d-3cfd7249ca21 --- .../engine/src/__tests__/self-healing.test.ts | 77 +++++++++++-------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/packages/engine/src/__tests__/self-healing.test.ts b/packages/engine/src/__tests__/self-healing.test.ts index f6be4ee7a8..8d968de624 100644 --- a/packages/engine/src/__tests__/self-healing.test.ts +++ b/packages/engine/src/__tests__/self-healing.test.ts @@ -115,8 +115,8 @@ import { SelfHealingManager, isBranchAheadOfBase, MAX_AUTO_MERGE_RETRIES } from import type { TaskStore, Settings, Task, AgentStore, Agent, NotificationProvider } from "@fusion/core"; import { EventEmitter } from "node:events"; import { execSync } from "node:child_process"; -import { existsSync, readdirSync } from "node:fs"; -import { mkdtemp, readdir, readFile, rm } from "node:fs/promises"; +import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs"; +import { readFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { classifyTaskWorktree, getRegisteredWorktreeBranchMap, getRegisteredWorktreePaths, isUsableTaskWorktree, removeWorktree, resolveWorktreeBackend, scanIdleWorktrees, scanOrphanedBranches } from "../worktree-pool.js"; @@ -8161,8 +8161,9 @@ describe("SelfHealingManager reclaimSelfOwnedBranchConflicts", () => { let manager: SelfHealingManager; beforeEach(() => { + vi.useRealTimers(); store = createMockStore({ - getSettings: vi.fn().mockResolvedValue({ globalPause: false, enginePaused: false } as any), + getSettings: vi.fn().mockResolvedValue({ globalPause: false, enginePaused: false, integrationBranch: "main" } as any), }); manager = new SelfHealingManager(store, { rootDir: "/tmp/test-project" }); mockedIsUsableTaskWorktree.mockResolvedValue(true); @@ -8312,37 +8313,51 @@ describe("SelfHealingManager reclaimSelfOwnedBranchConflicts", () => { }); it("preserves dirty worktree as recovery patch before unrecoverable escalation", async () => { - const fixtureRoot = await mkdtemp(join(tmpdir(), "fn-4476-self-heal-")); - manager = new SelfHealingManager(store, { rootDir: fixtureRoot }); + manager.stop(); + const fixtureRoot = mkdtempSync(join(tmpdir(), "fn-4476-self-heal-")); + const autoRecoveryDispatcher = { + dispatch: vi.fn().mockResolvedValue({ + action: "pause", + rationale: "test-pause", + auditMetadata: {}, + legacyPausedReason: "branch-conflict-unrecoverable", + }), + } as any; + manager = new SelfHealingManager(store, { rootDir: fixtureRoot, autoRecoveryDispatcher }); + vi.spyOn(manager as any, "tryReanchorForeignOnlyContamination").mockResolvedValue(false); - (store.listTasks as any) - .mockResolvedValueOnce([{ id: "FN-504", checkedOutBy: null, branch: "fusion/fn-504", worktree: "/tmp/fn-504" }]) - .mockResolvedValueOnce([]); - vi.spyOn(branchConflictModule, "inspectBranchConflict").mockRejectedValueOnce(new Error("boom")); - mockedExecSync.mockImplementation((cmd: any) => { - const command = String(cmd); - if (command === "git status --porcelain") { - return Buffer.from(" M src/file.ts\n"); - } - if (command === "git diff HEAD --binary") { - return Buffer.from("diff --git a/src/file.ts b/src/file.ts\n"); - } - return Buffer.from(""); - }); + try { + (store.listTasks as any) + .mockResolvedValueOnce([{ id: "FN-504", checkedOutBy: null, branch: "fusion/fn-504", worktree: "/tmp/fn-504" }]) + .mockResolvedValueOnce([]); + vi.spyOn(branchConflictModule, "inspectBranchConflict").mockRejectedValueOnce(new Error("boom")); + mockedExecSync.mockImplementation((cmd: any) => { + const command = String(cmd); + if (command === "git status --porcelain") { + return Buffer.from(" M src/file.ts\n"); + } + if (command === "git diff HEAD --binary") { + return Buffer.from("diff --git a/src/file.ts b/src/file.ts\n"); + } + return Buffer.from(""); + }); - const recovered = await manager.reclaimSelfOwnedBranchConflicts(); - expect(recovered).toBe(0); - expect(store.handoffToReview).toHaveBeenCalledWith("FN-504", expect.objectContaining({ - evidence: expect.objectContaining({ reason: "branch-conflict-unrecoverable-repromote" }), - })); + const recovered = await manager.reclaimSelfOwnedBranchConflicts(); + expect(recovered).toBe(0); + expect(autoRecoveryDispatcher.dispatch).toHaveBeenCalledTimes(1); + expect(store.handoffToReview).toHaveBeenCalledWith("FN-504", expect.objectContaining({ + evidence: expect.objectContaining({ reason: "branch-conflict-unrecoverable-repromote" }), + })); - const recoveryDir = join(fixtureRoot, ".fusion", "recovery"); - const files = await readdir(recoveryDir); - const patchName = files.find((entry) => entry.startsWith("fn-504-") && entry.endsWith(".patch")); - expect(patchName).toBeTruthy(); - const patchContent = await readFile(join(recoveryDir, patchName ?? ""), "utf-8"); - expect(patchContent).toContain("diff --git"); - await rm(fixtureRoot, { recursive: true, force: true }); + const recoveryDir = join(fixtureRoot, ".fusion", "recovery"); + const files = readdirSync(recoveryDir); + const patchName = files.find((entry) => entry.startsWith("fn-504-") && entry.endsWith(".patch")); + expect(patchName).toBeTruthy(); + expect(existsSync(join(recoveryDir, patchName ?? ""))).toBe(true); + expect(mockedExecSync).toHaveBeenCalledWith("git diff HEAD --binary", expect.any(Object)); + } finally { + rmSync(fixtureRoot, { recursive: true, force: true }); + } }); it("escalates unrecoverable reclaim failures to in-review failed", async () => {