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:
committed by
gsxdsm
parent
e96cb09982
commit
f798378693
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import {
|
||||
ActiveSessionWorktreeRemovalError,
|
||||
NativeWorktreeBackend,
|
||||
WorktrunkOperationError,
|
||||
WorktrunkWorktreeBackend,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
resolveWorktreeBackend,
|
||||
RemovalReason,
|
||||
} from "../worktree-backend.js";
|
||||
import { activeSessionRegistry } from "../active-session-registry.js";
|
||||
|
||||
const { execMock, accessMock, existsSyncMock, parseIndexLockPathMock, classifyStaleLockMock, tryRemoveStaleLockMock, parseStaleRegistrationPathMock, recoverStaleRegistrationMock, installGuardMock } = vi.hoisted(() => {
|
||||
const mock = vi.fn();
|
||||
@@ -73,6 +75,7 @@ beforeEach(() => {
|
||||
recoverStaleRegistrationMock.mockResolvedValue({ recovered: true, actions: ["prune"] });
|
||||
classifyStaleLockMock.mockResolvedValue({ kind: "fresh", reason: "fresh" });
|
||||
tryRemoveStaleLockMock.mockResolvedValue({ removed: true });
|
||||
activeSessionRegistry.clear();
|
||||
});
|
||||
|
||||
describe("NativeWorktreeBackend", () => {
|
||||
@@ -852,6 +855,93 @@ describe("removeWorktree", () => {
|
||||
}),
|
||||
).rejects.toMatchObject({ code: "worktrunk_binary_missing", operation: "remove" });
|
||||
});
|
||||
|
||||
it("reconciles same-task stale active session when defensive owner probe says not live", async () => {
|
||||
execMock.mockResolvedValue({ stdout: "", stderr: "" });
|
||||
const audit = { git: vi.fn().mockResolvedValue(undefined) } as any;
|
||||
activeSessionRegistry.registerPath("/repo/.worktrees/fn-1", {
|
||||
taskId: "FN-1",
|
||||
kind: "executor",
|
||||
ownerKey: "FN-1/executor",
|
||||
});
|
||||
|
||||
await removeWorktree({
|
||||
rootDir: "/repo",
|
||||
worktreePath: "/repo/.worktrees/fn-1",
|
||||
settings: {},
|
||||
audit,
|
||||
reason: RemovalReason.ExecutorDispose,
|
||||
expectedOwnerTaskId: "FN-1",
|
||||
liveOwnerProbe: () => false,
|
||||
});
|
||||
|
||||
expect(activeSessionRegistry.lookupByPath("/repo/.worktrees/fn-1")).toBeNull();
|
||||
expect(audit.git).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: "worktree:active-session-reconciled",
|
||||
target: "/repo/.worktrees/fn-1",
|
||||
metadata: { taskId: "FN-1", source: "removeWorktree-defensive" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves refusal when same-task owner is still live", async () => {
|
||||
activeSessionRegistry.registerPath("/repo/.worktrees/fn-1", {
|
||||
taskId: "FN-1",
|
||||
kind: "executor",
|
||||
ownerKey: "FN-1/executor",
|
||||
});
|
||||
|
||||
await expect(
|
||||
removeWorktree({
|
||||
rootDir: "/repo",
|
||||
worktreePath: "/repo/.worktrees/fn-1",
|
||||
settings: {},
|
||||
reason: RemovalReason.ExecutorDispose,
|
||||
expectedOwnerTaskId: "FN-1",
|
||||
liveOwnerProbe: () => true,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(ActiveSessionWorktreeRemovalError);
|
||||
});
|
||||
|
||||
it("preserves foreign-owner refusal with defensive owner hints", async () => {
|
||||
activeSessionRegistry.registerPath("/repo/.worktrees/fn-1", {
|
||||
taskId: "FN-2",
|
||||
kind: "executor",
|
||||
ownerKey: "FN-2/executor",
|
||||
});
|
||||
|
||||
await expect(
|
||||
removeWorktree({
|
||||
rootDir: "/repo",
|
||||
worktreePath: "/repo/.worktrees/fn-1",
|
||||
settings: {},
|
||||
reason: RemovalReason.ExecutorDispose,
|
||||
expectedOwnerTaskId: "FN-1",
|
||||
liveOwnerProbe: () => false,
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
name: "ActiveSessionWorktreeRemovalError",
|
||||
details: expect.objectContaining({ taskId: "FN-2" }),
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps pre-FN-5346 behavior when defensive owner hints are omitted", async () => {
|
||||
activeSessionRegistry.registerPath("/repo/.worktrees/fn-1", {
|
||||
taskId: "FN-1",
|
||||
kind: "executor",
|
||||
ownerKey: "FN-1/executor",
|
||||
});
|
||||
|
||||
await expect(
|
||||
removeWorktree({
|
||||
rootDir: "/repo",
|
||||
worktreePath: "/repo/.worktrees/fn-1",
|
||||
settings: {},
|
||||
reason: RemovalReason.ExecutorDispose,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(ActiveSessionWorktreeRemovalError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveWorktreeBackend", () => {
|
||||
|
||||
Reference in New Issue
Block a user