Files
fusion/packages/engine/src/__tests__/active-session-registry.test.ts
gsxdsm 627bdcfb0a fix(review): Phase C merge-loop hardening — double-land, lease clobber, retry storm
5-persona review of the Phase-C per-repo merge loop. No P0; the no-push invariant
and retry/park accounting verified clean. Fixed:

Land mechanics (merger-ai.ts / active-session-registry.ts):
- persistRepoLandedSha no longer swallows the DB write: a failed landedSha write
  after the ref advanced now escalates to WorkspacePartialLandError so the engine
  parks/retries instead of silently re-landing (duplicate squash). isRepoLanded
  gains a landedSha-independent fallback — it scans the integration ref for this
  task's Fusion-Task-Id trailer (a squash commit is NOT a branch descendant, so a
  branch-ancestor check is provably wrong), so an actually-landed repo is skipped
  on retry.
- The land lease is now taskId-aware across kinds: any foreign-task holder on a
  sub-repo path is contention (a merging task can't run over an executing task's
  acquire lease), and registerPath throws ActiveSessionPathHeldByForeignTaskError
  instead of silently clobbering a different task's entry.
- The per-repo loop is wrapped in try/finally(setStatus(null)) so the busy/partial
  throws can't leave the task stuck 'merging'. WorkspacePartialLandError is a real
  exported class (not a .name-mutated Error). finalizeWorkspaceTask re-reads fresh
  and no longer swallows the mergeDetails write (TOCTOU). isRepoLanded exported for
  Phase D.

Dispatch + doors (project-engine.ts / dashboard.ts / task.ts / @fusion/core):
- getTask-null in the partial-land catch fails closed (park) instead of defaulting
  retries to 0 and scheduling an indefinite retry storm.
- The merge-confirmed reachability fast-path skips workspace tasks (its
  representative commitSha is a sub-repo squash sha, unreachable in the root cwd —
  it was demoting fully-merged tasks); they're verified by per-repo landedSha.
- The CLI/dashboard merge doors now return merged:true on full land (were hardcoded
  merged:false). WorkspaceRepoLandBusyError re-enqueues with backoff WITHOUT burning
  the mergeRetries quota (bounded busy counter) so contention can't park a healthy
  task. Backoff capped at 60s. shouldRetryWorkspacePartialLand folded into
  shouldRetryAutoMergeConflict. Catch switched to instanceof. New canonical
  isWorkspaceTask predicate in @fusion/core.

Gate green: build, typecheck, lint, test:gate (649+58); workspace-merger + oracle
+ project-engine 174.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 01:11:52 -07:00

193 lines
8.2 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import {
activeSessionRegistry,
reconcileSelfOwnedActiveSessionForRemoval,
ActiveSessionPathHeldByForeignTaskError,
} from "../active-session-registry.js";
describe("activeSessionRegistry", () => {
beforeEach(() => {
activeSessionRegistry.clear();
});
it("registers and unregisters paths", () => {
activeSessionRegistry.registerPath("/tmp/w1", { taskId: "FN-1", kind: "executor", ownerKey: "FN-1" });
expect(activeSessionRegistry.isPathActive("/tmp/w1")).toBe(true);
activeSessionRegistry.unregisterPath("/tmp/w1");
expect(activeSessionRegistry.isPathActive("/tmp/w1")).toBe(false);
});
it("supports multiple paths for same task", () => {
activeSessionRegistry.registerPath("/tmp/w1", { taskId: "FN-1", kind: "executor", ownerKey: "FN-1" });
activeSessionRegistry.registerPath("/tmp/w2", { taskId: "FN-1", kind: "workflow-step", ownerKey: "FN-1#workflow-step" });
expect(activeSessionRegistry.pathsForTask("FN-1").sort()).toEqual(["/tmp/w1", "/tmp/w2"]);
});
it("returns null for unregistered path", () => {
expect(activeSessionRegistry.lookupByPath("/tmp/missing")).toBeNull();
});
// FNXC:Workspace 2026-06-22-04:10 (Phase C review A2 — taskId-aware lease across kinds):
// registerPath must NOT silently clobber an entry held by a DIFFERENT task (that was the
// cross-phase clobber bug: a merging task's land lease overwriting an executing task's
// acquire lease on a shared sub-repo). A foreign-task overwrite now THROWS; the existing
// foreign holder is preserved.
it("rejects a foreign-task overwrite (does not clobber the held entry)", () => {
activeSessionRegistry.registerPath("/tmp/w1", { taskId: "FN-1", kind: "executor", ownerKey: "FN-1" });
expect(() =>
activeSessionRegistry.registerPath("/tmp/w1", { taskId: "FN-2", kind: "workflow-step", ownerKey: "FN-2#workflow-step" }),
).toThrow(ActiveSessionPathHeldByForeignTaskError);
// The original holder is untouched.
expect(activeSessionRegistry.lookupByPath("/tmp/w1")?.taskId).toBe("FN-1");
});
// Same-task re-registration stays idempotent (an executor re-claiming/refreshing its own path).
it("allows same-task re-registration (idempotent re-claim)", () => {
activeSessionRegistry.registerPath("/tmp/w1", { taskId: "FN-1", kind: "executor", ownerKey: "FN-1" });
expect(() =>
activeSessionRegistry.registerPath("/tmp/w1", { taskId: "FN-1", kind: "step-session", ownerKey: "FN-1#step-session" }),
).not.toThrow();
expect(activeSessionRegistry.lookupByPath("/tmp/w1")?.kind).toBe("step-session");
});
it("reconcileStaleSelfOwned returns no-entry when path is unregistered", () => {
expect(activeSessionRegistry.reconcileStaleSelfOwned("/tmp/missing", "FN-1")).toEqual({
reconciled: false,
reason: "no-entry",
});
});
it("reconcileStaleSelfOwned returns foreign-task for mismatched owner", () => {
activeSessionRegistry.registerPath("/tmp/w1", { taskId: "FN-2", kind: "executor", ownerKey: "FN-2" });
expect(activeSessionRegistry.reconcileStaleSelfOwned("/tmp/w1", "FN-1")).toEqual({
reconciled: false,
reason: "foreign-task",
});
expect(activeSessionRegistry.lookupByPath("/tmp/w1")?.taskId).toBe("FN-2");
});
it("reconcileStaleSelfOwned unregisters matching self-owned entry", () => {
activeSessionRegistry.registerPath("/tmp/w1", { taskId: "FN-1", kind: "executor", ownerKey: "FN-1" });
expect(activeSessionRegistry.reconcileStaleSelfOwned("/tmp/w1", "FN-1")).toEqual({
reconciled: true,
reason: "reconciled",
});
expect(activeSessionRegistry.lookupByPath("/tmp/w1")).toBeNull();
});
it("reconcileSelfOwnedActiveSessionForRemoval returns no-entry when path is unregistered", () => {
expect(
reconcileSelfOwnedActiveSessionForRemoval(activeSessionRegistry, "/tmp/missing", "FN-1", () => false),
).toEqual({ action: "no-entry" });
});
it("reconcileSelfOwnedActiveSessionForRemoval returns foreign-task without clearing", () => {
activeSessionRegistry.registerPath("/tmp/w1", { taskId: "FN-2", kind: "executor", ownerKey: "FN-2" });
expect(
reconcileSelfOwnedActiveSessionForRemoval(activeSessionRegistry, "/tmp/w1", "FN-1", () => false),
).toEqual({ action: "foreign-task", ownerTaskId: "FN-2" });
expect(activeSessionRegistry.lookupByPath("/tmp/w1")?.taskId).toBe("FN-2");
});
it("reconcileSelfOwnedActiveSessionForRemoval returns live-binding-refuses without clearing", () => {
activeSessionRegistry.registerPath("/tmp/w1", { taskId: "FN-1", kind: "executor", ownerKey: "FN-1" });
expect(
reconcileSelfOwnedActiveSessionForRemoval(activeSessionRegistry, "/tmp/w1", "FN-1", () => true),
).toEqual({ action: "live-binding-refuses", ownerTaskId: "FN-1" });
expect(activeSessionRegistry.lookupByPath("/tmp/w1")?.taskId).toBe("FN-1");
});
it("reconcileSelfOwnedActiveSessionForRemoval clears stale same-task entry", () => {
activeSessionRegistry.registerPath("/tmp/w1", { taskId: "FN-1", kind: "executor", ownerKey: "FN-1" });
expect(
reconcileSelfOwnedActiveSessionForRemoval(activeSessionRegistry, "/tmp/w1", "FN-1", () => false, {
minIdleMs: 0,
}),
).toEqual({ action: "reconciled" });
expect(activeSessionRegistry.lookupByPath("/tmp/w1")).toBeNull();
});
it("reconcileSelfOwnedActiveSessionForRemoval is idempotent", () => {
activeSessionRegistry.registerPath("/tmp/w1", { taskId: "FN-1", kind: "executor", ownerKey: "FN-1" });
expect(
reconcileSelfOwnedActiveSessionForRemoval(activeSessionRegistry, "/tmp/w1", "FN-1", () => false, {
minIdleMs: 0,
}),
).toEqual({ action: "reconciled" });
expect(
reconcileSelfOwnedActiveSessionForRemoval(activeSessionRegistry, "/tmp/w1", "FN-1", () => false, {
minIdleMs: 0,
}),
).toEqual({ action: "no-entry" });
});
it("FN-5256: refuses reconcile when processActiveProbe returns true", () => {
activeSessionRegistry.registerPath("/tmp/w1", { taskId: "FN-1", kind: "executor", ownerKey: "FN-1" });
const outcome = reconcileSelfOwnedActiveSessionForRemoval(
activeSessionRegistry,
"/tmp/w1",
"FN-1",
() => false,
{ processActiveProbe: () => true, minIdleMs: 0 },
);
expect(outcome).toEqual({ action: "process-active-refuses", ownerTaskId: "FN-1" });
expect(activeSessionRegistry.lookupByPath("/tmp/w1")?.taskId).toBe("FN-1");
});
it("FN-5256: refuses reconcile when registration is younger than minIdleMs", () => {
activeSessionRegistry.registerPath("/tmp/w1", { taskId: "FN-1", kind: "executor", ownerKey: "FN-1" });
const registeredAt = activeSessionRegistry.lookupByPath("/tmp/w1")!.registeredAt;
const outcome = reconcileSelfOwnedActiveSessionForRemoval(
activeSessionRegistry,
"/tmp/w1",
"FN-1",
() => false,
{ minIdleMs: 5000, now: () => registeredAt + 100 },
);
expect(outcome).toMatchObject({ action: "too-recent-refuses", ownerTaskId: "FN-1", minIdleMs: 5000 });
expect(activeSessionRegistry.lookupByPath("/tmp/w1")?.taskId).toBe("FN-1");
});
it("FN-5256: reconciles when all signals clean (default min-idle window elapsed)", () => {
activeSessionRegistry.registerPath("/tmp/w1", { taskId: "FN-1", kind: "executor", ownerKey: "FN-1" });
const registeredAt = activeSessionRegistry.lookupByPath("/tmp/w1")!.registeredAt;
const outcome = reconcileSelfOwnedActiveSessionForRemoval(
activeSessionRegistry,
"/tmp/w1",
"FN-1",
() => false,
{
processActiveProbe: () => false,
minIdleMs: 5000,
now: () => registeredAt + 6000,
},
);
expect(outcome).toEqual({ action: "reconciled" });
expect(activeSessionRegistry.lookupByPath("/tmp/w1")).toBeNull();
});
it("FN-5256: live-binding takes precedence over process-active and too-recent", () => {
activeSessionRegistry.registerPath("/tmp/w1", { taskId: "FN-1", kind: "executor", ownerKey: "FN-1" });
const outcome = reconcileSelfOwnedActiveSessionForRemoval(
activeSessionRegistry,
"/tmp/w1",
"FN-1",
() => true,
{ processActiveProbe: () => true, minIdleMs: 5000 },
);
expect(outcome).toEqual({ action: "live-binding-refuses", ownerTaskId: "FN-1" });
});
});