test(FN-7249): cover phantom worktree recovery
This commit is contained in:
7
.changeset/phantom-reservation-worktree-preserve.md
Normal file
7
.changeset/phantom-reservation-worktree-preserve.md
Normal 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.
|
||||
@@ -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 });
|
||||
|
||||
@@ -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() });
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user