feat(FN-4742): complete Step 4 — migrate pool and self-healing removals

Fusion-Task-Id: FN-4742
Fusion-Task-Lineage: 59bca56e-c9d9-4066-9f5a-6d8ee252a100
This commit is contained in:
Fusion (runfusion.ai)
2026-05-16 09:25:18 -07:00
committed by gsxdsm
parent 5104553cfc
commit e2de8fed6d
4 changed files with 69 additions and 42 deletions

View File

@@ -113,7 +113,7 @@ describe("self-healing completion fan-out", () => {
const first = await mgr.reconcileCompletedTask("FN-B", { worktreeHint: "/wt/fn-b" });
expect(first.worktreeRemoved).toBe(true);
expect(execMock.mock.calls.some((c) => String(c[0]).includes("git worktree remove --force '/wt/fn-b'"))).toBe(true);
expect(execMock.mock.calls.some((c) => String(c[0]).includes("git worktree remove --force") && String(c[0]).includes("/wt/fn-b"))).toBe(true);
existsSyncMock.mockReturnValue(false);
execMock.mockImplementation((cmd: string, _opts: unknown, cb: (err: unknown, stdout: string, stderr: string) => void) => {
@@ -122,7 +122,7 @@ describe("self-healing completion fan-out", () => {
});
const second = await mgr.reconcileCompletedTask("FN-B");
expect(second.worktreeRemoved).toBe(false);
const rmCalls = execMock.mock.calls.filter((c) => String(c[0]).includes("git worktree remove --force"));
const rmCalls = execMock.mock.calls.filter((c) => String(c[0]).includes("git worktree remove --force") && String(c[0]).includes("/wt/fn-b"));
expect(rmCalls).toHaveLength(1);
});
@@ -138,7 +138,7 @@ describe("self-healing completion fan-out", () => {
const mgr = new SelfHealingManager(store, { rootDir: "/repo" });
vi.spyOn(mgr as any, "findWorktreePathForBranch").mockResolvedValue("/wt/fn-c");
const out = await mgr.reconcileCompletedTask("FN-C");
expect(execMock.mock.calls.some((c) => String(c[0]).includes("git worktree remove --force '/wt/fn-c'"))).toBe(true);
expect(execMock.mock.calls.some((c) => String(c[0]).includes("git worktree remove --force") && String(c[0]).includes("/wt/fn-c"))).toBe(true);
expect(out.branchRemoved).toBe(false);
expect(execMock.mock.calls.some((c) => String(c[0]).includes("git branch -D"))).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining("skip deletion"));

View File

@@ -55,6 +55,7 @@ vi.mock("../worktree-pool.js", () => ({
cleanupOrphanedWorktrees: vi.fn().mockResolvedValue(0),
scanOrphanedBranches: vi.fn().mockResolvedValue([]),
isUsableTaskWorktree: vi.fn().mockResolvedValue(true),
removeWorktree: vi.fn().mockResolvedValue(undefined),
resolveWorktreeBackend: vi.fn(),
}));
@@ -83,7 +84,7 @@ import { existsSync, readdirSync } from "node:fs";
import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { isUsableTaskWorktree, resolveWorktreeBackend, scanIdleWorktrees, scanOrphanedBranches } from "../worktree-pool.js";
import { isUsableTaskWorktree, removeWorktree, resolveWorktreeBackend, scanIdleWorktrees, scanOrphanedBranches } from "../worktree-pool.js";
import * as branchConflictModule from "../branch-conflicts.js";
import { createLogger } from "../logger.js";
import { NotificationService } from "../notification/notification-service.js";
@@ -93,6 +94,7 @@ const mockedExecSync = vi.mocked(execSync);
const mockedExistsSync = vi.mocked(existsSync);
const mockedScanOrphanedBranches = vi.mocked(scanOrphanedBranches);
const mockedIsUsableTaskWorktree = vi.mocked(isUsableTaskWorktree);
const mockedRemoveWorktree = vi.mocked(removeWorktree);
const mockedResolveWorktreeBackend = vi.mocked(resolveWorktreeBackend);
const mockedScanIdleWorktrees = vi.mocked(scanIdleWorktrees);
const mockedReaddirSync = vi.mocked(readdirSync);
@@ -152,6 +154,7 @@ describe("SelfHealingManager", () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
store = createMockStore();
manager = new SelfHealingManager(store, { rootDir: "/tmp/test-project" });
mockedRemoveWorktree.mockResolvedValue(undefined);
mockedClassifyOwnedLandedEvidence.mockResolvedValue({ kind: "proven-no-op", baseRef: "main", ownDiffEmpty: true });
});
@@ -1251,10 +1254,8 @@ describe("SelfHealingManager", () => {
mockedExistsSync.mockReset();
mockedExistsSync.mockReturnValueOnce(true);
mockedExecSync.mockReset();
mockedExecSync.mockImplementationOnce(() => {
throw new Error("cannot remove worktree");
});
mockedRemoveWorktree.mockReset();
mockedRemoveWorktree.mockRejectedValueOnce(new Error("cannot remove worktree"));
await (manager as any).cleanupInterruptedMergeArtifacts(task);
@@ -1264,7 +1265,7 @@ describe("SelfHealingManager", () => {
),
);
mockedExecSync.mockClear();
mockedRemoveWorktree.mockClear();
mockedExistsSync.mockReset();
});
@@ -3803,9 +3804,10 @@ describe("SelfHealingManager", () => {
mergeDetails: expect.objectContaining({ commitSha: "abc12345", mergeConfirmed: true }),
}));
expect(store.updateTask).toHaveBeenCalledWith("FN-dep", { blockedBy: null });
expect(
mockedExecSync.mock.calls.some((call) => String(call[0]).includes("git worktree remove '/tmp/wt' --force")),
).toBe(true);
expect(mockedRemoveWorktree).toHaveBeenCalledWith(expect.objectContaining({
rootDir: "/tmp/test-project",
worktreePath: "/tmp/wt",
}));
expect(getSelfHealingLogger().log).toHaveBeenCalledWith(expect.stringContaining("self-heal:deadlock-recovered"));
managerWithRecovery.stop();