From 7d20a997b964b7a6c5e20bc1e3a17ce8c9ee99ad Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 2 Jun 2026 07:46:38 -0700 Subject: [PATCH] FN-5872: clear active-session registry on PR cleanup Ensure PR-mode merge cleanup removes stale active-session registry state before deleting worktrees. - unregister the task worktree from the active-session registry during merged-task artifact cleanup - export the active-session registry from the engine entrypoint for lifecycle cleanup usage - add CLI and engine regression coverage for registry cleanup and missing-entry best-effort behavior - add a patch changeset for the published CLI package Files changed: .changeset/fn-5872-registry-cleanup.md | 5 ++++ .../src/commands/__tests__/task-lifecycle.test.ts | 29 +++++++++++++++++++++- packages/cli/src/commands/task-lifecycle.ts | 8 +++++- .../pr-mode-worktree-invariants.test.ts | 11 +++++--- packages/engine/src/index.ts | 1 + 5 files changed, 49 insertions(+), 5 deletions(-) Fusion-Task-Id: FN-5872 Fusion-Task-Lineage: d4db24a3-7ae0-4d07-b029-7f30b2f65d2e --- .changeset/fn-5872-registry-cleanup.md | 5 ++++ .../commands/__tests__/task-lifecycle.test.ts | 29 ++++++++++++++++++- packages/cli/src/commands/task-lifecycle.ts | 8 ++++- .../pr-mode-worktree-invariants.test.ts | 11 +++++-- packages/engine/src/index.ts | 1 + 5 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 .changeset/fn-5872-registry-cleanup.md diff --git a/.changeset/fn-5872-registry-cleanup.md b/.changeset/fn-5872-registry-cleanup.md new file mode 100644 index 000000000..f8d8797a1 --- /dev/null +++ b/.changeset/fn-5872-registry-cleanup.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Clear stale active-session registry entries when PR-mode merge cleanup removes a task worktree. diff --git a/packages/cli/src/commands/__tests__/task-lifecycle.test.ts b/packages/cli/src/commands/__tests__/task-lifecycle.test.ts index 773e15c2b..b1bffd32b 100644 --- a/packages/cli/src/commands/__tests__/task-lifecycle.test.ts +++ b/packages/cli/src/commands/__tests__/task-lifecycle.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { EventEmitter } from "node:events"; // Mock child_process so we can intercept the `git push -u origin ` @@ -23,6 +23,7 @@ vi.mock("node:child_process", () => ({ }, })); +import { activeSessionRegistry } from "@fusion/engine"; import { cleanupMergedTaskArtifacts, processPullRequestMergeTask, @@ -1252,6 +1253,11 @@ describe("cleanupMergedTaskArtifacts FN-5455", () => { beforeEach(() => { execMock.mockReset(); execMock.mockReturnValue(""); + activeSessionRegistry.clear(); + }); + + afterEach(() => { + activeSessionRegistry.clear(); }); it("FN-5455: releases pool lease before removing worktree and deleting branch", async () => { @@ -1284,4 +1290,25 @@ describe("cleanupMergedTaskArtifacts FN-5455", () => { expect(execMock).toHaveBeenCalledWith(expect.stringContaining('git worktree remove "/repo/wt-d" --force'), expect.any(Object)); expect(execMock).toHaveBeenCalledWith(expect.stringContaining('git branch -d "fusion/fn-5455-d"'), expect.any(Object)); }); + + it("FN-5872: cleanup clears active-session registry entry", async () => { + const worktree = "/repo/wt-fn-5872"; + activeSessionRegistry.registerPath(worktree, { + taskId: "FN-5872-A", + kind: "executor", + ownerKey: "FN-5872-A", + }); + + expect(activeSessionRegistry.lookupByPath(worktree)).not.toBeNull(); + + await cleanupMergedTaskArtifacts("/repo", { id: "FN-5872-A", worktree } as never); + + expect(activeSessionRegistry.lookupByPath(worktree)).toBeNull(); + }); + + it("FN-5872: cleanup remains a no-throw best-effort when no registry entry exists", async () => { + await expect( + cleanupMergedTaskArtifacts("/repo", { id: "FN-5872-B", worktree: "/repo/wt-fn-5872-missing" } as never), + ).resolves.toBeUndefined(); + }); }); diff --git a/packages/cli/src/commands/task-lifecycle.ts b/packages/cli/src/commands/task-lifecycle.ts index 6fe1decd0..2e867a480 100644 --- a/packages/cli/src/commands/task-lifecycle.ts +++ b/packages/cli/src/commands/task-lifecycle.ts @@ -19,7 +19,7 @@ const execAsync = promisify(exec); import type { TaskStore } from "@fusion/core"; import { resolveTaskMergeTarget } from "@fusion/core"; import type { Settings, TaskDetail, PrInfo, MergeResult, BranchGroup, BranchGroupPrState, Task } from "@fusion/core"; -import { resolveIntegrationBranch } from "@fusion/engine"; +import { activeSessionRegistry, resolveIntegrationBranch } from "@fusion/engine"; import type { WorktreePool } from "@fusion/engine"; /** @@ -196,6 +196,12 @@ export async function cleanupMergedTaskArtifacts( } } + try { + activeSessionRegistry.unregisterPath(task.worktree); + } catch { + // Best-effort cleanup — registry entry may already be absent or registry state divergent. + } + try { await execAsync(`git worktree remove "${task.worktree}" --force`, { cwd, diff --git a/packages/engine/src/__tests__/reliability-interactions/pr-mode-worktree-invariants.test.ts b/packages/engine/src/__tests__/reliability-interactions/pr-mode-worktree-invariants.test.ts index ef164ef0e..2f9b61a9f 100644 --- a/packages/engine/src/__tests__/reliability-interactions/pr-mode-worktree-invariants.test.ts +++ b/packages/engine/src/__tests__/reliability-interactions/pr-mode-worktree-invariants.test.ts @@ -283,10 +283,15 @@ describe("FN-5420 reliability interactions: PR mode worktree invariants", () => expect(releaseSpy).toHaveBeenCalledWith(path, "FN-5455-THROW"); }); - it.skip("FN-5456 follow-up required: cleanup should clear active-session registry entry", async () => { - const path = "/tmp/fn-5456-session"; - activeSessionRegistry.registerPath(path, { taskId: "FN-5456", kind: "executor", ownerKey: "FN-5456" }); + it("FN-5872: cleanup clears active-session registry entry", async () => { + const path = "/tmp/fn-5872-session"; + activeSessionRegistry.registerPath(path, { taskId: "FN-5872", kind: "executor", ownerKey: "FN-5872" }); expect(activeSessionRegistry.lookupByPath(path)).not.toBeNull(); + + const { cleanupMergedTaskArtifacts } = await loadPrLifecycleModule(); + await cleanupMergedTaskArtifacts("/tmp", { id: "FN-5872", worktree: path } as any); + + expect(activeSessionRegistry.lookupByPath(path)).toBeNull(); }); it("FN-5420/FN-5279: mergeIntegrationWorktree setting does not gate PR-mode processing", async () => { diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts index 5942f647b..71bed8fbb 100644 --- a/packages/engine/src/index.ts +++ b/packages/engine/src/index.ts @@ -172,6 +172,7 @@ export { type MockScript, type MockScriptContext, } from "./providers/index.js"; +export { activeSessionRegistry } from "./active-session-registry.js"; export { WorktreePool, scanIdleWorktrees, cleanupOrphanedWorktrees, reapOrphanWorktrees } from "./worktree-pool.js"; export { pruneWorktreeAdminEntries,