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:
committed by
gsxdsm
parent
0f372f67c3
commit
a2245e1959
@@ -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:
|
||||
|
||||
@@ -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"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 }}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user