FN-8827: repair executor workflow routing fixtures

Align executor worktree tests with mandatory workflow principal routing.

- Add durable workflow agent-store fixtures with capacity leases and checkout renewal.
- Route worktree executor fixtures through eligible executor principals.
- Cover role selection, capacity release, checkout preservation, and fixture reset behavior.

Files changed:
 packages/engine/src/__tests__/executor-test-helpers.ts |  71 ++++++
 packages/engine/src/__tests__/executor-worktree.test.ts | 246 ++++++++++++++-------
 2 files changed, 239 insertions(+), 78 deletions(-)

Fusion-Task-Id: FN-8827
Fusion-Task-Lineage: 800a5246-abf3-480e-b71d-3237164c491e
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-08-07 21:50:51 -07:00
parent 36f20f835e
commit 38cdf3a589
2 changed files with 239 additions and 78 deletions

View File

@@ -699,6 +699,7 @@ export function createMockStore() {
listGoals: vi.fn().mockReturnValue([]),
}),
getFusionDir: vi.fn().mockReturnValue("/tmp/test/.fusion"),
getRootDir: vi.fn().mockReturnValue("/tmp/test"),
clearStaleExecutionStartBranchReferences: vi.fn().mockReturnValue([]),
// FNXC:EngineTestDrift 2026-07-11-22:40:
// FN-7750 / Runfusion#1980 made isLiveSharedBranchGroupMemberIntegration
@@ -769,6 +770,76 @@ FNXC:TaskVerificationRequest 2026-07-19-04:30 (merged with U5f 2026-07-19-06:00)
return store as any;
}
/** Minimal durable routing seam for production-path executor fixture tests. */
export function createWorkflowRoutingAgentStore(store: Pick<any, "getTask" | "updateTask">) {
const leases = new Map<string, string>();
const agent = {
id: "workflow-test-executor",
name: "Workflow Test Executor",
role: "executor",
roles: ["executor"],
state: "active",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
runtimeConfig: {},
};
const countAgentLeases = (agentId: string) => [...leases.values()].filter((holder) => holder === agentId).length;
const agentStore = {
workflowProjectId: "executor-worktree-test-project",
listAgents: vi.fn(async () => [agent]),
getAgent: vi.fn(async (agentId: string) => agentId === agent.id ? agent : null),
acquireWorkflowSessionCapacity: vi.fn(async (input: {
agentId: string;
attemptId: string;
maxProjectSessions?: number;
maxAgentSessions?: number;
}) => {
const existing = leases.get(input.attemptId);
if (existing) return existing === input.agentId ? "acquired" : "agent-capacity";
if (input.maxProjectSessions !== undefined && leases.size >= input.maxProjectSessions) return "project-capacity";
if (input.maxAgentSessions !== undefined && countAgentLeases(input.agentId) >= input.maxAgentSessions) return "agent-capacity";
leases.set(input.attemptId, input.agentId);
return "acquired";
}),
renewWorkflowSessionCapacity: vi.fn(async (attemptId: string) => leases.has(attemptId)),
releaseWorkflowSessionCapacity: vi.fn(async (attemptId: string) => {
leases.delete(attemptId);
}),
checkoutTask: vi.fn(async (
agentId: string,
taskId: string,
leaseContext?: { nodeId?: string; runId?: string; leaseEpoch?: number; renewedAt?: string },
) => {
const task = await store.getTask(taskId);
if (!task) throw new Error(`Task ${taskId} not found`);
const renewedAt = leaseContext?.renewedAt ?? new Date().toISOString();
await store.updateTask(taskId, {
checkedOutBy: agentId,
checkedOutAt: task.checkedOutBy === agentId ? task.checkedOutAt ?? renewedAt : renewedAt,
checkoutNodeId: leaseContext?.nodeId ?? task.checkoutNodeId ?? null,
checkoutRunId: leaseContext?.runId ?? task.checkoutRunId ?? null,
checkoutLeaseRenewedAt: renewedAt,
checkoutLeaseEpoch: leaseContext?.leaseEpoch ?? task.checkoutLeaseEpoch ?? 0,
});
return await store.getTask(taskId);
}),
};
return {
agent,
agentStore,
leases,
reset() {
leases.clear();
agentStore.listAgents.mockClear();
agentStore.getAgent.mockClear();
agentStore.acquireWorkflowSessionCapacity.mockClear();
agentStore.renewWorkflowSessionCapacity.mockClear();
agentStore.releaseWorkflowSessionCapacity.mockClear();
agentStore.checkoutTask.mockClear();
},
};
}
/*
FNXC:ExecutorTests 2026-07-19-09:40:
Under graph ownership mockedCreateFnAgent fires once per graph session (e.g. Plan

View File

@@ -12,6 +12,7 @@ import { findWorktreeUser, aiMergeTask } from "../merger.js";
import { WorktreePool } from "../worktree/worktree-pool.js";
import * as worktreePoolModule from "../worktree/worktree-pool.js";
import { BranchConflictError } from "../execution/branch-conflicts.js";
import { routeWorkflowPrincipal } from "../agents/workflow-agent-router.js";
import * as branchConflictModule from "../execution/branch-conflicts.js";
import { activeSessionRegistry } from "../agents/active-session-registry.js";
import { ActiveSessionWorktreeRemovalError } from "../worktree/worktree-backend.js";
@@ -25,6 +26,7 @@ import { runVerificationCommand as mockedRunVerificationCommand } from "../execu
import { __resetSandboxBackendForTests, __setSandboxBackendForTests } from "../sandbox/index.js";
import {
createMockStore,
createWorkflowRoutingAgentStore,
mockedCreateFnAgent,
mockedSessionManager,
mockedGenerateWorktreeName,
@@ -49,6 +51,94 @@ import {
const mockedReviewStep = vi.mocked(mockedReviewStepFn);
/*
* FNXC:WorkflowPrincipalRouting 2026-08-08-04:27:
* Mandatory graph principal routing is part of every production-path executor run, including
* worktree tests. This factory supplies only a durable executor role, capacity leases, and
* refresh-safe checkout renewal; it must not bypass graph admission or worktree refresh, so
* each existing Git/worktree assertion continues to exercise the production acquisition path.
*/
function createWorktreeExecutor(store: any, rootDir: string, options: any = {}) {
return new TaskExecutor(store, rootDir, {
agentStore: createWorkflowRoutingAgentStore(store).agentStore,
...options,
});
}
describe("worktree workflow routing fixture", () => {
it("selects its eligible executor from the role pool", async () => {
const store = createMockStore();
const fixture = createWorkflowRoutingAgentStore(store);
const agents = await fixture.agentStore.listAgents({ includeEphemeral: true });
const route = routeWorkflowPrincipal({
task: {},
ir: { version: "v1", name: "fixture", nodes: [], edges: [] },
node: { id: "execute", kind: "script", config: { seam: "execute" } },
agents,
} as any);
expect(route).toEqual(expect.objectContaining({
status: "routed",
route: expect.objectContaining({
agent: expect.objectContaining({ id: fixture.agent.id }),
role: "executor",
authority: "role-pool",
}),
}));
expect(fixture.agentStore.listAgents).toHaveBeenCalledWith({ includeEphemeral: true });
});
it("releases capacity after successful and failed fixture work", async () => {
const fixture = createWorkflowRoutingAgentStore(createMockStore());
const run = async (attemptId: string, fail = false) => {
expect(await fixture.agentStore.acquireWorkflowSessionCapacity({ agentId: fixture.agent.id, attemptId })).toBe("acquired");
try {
if (fail) throw new Error("fixture failure");
} finally {
await fixture.agentStore.releaseWorkflowSessionCapacity(attemptId);
}
};
await run("success");
await expect(run("failure", true)).rejects.toThrow("fixture failure");
expect(fixture.leases).toHaveLength(0);
expect(fixture.agentStore.releaseWorkflowSessionCapacity).toHaveBeenCalledTimes(2);
});
it("renews checkout without replacing populated worktree state", async () => {
const store = createMockStore();
store._setRow("FN-routing", {
worktree: "/tmp/existing-worktree",
branch: "fusion/FN-routing",
baseBranch: "main",
baseSha: "abc123",
});
const fixture = createWorkflowRoutingAgentStore(store);
await fixture.agentStore.checkoutTask(fixture.agent.id, "FN-routing", { nodeId: "node", runId: "run", leaseEpoch: 1, renewedAt: "2026-01-01T00:00:00.000Z" });
await fixture.agentStore.checkoutTask(fixture.agent.id, "FN-routing", { nodeId: "node", runId: "run", leaseEpoch: 1, renewedAt: "2026-01-01T00:01:00.000Z" });
expect(await store.getTask("FN-routing")).toEqual(expect.objectContaining({
worktree: "/tmp/existing-worktree",
branch: "fusion/FN-routing",
baseBranch: "main",
baseSha: "abc123",
checkedOutBy: fixture.agent.id,
checkoutLeaseRenewedAt: "2026-01-01T00:01:00.000Z",
}));
});
it("resets lease state without leaking into the next fixture", async () => {
const fixture = createWorkflowRoutingAgentStore(createMockStore());
await fixture.agentStore.acquireWorkflowSessionCapacity({ agentId: fixture.agent.id, attemptId: "leaked" });
fixture.reset();
expect(fixture.leases).toHaveLength(0);
expect(fixture.agentStore.acquireWorkflowSessionCapacity).not.toHaveBeenCalled();
expect(createWorkflowRoutingAgentStore(createMockStore()).leases).toHaveLength(0);
});
});
describe("TaskExecutor with semaphore", () => {
beforeEach(() => {
resetExecutorMocks();
@@ -67,7 +157,7 @@ describe("TaskExecutor with semaphore", () => {
},
} as any);
const executor = new TaskExecutor(store, "/tmp/test", { semaphore: sem });
const executor = createWorktreeExecutor(store, "/tmp/test", { semaphore: sem });
await executor.execute({
id: "FN-001",
@@ -94,7 +184,7 @@ describe("TaskExecutor with semaphore", () => {
mockedCreateFnAgent.mockRejectedValue(new Error("agent failed"));
const onError = vi.fn();
const executor = new TaskExecutor(store, "/tmp/test", {
const executor = createWorktreeExecutor(store, "/tmp/test", {
semaphore: sem,
onError,
});
@@ -122,7 +212,7 @@ describe("TaskExecutor with semaphore", () => {
mockedCreateFnAgent.mockRejectedValue(new Error("agent crashed"));
const onError = vi.fn();
const executor = new TaskExecutor(store, "/tmp/test", { onError });
const executor = createWorktreeExecutor(store, "/tmp/test", { onError });
await executor.execute({
id: "FN-001",
@@ -170,7 +260,7 @@ describe("TaskExecutor with semaphore", () => {
} as any;
});
const executor = new TaskExecutor(store, "/tmp/test", { semaphore: sem });
const executor = createWorktreeExecutor(store, "/tmp/test", { semaphore: sem });
const task = (id: string) => ({
id,
@@ -259,7 +349,7 @@ describe("TaskExecutor worktreeInitCommand", () => {
worktreeInitCommand: "pnpm install --frozen-lockfile",
});
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
// Should log success
@@ -275,7 +365,7 @@ describe("TaskExecutor worktreeInitCommand", () => {
const store = createMockStore();
// getSettings returns default (no worktreeInitCommand)
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
// Only worktree creation calls to execSync, no "pnpm install --frozen-lockfile" etc.
@@ -307,7 +397,7 @@ describe("TaskExecutor worktreeInitCommand", () => {
});
const onError = vi.fn();
const executor = new TaskExecutor(store, "/tmp/test", { onError });
const executor = createWorktreeExecutor(store, "/tmp/test", { onError });
await executor.execute(makeTask());
// Should log the failure
@@ -344,7 +434,7 @@ describe("TaskExecutor worktreeInitCommand", () => {
// Worktree already exists (resume)
mockedExistsSync.mockReturnValue(true);
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
// getSettings is called (for project commands in execution prompt) but init command should not run
@@ -381,7 +471,7 @@ describe("TaskExecutor worktree naming", () => {
it("uses generateWorktreeName for fresh worktree directories", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
@@ -395,7 +485,7 @@ describe("TaskExecutor worktree naming", () => {
it("does NOT use task ID as worktree directory name for fresh worktrees", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask("FN-099"));
@@ -438,7 +528,7 @@ describe("TaskExecutor worktree naming", () => {
is generated for a resumed task) is unchanged.
*/
store._setRow("FN-031", { worktree: existingPath });
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask("FN-031", existingPath));
@@ -466,7 +556,7 @@ describe("TaskExecutor worktree naming", () => {
stored row for the clear-and-recreate branch to be reachable at all.
*/
store._setRow("FN-032", { worktree: stalePath });
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask("FN-032", stalePath));
@@ -490,7 +580,7 @@ describe("TaskExecutor worktree naming", () => {
worktreeNaming: "task-id",
});
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask("FN-042"));
// Should use task ID (lowercase) as worktree name
@@ -520,7 +610,7 @@ describe("TaskExecutor worktree naming", () => {
passed to `execute()` is provably ignored — the requirement is about stored task data.
*/
store._setRow("FN-043", { title: "Fix login bug with OAuth" });
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute({
...makeTask("FN-043"),
title: "Fix login bug with OAuth",
@@ -546,7 +636,7 @@ describe("TaskExecutor worktree naming", () => {
worktreeNaming: "task-title",
});
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
const taskDescription = "Implement user authentication flow";
/*
FNXC:EngineTests 2026-07-19-16:28 (U10b):
@@ -579,7 +669,7 @@ describe("TaskExecutor worktree naming", () => {
worktreeNaming: "random",
});
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask("FN-045"));
// Should use generateWorktreeName for random mode
@@ -601,7 +691,7 @@ describe("TaskExecutor worktree naming", () => {
// worktreeNaming is not set (undefined)
});
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask("FN-046"));
// Should default to random naming
@@ -637,7 +727,7 @@ describe("TaskExecutor worktree naming", () => {
reclaimed: false,
});
const executor = new TaskExecutor(store, "/tmp/test", { pool });
const executor = createWorktreeExecutor(store, "/tmp/test", { pool });
await executor.execute(makeTask("FN-047"));
// Worktree naming preference should not break task startup in recycle mode.
@@ -683,7 +773,7 @@ describe("TaskExecutor worktree recovery", () => {
it("creates worktree successfully on first attempt", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
@@ -715,7 +805,7 @@ describe("TaskExecutor worktree recovery", () => {
return Buffer.from("");
});
const executor = new TaskExecutor(store, "/tmp/test", { onError });
const executor = createWorktreeExecutor(store, "/tmp/test", { onError });
await executor.execute(makeTask());
expect(store.logEntry).toHaveBeenCalledWith(
@@ -748,7 +838,7 @@ describe("TaskExecutor worktree recovery", () => {
return Buffer.from("");
});
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
const worktreeAddCalls = mockedExecSync.mock.calls.filter(
@@ -772,7 +862,7 @@ describe("TaskExecutor worktree recovery", () => {
return Buffer.from("");
});
const executor = new TaskExecutor(store, rootDir, { onError });
const executor = createWorktreeExecutor(store, rootDir, { onError });
await executor.execute(makeTask());
const worktreeAddCalls = mockedExecSync.mock.calls.filter(
@@ -807,7 +897,7 @@ describe("TaskExecutor worktree recovery", () => {
it("extractWorktreeConflictInfo classifies not-a-git-repository errors", () => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
const error: any = new Error("fatal: not a git repository");
error.stderr = Buffer.from("fatal: not a git repository");
@@ -819,7 +909,7 @@ describe("TaskExecutor worktree recovery", () => {
it("extractWorktreeConflictInfo does not misclassify dubious ownership as not-git-repo", () => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
const rootDir = "C:/Users/drewd/Documents/1. App Development/1. Active/NextGenEHS";
const error: any = new Error(`fatal: detected dubious ownership in repository at '${rootDir}'`);
@@ -832,7 +922,7 @@ describe("TaskExecutor worktree recovery", () => {
it("treats not-a-git-repository as non-retryable in tryCreateWorktree flow", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
mockedExecSync.mockImplementation((cmd: string | string[]) => {
const command = typeof cmd === "string" ? cmd : cmd[0];
@@ -859,7 +949,7 @@ describe("TaskExecutor worktree recovery", () => {
it("extractWorktreeConflictInfo classifies already checked out errors as already-used", () => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
const error: any = new Error(
"fatal: 'fusion/fn-050' is already checked out at '/tmp/test/.worktrees/green-sage'",
@@ -893,7 +983,7 @@ describe("TaskExecutor worktree recovery", () => {
return Buffer.from("");
});
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
expect(store.logEntry).toHaveBeenCalledWith(
@@ -909,7 +999,7 @@ describe("TaskExecutor worktree recovery", () => {
it("reclaims an inactive same-task conflict when the branch preserves task commits", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
const conflictPath = "/tmp/test/.worktrees/light-cedar";
vi.spyOn(executor as any, "shouldGenerateNewWorktreeName").mockResolvedValue(false);
const cleanup = vi.spyOn(executor as any, "cleanupConflictingWorktree").mockResolvedValue(true);
@@ -945,7 +1035,7 @@ describe("TaskExecutor worktree recovery", () => {
"relocates an out-of-root %s same-task worktree before reclaiming it",
async (kind) => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
const conflictPath = "/tmp/legacy-worktrees/recover-fn-8400";
const targetPath = "/tmp/test/.worktrees/pearl-otter";
vi.spyOn(executor as any, "shouldGenerateNewWorktreeName").mockResolvedValue(false);
@@ -984,7 +1074,7 @@ describe("TaskExecutor worktree recovery", () => {
it("normalizes an out-of-root branch-conflict reclaim before persisting it", async () => {
const store = createMockStore();
store.getSettings.mockResolvedValue({ worktreesDir: ".worktrees" } as any);
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
const conflictPath = "/tmp/legacy-worktrees/recover-fn-8400";
const targetPath = "/tmp/test/.worktrees/recover-fn-8400";
vi.spyOn(branchConflictModule, "inspectBranchConflict").mockResolvedValueOnce({
@@ -1016,7 +1106,7 @@ describe("TaskExecutor worktree recovery", () => {
it("uses the task-pinned target when normalizing a branch-conflict reclaim", async () => {
const store = createMockStore();
store.getSettings.mockResolvedValue({ worktreesDir: ".worktrees", worktreeNaming: "task-id" } as any);
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
const conflictPath = "/tmp/legacy-worktrees/recover-fn-8400";
const pinnedPath = "/tmp/test/.worktrees/fn-8400";
vi.spyOn(branchConflictModule, "inspectBranchConflict").mockResolvedValueOnce({
@@ -1054,7 +1144,7 @@ describe("TaskExecutor worktree recovery", () => {
// the task to todo. status='failed' is no longer set; moveTask IS called.
const store = createMockStore();
const onError = vi.fn();
const executor = new TaskExecutor(store, "/tmp/test", { onError });
const executor = createWorktreeExecutor(store, "/tmp/test", { onError });
const result = await (executor as any).handleBranchConflict(
makeTask(),
@@ -1098,7 +1188,7 @@ describe("TaskExecutor worktree recovery", () => {
it("FN-4397 reproduces repeated branch-conflict recovery-required emissions for the same task", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
const conflictError = new BranchConflictError({
branchName: "fusion/fn-050",
conflictingWorktreePath: "/tmp/test/.worktrees/green-sage",
@@ -1132,7 +1222,7 @@ describe("TaskExecutor worktree recovery", () => {
it("FN-4397 tripwire pauses on 6th branch conflict and suppresses additional recovery-required agent logs", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
const conflictError = new BranchConflictError({
branchName: "fusion/fn-050",
conflictingWorktreePath: "/tmp/test/.worktrees/green-sage",
@@ -1184,7 +1274,7 @@ describe("TaskExecutor worktree recovery", () => {
});
const onError = vi.fn();
const executor = new TaskExecutor(store, "/tmp/test", { onError });
const executor = createWorktreeExecutor(store, "/tmp/test", { onError });
/*
FNXC:EngineTests 2026-07-19-16:32 (U10b):
`executionStartBranch` is a PERSISTED field: the missing-base fallback reads it from the row
@@ -1253,7 +1343,7 @@ describe("TaskExecutor worktree recovery", () => {
});
const onError = vi.fn();
const executor = new TaskExecutor(store, "/tmp/test", { onError });
const executor = createWorktreeExecutor(store, "/tmp/test", { onError });
/*
FNXC:EngineTests 2026-07-19-16:35 (U10b):
The nested-worktree refusal guards the PERSISTED worktree path. The graph re-reads the row,
@@ -1310,7 +1400,7 @@ describe("TaskExecutor worktree recovery", () => {
});
const onError = vi.fn();
const executor = new TaskExecutor(store, "/tmp/test", { onError });
const executor = createWorktreeExecutor(store, "/tmp/test", { onError });
await executor.execute(makeTask());
@@ -1367,7 +1457,7 @@ describe("TaskExecutor worktree recovery", () => {
return Buffer.from("");
});
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
// Mock the second call to tryCreateWorktree to succeed
// by making subsequent calls succeed after cleanup
@@ -1470,7 +1560,7 @@ describe("TaskExecutor worktree recovery", () => {
return Buffer.from("");
});
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
expect(removeSpy).toHaveBeenCalledTimes(1);
@@ -1537,7 +1627,7 @@ describe("TaskExecutor worktree recovery", () => {
return Buffer.from("");
});
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
expect(activeSessionRegistry.lookupByPath(conflictPath)?.kind).toBe("workflow-step");
@@ -1603,7 +1693,7 @@ describe("TaskExecutor worktree recovery", () => {
// Second generated name for the suffix-rename path.
mockedGenerateWorktreeName.mockReturnValueOnce("jade-finch");
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
/*
FNXC:EngineTests 2026-07-19-16:38 (U10b):
The suffix-rename retry must reuse the task's persisted start point, so `executionStartBranch`
@@ -1660,7 +1750,7 @@ describe("TaskExecutor worktree recovery", () => {
return Buffer.from("");
});
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
expect(mockedClassifyStaleLock).toHaveBeenCalled();
@@ -1687,7 +1777,7 @@ describe("TaskExecutor worktree recovery", () => {
return Buffer.from("");
});
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
expect(store.updateTask).toHaveBeenCalledWith(
@@ -1714,7 +1804,7 @@ describe("TaskExecutor worktree recovery", () => {
return Buffer.from("");
});
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
expect(mockedRecoverStaleRegistration).toHaveBeenCalled();
@@ -1744,7 +1834,7 @@ describe("TaskExecutor worktree recovery", () => {
return Buffer.from("");
});
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
(executor as any).MAX_WORKTREE_RETRIES = 1;
await expect(
@@ -1771,7 +1861,7 @@ describe("TaskExecutor worktree recovery", () => {
return Buffer.from("");
});
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
// Should have removed the stale branch
@@ -1801,7 +1891,7 @@ describe("TaskExecutor worktree recovery", () => {
return Buffer.from("");
});
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
// Should have called git worktree prune as the first recovery step
@@ -1857,7 +1947,7 @@ describe("TaskExecutor worktree recovery", () => {
return Buffer.from("");
});
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
// Should have tried branch -D first
@@ -1909,7 +1999,7 @@ describe("TaskExecutor worktree recovery", () => {
});
const onError = vi.fn();
const executor = new TaskExecutor(store, "/tmp/test", { onError });
const executor = createWorktreeExecutor(store, "/tmp/test", { onError });
const executePromise = executor.execute(makeTask());
await vi.advanceTimersByTimeAsync(5000);
await executePromise;
@@ -1954,7 +2044,7 @@ describe("TaskExecutor worktree recovery", () => {
});
const onError = vi.fn();
const executor = new TaskExecutor(store, "/tmp/test", { onError });
const executor = createWorktreeExecutor(store, "/tmp/test", { onError });
await executor.execute(makeTask());
// Should have logged terminal failure for the stale reference
@@ -1997,7 +2087,7 @@ describe("TaskExecutor worktree recovery", () => {
return Buffer.from("");
});
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
// Should have logged cleanup in fallback path
@@ -2028,7 +2118,7 @@ describe("TaskExecutor worktree recovery", () => {
return Buffer.from("");
});
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
// Should have triggered cleanup (stale branch reclaim)
@@ -2058,7 +2148,7 @@ describe("TaskExecutor worktree recovery", () => {
return Buffer.from("");
});
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
expect(store.logEntry).toHaveBeenCalledWith(
@@ -2083,7 +2173,7 @@ describe("TaskExecutor worktree recovery", () => {
return Buffer.from("");
});
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
expect(store.logEntry).toHaveBeenCalledWith(
@@ -2115,7 +2205,7 @@ describe("TaskExecutor worktree recovery", () => {
return Buffer.from("");
});
const executor = new TaskExecutor(store, rootDir);
const executor = createWorktreeExecutor(store, rootDir);
await executor.execute(makeTask());
expect(
@@ -2152,7 +2242,7 @@ describe("TaskExecutor worktree recovery", () => {
return Buffer.from("");
});
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
// Should attempt to unlock the worktree before removing
@@ -2193,7 +2283,7 @@ describe("TaskExecutor dependency-based worktree creation", () => {
it("creates worktree from baseBranch when set on task", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
/*
FNXC:EngineTests 2026-07-19-16:41 (U10b):
@@ -2217,7 +2307,7 @@ describe("TaskExecutor dependency-based worktree creation", () => {
it("creates worktree from integration branch when baseBranch is not set", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask({
id: "FN-061",
@@ -2234,7 +2324,7 @@ describe("TaskExecutor dependency-based worktree creation", () => {
it("logs base branch in worktree creation log entry", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
/*
FNXC:EngineTests 2026-07-19-16:42 (U10b):
@@ -2257,7 +2347,7 @@ describe("TaskExecutor dependency-based worktree creation", () => {
it("logs integration branch in worktree creation log when baseBranch is not set", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask({
id: "FN-063",
@@ -2272,7 +2362,7 @@ describe("TaskExecutor dependency-based worktree creation", () => {
it("retries worktree creation after cleaning up conflicting worktree", async () => {
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
const conflictingPath = "/tmp/test/.worktrees/sharp-stone";
const removeWorktreeSpy = vi.spyOn(worktreePoolModule, "removeWorktree");
@@ -2316,7 +2406,7 @@ describe("TaskExecutor dependency-based worktree creation", () => {
it("throws original error if cleanup also fails", async () => {
vi.useRealTimers();
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
const conflictingPath = "/tmp/test/.worktrees/sharp-stone";
mockedExecSync.mockImplementation((cmd: any) => {
@@ -2362,7 +2452,7 @@ describe("TaskExecutor dependency-based worktree creation", () => {
recycleWorktrees: true,
});
const executor = new TaskExecutor(store, "/tmp/test", { pool });
const executor = createWorktreeExecutor(store, "/tmp/test", { pool });
/*
FNXC:EngineTests 2026-07-19-16:44 (U10b):
@@ -2402,7 +2492,7 @@ describe("TaskExecutor dependency-based worktree creation", () => {
recycleWorktrees: true,
});
const executor = new TaskExecutor(store, "/tmp/test", { pool });
const executor = createWorktreeExecutor(store, "/tmp/test", { pool });
await executor.execute(makeTask({
id: "FN-065",
@@ -2440,7 +2530,7 @@ describe("TaskExecutor dependency-based worktree creation", () => {
recycleWorktrees: true,
});
const executor = new TaskExecutor(store, "/tmp/test", { pool });
const executor = createWorktreeExecutor(store, "/tmp/test", { pool });
await executor.execute(makeTask({ id: "FN-066" }));
expect(store.recordRunAuditEvent).toHaveBeenCalledWith(expect.objectContaining({
@@ -2476,7 +2566,7 @@ describe("TaskExecutor dependency-based worktree creation", () => {
recycleWorktrees: true,
});
const executor = new TaskExecutor(store, "/tmp/test", { pool });
const executor = createWorktreeExecutor(store, "/tmp/test", { pool });
await executor.execute(makeTask({
id: "FN-066",
@@ -2534,7 +2624,7 @@ describe("TaskExecutor worktree pool integration", () => {
recycleWorktrees: true,
});
const executor = new TaskExecutor(store, "/tmp/test", { pool });
const executor = createWorktreeExecutor(store, "/tmp/test", { pool });
await executor.execute(makeTask());
// Should NOT call git worktree add (no fresh worktree)
@@ -2612,7 +2702,7 @@ describe("TaskExecutor worktree pool integration", () => {
recycleWorktrees: true,
});
const executor = new TaskExecutor(store, "/tmp/test", { pool });
const executor = createWorktreeExecutor(store, "/tmp/test", { pool });
await executor.execute(makeTask());
expect(store.updateTask).toHaveBeenCalledWith("FN-020", { baseCommitSha: "newbase123" });
@@ -2632,7 +2722,7 @@ describe("TaskExecutor worktree pool integration", () => {
recycleWorktrees: true,
});
const executor = new TaskExecutor(store, "/tmp/test", { pool });
const executor = createWorktreeExecutor(store, "/tmp/test", { pool });
await executor.execute(makeTask());
// Should call git worktree add (fresh worktree)
@@ -2672,7 +2762,7 @@ describe("TaskExecutor worktree pool integration", () => {
worktreeInitCommand: "pnpm install --frozen-lockfile",
});
const executor = new TaskExecutor(store, "/tmp/test", { pool });
const executor = createWorktreeExecutor(store, "/tmp/test", { pool });
await executor.execute(makeTask());
// "pnpm install --frozen-lockfile" should NOT have been called (pooled worktree has warm cache)
@@ -2689,7 +2779,7 @@ describe("TaskExecutor worktree pool integration", () => {
const store = createMockStore();
// recycleWorktrees defaults to false
const executor = new TaskExecutor(store, "/tmp/test", { pool });
const executor = createWorktreeExecutor(store, "/tmp/test", { pool });
await executor.execute(makeTask());
// Should create a fresh worktree, NOT acquire from pool
@@ -2723,7 +2813,7 @@ describe("TaskExecutor worktree pool integration", () => {
recycleWorktrees: true,
});
const executor = new TaskExecutor(store, "/tmp/test", { pool });
const executor = createWorktreeExecutor(store, "/tmp/test", { pool });
await executor.execute(makeTask());
expect(releaseSpy).toHaveBeenCalledWith("/tmp/test/.worktrees/bad-wt", "FN-020");
@@ -2766,7 +2856,7 @@ describe("TaskExecutor worktree pool integration", () => {
recycleWorktrees: true,
});
const executor = new TaskExecutor(store, "/tmp/test", { pool });
const executor = createWorktreeExecutor(store, "/tmp/test", { pool });
await executor.execute(makeTask("FN-020"));
const worktreeAddCalls = mockedExecSync.mock.calls.filter(
@@ -2880,7 +2970,7 @@ describe("worktree DB hydration", () => {
it("runs once for fresh worktree", async () => {
mockedExistsSync.mockReturnValue(false);
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
expect(mockedHydrateWorktreeDb).toHaveBeenCalledTimes(1);
});
@@ -2894,7 +2984,7 @@ describe("worktree DB hydration", () => {
release: vi.fn(),
} as any;
store.getSettings.mockResolvedValue({ ...(await store.getSettings()), recycleWorktrees: true });
const executor = new TaskExecutor(store, "/tmp/test", { pool });
const executor = createWorktreeExecutor(store, "/tmp/test", { pool });
await executor.execute(makeTask());
expect(mockedHydrateWorktreeDb).toHaveBeenCalledTimes(1);
});
@@ -2904,7 +2994,7 @@ describe("worktree DB hydration", () => {
mockedClassifyTaskWorktree.mockResolvedValueOnce({ ok: false, classification: "incomplete", reason: "missing or invalid .git metadata" } as any);
mockedExistsSync.mockReturnValue(true);
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask({ worktree: "/tmp/test" }));
expect(mockedHydrateWorktreeDb).toHaveBeenCalledTimes(1);
});
@@ -2919,7 +3009,7 @@ describe("worktree DB hydration", () => {
});
mockedExistsSync.mockReturnValue(false);
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
expect((store.logEntry as ReturnType<typeof vi.fn>).mock.calls).toEqual(
@@ -2939,7 +3029,7 @@ describe("worktree DB hydration", () => {
mockedHydrateWorktreeDb.mockRejectedValueOnce(new Error("boom"));
mockedExistsSync.mockReturnValue(false);
const store = createMockStore();
const executor = new TaskExecutor(store, "/tmp/test");
const executor = createWorktreeExecutor(store, "/tmp/test");
await executor.execute(makeTask());
expect(mockedCreateFnAgent).toHaveBeenCalled();
});