// @vitest-environment happy-dom // // The sidebar's half of the command bar's action page (#151): the bar picks // kill or rename and raises the flow the card already has, so what the page // runs is what the ✕ button and the `r` key run. import { afterEach, beforeAll, describe, expect, it } from "vitest"; import { KILL_SESSION_EVENT, RENAME_SESSION_EVENT, renderSidebar } from "./catalog.js"; import type { CatalogListener, CatalogStore } from "./events.js"; import type { Session } from "./types.js"; beforeAll(() => { Object.defineProperty(window, "localStorage", { value: { getItem: () => null, setItem: () => {} }, }); }); let mounted: { el: HTMLElement; dispose: () => void } | null = null; afterEach(() => { document.querySelector(".kill-dialog")?.remove(); mounted?.dispose(); mounted = null; document.body.replaceChildren(); }); function session(overrides: Partial = {}): Session { return { id: "aaaaaaaa-0000-0000-0000-0000000000ff", cwd: "/home/naps/tea/maestro", command: "claude", env: {}, metadata: { name: "amber-falcon" }, status: "Running", created_at: "2026-08-24T10:00:00Z", ...overrides, }; } function storeWith(sessions: Session[]): CatalogStore { const state = { sessions: new Map(sessions.map((s) => [s.id, s])) }; return { snapshot: () => state, subscribe: (fn: CatalogListener) => { fn(state); return () => {}; }, } as unknown as CatalogStore; } function mount(s: Session): HTMLElement { const handle = renderSidebar(storeWith([s])); mounted = handle; document.body.appendChild(handle.el); return handle.el; } function raise(name: string, s: Session): void { document.dispatchEvent(new CustomEvent(name, { detail: s })); } /** Let the kill chain's awaits (fetch, its json body) settle. */ async function flush(): Promise { for (let i = 0; i < 8; i++) await Promise.resolve(); } describe("kill from the action page", () => { it("goes through the same confirmation the sidebar raises (#160)", async () => { const calls: string[] = []; window.fetch = ((url: string, init?: RequestInit) => { calls.push(`${init?.method ?? "GET"} ${url}`); return Promise.resolve(new Response(null, { status: 204 })); }) as typeof fetch; const s = session(); mount(s); raise(KILL_SESSION_EVENT, s); await flush(); expect(document.querySelector(".kill-dialog")?.getAttribute("aria-label")).toBe( "Kill amber-falcon", ); expect(calls.filter((c) => c.startsWith("DELETE"))).toHaveLength(0); document.querySelector(".kill-dialog-kill")?.click(); await flush(); expect(calls).toContain(`DELETE /api/sessions/${s.id}`); expect(document.querySelector(".kill-dialog")).toBeNull(); }); it("raises the existing confirm-and-blockers dialog on a guarded refusal", async () => { window.fetch = ((url: string) => { if (String(url).includes("force=true")) { return Promise.resolve(new Response(null, { status: 204 })); } return Promise.resolve( new Response( JSON.stringify({ error: { code: "kill_guarded", message: "worktree has unsaved work", unsaved_work: { untracked_files: 1 }, }, }), { status: 409, headers: { "content-type": "application/json" } }, ), ); }) as typeof fetch; const s = session({ blocked_work: { untracked_files: 1 } }); mount(s); raise(KILL_SESSION_EVENT, s); await flush(); document.querySelector(".kill-dialog-kill")?.click(); await flush(); const dialog = document.querySelector(".kill-dialog"); expect(dialog?.getAttribute("aria-label")).toBe("Kill amber-falcon"); expect(dialog?.textContent).toContain("1 untracked file"); }); }); describe("rename from the action page", () => { it("expands the folder hiding the card, since the prompt lives in the card", () => { window.fetch = (() => Promise.reject(new Error("offline"))) as typeof fetch; const s = session(); const el = mount(s); const folder = el.querySelector(".tree-folder-header"); folder?.click(); expect(el.querySelector(`[data-session-id="${s.id}"]`)).toBeNull(); raise(RENAME_SESSION_EVENT, s); expect(el.querySelector(".leaf-rename")?.value).toBe("amber-falcon"); }); it("raises the card's own prompt, pre-filled with the session's name", () => { window.fetch = (() => Promise.reject(new Error("offline"))) as typeof fetch; const s = session(); const el = mount(s); raise(RENAME_SESSION_EVENT, s); const input = el.querySelector(".leaf-rename"); expect(input?.value).toBe("amber-falcon"); expect(input?.getAttribute("aria-label")).toBe("Rename amber-falcon"); }); });