107 lines
3.7 KiB
TypeScript
107 lines
3.7 KiB
TypeScript
// @vitest-environment happy-dom
|
|
import { beforeAll, describe, expect, it } from "vitest";
|
|
|
|
import { renderSidebar } from "./catalog.js";
|
|
import type { CatalogListener, CatalogStore } from "./events.js";
|
|
import type { InTheWay, Session } from "./types.js";
|
|
|
|
// happy-dom runs without a backing store; renderSidebar reads the persisted
|
|
// sidebar width on mount.
|
|
beforeAll(() => {
|
|
Object.defineProperty(window, "localStorage", {
|
|
value: { getItem: () => null, setItem: () => {} },
|
|
});
|
|
// No daemon here; renderSidebar treats a failed info fetch as "absolute
|
|
// paths are a fine fallback".
|
|
window.fetch = () => Promise.reject(new Error("offline"));
|
|
});
|
|
|
|
function session(overrides: Partial<Session>): Session {
|
|
return {
|
|
id: "11111111-2222-3333-4444-555555555555",
|
|
cwd: "/home/naps/tea/maestro",
|
|
command: "claude",
|
|
env: {},
|
|
metadata: {},
|
|
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,
|
|
// Like the real store, deliver an immediate snapshot so the sidebar
|
|
// renders its rows during construction.
|
|
subscribe: (fn: CatalogListener) => {
|
|
fn(state);
|
|
return () => {};
|
|
},
|
|
} as unknown as CatalogStore;
|
|
}
|
|
|
|
function rowFor(el: HTMLElement, id: string): HTMLElement | null {
|
|
return el.querySelector<HTMLElement>(`[data-session-id="${id}"]`);
|
|
}
|
|
|
|
describe("blocked-work indicator (#83)", () => {
|
|
it("marks the row of a blocked session with the shared phrasing", () => {
|
|
const blocked = session({
|
|
id: "aaaaaaaa-0000-0000-0000-000000000001",
|
|
blocked_work: { uncommitted_files: 3, unpushed_commits: 2 },
|
|
});
|
|
const el = renderSidebar(storeWith([blocked])).el;
|
|
const badge = rowFor(el, blocked.id)?.querySelector(".leaf-blocked");
|
|
expect(badge?.textContent).toContain("⚠");
|
|
expect(badge?.textContent).toContain("3 uncommitted files, 2 unpushed commits");
|
|
});
|
|
|
|
it("does not mark clear or unprobed sessions", () => {
|
|
const clear = session({
|
|
id: "aaaaaaaa-0000-0000-0000-000000000002",
|
|
blocked_work: {} as InTheWay,
|
|
});
|
|
const unprobed = session({
|
|
id: "aaaaaaaa-0000-0000-0000-000000000003",
|
|
});
|
|
const el = renderSidebar(storeWith([clear, unprobed])).el;
|
|
expect(rowFor(el, clear.id)?.querySelector(".leaf-blocked")).toBeNull();
|
|
expect(rowFor(el, unprobed.id)?.querySelector(".leaf-blocked")).toBeNull();
|
|
});
|
|
|
|
it("restates the warning at the kill confirm", async () => {
|
|
const confirmations: string[] = [];
|
|
window.confirm = (msg?: string) => {
|
|
confirmations.push(msg ?? "");
|
|
return false;
|
|
};
|
|
const blocked = session({
|
|
id: "aaaaaaaa-0000-0000-0000-000000000004",
|
|
command: "bash",
|
|
blocked_work: { untracked_files: 1 },
|
|
});
|
|
const el = renderSidebar(storeWith([blocked])).el;
|
|
const kill = rowFor(el, blocked.id)?.querySelector<HTMLButtonElement>(".leaf-kill");
|
|
kill?.click();
|
|
await Promise.resolve();
|
|
expect(confirmations).toHaveLength(1);
|
|
expect(confirmations[0]).toContain("1 untracked file");
|
|
expect(confirmations[0]).toContain("refused unless forced");
|
|
});
|
|
|
|
it("confirms plainly when nothing blocks", async () => {
|
|
const confirmations: string[] = [];
|
|
window.confirm = (msg?: string) => {
|
|
confirmations.push(msg ?? "");
|
|
return false;
|
|
};
|
|
const plain = session({ id: "aaaaaaaa-0000-0000-0000-000000000005" });
|
|
const el = renderSidebar(storeWith([plain])).el;
|
|
rowFor(el, plain.id)?.querySelector<HTMLButtonElement>(".leaf-kill")?.click();
|
|
await Promise.resolve();
|
|
expect(confirmations).toEqual(['Kill session "claude"?']);
|
|
});
|
|
});
|