Files
fusion/packages/engine/src/__tests__/worktree-paths.test.ts
gsxdsm 283adce4d0 FN-6295: move AI merge clean rooms into worktrees
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
2026-06-12 12:49:34 -07:00

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"));
});
});