feat(FN-4678): add removeWorktree helper to worktree-backend
Adds a `removeWorktree` helper to the worktree backend with corresponding unit tests, and wires it into the worktree pool. Fusion-Task-Id: FN-4678
This commit is contained in:
committed by
gsxdsm
parent
401094c6a9
commit
156392410a
@@ -3,6 +3,7 @@ import {
|
||||
NativeWorktreeBackend,
|
||||
WorktrunkOperationError,
|
||||
WorktrunkWorktreeBackend,
|
||||
removeWorktree,
|
||||
resolveWorktreeBackend,
|
||||
} from "../worktree-backend.js";
|
||||
|
||||
@@ -334,6 +335,84 @@ describe("WorktrunkOperationError", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeWorktree", () => {
|
||||
it("uses native remove and emits worktree:remove audit", async () => {
|
||||
execMock.mockResolvedValue({ stdout: "", stderr: "" });
|
||||
const audit = { git: vi.fn().mockResolvedValue(undefined) } as any;
|
||||
|
||||
await removeWorktree({
|
||||
rootDir: "/repo",
|
||||
worktreePath: "/repo/.worktrees/fn-1",
|
||||
settings: {},
|
||||
audit,
|
||||
});
|
||||
|
||||
expect(execMock).toHaveBeenCalledWith(
|
||||
'git worktree remove --force "/repo/.worktrees/fn-1"',
|
||||
expect.objectContaining({ cwd: "/repo", timeout: 60000 }),
|
||||
);
|
||||
expect(audit.git).toHaveBeenCalledWith({ type: "worktree:remove", target: "/repo/.worktrees/fn-1" });
|
||||
});
|
||||
|
||||
it("uses worktrunk remove and emits worktree:worktrunk-remove", async () => {
|
||||
execMock.mockResolvedValue({ stdout: "", stderr: "" });
|
||||
const audit = { git: vi.fn().mockResolvedValue(undefined) } as any;
|
||||
|
||||
await removeWorktree({
|
||||
rootDir: "/repo",
|
||||
worktreePath: "/repo/.worktrees/fn-1",
|
||||
settings: { worktrunk: { enabled: true, binaryPath: "worktrunk", onFailure: "fail" } as any },
|
||||
audit,
|
||||
taskId: "FN-1",
|
||||
});
|
||||
|
||||
expect(audit.git).toHaveBeenCalledWith({ type: "worktree:worktrunk-remove", target: "/repo/.worktrees/fn-1" });
|
||||
});
|
||||
|
||||
it("falls back to native when worktrunk remove fails and onFailure=fallback-native", async () => {
|
||||
execMock
|
||||
.mockRejectedValueOnce(new WorktrunkOperationError({ operation: "remove", code: "worktrunk_operation_failed", stderr: "boom", exitCode: 1 }))
|
||||
.mockResolvedValueOnce({ stdout: "", stderr: "" });
|
||||
const audit = { git: vi.fn().mockResolvedValue(undefined) } as any;
|
||||
|
||||
await removeWorktree({
|
||||
rootDir: "/repo",
|
||||
worktreePath: "/repo/.worktrees/fn-1",
|
||||
settings: { worktrunk: { enabled: true, binaryPath: "worktrunk", onFailure: "fallback-native" } as any },
|
||||
audit,
|
||||
});
|
||||
|
||||
expect(audit.git).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: "worktree:worktrunk-fallback", target: "/repo/.worktrees/fn-1" }),
|
||||
);
|
||||
expect(audit.git).toHaveBeenCalledWith({ type: "worktree:remove", target: "/repo/.worktrees/fn-1" });
|
||||
});
|
||||
|
||||
it("rethrows worktrunk remove failure when onFailure=fail", async () => {
|
||||
execMock.mockRejectedValue(
|
||||
new WorktrunkOperationError({ operation: "remove", code: "worktrunk_operation_failed", stderr: "boom", exitCode: 1 }),
|
||||
);
|
||||
|
||||
await expect(
|
||||
removeWorktree({
|
||||
rootDir: "/repo",
|
||||
worktreePath: "/repo/.worktrees/fn-1",
|
||||
settings: { worktrunk: { enabled: true, binaryPath: "worktrunk", onFailure: "fail" } as any },
|
||||
}),
|
||||
).rejects.toMatchObject({ code: "worktrunk_operation_failed", operation: "remove" });
|
||||
});
|
||||
|
||||
it("surfaces missing worktrunk binary errors", async () => {
|
||||
await expect(
|
||||
removeWorktree({
|
||||
rootDir: "/repo",
|
||||
worktreePath: "/repo/.worktrees/fn-1",
|
||||
settings: { worktrunk: { enabled: true, onFailure: "fail" } as any },
|
||||
}),
|
||||
).rejects.toMatchObject({ code: "worktrunk_binary_missing", operation: "remove" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveWorktreeBackend", () => {
|
||||
it("uses native for undefined worktrunk", () => {
|
||||
expect(resolveWorktreeBackend({}).kind).toBe("native");
|
||||
|
||||
@@ -3,6 +3,7 @@ import { access } from "node:fs/promises";
|
||||
import { basename, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import type { Settings } from "@fusion/core";
|
||||
import type { RunAuditor } from "./run-audit.js";
|
||||
import { resolveTaskWorktreePath } from "./worktree-paths.js";
|
||||
import { inspectBranchConflict } from "./branch-conflicts.js";
|
||||
import { formatError } from "./logger.js";
|
||||
@@ -457,6 +458,64 @@ export class WorktrunkWorktreeBackend implements WorktreeBackend {
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeWorktree(input: {
|
||||
worktreePath: string;
|
||||
rootDir: string;
|
||||
settings: Partial<Settings>;
|
||||
taskId?: string;
|
||||
audit?: RunAuditor;
|
||||
force?: boolean;
|
||||
timeout?: number;
|
||||
}): Promise<void> {
|
||||
const logger = {
|
||||
log: (_message: string): void => {},
|
||||
warn: (_message: string): void => {},
|
||||
};
|
||||
|
||||
const backend = resolveWorktreeBackend(input.settings, { logger });
|
||||
const removeInput: WorktreeRemoveInput = {
|
||||
rootDir: input.rootDir,
|
||||
worktreePath: input.worktreePath,
|
||||
taskId: input.taskId,
|
||||
};
|
||||
|
||||
if (input.force === false || typeof input.timeout === "number") {
|
||||
// Backwards-compatible helper signature for callers that carried raw git flags/timeouts.
|
||||
// Current backend remove implementations are forceful and use backend-owned timeouts.
|
||||
}
|
||||
|
||||
try {
|
||||
await backend.remove(removeInput);
|
||||
if (input.audit) {
|
||||
await input.audit.git({
|
||||
type: backend.kind === "worktrunk" ? "worktree:worktrunk-remove" : "worktree:remove",
|
||||
target: input.worktreePath,
|
||||
});
|
||||
}
|
||||
return;
|
||||
} catch (error) {
|
||||
if (!(error instanceof WorktrunkOperationError) || input.settings.worktrunk?.onFailure !== "fallback-native") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
logger.warn(`[worktree-backend] falling back to native remove for ${input.worktreePath}`);
|
||||
|
||||
await input.audit?.git({
|
||||
type: "worktree:worktrunk-fallback",
|
||||
target: input.worktreePath,
|
||||
metadata: {
|
||||
op: "fallback-native",
|
||||
stderrPreview: error.stderr?.slice(0, 4096),
|
||||
exitCode: error.exitCode ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
const native = new NativeWorktreeBackend({ logger, settings: input.settings });
|
||||
await native.remove(removeInput);
|
||||
await input.audit?.git({ type: "worktree:remove", target: input.worktreePath });
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveWorktreeBackend(
|
||||
settings: Partial<Settings>,
|
||||
deps: { logger?: { log: (m: string) => void; warn: (m: string) => void } } = {},
|
||||
|
||||
@@ -14,6 +14,7 @@ export {
|
||||
NativeWorktreeBackend,
|
||||
WorktrunkOperationError,
|
||||
WorktrunkWorktreeBackend,
|
||||
removeWorktree,
|
||||
resolveWorktreeBackend,
|
||||
} from "./worktree-backend.js";
|
||||
export type { WorktreeBackend, WorktreeBackendKind } from "./worktree-backend.js";
|
||||
|
||||
Reference in New Issue
Block a user