Move AI-merge clean-room worktrees into the configured worktrees directory while preserving legacy cleanup. - Resolve AI-merge roots through the configured worktrees directory using a hidden .ai-merge container. - Keep local git excludes and stale cleanup compatible with legacy .fusion/ai-merge and tmpdir locations. - Exclude the .ai-merge container from worktree pool and self-healing one-level sweeps. - Add regression coverage for path resolution, cleanup, orphan reap, and cap enforcement behavior. - Update architecture and agent guidance for the relocated clean-room root. Files changed: AGENTS.md | 2 +- docs/architecture.md | 4 +- .../engine/src/__tests__/merger-ai-cleanup.test.ts | 50 +++++++++---- .../__tests__/self-healing-tempdir-sweep.test.ts | 87 +++++++++++++++++++--- .../engine/src/__tests__/worktree-paths.test.ts | 24 ++++++ .../engine/src/__tests__/worktree-pool.test.ts | 55 ++++++++++++++ packages/engine/src/merger-ai.ts | 27 +++++-- packages/engine/src/self-healing.ts | 25 +++---- packages/engine/src/worktree-paths.ts | 17 +++++ packages/engine/src/worktree-pool.ts | 10 +-- 10 files changed, 247 insertions(+), 54 deletions(-) Fusion-Task-Id: FN-6295 Fusion-Task-Lineage: d95d32c2-3253-4964-b68a-252b4cb65109
93 lines
4.3 KiB
TypeScript
93 lines
4.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { homedir } from "node:os";
|
|
import { join, resolve } from "node:path";
|
|
import {
|
|
AI_MERGE_DIRNAME,
|
|
isAiMergeContainerDir,
|
|
isInsideConfiguredWorktreesDir,
|
|
resolveAiMergeRootPath,
|
|
resolveTaskWorktreePath,
|
|
resolveTaskWorktreePathForBackend,
|
|
resolveWorktreesDir,
|
|
} from "../worktree-paths.js";
|
|
|
|
describe("worktree-paths", () => {
|
|
const rootDir = "/tmp/repo-name";
|
|
|
|
it("defaults to <rootDir>/.worktrees when unset", () => {
|
|
expect(resolveWorktreesDir(rootDir, undefined)).toBe(join(rootDir, ".worktrees"));
|
|
});
|
|
|
|
it("defaults to <rootDir>/.worktrees when settings object is present but worktreesDir is unset", () => {
|
|
expect(resolveWorktreesDir(rootDir, {} as any)).toBe(join(rootDir, ".worktrees"));
|
|
});
|
|
|
|
it("supports absolute path", () => {
|
|
expect(resolveWorktreesDir(rootDir, { worktreesDir: "/var/tmp/fn-worktrees" } as any)).toBe("/var/tmp/fn-worktrees");
|
|
});
|
|
|
|
it("supports ~ expansion", () => {
|
|
expect(resolveWorktreesDir(rootDir, { worktreesDir: "~/.fn-worktrees" } as any)).toBe(join(homedir(), ".fn-worktrees"));
|
|
});
|
|
|
|
it("supports relative path with {repo}", () => {
|
|
expect(resolveWorktreesDir(rootDir, { worktreesDir: "../{repo}.worktrees" } as any)).toBe(resolve(rootDir, "../repo-name.worktrees"));
|
|
});
|
|
|
|
it("supports {repo} substitution mid-path", () => {
|
|
expect(resolveWorktreesDir(rootDir, { worktreesDir: "~/.fn/{repo}/trees" } as any)).toBe(join(homedir(), ".fn/repo-name/trees"));
|
|
});
|
|
|
|
it("builds task worktree path under configured dir", () => {
|
|
expect(resolveTaskWorktreePath(rootDir, { worktreesDir: "../{repo}.worktrees" } as any, "fn-123")).toBe(
|
|
resolve(rootDir, "../repo-name.worktrees/fn-123"),
|
|
);
|
|
});
|
|
|
|
it("builds the AI-merge root under the default worktrees dir", () => {
|
|
expect(resolveAiMergeRootPath(rootDir, undefined)).toBe(join(rootDir, ".worktrees", AI_MERGE_DIRNAME));
|
|
});
|
|
|
|
it("builds the AI-merge root under an absolute custom worktrees dir", () => {
|
|
expect(resolveAiMergeRootPath(rootDir, { worktreesDir: "/tmp/ext-worktrees" } as any)).toBe(join("/tmp/ext-worktrees", AI_MERGE_DIRNAME));
|
|
});
|
|
|
|
it("builds the AI-merge root under expanded {repo} and ~ worktrees dirs", () => {
|
|
expect(resolveAiMergeRootPath(rootDir, { worktreesDir: "../{repo}.worktrees" } as any)).toBe(
|
|
resolve(rootDir, "../repo-name.worktrees", AI_MERGE_DIRNAME),
|
|
);
|
|
expect(resolveAiMergeRootPath(rootDir, { worktreesDir: "~/.fn/{repo}/trees" } as any)).toBe(join(homedir(), ".fn/repo-name/trees", AI_MERGE_DIRNAME));
|
|
});
|
|
|
|
it("identifies only the dedicated AI-merge container name", () => {
|
|
expect(isAiMergeContainerDir(AI_MERGE_DIRNAME)).toBe(true);
|
|
expect(isAiMergeContainerDir("fusion-ai-merge-fn-1-abc")).toBe(false);
|
|
expect(isAiMergeContainerDir(".ai-merge-child")).toBe(false);
|
|
});
|
|
|
|
it("detects paths inside and outside configured dir", () => {
|
|
const dir = resolveWorktreesDir(rootDir, { worktreesDir: "../{repo}.worktrees" } as any);
|
|
expect(isInsideConfiguredWorktreesDir(rootDir, { worktreesDir: "../{repo}.worktrees" } as any, join(dir, "fn-1"))).toBe(true);
|
|
expect(isInsideConfiguredWorktreesDir(rootDir, { worktreesDir: "../{repo}.worktrees" } as any, join(rootDir, "elsewhere", "fn-1"))).toBe(false);
|
|
expect(isInsideConfiguredWorktreesDir(rootDir, { worktreesDir: "../{repo}.worktrees" } as any, dir)).toBe(false);
|
|
});
|
|
|
|
it("legacy .worktrees default still works for containment checks", () => {
|
|
expect(isInsideConfiguredWorktreesDir(rootDir, undefined, join(rootDir, ".worktrees", "fn-1"))).toBe(true);
|
|
expect(isInsideConfiguredWorktreesDir(rootDir, undefined, join(rootDir, "fn-1"))).toBe(false);
|
|
});
|
|
|
|
it("delegates to worktrunk backend path resolver", async () => {
|
|
const resolver = async () => "/tmp/custom/fusion-fn-1";
|
|
await expect(
|
|
resolveTaskWorktreePathForBackend(rootDir, "fn-1", undefined, { kind: "worktrunk", resolveWorktreePath: resolver }, "fusion/fn-1"),
|
|
).resolves.toBe("/tmp/custom/fusion-fn-1");
|
|
});
|
|
|
|
it("falls back to native resolver for non-worktrunk backends", async () => {
|
|
await expect(
|
|
resolveTaskWorktreePathForBackend(rootDir, "fn-1", { worktreesDir: "../{repo}.worktrees" } as any, { kind: "native" }, "fusion/fn-1"),
|
|
).resolves.toBe(resolve(rootDir, "../repo-name.worktrees/fn-1"));
|
|
});
|
|
});
|