feat(FN-5346): add post-completion defensive backstop and shared reconcile

The merge adds a post-completion defensive backstop that probes and removes stale same-task `activeSessionRegistry` entries on `done`/`archived` transitions, completing FN-5346 with a shared reconcile helper, a defensive ownership probe wired into paused cleanup, audit event alignment, and regressio

Fusion-Task-Id: FN-5346
This commit is contained in:
Fusion (runfusion.ai)
2026-05-20 21:52:16 -07:00
committed by gsxdsm
parent e96cb09982
commit f798378693
11 changed files with 413 additions and 43 deletions

View File

@@ -0,0 +1,128 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import "../executor-test-helpers.js";
import { TaskExecutor } from "../../executor.js";
import { activeSessionRegistry } from "../../active-session-registry.js";
import { ActiveSessionWorktreeRemovalError, RemovalReason } from "../../worktree-backend.js";
import { executorLog } from "../../logger.js";
import { WorktreePool } from "../../worktree-pool.js";
import * as worktreePoolModule from "../../worktree-pool.js";
import { createMockStore, mockedExistsSync, resetExecutorMocks } from "../executor-test-helpers.js";
const ROOT = "/tmp/test";
const PATH = "/tmp/test/.worktrees/fn-5346";
const TASK_ID = "FN-5346";
describe("FN-5346 reliability interactions: post-completion stale self-owned binding", () => {
beforeEach(() => {
resetExecutorMocks();
vi.restoreAllMocks();
activeSessionRegistry.clear();
mockedExistsSync.mockReturnValue(true);
});
it("reconciles stale same-task registry entry during cleanup()", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, ROOT);
(executor as any).activeWorktrees.set(TASK_ID, PATH);
activeSessionRegistry.registerPath(PATH, { taskId: TASK_ID, kind: "executor", ownerKey: TASK_ID });
const removeSpy = vi.spyOn(worktreePoolModule, "removeWorktree").mockResolvedValue(undefined);
await executor.cleanup(TASK_ID);
expect(removeSpy).toHaveBeenCalledTimes(1);
expect(activeSessionRegistry.lookupByPath(PATH)).toBeNull();
expect(store.logEntry).toHaveBeenCalledWith(
TASK_ID,
"Cleared stale self-owned active-session entry before remove",
PATH,
);
expect((executorLog.warn as any).mock.calls.some((call: unknown[]) => String(call[0]).includes("[FN-5346]"))).toBe(true);
});
it("recovers same stale registry entry on first attempt after restart-style fresh executor", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, ROOT);
activeSessionRegistry.registerPath(PATH, { taskId: TASK_ID, kind: "executor", ownerKey: TASK_ID });
const removeSpy = vi.spyOn(worktreePoolModule, "removeWorktree").mockResolvedValue(undefined);
await (executor as any).handleDepAbortCleanup(TASK_ID, PATH);
expect(removeSpy).toHaveBeenCalledTimes(1);
expect(activeSessionRegistry.lookupByPath(PATH)).toBeNull();
});
it("preserves refusal for truly-live same-task bindings", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, ROOT);
(executor as any).activeWorktrees.set(TASK_ID, PATH);
activeSessionRegistry.registerPath(PATH, { taskId: TASK_ID, kind: "executor", ownerKey: TASK_ID });
vi.spyOn(worktreePoolModule, "removeWorktree").mockRejectedValue(
new ActiveSessionWorktreeRemovalError({
worktreePath: PATH,
taskId: TASK_ID,
kind: "executor",
ownerKey: TASK_ID,
reason: RemovalReason.ExecutorDispose,
}),
);
await (executor as any).cleanupConflictingWorktree(PATH, "fusion/fn-5346", TASK_ID);
expect(activeSessionRegistry.lookupByPath(PATH)?.taskId).toBe(TASK_ID);
expect(store.logEntry).not.toHaveBeenCalledWith(
TASK_ID,
"Cleared stale self-owned active-session entry before remove",
PATH,
);
});
it("preserves FN-4811 refusal for foreign active owner", async () => {
const store = createMockStore();
store.listTasks.mockResolvedValue([]);
const executor = new TaskExecutor(store, ROOT);
(executor as any).activeWorktrees.set("FN-FOREIGN", PATH);
activeSessionRegistry.registerPath(PATH, { taskId: "FN-FOREIGN", kind: "executor", ownerKey: "FN-FOREIGN" });
const removeSpy = vi.spyOn(worktreePoolModule, "removeWorktree").mockResolvedValue(undefined);
const cleaned = await (executor as any).cleanupConflictingWorktree(PATH, "fusion/fn-5346", TASK_ID);
expect(cleaned).toBe(false);
expect(removeSpy).not.toHaveBeenCalled();
expect(activeSessionRegistry.lookupByPath(PATH)?.taskId).toBe("FN-FOREIGN");
});
it("is idempotent across repeated cleanup sweeps", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, ROOT);
(executor as any).activeWorktrees.set(TASK_ID, PATH);
activeSessionRegistry.registerPath(PATH, { taskId: TASK_ID, kind: "executor", ownerKey: TASK_ID });
const removeSpy = vi.spyOn(worktreePoolModule, "removeWorktree").mockResolvedValue(undefined);
await executor.cleanup(TASK_ID);
(executor as any).activeWorktrees.set(TASK_ID, PATH);
await executor.cleanup(TASK_ID);
const clearedCalls = (store.logEntry as any).mock.calls.filter(
(call: unknown[]) => call[1] === "Cleared stale self-owned active-session entry before remove",
);
expect(clearedCalls).toHaveLength(1);
expect(removeSpy).toHaveBeenCalledTimes(2);
});
it("preserves FN-4954 pool lease bookkeeping while reconciling stale registry", async () => {
const pool = new WorktreePool();
pool.rehydrate([PATH]);
expect(pool.acquire(TASK_ID)).toBe(PATH);
const beforeLeased = new Map(pool.getLeasedPaths());
const store = createMockStore();
const executor = new TaskExecutor(store, ROOT);
(executor as any).activeWorktrees.set(TASK_ID, PATH);
activeSessionRegistry.registerPath(PATH, { taskId: TASK_ID, kind: "executor", ownerKey: TASK_ID });
vi.spyOn(worktreePoolModule, "removeWorktree").mockResolvedValue(undefined);
await executor.cleanup(TASK_ID);
expect(new Map(pool.getLeasedPaths())).toEqual(beforeLeased);
});
});

View File

@@ -35,7 +35,7 @@ describe("FN-4976: stale self-owned activeSessionRegistry deadlock backstop", ()
expect(activeSessionRegistry.lookupByPath(PATH)).toBeNull();
expect(store.logEntry).toHaveBeenCalledWith(
TASK_ID,
"Cleared stale self-owned activeSessionRegistry entry",
"Cleared stale self-owned active-session entry before remove",
PATH,
);
});
@@ -53,7 +53,7 @@ describe("FN-4976: stale self-owned activeSessionRegistry deadlock backstop", ()
expect(activeSessionRegistry.lookupByPath(PATH)?.taskId).toBe("FN-OTHER");
const messages = store.logEntry.mock.calls.map((call: any[]) => String(call[1] ?? ""));
expect(messages.some((m: string) => m.includes("Refused to remove conflicting worktree"))).toBe(true);
expect(messages.some((m: string) => m.includes("Cleared stale self-owned activeSessionRegistry entry"))).toBe(false);
expect(messages.some((m: string) => m.includes("Cleared stale self-owned active-session entry before remove"))).toBe(false);
});
it("FN-4976 leaves behavior unchanged when no stale entry exists", async () => {
@@ -70,6 +70,6 @@ describe("FN-4976: stale self-owned activeSessionRegistry deadlock backstop", ()
expect(result).toBe(true);
expect(unregisterSpy).not.toHaveBeenCalledWith(PATH);
const messages = store.logEntry.mock.calls.map((call: any[]) => String(call[1] ?? ""));
expect(messages.some((m: string) => m.includes("Cleared stale self-owned activeSessionRegistry entry"))).toBe(false);
expect(messages.some((m: string) => m.includes("Cleared stale self-owned active-session entry before remove"))).toBe(false);
});
});