// @vitest-environment happy-dom import { describe, expect, it } from "vitest"; import { createWorktreesView } from "./worktreesView.js"; import type { RetainedWorktree } from "./types.js"; function worktree(overrides: Partial): RetainedWorktree { return { id: "11111111-2222-3333-4444-555555555555", path: "/home/naps/tea/maestro/worktrees/feat/login", base_repo: "/home/naps/tea/maestro", branch: "feat/login", reason: "force-killed", retained_at: "2026-08-24T10:00:00Z", in_the_way: { uncommitted_files: 2, untracked_files: 1, unpushed_commits: 3, }, ...overrides, }; } /** Mount a view over a scripted store and return (el, reclaimed-ids). */ function mount(items: RetainedWorktree[], homeDir = "/home/naps") { const reclaimed: string[] = []; const view = createWorktreesView({ load: async () => items, reclaim: async (id) => { reclaimed.push(id); items.splice( items.findIndex((w) => w.id === id), 1, ); }, homeDir: () => homeDir, }); document.body.appendChild(view.el); return { view, reclaimed }; } describe("retained-worktrees view (#84)", () => { it("lists branch, abbreviated path, reason and what is in the way", async () => { const { view } = mount([ worktree({ path: "/home/naps/tea/maestro/worktrees/feat/login", }), ]); await view.refresh(); const text = view.el.textContent ?? ""; expect(text).toContain("feat/login"); // Sidebar-style abbreviation of the path against the daemon's home dir, // full path kept in the tooltip. expect(text).toContain("t/m/w/f/login"); const pathEl = view.el.querySelector(".wt-path")!; expect(pathEl.title).toBe("/home/naps/tea/maestro/worktrees/feat/login"); // Same reason labels as `maestro worktrees list` and the TUI overlay. expect(text).toContain("force-killed"); // Same phrasing as InTheWay's Display impl — shared with the kill refusal. expect(text).toContain("2 uncommitted files, 1 untracked file, 3 unpushed commits"); }); it("says a tree whose work has since been pushed is clear", async () => { const { view } = mount([worktree({ in_the_way: {} })]); await view.refresh(); expect(view.el.textContent).toContain("no unsaved work"); }); it("renders an empty store as a normal state, not an error", async () => { const { view } = mount([]); await view.refresh(); const text = view.el.textContent ?? ""; expect(text).toContain("no retained worktrees"); expect(text.toLowerCase()).not.toContain("error"); expect(text.toLowerCase()).not.toContain("failed"); }); it("guards reclaim with a confirmation that names what is lost and that the branch survives", async () => { const wt = worktree({ branch: "feat/login" }); const { view, reclaimed } = mount([wt]); await view.refresh(); view.el.querySelector(".wt-reclaim-btn")!.click(); const confirmText = view.el.querySelector(".wt-confirm")!.textContent ?? ""; expect(confirmText).toContain("Reclaim worktree feat/login?"); // Names what will be lost before it happens… expect(confirmText).toContain( "2 uncommitted files, 1 untracked file, 3 unpushed commits", ); // …and says reclaim does not take the commits too. expect(confirmText).toContain("never deleted"); view.el.querySelector(".wt-confirm-cancel")!.click(); expect(reclaimed).toEqual([]); view.el.querySelector(".wt-reclaim-btn")!.click(); view.el.querySelector(".wt-confirm-go")!.click(); await Promise.resolve(); await Promise.resolve(); expect(reclaimed).toEqual(["11111111-2222-3333-4444-555555555555"]); // The row is gone once the refresh lands. expect(view.el.textContent).toContain("no retained worktrees"); }); it("sorts oldest-retained first, like every other client", async () => { const { view } = mount([ worktree({ branch: "newer", retained_at: "2026-08-24T12:00:00Z" }), worktree({ branch: "older", retained_at: "2026-08-24T08:00:00Z" }), ]); await view.refresh(); const branches = [...view.el.querySelectorAll(".wt-branch")].map((n) => n.textContent); expect(branches).toEqual(["older", "newer"]); }); });