test(FN-4623): cover backend layout resolution paths

Fusion-Task-Id: FN-4623
Fusion-Task-Lineage: 58122853-cab7-4102-9649-4ceda4c5959e
This commit is contained in:
Fusion (runfusion.ai)
2026-05-15 19:23:03 -07:00
committed by gsxdsm
parent 0f372f67c3
commit a2245e1959
3 changed files with 82 additions and 9 deletions

View File

@@ -112,6 +112,13 @@ describe("NativeWorktreeBackend", () => {
expect.objectContaining({ cwd: "/repo", timeout: 120000, maxBuffer: 10485760 }),
);
});
it("resolves native worktree path via configured worktreesDir", async () => {
const backend = new NativeWorktreeBackend({ settings: { worktreesDir: "../{repo}.worktrees" } as any });
await expect(
backend.resolveWorktreePath({ rootDir: "/repo/project", worktreeName: "fn-1", branch: "fusion/fn-1" }),
).resolves.toBe("/repo/project.worktrees/fn-1");
});
});
describe("WorktrunkWorktreeBackend", () => {
@@ -268,6 +275,29 @@ describe("WorktrunkWorktreeBackend", () => {
).rejects.toMatchObject({ code: "worktrunk_sync_conflict", operation: "sync" });
});
it("resolves worktrunk path from wt config show template", async () => {
execFileMock.mockResolvedValue({ stdout: '{"config":{"worktree-path":"{{ repo_path }}/../{{ repo }}.{{ branch | sanitize }}"}}', stderr: "" });
const backend = new WorktrunkWorktreeBackend({ binaryPath: "worktrunk" });
await expect(
backend.resolveWorktreePath({ rootDir: "/repo/project", worktreeName: "ignored", branch: "fusion/fn-1" }),
).resolves.toBe("/repo/project.fusion-fn-1");
expect(execFileMock).toHaveBeenCalledWith(
"worktrunk",
["config", "show", "--format", "json"],
expect.objectContaining({ cwd: "/repo/project", timeout: 5000, maxBuffer: 10485760 }),
);
});
it("falls back to default layout template when config cannot be read", async () => {
execFileMock.mockRejectedValue(new Error("missing config"));
const backend = new WorktrunkWorktreeBackend({ binaryPath: "worktrunk" });
await expect(
backend.resolveWorktreePath({ rootDir: "/repo/project", worktreeName: "ignored", branch: "fusion/fn-1" }),
).resolves.toBe("/repo/project/.worktrees/fusion-fn-1");
});
it("prunes by listing worktrees and removing worktrunk managed entries", async () => {
execMock.mockResolvedValue({
stdout:

View File

@@ -4,6 +4,7 @@ import { join, resolve } from "node:path";
import {
isInsideConfiguredWorktreesDir,
resolveTaskWorktreePath,
resolveTaskWorktreePathForBackend,
resolveWorktreesDir,
} from "../worktree-paths.js";
@@ -47,4 +48,17 @@ describe("worktree-paths", () => {
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"));
});
});

View File

@@ -1,5 +1,6 @@
import { exec, execFile } from "node:child_process";
import { access } from "node:fs/promises";
import { basename, resolve } from "node:path";
import { promisify } from "node:util";
import type { Settings } from "@fusion/core";
import { resolveTaskWorktreePath } from "./worktree-paths.js";
@@ -119,6 +120,24 @@ function getErrorExitCode(error: unknown): number | null {
return null;
}
function findStringByKey(value: unknown, key: string): string | null {
if (!value || typeof value !== "object") return null;
if (Array.isArray(value)) {
for (const item of value) {
const found = findStringByKey(item, key);
if (found) return found;
}
return null;
}
const record = value as Record<string, unknown>;
if (typeof record[key] === "string") return record[key] as string;
for (const nested of Object.values(record)) {
const found = findStringByKey(nested, key);
if (found) return found;
}
return null;
}
function parseWorktreesFromPorcelain(porcelain: string): Array<{ path: string; branch?: string }> {
const lines = porcelain.split("\n");
const rows: Array<{ path: string; branch?: string }> = [];
@@ -408,18 +427,28 @@ export class WorktrunkWorktreeBackend implements WorktreeBackend {
async resolveWorktreePath(input: { rootDir: string; worktreeName: string; branch: string }): Promise<string> {
const template = await this.resolveWorktrunkTemplate(input.rootDir);
const sanitizedBranch = input.branch.replace(/[\\/]/g, "-");
return template.replace(/\{\{\s*branch\s*\|\s*sanitize\s*\}\}/g, sanitizedBranch).replace(/\{\{\s*branch\s*\}\}/g, input.branch);
const expanded = template
.replace(/^~(?=$|[\\/])/, process.env.HOME ?? "~")
.replace(/\{\{\s*repo_path\s*\}\}/g, input.rootDir)
.replace(/\{\{\s*repo\s*\}\}/g, basename(input.rootDir))
.replace(/\{\{\s*branch\s*\|\s*sanitize\s*\}\}/g, sanitizedBranch)
.replace(/\{\{\s*branch\s*\}\}/g, input.branch);
return resolve(input.rootDir, expanded);
}
private async resolveWorktrunkTemplate(rootDir: string): Promise<string> {
const { stdout } = await execAsync("git config --get worktrunk.worktree-path", {
cwd: rootDir,
encoding: "utf-8",
timeout: WORKTRUNK_TIMEOUTS_MS.layout,
maxBuffer: MAX_BUFFER,
});
const configured = stdout.trim();
return configured || `${rootDir}/.worktrees/{{ branch | sanitize }}`;
try {
const { stdout } = await this.runWorktrunk(["config", "show", "--format", "json"], {
cwd: rootDir,
operation: "layout",
});
const parsed = JSON.parse(stdout) as Record<string, unknown>;
const fromJson = findStringByKey(parsed, "worktree-path");
if (fromJson) return fromJson;
} catch {
// fall back to documented default template when config cannot be read.
}
return "{{ repo_path }}/.worktrees/{{ branch | sanitize }}";
}
}