test: align workspace callback fixtures and gate (#3500)
## Summary - update workspace worktree test stores for callback-based entry mutations - preserve validation, existing-entry, and singular-routing behavior in the fakes - canonicalize the macOS worktree path fixture before comparing persisted state - remove the stale inert-seam exception left after the review-column callback became fully supplied ## Test plan - `pnpm --filter @fusion/engine exec vitest run src/__tests__/node-worktree-isolation.test.ts src/__tests__/workspace-root-worktree-routing.test.ts src/__tests__/worktree-acquisition.test.ts --silent=passed-only --reporter=dot` - `pnpm --filter @fusion/engine exec vitest run src/__tests__/worktree-acquisition-workspace.test.ts --silent=passed-only --reporter=dot` - Directly changed engine workspace suite with PostgreSQL-dependent blocks skipped: 338 passed, 4 skipped - `pnpm --filter @fusion/engine typecheck` - `node scripts/check-inert-flag-seams.mjs` - `node --test scripts/__tests__/check-inert-flag-seams.test.mjs` - `pnpm lint` - `pnpm check:changesets` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved recovery of orphaned worktrees by consistently using canonical paths. * Strengthened worktree assignment and updates to preserve existing entries and prevent stale task state. * Improved synchronization and validation during concurrent workspace updates. * Refined self-healing recovery so branch metadata changes only when necessary. * Improved handling of stalled tasks, retry exhaustion, and clearing obsolete worktree or branch information. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { vi } from "vitest";
|
||||
import type { Mock } from "vitest";
|
||||
import type { Task } from "@fusion/core";
|
||||
import { installTaskWorktreeIdentityGuard } from "../worktree/worktree-hooks.js";
|
||||
import type * as ReviewerModule from "../execution/reviewer.js";
|
||||
|
||||
@@ -553,6 +554,13 @@ export function createMockStore() {
|
||||
`getTask`/`updateTask` implementations replace these outright and are unaffected.
|
||||
*/
|
||||
const patches = new Map<string, Record<string, unknown>>();
|
||||
/*
|
||||
FNXC:EngineTests 2026-08-21-08:34:
|
||||
The shared executor store fake must preserve TaskStore's per-task atomic merge contract. Queue merge
|
||||
calls and re-read after an async callback so sibling merges and simulated external task updates cannot
|
||||
be overwritten by a stale workspaceWorktrees snapshot.
|
||||
*/
|
||||
const workspaceMergeTails = new Map<string, Promise<void>>();
|
||||
const applyPatch = (id: string, patch: Record<string, unknown> | undefined) => {
|
||||
if (!patch || typeof patch !== "object") return;
|
||||
patches.set(id, { ...(patches.get(id) ?? {}), ...patch });
|
||||
@@ -672,17 +680,46 @@ export function createMockStore() {
|
||||
applyPatch(id, patch);
|
||||
return { ...(patches.get(id) ?? {}), id };
|
||||
}),
|
||||
mergeWorkspaceWorktreeEntry: vi.fn(async (id: string, repoRelPath: string, patch: Record<string, unknown>) => {
|
||||
const live = await store.getTask(id);
|
||||
const workspaceWorktrees = (live?.workspaceWorktrees ?? {}) as Record<string, unknown>;
|
||||
const existing = workspaceWorktrees[repoRelPath];
|
||||
applyPatch(id, {
|
||||
workspaceWorktrees: {
|
||||
...workspaceWorktrees,
|
||||
[repoRelPath]: { ...(existing && typeof existing === "object" ? existing : {}), ...patch },
|
||||
},
|
||||
mergeWorkspaceWorktreeEntry: vi.fn((
|
||||
id: string,
|
||||
repoRelPath: string,
|
||||
patch: Partial<NonNullable<Task["workspaceWorktrees"]>[string]>
|
||||
| ((current: Task) => Promise<Partial<NonNullable<Task["workspaceWorktrees"]>[string]>>),
|
||||
options?: {
|
||||
requireExistingEntry?: boolean;
|
||||
clearSingularWorktree?: boolean;
|
||||
validateBeforePersist?: (current: Task) => Promise<void>;
|
||||
},
|
||||
) => {
|
||||
const operation = (workspaceMergeTails.get(id) ?? Promise.resolve()).then(async () => {
|
||||
const callbackTask = await store.getTask(id) as Task;
|
||||
const callbackExisting = callbackTask.workspaceWorktrees?.[repoRelPath];
|
||||
if (options?.requireExistingEntry && !callbackExisting) return callbackTask;
|
||||
const resolvedPatch = typeof patch === "function" ? await patch(callbackTask) : patch;
|
||||
const current = await store.getTask(id) as Task;
|
||||
const workspaceWorktrees = current.workspaceWorktrees ?? {};
|
||||
const existing = workspaceWorktrees[repoRelPath];
|
||||
if (options?.requireExistingEntry && !existing) return current;
|
||||
await options?.validateBeforePersist?.(current);
|
||||
applyPatch(id, {
|
||||
workspaceWorktrees: {
|
||||
...workspaceWorktrees,
|
||||
[repoRelPath]: { ...existing, ...resolvedPatch },
|
||||
},
|
||||
...(options?.clearSingularWorktree
|
||||
? {
|
||||
worktree: null,
|
||||
branch: null,
|
||||
branchWriteOrigin: "engine",
|
||||
executionStartBranch: null,
|
||||
baseCommitSha: null,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
return store.getTask(id);
|
||||
});
|
||||
return store.getTask(id);
|
||||
workspaceMergeTails.set(id, operation.then(() => undefined, () => undefined));
|
||||
return operation;
|
||||
}),
|
||||
updateWorkspaceReviewState: vi.fn(async (id: string, _revision: number, reviewRemediation: unknown) => {
|
||||
const current = await store.getTask(id);
|
||||
|
||||
@@ -66,6 +66,79 @@ function createWorktreeExecutor(store: any, rootDir: string, options: any = {})
|
||||
}
|
||||
|
||||
describe("worktree workflow routing fixture", () => {
|
||||
/*
|
||||
FNXC:EngineTests 2026-08-21-08:34:
|
||||
The shared executor fake is a production-contract seam: concurrent callback merges must preserve
|
||||
every repository entry, missing required entries are no-ops, and workspace routing clears all
|
||||
singular-checkout metadata.
|
||||
|
||||
FNXC:EngineTests 2026-08-21-09:29:
|
||||
Cleared singular-checkout metadata must use null, matching the persisted TaskStore row shape;
|
||||
undefined would let fixture-only behavior diverge from production reads.
|
||||
*/
|
||||
it("mirrors atomic workspace worktree merge semantics", async () => {
|
||||
const store = createMockStore();
|
||||
store._setRow("FN-workspace-merge", {
|
||||
worktree: "/tmp/singular",
|
||||
branch: "fusion/singular",
|
||||
executionStartBranch: "main",
|
||||
baseCommitSha: "base",
|
||||
workspaceWorktrees: {},
|
||||
});
|
||||
|
||||
const missing = await store.mergeWorkspaceWorktreeEntry(
|
||||
"FN-workspace-merge",
|
||||
"missing",
|
||||
{ worktreePath: "/tmp/missing" },
|
||||
{ requireExistingEntry: true },
|
||||
);
|
||||
expect(missing.workspaceWorktrees?.missing).toBeUndefined();
|
||||
|
||||
let releaseFirst!: () => void;
|
||||
let markFirstStarted!: () => void;
|
||||
const firstGate = new Promise<void>((resolve) => { releaseFirst = resolve; });
|
||||
const firstStarted = new Promise<void>((resolve) => { markFirstStarted = resolve; });
|
||||
const first = store.mergeWorkspaceWorktreeEntry(
|
||||
"FN-workspace-merge",
|
||||
"repo-a",
|
||||
async () => {
|
||||
markFirstStarted();
|
||||
await firstGate;
|
||||
return { worktreePath: "/tmp/repo-a", branch: "fusion/a" };
|
||||
},
|
||||
);
|
||||
await firstStarted;
|
||||
store._setRow("FN-workspace-merge", {
|
||||
workspaceWorktrees: {
|
||||
"repo-c": { worktreePath: "/tmp/repo-c", branch: "fusion/c" },
|
||||
},
|
||||
});
|
||||
|
||||
let secondStarted = false;
|
||||
const second = store.mergeWorkspaceWorktreeEntry(
|
||||
"FN-workspace-merge",
|
||||
"repo-b",
|
||||
async (freshTask: Task) => {
|
||||
secondStarted = true;
|
||||
expect(freshTask.workspaceWorktrees?.["repo-a"]?.worktreePath).toBe("/tmp/repo-a");
|
||||
return { worktreePath: "/tmp/repo-b", branch: "fusion/b" };
|
||||
},
|
||||
{ clearSingularWorktree: true },
|
||||
);
|
||||
|
||||
await Promise.resolve();
|
||||
expect(secondStarted).toBe(false);
|
||||
releaseFirst();
|
||||
const [, result] = await Promise.all([first, second]);
|
||||
|
||||
expect(Object.keys(result.workspaceWorktrees ?? {}).sort()).toEqual(["repo-a", "repo-b", "repo-c"]);
|
||||
expect(result).toEqual(expect.objectContaining({ branchWriteOrigin: "engine" }));
|
||||
expect(result.worktree).toBeNull();
|
||||
expect(result.branch).toBeNull();
|
||||
expect(result.executionStartBranch).toBeNull();
|
||||
expect(result.baseCommitSha).toBeNull();
|
||||
});
|
||||
|
||||
it("selects its eligible executor from the role pool", async () => {
|
||||
const store = createMockStore();
|
||||
const fixture = createWorkflowRoutingAgentStore(store);
|
||||
|
||||
@@ -2673,6 +2673,11 @@ describe("SelfHealingManager", () => {
|
||||
expect(store.archiveTaskAndCleanup).not.toHaveBeenCalledWith("FN-100");
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:SelfHealing 2026-08-21-08:44:
|
||||
Archive retry tests must drive past the prior failure budget on one manager instance. Separate
|
||||
task IDs make same-reason exhaustion and failure-class reset independently observable.
|
||||
*/
|
||||
it("bounds same-reason archive failures and resets the budget when the failure class changes", async () => {
|
||||
vi.setSystemTime(new Date("2026-01-04T00:00:00.000Z"));
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
@@ -2680,28 +2685,29 @@ describe("SelfHealingManager", () => {
|
||||
autoArchiveDoneAfterMs: 24 * 60 * 60 * 1000,
|
||||
doneAutoArchiveDays: 0,
|
||||
} as unknown as Settings);
|
||||
const stale = [{ id: "FN-RETRY", column: "done", columnMovedAt: "2026-01-02T00:00:00.000Z", updatedAt: "2026-01-02T00:00:00.000Z" }];
|
||||
const stale = [
|
||||
{ id: "FN-BOUNDED", column: "done", columnMovedAt: "2026-01-02T00:00:00.000Z", updatedAt: "2026-01-02T00:00:00.000Z" },
|
||||
{ id: "FN-CHANGING", column: "done", columnMovedAt: "2026-01-02T00:00:00.000Z", updatedAt: "2026-01-02T00:00:00.000Z" },
|
||||
];
|
||||
(store.listTasks as ReturnType<typeof vi.fn>).mockResolvedValue(stale);
|
||||
(store.archiveTaskAndCleanup as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("disk busy"));
|
||||
const changingFailures = [
|
||||
new Error("disk busy"),
|
||||
new Error("disk busy"),
|
||||
Object.assign(new Error("live"), { name: "TaskIsLiveError" }),
|
||||
new Error("disk busy"),
|
||||
new Error("disk busy"),
|
||||
new Error("disk busy"),
|
||||
];
|
||||
(store.archiveTaskAndCleanup as ReturnType<typeof vi.fn>).mockImplementation(async (id: string) => {
|
||||
if (id === "FN-BOUNDED") throw new Error("disk busy");
|
||||
throw changingFailures.shift() ?? new Error("disk busy");
|
||||
});
|
||||
|
||||
for (let index = 0; index < 10; index++) await manager.archiveStaleDoneTasks();
|
||||
|
||||
expect(store.archiveTaskAndCleanup).toHaveBeenCalledTimes(3);
|
||||
|
||||
(store.archiveTaskAndCleanup as ReturnType<typeof vi.fn>).mockClear();
|
||||
const taskLive = Object.assign(new Error("live"), { name: "TaskIsLiveError" });
|
||||
(store.archiveTaskAndCleanup as ReturnType<typeof vi.fn>)
|
||||
.mockRejectedValueOnce(new Error("disk busy"))
|
||||
.mockRejectedValueOnce(taskLive)
|
||||
.mockRejectedValueOnce(new Error("disk busy"));
|
||||
const managerWithChangingFailure = new SelfHealingManager(store, { rootDir: "/tmp/test-project" });
|
||||
|
||||
await managerWithChangingFailure.archiveStaleDoneTasks();
|
||||
await managerWithChangingFailure.archiveStaleDoneTasks();
|
||||
await managerWithChangingFailure.archiveStaleDoneTasks();
|
||||
|
||||
expect(store.archiveTaskAndCleanup).toHaveBeenCalledTimes(3);
|
||||
managerWithChangingFailure.stop();
|
||||
const calls = (store.archiveTaskAndCleanup as ReturnType<typeof vi.fn>).mock.calls;
|
||||
expect(calls.filter(([id]) => id === "FN-BOUNDED")).toHaveLength(3);
|
||||
expect(calls.filter(([id]) => id === "FN-CHANGING")).toHaveLength(6);
|
||||
});
|
||||
|
||||
it("escalates an exhausted archive budget once without letting log or audit failures stop other archives", async () => {
|
||||
@@ -2742,23 +2748,29 @@ describe("SelfHealingManager", () => {
|
||||
autoArchiveDoneAfterMs: 24 * 60 * 60 * 1000,
|
||||
doneAutoArchiveDays: 0,
|
||||
} as unknown as Settings);
|
||||
const stale = [{ id: "FN-RESET", column: "done", columnMovedAt: "2026-01-02T00:00:00.000Z", updatedAt: "2026-01-02T00:00:00.000Z" }];
|
||||
const successTask = { id: "FN-RESET-SUCCESS", column: "done", columnMovedAt: "2026-01-02T00:00:00.000Z", updatedAt: "2026-01-02T00:00:00.000Z" };
|
||||
const candidateTask = { id: "FN-RESET-CANDIDATE", column: "done", columnMovedAt: "2026-01-02T00:00:00.000Z", updatedAt: "2026-01-02T00:00:00.000Z" };
|
||||
(store.listTasks as ReturnType<typeof vi.fn>)
|
||||
.mockResolvedValueOnce(stale)
|
||||
.mockResolvedValueOnce(stale)
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce(stale);
|
||||
(store.archiveTaskAndCleanup as ReturnType<typeof vi.fn>)
|
||||
.mockRejectedValueOnce(new Error("disk busy"))
|
||||
.mockResolvedValueOnce({})
|
||||
.mockRejectedValueOnce(new Error("disk busy"));
|
||||
.mockResolvedValueOnce([successTask, candidateTask])
|
||||
.mockResolvedValueOnce([successTask, candidateTask])
|
||||
.mockResolvedValueOnce([successTask])
|
||||
.mockResolvedValueOnce([successTask, candidateTask])
|
||||
.mockResolvedValueOnce([successTask, candidateTask])
|
||||
.mockResolvedValueOnce([successTask, candidateTask]);
|
||||
let successCalls = 0;
|
||||
(store.archiveTaskAndCleanup as ReturnType<typeof vi.fn>).mockImplementation(async (id: string) => {
|
||||
if (id === "FN-RESET-SUCCESS") {
|
||||
successCalls++;
|
||||
if (successCalls === 3) return {};
|
||||
}
|
||||
throw new Error("disk busy");
|
||||
});
|
||||
|
||||
await manager.archiveStaleDoneTasks();
|
||||
await manager.archiveStaleDoneTasks();
|
||||
await manager.archiveStaleDoneTasks();
|
||||
await manager.archiveStaleDoneTasks();
|
||||
for (let index = 0; index < 6; index++) await manager.archiveStaleDoneTasks();
|
||||
|
||||
expect(store.archiveTaskAndCleanup).toHaveBeenCalledTimes(3);
|
||||
const calls = (store.archiveTaskAndCleanup as ReturnType<typeof vi.fn>).mock.calls;
|
||||
expect(calls.filter(([id]) => id === "FN-RESET-SUCCESS")).toHaveLength(6);
|
||||
expect(calls.filter(([id]) => id === "FN-RESET-CANDIDATE")).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("skips stale done lineage parents, including complete children, without blocking unrelated archives", async () => {
|
||||
@@ -4251,6 +4263,11 @@ describe("SelfHealingManager", () => {
|
||||
worktree: null,
|
||||
sessionFile: null,
|
||||
}));
|
||||
const workspacePatch = (store.updateTask as ReturnType<typeof vi.fn>).mock.calls
|
||||
.find(([taskId]) => taskId === "FN-7802-WORKSPACE")?.[1];
|
||||
// FNXC:WorkspaceRecovery 2026-08-21-08:44: Prove recovery emitted the workspace patch before asserting that it preserves repository routing.
|
||||
expect(workspacePatch).toBeDefined();
|
||||
expect(workspacePatch).not.toHaveProperty("branch");
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-7802-WORKSPACE", "todo", { preserveProgress: true, moveSource: "engine", recoveryRehome: true });
|
||||
expect(store.recordRunAuditEvent).toHaveBeenCalledWith(expect.objectContaining({
|
||||
mutationType: "task:reconcile-missing-worktree-merge-active",
|
||||
|
||||
@@ -19,6 +19,12 @@ const describeIfGit = hasGit ? describe : describe.skip;
|
||||
|
||||
function makeFakeStore(task: Task): { store: TaskStore; current: () => Task } {
|
||||
let current = task;
|
||||
/*
|
||||
FNXC:WorkspaceRootRouting 2026-08-21-08:34:
|
||||
This focused routing fake mirrors TaskStore's per-task merge lock and post-callback authoritative
|
||||
re-read so callback-based acquisitions cannot clobber sibling entries or external task updates.
|
||||
*/
|
||||
let workspaceMergeTail = Promise.resolve();
|
||||
const store = {
|
||||
async updateTask(id: string, patch: Partial<Task>): Promise<void> {
|
||||
if (id === current.id) current = { ...current, ...patch };
|
||||
@@ -26,20 +32,46 @@ function makeFakeStore(task: Task): { store: TaskStore; current: () => Task } {
|
||||
async mergeWorkspaceWorktreeEntry(
|
||||
id: string,
|
||||
repoRelPath: string,
|
||||
patch: Partial<NonNullable<Task["workspaceWorktrees"]>[string]>,
|
||||
options?: { clearSingularWorktree?: boolean },
|
||||
patch: Partial<NonNullable<Task["workspaceWorktrees"]>[string]>
|
||||
| ((freshTask: Task) => Promise<Partial<NonNullable<Task["workspaceWorktrees"]>[string]>>),
|
||||
options?: {
|
||||
requireExistingEntry?: boolean;
|
||||
clearSingularWorktree?: boolean;
|
||||
validateBeforePersist?: (freshTask: Task) => Promise<void>;
|
||||
},
|
||||
): Promise<Task> {
|
||||
if (id !== current.id) throw new Error(`Task ${id} not found`);
|
||||
const existing = current.workspaceWorktrees?.[repoRelPath];
|
||||
current = {
|
||||
...current,
|
||||
workspaceWorktrees: {
|
||||
...(current.workspaceWorktrees ?? {}),
|
||||
[repoRelPath]: { ...existing, ...patch },
|
||||
},
|
||||
...(options?.clearSingularWorktree ? { worktree: undefined, branch: undefined } : {}),
|
||||
};
|
||||
return current;
|
||||
const operation = workspaceMergeTail.then(async () => {
|
||||
if (id !== current.id) throw new Error(`Task ${id} not found`);
|
||||
const existing = current.workspaceWorktrees?.[repoRelPath];
|
||||
if (options?.requireExistingEntry && !existing) return current;
|
||||
const callbackTask = current;
|
||||
const resolvedPatch = typeof patch === "function" ? await patch(callbackTask) : patch;
|
||||
const freshExisting = current.workspaceWorktrees?.[repoRelPath];
|
||||
if (options?.requireExistingEntry && !freshExisting) return current;
|
||||
await options?.validateBeforePersist?.(current);
|
||||
current = {
|
||||
...current,
|
||||
workspaceWorktrees: {
|
||||
...(current.workspaceWorktrees ?? {}),
|
||||
[repoRelPath]: {
|
||||
...freshExisting,
|
||||
...resolvedPatch,
|
||||
} as NonNullable<Task["workspaceWorktrees"]>[string],
|
||||
},
|
||||
...(options?.clearSingularWorktree
|
||||
? {
|
||||
worktree: null,
|
||||
branch: null,
|
||||
branchWriteOrigin: "engine" as const,
|
||||
executionStartBranch: null,
|
||||
baseCommitSha: null,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
return current;
|
||||
});
|
||||
workspaceMergeTail = operation.then(() => undefined, () => undefined);
|
||||
return operation;
|
||||
},
|
||||
async logEntry(): Promise<void> {},
|
||||
async getTask(id: string): Promise<Task> {
|
||||
@@ -65,6 +97,73 @@ function makeTask(id: string): Task {
|
||||
} as Task;
|
||||
}
|
||||
|
||||
describe("workspace root routing store fake", () => {
|
||||
/*
|
||||
FNXC:WorkspaceRootRouting 2026-08-21-08:34:
|
||||
The focused routing fake must hold the same concurrent-merge invariant as the shared executor fake;
|
||||
exercising both surfaces prevents one fixture from silently reverting to stale snapshot writes.
|
||||
|
||||
FNXC:WorkspaceRootRouting 2026-08-21-16:42:
|
||||
Clearing singular routing metadata must return null on this focused fake, matching persisted TaskStore
|
||||
reads and the shared executor fake instead of exposing fixture-only undefined values.
|
||||
*/
|
||||
it("serializes callback merges without dropping sibling repository entries", async () => {
|
||||
const task = makeTask("FN-routing-merge");
|
||||
Object.assign(task, {
|
||||
worktree: "/tmp/singular",
|
||||
branch: "fusion/singular",
|
||||
executionStartBranch: "main",
|
||||
baseCommitSha: "base",
|
||||
});
|
||||
const { store, current } = makeFakeStore(task);
|
||||
let releaseFirst!: () => void;
|
||||
let markFirstStarted!: () => void;
|
||||
const firstGate = new Promise<void>((resolve) => { releaseFirst = resolve; });
|
||||
const firstStarted = new Promise<void>((resolve) => { markFirstStarted = resolve; });
|
||||
|
||||
const first = store.mergeWorkspaceWorktreeEntry(
|
||||
"FN-routing-merge",
|
||||
"repo-a",
|
||||
async () => {
|
||||
markFirstStarted();
|
||||
await firstGate;
|
||||
return { worktreePath: "/tmp/repo-a", branch: "fusion/a" };
|
||||
},
|
||||
);
|
||||
await firstStarted;
|
||||
await store.updateTask("FN-routing-merge", {
|
||||
workspaceWorktrees: {
|
||||
"repo-c": { worktreePath: "/tmp/repo-c", branch: "fusion/c" },
|
||||
},
|
||||
});
|
||||
|
||||
let secondStarted = false;
|
||||
const second = store.mergeWorkspaceWorktreeEntry(
|
||||
"FN-routing-merge",
|
||||
"repo-b",
|
||||
async (freshTask) => {
|
||||
secondStarted = true;
|
||||
expect(freshTask.workspaceWorktrees?.["repo-a"]?.worktreePath).toBe("/tmp/repo-a");
|
||||
return { worktreePath: "/tmp/repo-b", branch: "fusion/b" };
|
||||
},
|
||||
{ clearSingularWorktree: true },
|
||||
);
|
||||
|
||||
await Promise.resolve();
|
||||
expect(secondStarted).toBe(false);
|
||||
releaseFirst();
|
||||
await Promise.all([first, second]);
|
||||
expect(Object.keys(current().workspaceWorktrees ?? {}).sort()).toEqual(["repo-a", "repo-b", "repo-c"]);
|
||||
expect(current()).toEqual(expect.objectContaining({
|
||||
worktree: null,
|
||||
branch: null,
|
||||
branchWriteOrigin: "engine",
|
||||
executionStartBranch: null,
|
||||
baseCommitSha: null,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
const settings: Partial<Settings> = {
|
||||
worktreeNaming: "task-id",
|
||||
commitMsgHookEnabled: true,
|
||||
@@ -127,7 +226,7 @@ describeIfGit("FN-034 workspace root worktree routing", { timeout: 60_000 }, ()
|
||||
|
||||
expect(result.coordinatorWorktreePath).toBe(result.task.workspaceWorktrees?.["repo-b"]?.worktreePath);
|
||||
expect(result.coordinatorWorktreePath).not.toContain(join(fixture.rootDir, ".worktrees"));
|
||||
expect(result.task.worktree).toBeUndefined();
|
||||
expect(result.task.worktree).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects a stale review target instead of falling back to another repository", async () => {
|
||||
|
||||
@@ -244,6 +244,7 @@ describe("acquireTaskWorktree", () => {
|
||||
writeFileSync(join(orphanedPath, "preserved.txt"), "keep this checkout\n", "utf-8");
|
||||
git(orphanedPath, "git add preserved.txt");
|
||||
git(orphanedPath, 'git commit -m "FN-1: preserved pre-fix work"');
|
||||
const canonicalOrphanedPath = realpathSync(orphanedPath);
|
||||
const preservedTip = git(orphanedPath, "git rev-parse HEAD");
|
||||
const createWorktree = vi.fn();
|
||||
|
||||
@@ -255,11 +256,11 @@ describe("acquireTaskWorktree", () => {
|
||||
createWorktree,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({ worktreePath: orphanedPath, branch: "fusion/fn-1", source: "existing" });
|
||||
expect(result).toMatchObject({ worktreePath: canonicalOrphanedPath, branch: "fusion/fn-1", source: "existing" });
|
||||
expect(git(orphanedPath, "git rev-parse HEAD")).toBe(preservedTip);
|
||||
expect(createWorktree).not.toHaveBeenCalled();
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-1", {
|
||||
worktree: orphanedPath,
|
||||
worktree: canonicalOrphanedPath,
|
||||
branch: "fusion/fn-1",
|
||||
branchWriteOrigin: "engine",
|
||||
});
|
||||
|
||||
@@ -91,23 +91,7 @@ export function effectiveArgCount(args) {
|
||||
const PREFILTER = /(olumnFlags|ifecycleColumns|eviewColumns|erminalColumns|lannerLanes|isplayColumnOptions)/;
|
||||
|
||||
/** Known-unsupplied seams, each with why it is tolerated. Shrink this list; never grow it casually. */
|
||||
const ALLOWED = new Map([
|
||||
/*
|
||||
TEMPORARY — real offenders in packages owned by other batches, reported to them rather than edited
|
||||
from outside. Remove each entry when that batch wires or deletes the parameter; the check will then
|
||||
start guarding those files too. All three are the same shape this check exists to catch.
|
||||
*/
|
||||
[
|
||||
"isRecoverableMissingWorktreeReviewFailure",
|
||||
"No production caller; 5 test call sites. The previous entry blamed the scanner for excluding "
|
||||
+ "__tests__ — that reason was wrong, the scan now reads tests and the count is real. The two "
|
||||
+ "SIBLINGS it delegates to (`...WithProgress` / `...NoProgress`) are the live pair, called from "
|
||||
+ "self-healing.ts and both supplying `reviewColumns`. This is the convenience wrapper over them, "
|
||||
+ "kept as a public predicate and exercised only by its own tests. Engine-owned; left alone.",
|
||||
],
|
||||
|
||||
|
||||
]);
|
||||
const ALLOWED = new Map([]);
|
||||
|
||||
/*
|
||||
PARTIAL-SUPPLY exemptions are keyed by CALL SITE, not by function name.
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"packages/core/src/task-store/task-update.ts": 1,
|
||||
"packages/engine/src/merge/auto-merge-finalization.ts": 1,
|
||||
"packages/engine/src/project-engine.ts": 1,
|
||||
"packages/engine/src/self-healing.ts": 1,
|
||||
"packages/engine/src/runtimes/in-process-runtime.ts": 1,
|
||||
"packages/engine/src/self-healing.ts": 1,
|
||||
"packages/dashboard/src/routes/register-task-workflow-routes.ts": 1,
|
||||
|
||||
Reference in New Issue
Block a user