## 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 -->
1384 lines
60 KiB
TypeScript
1384 lines
60 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { execSync } from "node:child_process";
|
|
import { randomUUID } from "node:crypto";
|
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, symlinkSync, utimesSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { acquireWorktreePathReservation } from "@fusion/core";
|
|
import { acquireTaskWorktree, RepoRootWorktreeError, WorktreeBaseRefreshError } from "../worktree/worktree-acquisition.js";
|
|
import { classifyTaskWorktree, PoolDoubleLeaseError } from "../worktree/worktree-pool.js";
|
|
import * as desktopArtifacts from "../worktree/worktree-desktop-artifacts.js";
|
|
import * as branchConflicts from "../execution/branch-conflicts.js";
|
|
import { activeSessionRegistry } from "../agents/active-session-registry.js";
|
|
import { NativeWorktreeBackend } from "../worktree/worktree-backend.js";
|
|
|
|
|
|
vi.mock("../worktree/worktree-pool.js", async () => {
|
|
const actual = await vi.importActual<any>("../worktree/worktree-pool.js");
|
|
return {
|
|
...actual,
|
|
classifyTaskWorktree: vi.fn().mockResolvedValue({ ok: true }),
|
|
isInsideWorktreesDir: vi.fn().mockReturnValue(true),
|
|
};
|
|
});
|
|
|
|
vi.mock("../execution/branch-conflicts.js", async () => {
|
|
const actual = await vi.importActual<any>("../execution/branch-conflicts.js");
|
|
return {
|
|
...actual,
|
|
classifyBootstrapMisbinding: vi.fn().mockResolvedValue({
|
|
isBootstrapMisbinding: false,
|
|
ownCommitCount: 0,
|
|
foreignCommitCount: 0,
|
|
nonAttributedCount: 0,
|
|
}),
|
|
reanchorBranchToBase: vi.fn().mockResolvedValue({ previousTipSha: "abc", newTipSha: "def" }),
|
|
};
|
|
});
|
|
|
|
vi.mock("../worktree/worktree-db-hydrate.js", () => ({
|
|
hydrateWorktreeDb: vi.fn().mockResolvedValue({ degraded: false, tasksCopied: 1, documentsCopied: 1, artifactsCopied: 0 }),
|
|
}));
|
|
|
|
vi.mock("../worktree/worktree-desktop-artifacts.js", () => ({
|
|
removeDesktopBuildArtifacts: vi.fn().mockResolvedValue({ removed: [], skipped: [], failures: [] }),
|
|
}));
|
|
|
|
/*
|
|
FNXC:EngineTests 2026-07-21-00:10:
|
|
Pool unit tests use temp dirs that are not real git worktrees. installTaskWorktreeIdentityGuard
|
|
resolves git paths and throws there, which the pool catch treats as prepare failure and falls
|
|
through to fresh. No-op the guard so classification + pool wiring stay under test.
|
|
*/
|
|
vi.mock("../worktree/worktree-hooks.js", async () => {
|
|
const actual = await vi.importActual<any>("../worktree/worktree-hooks.js");
|
|
return {
|
|
...actual,
|
|
installTaskWorktreeIdentityGuard: vi.fn().mockResolvedValue(undefined),
|
|
};
|
|
});
|
|
|
|
const cleanupPaths: string[] = [];
|
|
function track(path: string): string {
|
|
cleanupPaths.push(path);
|
|
return path;
|
|
}
|
|
|
|
function git(cwd: string, command: string): string {
|
|
return execSync(command, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
}
|
|
|
|
function makeRepo(): string {
|
|
const rootDir = track(mkdtempSync(join(tmpdir(), "fn-6861-acquisition-root-")));
|
|
git(rootDir, "git init -b main");
|
|
git(rootDir, 'git config user.email "test@example.com"');
|
|
git(rootDir, 'git config user.name "Test User"');
|
|
writeFileSync(join(rootDir, "README.md"), "root\n", "utf-8");
|
|
git(rootDir, "git add README.md");
|
|
git(rootDir, 'git commit -m "init"');
|
|
return rootDir;
|
|
}
|
|
|
|
function seedPreservedOrphans(recoveryRoot: string, count: number): string[] {
|
|
mkdirSync(recoveryRoot, { recursive: true });
|
|
return Array.from({ length: count }, (_, index) => {
|
|
const name = `fn-${100 + index}-${randomUUID()}`;
|
|
const path = join(recoveryRoot, name);
|
|
mkdirSync(path);
|
|
writeFileSync(join(path, "artifact"), `${index}\n`, "utf-8");
|
|
const timestamp = new Date(Date.now() - (count - index) * 60_000);
|
|
utimesSync(path, timestamp, timestamp);
|
|
return name;
|
|
});
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
activeSessionRegistry.clear();
|
|
for (const path of cleanupPaths.splice(0)) {
|
|
rmSync(path, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe("acquireTaskWorktree", () => {
|
|
const task = {
|
|
id: "FN-1",
|
|
title: "Task",
|
|
description: "Desc",
|
|
branch: null,
|
|
worktree: null,
|
|
} as any;
|
|
|
|
let store: any;
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
activeSessionRegistry.clear();
|
|
/*
|
|
FNXC:EngineTests 2026-07-20-23:55:
|
|
clearAllMocks wipes the hoisted classifyTaskWorktree mockResolvedValue({ ok: true }).
|
|
Re-arm it every test so pool acquisition does not treat undefined as unusable and fall through to fresh.
|
|
*/
|
|
vi.mocked(classifyTaskWorktree).mockResolvedValue({ ok: true });
|
|
vi.mocked(desktopArtifacts.removeDesktopBuildArtifacts).mockResolvedValue({ removed: [], skipped: [], failures: [] });
|
|
store = {
|
|
updateTask: vi.fn().mockResolvedValue(undefined),
|
|
logEntry: vi.fn().mockResolvedValue(undefined),
|
|
};
|
|
});
|
|
|
|
it("reuses existing usable worktree", async () => {
|
|
const worktreePath = process.cwd();
|
|
const result = await acquireTaskWorktree({
|
|
task: { ...task, worktree: worktreePath, branch: "fusion/fn-1" },
|
|
rootDir: dirname(worktreePath),
|
|
store,
|
|
settings: {},
|
|
createWorktree: vi.fn().mockResolvedValue({ path: "/tmp/fn-worktree-fallback", branch: "fusion/fn-1" }),
|
|
});
|
|
expect(result.source).toBe("existing");
|
|
expect(result.worktreePath).toBe(worktreePath);
|
|
});
|
|
|
|
// Regression: FN-5475 — when a resumed worktree's branch was created from
|
|
// a poisoned local-main tip carrying a sibling task's commits and has zero
|
|
// commits of its own, acquireTaskWorktree must re-anchor inline so the
|
|
// executor preflight doesn't pause on contamination forever.
|
|
it("re-anchors a resumed branch when classified as bootstrap-misbinding", async () => {
|
|
const audit = { git: vi.fn().mockResolvedValue(undefined), filesystem: vi.fn() } as any;
|
|
vi.mocked(branchConflicts.classifyBootstrapMisbinding).mockResolvedValueOnce({
|
|
isBootstrapMisbinding: true,
|
|
ownCommitCount: 0,
|
|
foreignCommitCount: 2,
|
|
nonAttributedCount: 0,
|
|
});
|
|
|
|
const worktreePath = process.cwd();
|
|
const result = await acquireTaskWorktree({
|
|
task: { ...task, worktree: worktreePath, branch: "fusion/fn-1" },
|
|
rootDir: dirname(worktreePath),
|
|
store,
|
|
settings: {},
|
|
audit,
|
|
createWorktree: vi.fn().mockResolvedValue({ path: "/tmp/fn-worktree-fallback", branch: "fusion/fn-1" }),
|
|
});
|
|
|
|
expect(result.source).toBe("existing");
|
|
expect(vi.mocked(branchConflicts.reanchorBranchToBase)).toHaveBeenCalledTimes(1);
|
|
expect(audit.git).toHaveBeenCalledWith(expect.objectContaining({
|
|
type: "branch:reanchor",
|
|
metadata: expect.objectContaining({ trigger: "resume-misbinding" }),
|
|
}));
|
|
});
|
|
|
|
it("does not re-anchor a resumed branch when not misbound", async () => {
|
|
const worktreePath = process.cwd();
|
|
const result = await acquireTaskWorktree({
|
|
task: { ...task, worktree: worktreePath, branch: "fusion/fn-1" },
|
|
rootDir: dirname(worktreePath),
|
|
store,
|
|
settings: {},
|
|
createWorktree: vi.fn().mockResolvedValue({ path: "/tmp/fn-worktree-fallback", branch: "fusion/fn-1" }),
|
|
});
|
|
expect(result.source).toBe("existing");
|
|
expect(vi.mocked(branchConflicts.reanchorBranchToBase)).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("derives distinct per-task working branches for shared branch-group members", async () => {
|
|
const createWorktree = vi.fn(async (branchName: string, worktreePath: string) => ({ path: worktreePath, branch: branchName }));
|
|
const sharedBranch = "clionboarding";
|
|
const sharedContext = { assignmentMode: "shared", groupId: "BG-1", source: "planning" } as const;
|
|
|
|
const [first, second] = await Promise.all([
|
|
acquireTaskWorktree({
|
|
task: { ...task, id: "FN-100", worktree: null, branch: sharedBranch, branchContext: sharedContext },
|
|
rootDir: process.cwd(),
|
|
store,
|
|
settings: {},
|
|
createWorktree,
|
|
}),
|
|
acquireTaskWorktree({
|
|
task: { ...task, id: "FN-101", worktree: null, branch: sharedBranch, branchContext: sharedContext },
|
|
rootDir: process.cwd(),
|
|
store,
|
|
settings: {},
|
|
createWorktree,
|
|
}),
|
|
]);
|
|
|
|
expect(first.branch).toBe("fusion/fn-100");
|
|
expect(second.branch).toBe("fusion/fn-101");
|
|
expect(first.branch).not.toBe(second.branch);
|
|
expect(createWorktree).toHaveBeenCalledWith("fusion/fn-100", expect.any(String), "FN-100", "main", false);
|
|
expect(createWorktree).toHaveBeenCalledWith("fusion/fn-101", expect.any(String), "FN-101", "main", false);
|
|
});
|
|
|
|
it("keeps per-task-derived and ungrouped branch derivation unchanged", async () => {
|
|
const createWorktree = vi.fn(async (branchName: string, worktreePath: string) => ({ path: worktreePath, branch: branchName }));
|
|
|
|
const perTaskDerived = await acquireTaskWorktree({
|
|
task: { ...task, id: "FN-102", worktree: null, branch: "fusion/custom-derived", branchContext: { assignmentMode: "per-task-derived", groupId: "BG-1", source: "planning" } },
|
|
rootDir: process.cwd(),
|
|
store,
|
|
settings: {},
|
|
createWorktree,
|
|
});
|
|
|
|
const ungrouped = await acquireTaskWorktree({
|
|
task: { ...task, id: "FN-103", worktree: null, branch: null },
|
|
rootDir: process.cwd(),
|
|
store,
|
|
settings: {},
|
|
createWorktree,
|
|
});
|
|
|
|
expect(perTaskDerived.branch).toBe("fusion/custom-derived");
|
|
expect(ungrouped.branch).toBe("fusion/fn-103");
|
|
});
|
|
|
|
it("adopts a registered pre-fix task checkout before attempting fresh creation", async () => {
|
|
const rootDir = makeRepo();
|
|
const orphanedPath = join(rootDir, ".worktrees", "pre-fix-fn-1");
|
|
mkdirSync(dirname(orphanedPath), { recursive: true });
|
|
git(rootDir, `git worktree add -b fusion/fn-1 ${JSON.stringify(orphanedPath)} main`);
|
|
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();
|
|
|
|
const result = await acquireTaskWorktree({
|
|
task: { ...task, worktree: null, branch: null },
|
|
rootDir,
|
|
store,
|
|
settings: {},
|
|
createWorktree,
|
|
});
|
|
|
|
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: canonicalOrphanedPath,
|
|
branch: "fusion/fn-1",
|
|
branchWriteOrigin: "engine",
|
|
});
|
|
});
|
|
|
|
it("persists a fresh canonical assignment through strict provenance validation", async () => {
|
|
const rootDir = makeRepo();
|
|
const strictStore = {
|
|
...store,
|
|
updateTask: vi.fn(async (_id: string, patch: Record<string, unknown>) => {
|
|
if (patch.branch !== undefined && patch.branchWriteOrigin === undefined) {
|
|
throw new Error("branchWriteOrigin is required when branch is provided");
|
|
}
|
|
}),
|
|
};
|
|
|
|
const result = await acquireTaskWorktree({
|
|
task: { ...task, worktree: null, branch: null },
|
|
rootDir,
|
|
store: strictStore,
|
|
settings: {},
|
|
});
|
|
|
|
expect(result.source).toBe("fresh");
|
|
expect(strictStore.updateTask).toHaveBeenCalledWith("FN-1", expect.objectContaining({
|
|
worktree: result.worktreePath,
|
|
branch: "fusion/fn-1",
|
|
branchWriteOrigin: "engine",
|
|
}));
|
|
});
|
|
|
|
it("compensates a post-create assignment failure without deleting the task branch", async () => {
|
|
const rootDir = makeRepo();
|
|
let createdPath: string | undefined;
|
|
const rejectingStore = {
|
|
...store,
|
|
updateTask: vi.fn().mockRejectedValue(new Error("simulated assignment persistence failure")),
|
|
};
|
|
const createWorktree = vi.fn(async (branchName: string, path: string, _taskId: string, startPoint?: string) => {
|
|
createdPath = path;
|
|
git(rootDir, `git worktree add -b ${branchName} ${JSON.stringify(path)} ${startPoint ?? "main"}`);
|
|
return { path, branch: branchName };
|
|
});
|
|
|
|
await expect(acquireTaskWorktree({
|
|
task: { ...task, worktree: null, branch: null },
|
|
rootDir,
|
|
store: rejectingStore,
|
|
settings: {},
|
|
createWorktree,
|
|
})).rejects.toThrow("simulated assignment persistence failure");
|
|
|
|
expect(createdPath).toBeDefined();
|
|
expect(existsSync(createdPath!)).toBe(false);
|
|
expect(git(rootDir, "git show-ref --verify --quiet refs/heads/fusion/fn-1; echo $?")) .toBe("0");
|
|
});
|
|
|
|
it("creates fresh worktrees from the integration branch instead of ambient root HEAD", async () => {
|
|
const rootDir = makeRepo();
|
|
writeFileSync(join(rootDir, "foreign.txt"), "foreign\n", "utf-8");
|
|
git(rootDir, "git checkout -b fusion/fn-foreign");
|
|
git(rootDir, "git add foreign.txt");
|
|
git(rootDir, 'git commit -m "FN-9999: foreign work"');
|
|
const foreignHead = git(rootDir, "git rev-parse HEAD");
|
|
const mainHead = git(rootDir, "git rev-parse main");
|
|
expect(foreignHead).not.toBe(mainHead);
|
|
|
|
const createWorktree = vi.fn(async (branchName: string, worktreePath: string, _taskId: string, startPoint?: string) => {
|
|
git(rootDir, `git worktree add -b ${branchName} ${JSON.stringify(worktreePath)} ${startPoint ?? ""}`);
|
|
return { path: worktreePath, branch: branchName };
|
|
});
|
|
|
|
const result = await acquireTaskWorktree({
|
|
task: { ...task, id: "FN-200", worktree: null, branch: null },
|
|
rootDir,
|
|
store,
|
|
settings: {},
|
|
createWorktree,
|
|
});
|
|
|
|
expect(createWorktree).toHaveBeenCalledWith("fusion/fn-200", expect.any(String), "FN-200", "main", false);
|
|
expect(git(result.worktreePath, "git rev-parse HEAD")).toBe(mainHead);
|
|
});
|
|
|
|
it("acquires from pool when enabled", async () => {
|
|
const prepareForTask = vi.fn().mockResolvedValue({ branch: "fusion/fn-1", worktreePath: "/tmp/pooled", reclaimed: false });
|
|
const release = vi.fn();
|
|
const result = await acquireTaskWorktree({
|
|
task,
|
|
rootDir: process.cwd(),
|
|
store,
|
|
settings: { recycleWorktrees: true } as any,
|
|
pool: {
|
|
acquire: (_taskId: string) => "/tmp/pooled",
|
|
prepareForTask,
|
|
release,
|
|
} as any,
|
|
createWorktree: vi.fn().mockResolvedValue({ path: "/tmp/fn-worktree-fallback", branch: "fusion/fn-1" }),
|
|
});
|
|
expect(release).not.toHaveBeenCalled();
|
|
expect(result.source).toBe("pool");
|
|
expect(prepareForTask).toHaveBeenCalledWith(
|
|
"/tmp/pooled",
|
|
"fusion/fn-1",
|
|
"main",
|
|
expect.objectContaining({ requestingTaskId: "FN-1" }),
|
|
);
|
|
expect(store.updateTask).toHaveBeenCalledWith("FN-1", { worktree: "/tmp/pooled", branch: "fusion/fn-1", branchWriteOrigin: "engine" });
|
|
expect(desktopArtifacts.removeDesktopBuildArtifacts).toHaveBeenCalledWith("/tmp/pooled", undefined);
|
|
});
|
|
|
|
it("releases acquired pooled worktree when prepareForTask returns reclaimed path", async () => {
|
|
const release = vi.fn();
|
|
await acquireTaskWorktree({
|
|
task,
|
|
rootDir: process.cwd(),
|
|
store,
|
|
settings: { recycleWorktrees: true } as any,
|
|
pool: {
|
|
acquire: (_taskId: string) => "/tmp/pooled",
|
|
prepareForTask: vi.fn().mockResolvedValue({
|
|
branch: "fusion/fn-1",
|
|
worktreePath: "/tmp/live-existing",
|
|
reclaimed: true,
|
|
existingTipSha: "abc123",
|
|
strandedCommitCount: 2,
|
|
}),
|
|
release,
|
|
} as any,
|
|
createWorktree: vi.fn().mockResolvedValue({ path: "/tmp/fn-worktree-fallback", branch: "fusion/fn-1" }),
|
|
});
|
|
|
|
expect(release).toHaveBeenCalledWith("/tmp/pooled", "FN-1");
|
|
});
|
|
|
|
it("falls through to fresh creation when pooled worktree is incomplete and emits detection audit", async () => {
|
|
vi.mocked(classifyTaskWorktree).mockResolvedValueOnce({ ok: false, classification: "incomplete", reason: "missing or invalid .git metadata" });
|
|
const createWorktree = vi.fn().mockResolvedValue({ path: "/tmp/new", branch: "fusion/fn-1" });
|
|
const auditGit = vi.fn().mockResolvedValue(undefined);
|
|
const remove = vi.fn().mockResolvedValue(undefined);
|
|
|
|
const result = await acquireTaskWorktree({
|
|
task,
|
|
rootDir: process.cwd(),
|
|
store,
|
|
settings: { recycleWorktrees: true } as any,
|
|
pool: {
|
|
acquire: (_taskId: string) => "/tmp/pooled",
|
|
prepareForTask: vi.fn().mockResolvedValue({ branch: "fusion/fn-1", worktreePath: "/tmp/pooled", reclaimed: false }),
|
|
release: vi.fn(),
|
|
} as any,
|
|
createWorktree,
|
|
audit: { git: auditGit } as any,
|
|
backend: { kind: "native", create: vi.fn(), remove } as any,
|
|
});
|
|
|
|
expect(result.source).toBe("fresh");
|
|
expect(auditGit).toHaveBeenCalledWith(expect.objectContaining({
|
|
type: "worktree:incomplete-detected",
|
|
metadata: expect.objectContaining({ classification: "incomplete", source: "pool-acquire" }),
|
|
}));
|
|
expect(store.logEntry).toHaveBeenCalledWith("FN-1", expect.stringContaining("Pool returned incomplete worktree"), undefined, undefined);
|
|
expect(store.logEntry).not.toHaveBeenCalledWith("FN-1", expect.stringMatching(/Refusing to start coding agent/), expect.anything(), expect.anything());
|
|
});
|
|
|
|
it("emits resume detection audit and clears session file when assigned worktree is unregistered", async () => {
|
|
vi.mocked(classifyTaskWorktree).mockResolvedValueOnce({ ok: false, classification: "unregistered", reason: "not registered in git worktree list" });
|
|
const createWorktree = vi.fn().mockResolvedValue({ path: "/tmp/new", branch: "fusion/fn-1" });
|
|
const auditGit = vi.fn().mockResolvedValue(undefined);
|
|
|
|
await acquireTaskWorktree({
|
|
task: { ...task, worktree: process.cwd(), branch: "fusion/fn-1", sessionFile: "/tmp/session.json" },
|
|
rootDir: process.cwd(),
|
|
store,
|
|
settings: {} as any,
|
|
createWorktree,
|
|
audit: { git: auditGit } as any,
|
|
});
|
|
|
|
expect(auditGit).toHaveBeenCalledWith(expect.objectContaining({
|
|
type: "worktree:incomplete-detected",
|
|
metadata: expect.objectContaining({ classification: "unregistered", source: "resume" }),
|
|
}));
|
|
expect(store.updateTask).toHaveBeenCalledWith("FN-1", { worktree: null, branch: null, branchWriteOrigin: "engine", sessionFile: null });
|
|
});
|
|
|
|
it.each([
|
|
{ classification: "incomplete" as const, reason: "missing .git metadata" },
|
|
{ classification: "unregistered" as const, reason: "not registered in git worktree list" },
|
|
])("reclaims a $classification task-pinned directory before recreating the worktree", async ({ classification, reason }) => {
|
|
const rootDir = makeRepo();
|
|
const pinnedPath = join(rootDir, ".worktrees", "fn-1");
|
|
mkdirSync(join(pinnedPath, ".build"), { recursive: true });
|
|
mkdirSync(join(pinnedPath, ".swiftpm"), { recursive: true });
|
|
writeFileSync(join(pinnedPath, ".build", "cache"), "stale\n", "utf-8");
|
|
vi.mocked(classifyTaskWorktree).mockResolvedValueOnce({
|
|
ok: false,
|
|
classification,
|
|
reason,
|
|
});
|
|
|
|
const result = await acquireTaskWorktree({
|
|
task: { ...task, worktree: pinnedPath, branch: "fusion/fn-1" },
|
|
rootDir,
|
|
store,
|
|
settings: { worktreeNaming: "task-id", recycleWorktrees: false },
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
worktreePath: pinnedPath,
|
|
branch: "fusion/fn-1",
|
|
source: "fresh",
|
|
isResume: false,
|
|
});
|
|
expect(existsSync(join(pinnedPath, ".git"))).toBe(true);
|
|
expect(git(rootDir, "git worktree list --porcelain")).toContain(pinnedPath);
|
|
const recoveryRoot = join(rootDir, ".fusion", "recovery", "worktrees");
|
|
const preserved = readdirSync(recoveryRoot);
|
|
expect(preserved).toHaveLength(1);
|
|
expect(readFileSync(join(recoveryRoot, preserved[0], ".build", "cache"), "utf-8")).toBe("stale\n");
|
|
expect(store.logEntry).toHaveBeenCalledWith(
|
|
"FN-1",
|
|
expect.stringContaining("Preserved orphaned task-pinned directory"),
|
|
expect.stringContaining(join(".fusion", "recovery", "worktrees")),
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
it("preserves an orphan beside an external worktree root when project recovery is cross-device", async () => {
|
|
const rootDir = makeRepo();
|
|
const externalWorktrees = track(mkdtempSync(join(tmpdir(), "fn-external-worktrees-")));
|
|
const pinnedPath = join(externalWorktrees, "fn-1");
|
|
mkdirSync(join(pinnedPath, ".build"), { recursive: true });
|
|
writeFileSync(join(pinnedPath, ".build", "cache"), "stale\n", "utf-8");
|
|
vi.mocked(classifyTaskWorktree).mockResolvedValueOnce({
|
|
ok: false,
|
|
classification: "incomplete",
|
|
reason: "missing .git metadata",
|
|
});
|
|
const actualFs = await vi.importActual<typeof import("node:fs/promises")>("node:fs/promises");
|
|
const renameWorktreeDirectory = vi.fn()
|
|
.mockRejectedValueOnce(Object.assign(new Error("cross-device link"), { code: "EXDEV" }))
|
|
.mockImplementationOnce(actualFs.rename);
|
|
|
|
const result = await acquireTaskWorktree({
|
|
task: { ...task, worktree: pinnedPath, branch: "fusion/fn-1" },
|
|
rootDir,
|
|
store,
|
|
settings: { worktreeNaming: "task-id", worktreesDir: externalWorktrees, recycleWorktrees: false },
|
|
renameWorktreeDirectory,
|
|
});
|
|
|
|
const recoveryRoot = join(externalWorktrees, ".fusion-recovery", "worktrees");
|
|
const preservedPath = join(recoveryRoot, readdirSync(recoveryRoot)[0]);
|
|
expect(result.worktreePath).toBe(pinnedPath);
|
|
expect(readFileSync(join(preservedPath, ".build", "cache"), "utf-8")).toBe("stale\n");
|
|
expect(renameWorktreeDirectory).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("retains only the newest ten generated orphan directories in the primary recovery root", async () => {
|
|
const rootDir = makeRepo();
|
|
const pinnedPath = join(rootDir, ".worktrees", "fn-1");
|
|
const recoveryRoot = join(rootDir, ".fusion", "recovery", "worktrees");
|
|
const seeded = seedPreservedOrphans(recoveryRoot, 11);
|
|
const unknownPath = join(recoveryRoot, "operator-notes");
|
|
const symlinkTarget = track(mkdtempSync(join(tmpdir(), "fn-orphan-retention-symlink-target-")));
|
|
const generatedSymlink = join(recoveryRoot, `fn-999-${randomUUID()}`);
|
|
mkdirSync(pinnedPath, { recursive: true });
|
|
mkdirSync(unknownPath);
|
|
symlinkSync(symlinkTarget, generatedSymlink);
|
|
activeSessionRegistry.registerPath(join(realpathSync(recoveryRoot), seeded[0]), {
|
|
taskId: "FN-RETAIN",
|
|
kind: "executor",
|
|
ownerKey: "executor:FN-RETAIN",
|
|
});
|
|
writeFileSync(join(pinnedPath, "artifact"), "fresh\n", "utf-8");
|
|
vi.mocked(classifyTaskWorktree).mockResolvedValueOnce({
|
|
ok: false,
|
|
classification: "unregistered",
|
|
reason: "not registered in git worktree list",
|
|
});
|
|
|
|
const result = await acquireTaskWorktree({
|
|
task: { ...task, worktree: pinnedPath, branch: "fusion/fn-1" },
|
|
rootDir,
|
|
store,
|
|
settings: { worktreeNaming: "task-id", recycleWorktrees: false },
|
|
});
|
|
|
|
const generatedDirectories = readdirSync(recoveryRoot, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory() && /^fn-\d+-[0-9a-f-]{36}$/.test(entry.name))
|
|
.map((entry) => entry.name);
|
|
expect(result.worktreePath).toBe(pinnedPath);
|
|
expect(generatedDirectories).toHaveLength(11);
|
|
expect(generatedDirectories).toContain(seeded[0]);
|
|
expect(generatedDirectories).not.toContain(seeded[1]);
|
|
expect(existsSync(unknownPath)).toBe(true);
|
|
expect(existsSync(generatedSymlink)).toBe(true);
|
|
});
|
|
|
|
it("retains only the newest ten generated orphan directories in the EXDEV fallback root", async () => {
|
|
const rootDir = makeRepo();
|
|
const externalWorktrees = track(mkdtempSync(join(tmpdir(), "fn-external-retention-worktrees-")));
|
|
const pinnedPath = join(externalWorktrees, "fn-1");
|
|
const recoveryRoot = join(externalWorktrees, ".fusion-recovery", "worktrees");
|
|
const seeded = seedPreservedOrphans(recoveryRoot, 11);
|
|
const unknownPath = join(recoveryRoot, "operator-notes");
|
|
mkdirSync(pinnedPath, { recursive: true });
|
|
mkdirSync(unknownPath);
|
|
activeSessionRegistry.registerPath(join(realpathSync(recoveryRoot), seeded[0]), {
|
|
taskId: "FN-RETAIN-EXDEV",
|
|
kind: "executor",
|
|
ownerKey: "executor:FN-RETAIN-EXDEV",
|
|
});
|
|
writeFileSync(join(pinnedPath, "artifact"), "fresh\n", "utf-8");
|
|
vi.mocked(classifyTaskWorktree).mockResolvedValueOnce({
|
|
ok: false,
|
|
classification: "incomplete",
|
|
reason: "missing .git metadata",
|
|
});
|
|
const actualFs = await vi.importActual<typeof import("node:fs/promises")>("node:fs/promises");
|
|
const renameWorktreeDirectory = vi.fn()
|
|
.mockRejectedValueOnce(Object.assign(new Error("cross-device link"), { code: "EXDEV" }))
|
|
.mockImplementationOnce(actualFs.rename);
|
|
|
|
const result = await acquireTaskWorktree({
|
|
task: { ...task, worktree: pinnedPath, branch: "fusion/fn-1" },
|
|
rootDir,
|
|
store,
|
|
settings: { worktreeNaming: "task-id", worktreesDir: externalWorktrees, recycleWorktrees: false },
|
|
renameWorktreeDirectory,
|
|
});
|
|
|
|
const generatedDirectories = readdirSync(recoveryRoot, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory() && /^fn-\d+-[0-9a-f-]{36}$/.test(entry.name))
|
|
.map((entry) => entry.name);
|
|
expect(result.worktreePath).toBe(pinnedPath);
|
|
expect(generatedDirectories).toHaveLength(11);
|
|
expect(generatedDirectories).toContain(seeded[0]);
|
|
expect(generatedDirectories).not.toContain(seeded[1]);
|
|
expect(existsSync(unknownPath)).toBe(true);
|
|
});
|
|
|
|
it("continues recreation when orphan-preservation audit and task logging fail after rename", async () => {
|
|
const rootDir = makeRepo();
|
|
const pinnedPath = join(rootDir, ".worktrees", "fn-1");
|
|
mkdirSync(pinnedPath, { recursive: true });
|
|
writeFileSync(join(pinnedPath, "artifact"), "stale\n", "utf-8");
|
|
vi.mocked(classifyTaskWorktree).mockResolvedValueOnce({
|
|
ok: false,
|
|
classification: "incomplete",
|
|
reason: "missing .git metadata",
|
|
});
|
|
const auditFilesystem = vi.fn().mockRejectedValue(new Error("audit unavailable"));
|
|
const loggerWarn = vi.fn();
|
|
store.logEntry.mockImplementation(async (_taskId: string, message: string) => {
|
|
if (message.includes("Preserved orphaned task-pinned directory")) throw new Error("task log unavailable");
|
|
});
|
|
|
|
const result = await acquireTaskWorktree({
|
|
task: { ...task, worktree: pinnedPath, branch: "fusion/fn-1" },
|
|
rootDir,
|
|
store,
|
|
settings: { worktreeNaming: "task-id", recycleWorktrees: false },
|
|
audit: { filesystem: auditFilesystem, git: vi.fn().mockResolvedValue(undefined) } as any,
|
|
logger: { log: vi.fn(), warn: loggerWarn },
|
|
});
|
|
|
|
expect(result.worktreePath).toBe(pinnedPath);
|
|
expect(existsSync(join(pinnedPath, ".git"))).toBe(true);
|
|
expect(auditFilesystem).toHaveBeenCalledTimes(1);
|
|
expect(store.logEntry).toHaveBeenCalledWith(
|
|
"FN-1",
|
|
expect.stringContaining("Preserved orphaned task-pinned directory"),
|
|
expect.any(String),
|
|
undefined,
|
|
);
|
|
expect(loggerWarn).toHaveBeenCalledWith(expect.stringContaining("audit unavailable"));
|
|
expect(loggerWarn).toHaveBeenCalledWith(expect.stringContaining("task log unavailable"));
|
|
expect(loggerWarn).not.toHaveBeenCalledWith(expect.stringContaining("[object Object]"));
|
|
});
|
|
|
|
it("reconciles a durable quarantine for an absent pinned path before recreation", async () => {
|
|
const rootDir = makeRepo();
|
|
const worktreesDir = join(rootDir, ".worktrees");
|
|
const pinnedPath = join(worktreesDir, "fn-1");
|
|
mkdirSync(worktreesDir, { recursive: true });
|
|
git(rootDir, `git worktree add -b fusion/fn-1 ${JSON.stringify(pinnedPath)} main`);
|
|
rmSync(pinnedPath, { recursive: true, force: true });
|
|
const failedArchiveReservation = await acquireWorktreePathReservation({
|
|
canonicalPath: pinnedPath,
|
|
worktreesDir,
|
|
rootDir,
|
|
});
|
|
await failedArchiveReservation.quarantine("simulated archive failure");
|
|
|
|
const result = await acquireTaskWorktree({
|
|
task: { ...task, worktree: pinnedPath, branch: "fusion/fn-1" },
|
|
rootDir,
|
|
store,
|
|
settings: { worktreeNaming: "task-id", recycleWorktrees: false },
|
|
});
|
|
|
|
expect(result.worktreePath).toBe(pinnedPath);
|
|
expect(existsSync(join(pinnedPath, ".git"))).toBe(true);
|
|
expect(git(rootDir, "git worktree list --porcelain")).toContain(pinnedPath);
|
|
});
|
|
|
|
it("refuses quarantine reconciliation when an active session owns the absent pinned path", async () => {
|
|
const rootDir = makeRepo();
|
|
const worktreesDir = join(rootDir, ".worktrees");
|
|
const pinnedPath = join(worktreesDir, "fn-1");
|
|
mkdirSync(worktreesDir, { recursive: true });
|
|
const failedArchiveReservation = await acquireWorktreePathReservation({
|
|
canonicalPath: pinnedPath,
|
|
worktreesDir,
|
|
rootDir,
|
|
});
|
|
await failedArchiveReservation.quarantine("simulated archive failure");
|
|
activeSessionRegistry.registerPath(pinnedPath, { taskId: "FN-2", kind: "executor", ownerKey: "executor:FN-2" });
|
|
const createWorktree = vi.fn();
|
|
|
|
await expect(acquireTaskWorktree({
|
|
task: { ...task, worktree: pinnedPath, branch: "fusion/fn-1" },
|
|
rootDir,
|
|
store,
|
|
settings: { worktreeNaming: "task-id", recycleWorktrees: false },
|
|
createWorktree,
|
|
})).rejects.toThrow(`Refusing to reconcile absent task-pinned worktree owned by an active session: ${pinnedPath}`);
|
|
|
|
expect(createWorktree).not.toHaveBeenCalled();
|
|
expect(existsSync(pinnedPath)).toBe(false);
|
|
});
|
|
|
|
it("preserves the orphan in place when its path becomes active during recovery", async () => {
|
|
const rootDir = makeRepo();
|
|
const pinnedPath = join(rootDir, ".worktrees", "fn-1");
|
|
mkdirSync(join(pinnedPath, ".build"), { recursive: true });
|
|
writeFileSync(join(pinnedPath, ".build", "cache"), "stale\n", "utf-8");
|
|
vi.mocked(classifyTaskWorktree).mockResolvedValueOnce({
|
|
ok: false,
|
|
classification: "incomplete",
|
|
reason: "missing .git metadata",
|
|
});
|
|
let becameActive = false;
|
|
vi.spyOn(activeSessionRegistry, "isPathActive").mockImplementation((path: string) => {
|
|
if (path !== pinnedPath) return false;
|
|
if (!becameActive) {
|
|
becameActive = true;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
await expect(acquireTaskWorktree({
|
|
task: { ...task, worktree: pinnedPath, branch: "fusion/fn-1" },
|
|
rootDir,
|
|
store,
|
|
settings: { worktreeNaming: "task-id", recycleWorktrees: false },
|
|
})).rejects.toThrow(/became active/);
|
|
|
|
expect(readFileSync(join(pinnedPath, ".build", "cache"), "utf-8")).toBe("stale\n");
|
|
expect(existsSync(join(rootDir, ".fusion", "recovery", "worktrees"))).toBe(true);
|
|
expect(readdirSync(join(rootDir, ".fusion", "recovery", "worktrees"))).toHaveLength(0);
|
|
});
|
|
|
|
it("refuses to preserve an orphan through a recovery-directory symlink", async () => {
|
|
const rootDir = makeRepo();
|
|
const pinnedPath = join(rootDir, ".worktrees", "fn-1");
|
|
const outside = track(mkdtempSync(join(tmpdir(), "fn-orphan-recovery-outside-")));
|
|
mkdirSync(join(pinnedPath, ".build"), { recursive: true });
|
|
writeFileSync(join(pinnedPath, ".build", "cache"), "stale\n", "utf-8");
|
|
mkdirSync(join(rootDir, ".fusion", "recovery"), { recursive: true });
|
|
symlinkSync(outside, join(rootDir, ".fusion", "recovery", "worktrees"));
|
|
vi.mocked(classifyTaskWorktree).mockResolvedValueOnce({
|
|
ok: false,
|
|
classification: "incomplete",
|
|
reason: "missing .git metadata",
|
|
});
|
|
|
|
await expect(acquireTaskWorktree({
|
|
task: { ...task, worktree: pinnedPath, branch: "fusion/fn-1" },
|
|
rootDir,
|
|
store,
|
|
settings: { worktreeNaming: "task-id", recycleWorktrees: false },
|
|
})).rejects.toThrow(`Refusing to use recovery directory outside ${realpathSync(join(rootDir, ".fusion", "recovery"))}`);
|
|
|
|
expect(readFileSync(join(pinnedPath, ".build", "cache"), "utf-8")).toBe("stale\n");
|
|
expect(readdirSync(outside)).toHaveLength(0);
|
|
});
|
|
|
|
it("refuses to create recovery contents through an ancestor symlink", async () => {
|
|
const rootDir = makeRepo();
|
|
const pinnedPath = join(rootDir, ".worktrees", "fn-1");
|
|
const outside = track(mkdtempSync(join(tmpdir(), "fn-orphan-recovery-ancestor-outside-")));
|
|
mkdirSync(join(pinnedPath, ".build"), { recursive: true });
|
|
writeFileSync(join(pinnedPath, ".build", "cache"), "stale\n", "utf-8");
|
|
mkdirSync(join(rootDir, ".fusion"), { recursive: true });
|
|
symlinkSync(outside, join(rootDir, ".fusion", "recovery"));
|
|
vi.mocked(classifyTaskWorktree).mockResolvedValueOnce({
|
|
ok: false,
|
|
classification: "incomplete",
|
|
reason: "missing .git metadata",
|
|
});
|
|
|
|
await expect(acquireTaskWorktree({
|
|
task: { ...task, worktree: pinnedPath, branch: "fusion/fn-1" },
|
|
rootDir,
|
|
store,
|
|
settings: { worktreeNaming: "task-id", recycleWorktrees: false },
|
|
})).rejects.toThrow(`Refusing to use recovery directory outside ${realpathSync(join(rootDir, ".fusion"))}`);
|
|
|
|
expect(readFileSync(join(pinnedPath, ".build", "cache"), "utf-8")).toBe("stale\n");
|
|
expect(readdirSync(outside)).toHaveLength(0);
|
|
});
|
|
|
|
it("serializes concurrent orphan recovery through fresh recreation", async () => {
|
|
const rootDir = makeRepo();
|
|
const pinnedPath = join(rootDir, ".worktrees", "fn-1");
|
|
mkdirSync(join(pinnedPath, ".build"), { recursive: true });
|
|
writeFileSync(join(pinnedPath, ".build", "cache"), "stale\n", "utf-8");
|
|
const actualPool = await vi.importActual<typeof import("../worktree/worktree-pool.js")>("../worktree/worktree-pool.js");
|
|
vi.mocked(classifyTaskWorktree).mockResolvedValueOnce({
|
|
ok: false,
|
|
classification: "incomplete",
|
|
reason: "missing .git metadata",
|
|
}).mockImplementation(actualPool.classifyTaskWorktree);
|
|
let signalCreateStarted!: () => void;
|
|
const createStarted = new Promise<void>((resolve) => { signalCreateStarted = resolve; });
|
|
let allowCreate!: () => void;
|
|
const createGate = new Promise<void>((resolve) => { allowCreate = resolve; });
|
|
const createWorktree = vi.fn(async (branch: string, path: string) => {
|
|
signalCreateStarted();
|
|
await createGate;
|
|
git(rootDir, `git worktree add -b ${JSON.stringify(branch)} ${JSON.stringify(path)} main`);
|
|
return { path, branch };
|
|
});
|
|
const input = {
|
|
task: { ...task, worktree: pinnedPath, branch: "fusion/fn-1" },
|
|
rootDir,
|
|
store,
|
|
settings: { worktreeNaming: "task-id", recycleWorktrees: false },
|
|
createWorktree,
|
|
} as const;
|
|
|
|
const first = acquireTaskWorktree(input);
|
|
await createStarted;
|
|
const second = acquireTaskWorktree(input);
|
|
allowCreate();
|
|
|
|
const [firstResult, secondResult] = await Promise.all([first, second]);
|
|
expect(firstResult.worktreePath).toBe(pinnedPath);
|
|
expect(secondResult.worktreePath).toBe(pinnedPath);
|
|
expect(createWorktree).toHaveBeenCalledTimes(1);
|
|
expect(readFileSync(join(rootDir, ".fusion", "recovery", "worktrees", readdirSync(join(rootDir, ".fusion", "recovery", "worktrees"))[0], ".build", "cache"), "utf-8")).toBe("stale\n");
|
|
});
|
|
|
|
it("refreshes a recreated existing task branch after its dependency branch is deleted", async () => {
|
|
const rootDir = makeRepo();
|
|
const staleBase = git(rootDir, "git rev-parse HEAD");
|
|
git(rootDir, `git branch fusion/fn-4 ${staleBase}`);
|
|
git(rootDir, `git checkout -b fusion/deleted-dependency ${staleBase}`);
|
|
writeFileSync(join(rootDir, "dependency-output.ts"), "export const dependencyOutput = true;\n", "utf-8");
|
|
git(rootDir, "git add dependency-output.ts");
|
|
git(rootDir, 'git commit -m "land dependency"');
|
|
git(rootDir, "git checkout main");
|
|
git(rootDir, "git merge --ff-only fusion/deleted-dependency");
|
|
git(rootDir, "git branch -d fusion/deleted-dependency");
|
|
const landedBase = git(rootDir, "git rev-parse HEAD");
|
|
|
|
const result = await acquireTaskWorktree({
|
|
task: {
|
|
...task,
|
|
id: "FN-4",
|
|
branch: "fusion/fn-4",
|
|
baseCommitSha: staleBase,
|
|
executionStartBranch: "fusion/deleted-dependency",
|
|
},
|
|
rootDir,
|
|
store,
|
|
settings: { worktreeNaming: "task-id", recycleWorktrees: false },
|
|
refreshStaleBase: true,
|
|
createWorktree: async (branch, path) => {
|
|
git(rootDir, `git worktree add ${JSON.stringify(path)} ${JSON.stringify(branch)}`);
|
|
return { path, branch };
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
source: "fresh",
|
|
baseRefresh: { kind: "reset-to-base", executionSafe: true, baseSha: landedBase },
|
|
});
|
|
expect(git(result.worktreePath, "git rev-parse HEAD")).toBe(landedBase);
|
|
expect(existsSync(join(result.worktreePath, "dependency-output.ts"))).toBe(true);
|
|
expect(store.updateTask).toHaveBeenCalledWith("FN-4", { baseCommitSha: landedBase });
|
|
});
|
|
|
|
it("refreshes a retained task branch acquired from the worktree pool", async () => {
|
|
const rootDir = makeRepo();
|
|
const staleBase = git(rootDir, "git rev-parse HEAD");
|
|
const pooledPath = join(rootDir, ".worktrees", "pooled-fn-4");
|
|
git(rootDir, `git worktree add -b fusion/fn-4 ${JSON.stringify(pooledPath)} ${staleBase}`);
|
|
writeFileSync(join(rootDir, "dependency-output.ts"), "export const dependencyOutput = true;\n", "utf-8");
|
|
git(rootDir, "git add dependency-output.ts");
|
|
git(rootDir, 'git commit -m "land dependency"');
|
|
const landedBase = git(rootDir, "git rev-parse HEAD");
|
|
const pool = {
|
|
acquire: vi.fn().mockReturnValue(pooledPath),
|
|
prepareForTask: vi.fn().mockResolvedValue({
|
|
branch: "fusion/fn-4",
|
|
worktreePath: pooledPath,
|
|
reclaimed: false,
|
|
}),
|
|
release: vi.fn(),
|
|
} as any;
|
|
|
|
const result = await acquireTaskWorktree({
|
|
task: { ...task, id: "FN-4", branch: "fusion/fn-4", baseCommitSha: staleBase },
|
|
rootDir,
|
|
store,
|
|
settings: { recycleWorktrees: true },
|
|
pool,
|
|
refreshStaleBase: true,
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
source: "pool",
|
|
baseRefresh: { kind: "reset-to-base", executionSafe: true, baseSha: landedBase },
|
|
});
|
|
expect(git(pooledPath, "git rev-parse HEAD")).toBe(landedBase);
|
|
expect(existsSync(join(pooledPath, "dependency-output.ts"))).toBe(true);
|
|
});
|
|
|
|
it("does not apply the native stale-base refresh to fresh Worktrunk acquisitions", async () => {
|
|
const result = await acquireTaskWorktree({
|
|
task,
|
|
rootDir: process.cwd(),
|
|
store,
|
|
settings: { worktrunk: { enabled: true } } as any,
|
|
backend: { kind: "worktrunk" } as any,
|
|
createWorktreeBackendKind: "worktrunk",
|
|
refreshStaleBase: true,
|
|
createWorktree: vi.fn().mockResolvedValue({ path: "/tmp/worktrunk-fresh", branch: "fusion/fn-1" }),
|
|
});
|
|
|
|
expect(result).toMatchObject({ source: "fresh", baseRefresh: undefined });
|
|
});
|
|
|
|
it("persists the backend used by an internally created worktree", async () => {
|
|
const rootDir = makeRepo();
|
|
|
|
const result = await acquireTaskWorktree({
|
|
task,
|
|
rootDir,
|
|
store,
|
|
settings: { recycleWorktrees: false },
|
|
backend: new NativeWorktreeBackend(),
|
|
});
|
|
|
|
const markerPath = git(result.worktreePath, "git rev-parse --git-path fusion-worktree-backend-kind");
|
|
expect(readFileSync(markerPath, "utf-8")).toBe("native\n");
|
|
});
|
|
|
|
it("refreshes a native fallback acquisition even when Worktrunk is enabled", async () => {
|
|
const rootDir = makeRepo();
|
|
const staleBase = git(rootDir, "git rev-parse HEAD");
|
|
git(rootDir, `git branch fusion/fn-4 ${staleBase}`);
|
|
writeFileSync(join(rootDir, "dependency-output.ts"), "export const dependencyOutput = true;\n", "utf-8");
|
|
git(rootDir, "git add dependency-output.ts");
|
|
git(rootDir, 'git commit -m "land dependency"');
|
|
const landedBase = git(rootDir, "git rev-parse HEAD");
|
|
|
|
const result = await acquireTaskWorktree({
|
|
task: { ...task, id: "FN-4", branch: "fusion/fn-4", baseCommitSha: staleBase },
|
|
rootDir,
|
|
store,
|
|
settings: { worktreeNaming: "task-id", recycleWorktrees: false, worktrunk: { enabled: true } } as any,
|
|
backend: { kind: "worktrunk" } as any,
|
|
createWorktreeBackendKind: "native",
|
|
refreshStaleBase: true,
|
|
createWorktree: async (branch, path) => {
|
|
git(rootDir, `git worktree add ${JSON.stringify(path)} ${JSON.stringify(branch)}`);
|
|
return { path, branch };
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
source: "fresh",
|
|
baseRefresh: { kind: "reset-to-base", executionSafe: true, baseSha: landedBase },
|
|
});
|
|
});
|
|
|
|
it("uses the persisted native backend when a Worktrunk fallback is reused", async () => {
|
|
const rootDir = makeRepo();
|
|
const staleBase = git(rootDir, "git rev-parse HEAD");
|
|
const worktreePath = join(rootDir, ".worktrees", "fallback-fn-4");
|
|
git(rootDir, `git worktree add -b fusion/fn-4 ${JSON.stringify(worktreePath)} ${staleBase}`);
|
|
const markerPath = git(worktreePath, "git rev-parse --git-path fusion-worktree-backend-kind");
|
|
writeFileSync(markerPath, "native\n", "utf-8");
|
|
writeFileSync(join(rootDir, "dependency-output.ts"), "export const dependencyOutput = true;\n", "utf-8");
|
|
git(rootDir, "git add dependency-output.ts");
|
|
git(rootDir, 'git commit -m "land dependency"');
|
|
const landedBase = git(rootDir, "git rev-parse HEAD");
|
|
|
|
const result = await acquireTaskWorktree({
|
|
task: { ...task, id: "FN-4", worktree: worktreePath, branch: "fusion/fn-4", baseCommitSha: staleBase },
|
|
rootDir,
|
|
store,
|
|
settings: { worktrunk: { enabled: true } } as any,
|
|
backend: { kind: "worktrunk" } as any,
|
|
refreshStaleBase: true,
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
source: "existing",
|
|
baseRefresh: { kind: "reset-to-base", executionSafe: true, baseSha: landedBase },
|
|
});
|
|
expect(git(worktreePath, "git rev-parse HEAD")).toBe(landedBase);
|
|
});
|
|
|
|
it("preserves a persisted Worktrunk backend when an injected native creator is available", async () => {
|
|
const rootDir = makeRepo();
|
|
const staleBase = git(rootDir, "git rev-parse HEAD");
|
|
const worktreePath = join(rootDir, ".worktrees", "worktrunk-fn-4");
|
|
git(rootDir, `git worktree add -b fusion/fn-4 ${JSON.stringify(worktreePath)} ${staleBase}`);
|
|
const markerPath = git(worktreePath, "git rev-parse --git-path fusion-worktree-backend-kind");
|
|
writeFileSync(markerPath, "worktrunk\n", "utf-8");
|
|
writeFileSync(join(rootDir, "dependency-output.ts"), "export const dependencyOutput = true;\n", "utf-8");
|
|
git(rootDir, "git add dependency-output.ts");
|
|
git(rootDir, 'git commit -m "land dependency"');
|
|
|
|
const result = await acquireTaskWorktree({
|
|
task: { ...task, id: "FN-4", worktree: worktreePath, branch: "fusion/fn-4", baseCommitSha: staleBase },
|
|
rootDir,
|
|
store,
|
|
settings: { worktrunk: { enabled: true } } as any,
|
|
backend: { kind: "worktrunk" } as any,
|
|
createWorktreeBackendKind: "native",
|
|
refreshStaleBase: true,
|
|
});
|
|
|
|
expect(result).toMatchObject({ source: "existing", baseRefresh: undefined });
|
|
expect(git(worktreePath, "git rev-parse HEAD")).toBe(staleBase);
|
|
});
|
|
|
|
/*
|
|
FNXC:WorktreeBaseRefresh 2026-08-09-23:49:
|
|
The invariant here is the RESOURCE-HYGIENE ordering — the task binding is cleared before the worktree goes
|
|
back to the pool — not the refusal policy that used to trigger it. A dirty checkout no longer fails base
|
|
refresh (it declines and executes on the existing base), and in production a pooled worktree never reaches
|
|
the refresh dirty anyway: `prepareForTask` runs `git checkout -- .` + `git clean -fd` first, so the old
|
|
fixture's dirt only survived because the pool is mocked here. Drive the ordering through the secrets-record
|
|
reconciliation refusal instead, which is a secrets-safety gate and remains unconditionally blocking.
|
|
*/
|
|
it("clears a pooled task binding before releasing a worktree that fails base refresh", async () => {
|
|
const rootDir = makeRepo();
|
|
const pooledPath = join(rootDir, ".worktrees", "pooled-fn-4-dirty");
|
|
git(rootDir, `git worktree add -b fusion/fn-4-dirty ${JSON.stringify(pooledPath)}`);
|
|
// A malformed root secrets-env record: reconciliation cannot prove the checkout is safe to hand an agent.
|
|
writeFileSync(join(pooledPath, ".fusion-secrets-env.fingerprint"), "not-a-valid-record\n", "utf-8");
|
|
const pool = {
|
|
acquire: vi.fn().mockReturnValue(pooledPath),
|
|
prepareForTask: vi.fn().mockResolvedValue({
|
|
branch: "fusion/fn-4-dirty",
|
|
worktreePath: pooledPath,
|
|
reclaimed: false,
|
|
}),
|
|
release: vi.fn(),
|
|
} as any;
|
|
|
|
await expect(acquireTaskWorktree({
|
|
task: { ...task, id: "FN-4", branch: "fusion/fn-4-dirty" },
|
|
rootDir,
|
|
store,
|
|
settings: { recycleWorktrees: true },
|
|
pool,
|
|
refreshStaleBase: true,
|
|
})).rejects.toThrow(WorktreeBaseRefreshError);
|
|
|
|
expect(store.updateTask).toHaveBeenCalledWith("FN-4", { worktree: null, branch: null, branchWriteOrigin: "engine", sessionFile: null });
|
|
expect(store.updateTask.mock.invocationCallOrder.at(-1)).toBeLessThan(pool.release.mock.invocationCallOrder[0]);
|
|
});
|
|
|
|
it("FN-6861 creates a fresh configured worktree when a resumed assignment points at the repo root", async () => {
|
|
const rootDir = makeRepo();
|
|
const actualPool = await vi.importActual<typeof import("../worktree/worktree-pool.js")>("../worktree/worktree-pool.js");
|
|
vi.mocked(classifyTaskWorktree).mockImplementationOnce(actualPool.classifyTaskWorktree);
|
|
const freshPath = join(rootDir, ".worktrees", "fn-6861-fresh");
|
|
const createWorktree = vi.fn().mockResolvedValue({ path: freshPath, branch: "fusion/fn-1" });
|
|
const auditGit = vi.fn().mockResolvedValue(undefined);
|
|
|
|
const result = await acquireTaskWorktree({
|
|
task: { ...task, worktree: rootDir, branch: "fusion/fn-1", sessionFile: "/tmp/session.json" },
|
|
rootDir,
|
|
store,
|
|
settings: {} as any,
|
|
createWorktree,
|
|
audit: { git: auditGit } as any,
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
worktreePath: freshPath,
|
|
branch: "fusion/fn-1",
|
|
source: "fresh",
|
|
isResume: false,
|
|
});
|
|
expect(result.worktreePath).not.toBe(rootDir);
|
|
expect(result.worktreePath).toContain(`${join(rootDir, ".worktrees")}/`);
|
|
expect(auditGit).toHaveBeenCalledWith(expect.objectContaining({
|
|
type: "worktree:incomplete-detected",
|
|
target: rootDir,
|
|
metadata: expect.objectContaining({ classification: "repo-root", source: "resume" }),
|
|
}));
|
|
expect(store.updateTask).toHaveBeenCalledWith("FN-1", { worktree: null, branch: null, branchWriteOrigin: "engine", sessionFile: null });
|
|
expect(store.updateTask).toHaveBeenCalledWith("FN-1", { worktree: freshPath, branch: "fusion/fn-1", branchWriteOrigin: "engine" });
|
|
});
|
|
|
|
it("FN-6922 rejects a canonical-equal resumed repo root before returning", async () => {
|
|
const rootDir = makeRepo();
|
|
const actualPool = await vi.importActual<typeof import("../worktree/worktree-pool.js")>("../worktree/worktree-pool.js");
|
|
vi.mocked(classifyTaskWorktree).mockImplementationOnce(actualPool.classifyTaskWorktree);
|
|
const freshPath = join(rootDir, ".worktrees", "fn-6922-trailing-slash");
|
|
const createWorktree = vi.fn().mockResolvedValue({ path: freshPath, branch: "fusion/fn-1" });
|
|
|
|
const result = await acquireTaskWorktree({
|
|
task: { ...task, worktree: `${rootDir}/`, branch: "fusion/fn-1", sessionFile: "/tmp/session.json" },
|
|
rootDir,
|
|
store,
|
|
settings: {} as any,
|
|
createWorktree,
|
|
});
|
|
|
|
expect(result.worktreePath).toBe(freshPath);
|
|
expect(result.worktreePath).not.toBe(rootDir);
|
|
expect(result.isResume).toBe(false);
|
|
expect(store.updateTask).toHaveBeenCalledWith("FN-1", { worktree: null, branch: null, branchWriteOrigin: "engine", sessionFile: null });
|
|
expect(store.updateTask).toHaveBeenCalledWith("FN-1", { worktree: freshPath, branch: "fusion/fn-1", branchWriteOrigin: "engine" });
|
|
});
|
|
|
|
it("FN-6922 self-heals when the return guard catches a mocked repo-root resume", async () => {
|
|
const rootDir = makeRepo();
|
|
vi.mocked(classifyTaskWorktree).mockResolvedValueOnce({ ok: true });
|
|
const freshPath = join(rootDir, ".worktrees", "fn-6922-guard-fresh");
|
|
const createWorktree = vi.fn().mockResolvedValue({ path: freshPath, branch: "fusion/fn-1" });
|
|
const auditGit = vi.fn().mockResolvedValue(undefined);
|
|
|
|
const result = await acquireTaskWorktree({
|
|
task: { ...task, worktree: rootDir, branch: "fusion/fn-1", sessionFile: "/tmp/session.json" },
|
|
rootDir,
|
|
store,
|
|
settings: {} as any,
|
|
createWorktree,
|
|
audit: { git: auditGit } as any,
|
|
});
|
|
|
|
expect(result).toMatchObject({ worktreePath: freshPath, source: "fresh", isResume: false });
|
|
expect(createWorktree).toHaveBeenCalledWith("fusion/fn-1", expect.stringContaining(`${join(rootDir, ".worktrees")}/`), "FN-1", "main", false);
|
|
expect(auditGit).toHaveBeenCalledWith(expect.objectContaining({
|
|
type: "worktree:incomplete-detected",
|
|
target: rootDir,
|
|
metadata: expect.objectContaining({ classification: "repo-root", source: "acquire-return-guard", returnSource: "existing" }),
|
|
}));
|
|
expect(store.updateTask).toHaveBeenCalledWith("FN-1", { worktree: null, branch: null, branchWriteOrigin: "engine", sessionFile: null });
|
|
expect(store.updateTask).toHaveBeenCalledWith("FN-1", { worktree: freshPath, branch: "fusion/fn-1", branchWriteOrigin: "engine" });
|
|
});
|
|
|
|
it("FN-6922 throws a typed error when fresh creation returns the repo root", async () => {
|
|
const rootDir = makeRepo();
|
|
const auditGit = vi.fn().mockResolvedValue(undefined);
|
|
|
|
await expect(acquireTaskWorktree({
|
|
task: { ...task, worktree: null, branch: null },
|
|
rootDir,
|
|
store,
|
|
settings: {} as any,
|
|
createWorktree: vi.fn().mockResolvedValue({ path: rootDir, branch: "fusion/fn-1" }),
|
|
audit: { git: auditGit } as any,
|
|
})).rejects.toBeInstanceOf(RepoRootWorktreeError);
|
|
|
|
expect(auditGit).toHaveBeenCalledWith(expect.objectContaining({
|
|
type: "worktree:incomplete-detected",
|
|
target: rootDir,
|
|
metadata: expect.objectContaining({ classification: "repo-root", source: "acquire-return-guard", returnSource: "fresh" }),
|
|
}));
|
|
expect(store.updateTask).toHaveBeenCalledWith("FN-1", { worktree: null, branch: null, branchWriteOrigin: "engine", sessionFile: null });
|
|
});
|
|
|
|
it("falls through to fresh creation when pool acquire throws PoolDoubleLeaseError", async () => {
|
|
const createWorktree = vi.fn().mockResolvedValue({ path: "/tmp/new", branch: "fusion/fn-1" });
|
|
const result = await acquireTaskWorktree({
|
|
task,
|
|
rootDir: process.cwd(),
|
|
store,
|
|
settings: { recycleWorktrees: true } as any,
|
|
pool: {
|
|
acquire: () => {
|
|
throw new PoolDoubleLeaseError("/tmp/pooled", "FN-OTHER", "FN-1", "acquire");
|
|
},
|
|
prepareForTask: vi.fn(),
|
|
release: vi.fn(),
|
|
} as any,
|
|
createWorktree,
|
|
logger: { log: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
});
|
|
|
|
expect(result.source).toBe("fresh");
|
|
expect(createWorktree).toHaveBeenCalled();
|
|
expect(store.logEntry).toHaveBeenCalledWith("FN-1", expect.stringContaining("Pool double-lease guard triggered"), undefined, undefined);
|
|
});
|
|
|
|
it("creates fresh when pool disabled", async () => {
|
|
const createWorktree = vi.fn().mockResolvedValue({ path: "/tmp/new", branch: "fusion/fn-1" });
|
|
const result = await acquireTaskWorktree({
|
|
task,
|
|
rootDir: process.cwd(),
|
|
store,
|
|
settings: {},
|
|
createWorktree,
|
|
});
|
|
expect(result.source).toBe("fresh");
|
|
expect(createWorktree).toHaveBeenCalled();
|
|
expect(store.updateTask).toHaveBeenCalledWith("FN-1", { worktree: "/tmp/new", branch: "fusion/fn-1", branchWriteOrigin: "engine" });
|
|
});
|
|
|
|
it("skips init command when runInitCommand false", async () => {
|
|
const runConfiguredCommand = vi.fn();
|
|
await acquireTaskWorktree({
|
|
task,
|
|
rootDir: process.cwd(),
|
|
store,
|
|
settings: { worktreeInitCommand: "pnpm i" } as any,
|
|
createWorktree: vi.fn().mockResolvedValue({ path: "/tmp/new", branch: "fusion/fn-1" }),
|
|
runConfiguredCommand,
|
|
runInitCommand: false,
|
|
});
|
|
expect(runConfiguredCommand).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("invokes desktop artifact cleanup before init command for fresh acquisition", async () => {
|
|
const runConfiguredCommand = vi.fn().mockResolvedValue({ exitCode: 0, stderr: "", stdout: "" });
|
|
|
|
await acquireTaskWorktree({
|
|
task,
|
|
rootDir: process.cwd(),
|
|
store,
|
|
settings: { worktreeInitCommand: "pnpm install" } as any,
|
|
createWorktree: vi.fn().mockResolvedValue({ path: "/tmp/new", branch: "fusion/fn-1" }),
|
|
runConfiguredCommand,
|
|
runInitCommand: true,
|
|
});
|
|
|
|
expect(desktopArtifacts.removeDesktopBuildArtifacts).toHaveBeenCalledWith("/tmp/new", undefined);
|
|
const cleanupOrder = vi.mocked(desktopArtifacts.removeDesktopBuildArtifacts).mock.invocationCallOrder[0];
|
|
const initOrder = runConfiguredCommand.mock.invocationCallOrder[0];
|
|
expect(cleanupOrder).toBeLessThan(initOrder);
|
|
});
|
|
|
|
it("copies configured files for fresh acquisition before init command", async () => {
|
|
const rootDir = track(mkdtempSync(join(tmpdir(), "fn-copy-fresh-root-")));
|
|
const worktreePath = track(mkdtempSync(join(tmpdir(), "fn-copy-fresh-worktree-")));
|
|
writeFileSync(join(rootDir, ".env"), "SECRET=redacted\n", "utf-8");
|
|
const runConfiguredCommand = vi.fn().mockImplementation(async () => {
|
|
expect(readFileSync(join(worktreePath, ".env"), "utf-8")).toBe("SECRET=redacted\n");
|
|
return { exitCode: 0, stderr: "", stdout: "" };
|
|
});
|
|
|
|
await acquireTaskWorktree({
|
|
task,
|
|
rootDir,
|
|
store,
|
|
settings: { worktreeCopyFiles: [".env"], worktreeInitCommand: "pnpm install" } as any,
|
|
createWorktree: vi.fn().mockResolvedValue({ path: worktreePath, branch: "fusion/fn-1" }),
|
|
runConfiguredCommand,
|
|
runInitCommand: true,
|
|
});
|
|
|
|
expect(readFileSync(join(worktreePath, ".env"), "utf-8")).toBe("SECRET=redacted\n");
|
|
expect(store.logEntry).toHaveBeenCalledWith("FN-1", "Copied configured worktree files into fresh worktree: .env", undefined, undefined);
|
|
});
|
|
|
|
it("invokes desktop artifact cleanup once for pooled acquisition", async () => {
|
|
const runConfiguredCommand = vi.fn();
|
|
const pooledPath = track(mkdtempSync(join(tmpdir(), "fn-pooled-cleanup-")));
|
|
|
|
await acquireTaskWorktree({
|
|
task,
|
|
rootDir: process.cwd(),
|
|
store,
|
|
settings: { recycleWorktrees: true, worktreeInitCommand: "pnpm install" } as any,
|
|
pool: {
|
|
acquire: (_taskId: string) => pooledPath,
|
|
prepareForTask: vi.fn().mockResolvedValue({ branch: "fusion/fn-1", worktreePath: pooledPath, reclaimed: false }),
|
|
release: vi.fn(),
|
|
} as any,
|
|
createWorktree: vi.fn().mockResolvedValue({ path: "/tmp/fn-worktree-fallback", branch: "fusion/fn-1" }),
|
|
runConfiguredCommand,
|
|
runInitCommand: true,
|
|
});
|
|
|
|
expect(desktopArtifacts.removeDesktopBuildArtifacts).toHaveBeenCalledTimes(1);
|
|
expect(desktopArtifacts.removeDesktopBuildArtifacts).toHaveBeenCalledWith(pooledPath, undefined);
|
|
expect(runConfiguredCommand).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("copies configured files into pooled worktrees after preparation", async () => {
|
|
const rootDir = track(mkdtempSync(join(tmpdir(), "fn-copy-pool-root-")));
|
|
const worktreePath = track(mkdtempSync(join(tmpdir(), "fn-copy-pool-worktree-")));
|
|
writeFileSync(join(rootDir, ".env"), "POOL=updated\n", "utf-8");
|
|
writeFileSync(join(worktreePath, ".env"), "POOL=old\n", "utf-8");
|
|
|
|
const result = await acquireTaskWorktree({
|
|
task,
|
|
rootDir,
|
|
store,
|
|
settings: { recycleWorktrees: true, worktreeCopyFiles: [".env"] } as any,
|
|
pool: {
|
|
acquire: (_taskId: string) => worktreePath,
|
|
prepareForTask: vi.fn().mockResolvedValue({ branch: "fusion/fn-1", worktreePath, reclaimed: false }),
|
|
release: vi.fn(),
|
|
} as any,
|
|
createWorktree: vi.fn().mockResolvedValue({ path: "/tmp/fn-worktree-fallback", branch: "fusion/fn-1" }),
|
|
});
|
|
|
|
expect(result.source).toBe("pool");
|
|
expect(readFileSync(join(worktreePath, ".env"), "utf-8")).toBe("POOL=updated\n");
|
|
expect(store.logEntry).toHaveBeenCalledWith("FN-1", "Copied configured worktree files into pool worktree: .env", undefined, undefined);
|
|
});
|
|
|
|
it("does not copy configured files over resumed worktree state", async () => {
|
|
const rootDir = track(mkdtempSync(join(tmpdir(), "fn-copy-resume-root-")));
|
|
const worktreePath = track(mkdtempSync(join(tmpdir(), "fn-copy-resume-worktree-")));
|
|
writeFileSync(join(rootDir, ".env"), "ROOT=updated\n", "utf-8");
|
|
writeFileSync(join(worktreePath, ".env"), "RESUME=keep\n", "utf-8");
|
|
|
|
const result = await acquireTaskWorktree({
|
|
task: { ...task, worktree: worktreePath, branch: "fusion/fn-1" },
|
|
rootDir,
|
|
store,
|
|
settings: { worktreeCopyFiles: [".env"] } as any,
|
|
createWorktree: vi.fn().mockResolvedValue({ path: "/tmp/fn-worktree-fallback", branch: "fusion/fn-1" }),
|
|
});
|
|
|
|
expect(result.source).toBe("existing");
|
|
expect(readFileSync(join(worktreePath, ".env"), "utf-8")).toBe("RESUME=keep\n");
|
|
expect(store.logEntry).not.toHaveBeenCalledWith("FN-1", expect.stringContaining("Copied configured worktree files"), undefined, undefined);
|
|
expect(existsSync(join(worktreePath, ".env"))).toBe(true);
|
|
});
|
|
|
|
it("FN-4834: logs worktree init stderr in task log outcome", async () => {
|
|
const runConfiguredCommand = vi.fn().mockResolvedValue({
|
|
exitCode: 1,
|
|
stderr: "ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE Cannot install with \"frozen-lockfile\" because pnpm-lock.yaml is not up to date",
|
|
stdout: "",
|
|
});
|
|
|
|
await expect(acquireTaskWorktree({
|
|
task,
|
|
rootDir: process.cwd(),
|
|
store,
|
|
settings: { worktreeInitCommand: "pnpm install --frozen-lockfile" } as any,
|
|
createWorktree: vi.fn().mockResolvedValue({ path: "/tmp/new", branch: "fusion/fn-1" }),
|
|
runConfiguredCommand,
|
|
runInitCommand: true,
|
|
logger: { log: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
})).resolves.toBeTruthy();
|
|
|
|
const failureCall = store.logEntry.mock.calls.find((call: unknown[]) => String(call[1]).startsWith("Worktree init command failed"));
|
|
expect(failureCall).toBeDefined();
|
|
expect(failureCall?.[2]).toContain("ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE");
|
|
});
|
|
});
|
|
|
|
describe("acquireTaskWorktree foreign start-point warning", () => {
|
|
it("emits warning/log for fusion/fn-* start point with foreign-attributed tip and stays silent for main", async () => {
|
|
vi.resetModules();
|
|
const warn = vi.fn();
|
|
const logEntry = vi.fn().mockResolvedValue(undefined);
|
|
|
|
const execMock: any = (_command: string, _opts: any, cb: any) => cb(null, "", "");
|
|
execMock[promisify.custom] = (command: string) => {
|
|
if (command.startsWith("git rev-parse --verify \"fusion/fn-4367^")) {
|
|
return Promise.resolve({ stdout: "deadbeefdeadbeef\n", stderr: "" });
|
|
}
|
|
if (command.startsWith("git log -1 --format=%s%x1f%b")) {
|
|
return Promise.resolve({ stdout: "feat(FN-4367): dep\u001fFusion-Task-Id: FN-4367\n", stderr: "" });
|
|
}
|
|
return Promise.resolve({ stdout: "", stderr: "" });
|
|
};
|
|
|
|
vi.doMock("node:child_process", () => ({ exec: execMock, execFile: execMock }));
|
|
const mod = await import("../worktree/worktree-acquisition.js");
|
|
|
|
await mod.acquireTaskWorktree({
|
|
task: { id: "FN-4488", title: "Task", description: "Desc", branch: null, worktree: null, executionStartBranch: "fusion/fn-4367" } as any,
|
|
rootDir: "/tmp/repo",
|
|
store: { updateTask: vi.fn().mockResolvedValue(undefined), logEntry } as any,
|
|
settings: {},
|
|
logger: { log: vi.fn(), warn, error: vi.fn() },
|
|
createWorktree: vi.fn().mockResolvedValue({ path: "/tmp/repo/.worktrees/x", branch: "fusion/fn-4488" }),
|
|
});
|
|
|
|
expect(warn).toHaveBeenCalledWith(expect.stringContaining("worktree acquired with foreign-task start point: fusion/fn-4367"));
|
|
expect(logEntry).toHaveBeenCalledWith("FN-4488", expect.stringContaining("worktree acquired with foreign-task start point: fusion/fn-4367"), undefined, undefined);
|
|
|
|
warn.mockClear();
|
|
logEntry.mockClear();
|
|
|
|
await mod.acquireTaskWorktree({
|
|
task: { id: "FN-4488", title: "Task", description: "Desc", branch: null, worktree: null, executionStartBranch: "main" } as any,
|
|
rootDir: "/tmp/repo",
|
|
store: { updateTask: vi.fn().mockResolvedValue(undefined), logEntry } as any,
|
|
settings: {},
|
|
logger: { log: vi.fn(), warn, error: vi.fn() },
|
|
createWorktree: vi.fn().mockResolvedValue({ path: "/tmp/repo/.worktrees/x", branch: "fusion/fn-4488" }),
|
|
});
|
|
|
|
expect(warn).not.toHaveBeenCalled();
|
|
expect(logEntry).not.toHaveBeenCalledWith("FN-4488", expect.stringContaining("foreign-task start point"), undefined, undefined);
|
|
});
|
|
});
|