test(FN-7249): cover phantom worktree recovery

This commit is contained in:
gsxdsm
2026-06-29 22:30:23 -07:00
parent 9e23d6c403
commit cfd9d5b32e
4 changed files with 40 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Stop repeat no-op phantom-reservation audit writes and preserve the worktree across phantom binding reclaim.
category: fix
dev: reconcilePhantomCommittedReservations now emits the task:reconcile-phantom-committed-reservation audit row only when orphaned child rows were actually pruned, instead of every maintenance tick (~19k wasted writes/day); the committed reservation stays committed so the ID is never reused. clearPhantomExecutorBinding gains a preserveWorktrees option the self-healing phantom reclaim uses so moveTask(preserveWorktree:true) re-dispatch reattaches to the same worktree instead of orphaning it and acquiring a new one (FN-7249). Regression: store-phantom-reservation-reconcile.test.ts, executor-workspace.test.ts.

View File

@@ -124,6 +124,22 @@ describe("TaskStore phantom committed-reservation reconciliation", () => {
expect(store.getRunAuditEvents({ taskId: phantom.id, mutationType: "task:reconcile-phantom-committed-reservation" })).toHaveLength(1);
});
it("does not re-emit the reconcile audit row on a second tick once orphaned rows are pruned (idempotency)", async () => {
const phantom = await createCommittedReservationPhantom("Phantom committed reservation (idempotency)");
seedOrphanedChildRows(phantom.id);
const first = await store.reconcilePhantomCommittedReservations();
expect(first.reconciled).toContain(phantom.id);
// Second maintenance tick: the phantom still re-matches (committed reservation, no row/dir),
// but the orphaned child rows are already gone, so no new audit row is written.
const second = await store.reconcilePhantomCommittedReservations();
expect(second.reconciled).toContain(phantom.id);
expect(store.getRunAuditEvents({ taskId: phantom.id, mutationType: "task:reconcile-phantom-committed-reservation" })).toHaveLength(1);
expect(reservationStatus(phantom.id)).toBe("committed");
});
it("is a safe no-op for in-memory stores", async () => {
store.close();
store = new TaskStore(rootDir, globalDir, { inMemoryDb: true });

View File

@@ -250,6 +250,12 @@ describe("workflow routes (U4)", () => {
// which carry no icon. The retired legacy `ir.optionalSteps` declaration
// supplied `icon: "globe"`; the group node instead yields `description: ""`
// and `phase: "pre-merge"`. Assert the current group-sourced shape.
//
// FNXC:WorkflowOptionalGroup 2026-06-29-17:40: builtin:coding now also declares the
// default-off `post-merge-verification` optional group (FN-7039/FN-7233 routed
// post-merge lifecycle policy through graph-native nodes). The optional-steps
// resolver exposes it as a fourth `phase: "post-merge"` step; assert it here so the
// route contract tracks the workflow IR instead of a stale three-step snapshot.
expect(builtin.body).toEqual([
expect.objectContaining({
templateId: "plan-review",
@@ -272,6 +278,13 @@ describe("workflow routes (U4)", () => {
phase: "pre-merge",
defaultOn: true,
}),
expect.objectContaining({
templateId: "post-merge-verification",
name: "Post-merge verification",
description: "",
phase: "post-merge",
defaultOn: false,
}),
]);
const custom = await post("/api/workflows", { name: "A", ir: linearIr() });

View File

@@ -153,10 +153,14 @@ describeIfGit("U1 KTD2 — activeWorktrees Set + every enumerated consumer", ()
const pB = repoBPath(fx);
(executor as any).addActiveWorktree("FN-WS-1", pA);
(executor as any).addActiveWorktree("FN-WS-1", pB);
activeSessionRegistry.registerPath(pA, { taskId: "FN-WS-1", kind: "executor", ownerKey: "exec:FN-WS-1:a" });
activeSessionRegistry.registerPath(pB, { taskId: "FN-WS-1", kind: "executor", ownerKey: "exec:FN-WS-1:b" });
const ok = (executor as any).clearPhantomExecutorBinding("FN-WS-1");
expect(ok).toBe(true);
expect((executor as any).activeWorktrees.has("FN-WS-1")).toBe(false);
// Default path must sweep the session-registry entries (inverse of the preserveWorktrees branch).
expect(activeSessionRegistry.pathsForTask("FN-WS-1")).toEqual([]);
});
it("clearPhantomExecutorBinding (FN-7249) preserveWorktrees keeps session-registry paths for re-dispatch", async () => {