diff --git a/src/application/use-cases/cleanup-worktrees.test.ts b/src/application/use-cases/cleanup-worktrees.test.ts index 9b1f0e6..090084c 100644 --- a/src/application/use-cases/cleanup-worktrees.test.ts +++ b/src/application/use-cases/cleanup-worktrees.test.ts @@ -330,6 +330,31 @@ describe("cleanupWorktrees", () => { }); }); + test("branch deletion failure surfaces as error report", async () => { + // Unmerged branch → force fallback. Stub deleteBranchForce to fail so the + // delete-branch use case returns `failed`, which cleanup must surface as + // an `error` report. + const baseGit = createFakeGit({ + worktrees: [mainWt, featureA], + branches: ["main", "feature-a"], + goneBranches: ["feature-a"], + mergedBranches: [], + commitCountMap: new Map([["main..feature-a", 3]]), + }); + const git = { + ...baseGit, + async deleteBranchForce(_branch: string) { + return { success: false as const, error: { code: "UNKNOWN" as const, message: "force boom" } }; + }, + }; + const output = expectOk(await cleanupWorktrees({ force: true, dryRun: false }, { git })); + + expect(output.reports[0]).toMatchObject({ + branch: "feature-a", + result: { status: "error", message: "force boom" }, + }); + }); + test("default branch is never cleaned", async () => { const git = createFakeGit({ worktrees: [mainWt], diff --git a/src/application/use-cases/cleanup-worktrees.ts b/src/application/use-cases/cleanup-worktrees.ts index 09cd0e9..837ff84 100644 --- a/src/application/use-cases/cleanup-worktrees.ts +++ b/src/application/use-cases/cleanup-worktrees.ts @@ -1,6 +1,7 @@ import type { GitPort } from "../../domain/ports/git-port.ts"; import { Result as R, type Result } from "../../shared/result.ts"; import { classifyGoneBranch } from "./classify-gone-branch.ts"; +import { deleteBranch } from "./delete-branch.ts"; import { isFullyMerged } from "./is-fully-merged.ts"; export interface CleanupWorktreesInput { @@ -86,33 +87,37 @@ export async function cleanupWorktrees( } } - const deleteResult = await git.deleteBranch(branch); - if (!deleteResult.success) { - if (deleteResult.error.code === "BRANCH_NOT_MERGED") { - const forceResult = await git.deleteBranchForce(branch); - if (!forceResult.success) { - reports.push({ - branch, - worktreePath, - result: { status: "error", message: forceResult.error.message }, - }); - return; - } - } else { + // Mapping: + // - `deleted` → existing `cleaned` / `branch-only` report. + // - `failed` → `error` report carrying the git error message. + // - `not-merged` → unreachable because we pass `force: true`. Handled + // defensively so a future contract change in + // `deleteBranch` doesn't silently report a non-deleted + // branch as `cleaned`. + const outcome = await deleteBranch({ branch, force: true, deleteRemote: false }, { git }); + switch (outcome.status) { + case "deleted": reports.push({ branch, worktreePath, - result: { status: "error", message: deleteResult.error.message }, + result: { status: worktree ? "cleaned" : "branch-only" }, + }); + return; + case "failed": + reports.push({ + branch, + worktreePath, + result: { status: "error", message: outcome.message }, + }); + return; + case "not-merged": + reports.push({ + branch, + worktreePath, + result: { status: "error", message: `Branch "${branch}" is not fully merged` }, }); return; - } } - - reports.push({ - branch, - worktreePath, - result: { status: worktree ? "cleaned" : "branch-only" }, - }); }; if (goneBranches.length > 0) {