import { beforeAll, describe, expect, it } from "vitest"; import type { SessionInfo } from "../src/core/session-manager.js"; import { SessionSelectorComponent } from "../src/modes/interactive/components/session-selector.js"; import { initTheme } from "../src/modes/interactive/theme/theme.js"; type Deferred = { promise: Promise; resolve: (value: T) => void; reject: (err: unknown) => void; }; function createDeferred(): Deferred { let resolve: (value: T) => void = () => {}; let reject: (err: unknown) => void = () => {}; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; } async function flushPromises(): Promise { await new Promise((resolve) => { setImmediate(resolve); }); } function makeSession(overrides: Partial & { id: string }): SessionInfo { return { path: overrides.path ?? `/tmp/${overrides.id}.jsonl`, id: overrides.id, cwd: overrides.cwd ?? "", name: overrides.name, created: overrides.created ?? new Date(0), modified: overrides.modified ?? new Date(0), messageCount: overrides.messageCount ?? 1, firstMessage: overrides.firstMessage ?? "hello", allMessagesText: overrides.allMessagesText ?? "hello", }; } const CTRL_D = "\x04"; const CTRL_BACKSPACE = "\x1b[127;5u"; describe("session selector path/delete interactions", () => { beforeAll(() => { // session selector uses the global theme instance initTheme("dark"); }); it("does not treat Ctrl+Backspace as delete when search query is non-empty", async () => { const sessions = [makeSession({ id: "a" }), makeSession({ id: "b" })]; const selector = new SessionSelectorComponent( async () => sessions, async () => [], () => {}, () => {}, () => {}, () => {}, ); await flushPromises(); const list = selector.getSessionList(); const confirmationChanges: Array = []; list.onDeleteConfirmationChange = (path) => confirmationChanges.push(path); list.handleInput("a"); list.handleInput(CTRL_BACKSPACE); expect(confirmationChanges).toEqual([]); }); it("enters confirmation mode on Ctrl+D even with a non-empty search query", async () => { const sessions = [makeSession({ id: "a" }), makeSession({ id: "b" })]; const selector = new SessionSelectorComponent( async () => sessions, async () => [], () => {}, () => {}, () => {}, () => {}, ); await flushPromises(); const list = selector.getSessionList(); const confirmationChanges: Array = []; list.onDeleteConfirmationChange = (path) => confirmationChanges.push(path); list.handleInput("a"); list.handleInput(CTRL_D); expect(confirmationChanges).toEqual([sessions[0]!.path]); }); it("enters confirmation mode on Ctrl+Backspace when search query is empty", async () => { const sessions = [makeSession({ id: "a" }), makeSession({ id: "b" })]; const selector = new SessionSelectorComponent( async () => sessions, async () => [], () => {}, () => {}, () => {}, () => {}, ); await flushPromises(); const list = selector.getSessionList(); const confirmationChanges: Array = []; list.onDeleteConfirmationChange = (path) => confirmationChanges.push(path); let deletedPath: string | null = null; list.onDeleteSession = async (sessionPath) => { deletedPath = sessionPath; }; list.handleInput(CTRL_BACKSPACE); expect(confirmationChanges).toEqual([sessions[0]!.path]); list.handleInput("\r"); expect(confirmationChanges).toEqual([sessions[0]!.path, null]); expect(deletedPath).toBe(sessions[0]!.path); }); it("does not switch scope back to All when All load resolves after toggling back to Current", async () => { const currentSessions = [makeSession({ id: "current" })]; const allDeferred = createDeferred(); let allLoadCalls = 0; const selector = new SessionSelectorComponent( async () => currentSessions, async () => { allLoadCalls++; return allDeferred.promise; }, () => {}, () => {}, () => {}, () => {}, ); await flushPromises(); const list = selector.getSessionList(); list.handleInput("\t"); // current -> all (starts async load) list.handleInput("\t"); // all -> current allDeferred.resolve([makeSession({ id: "all" })]); await flushPromises(); expect(allLoadCalls).toBe(1); const output = selector.render(120).join("\n"); expect(output).toContain("Resume Session (Current Folder)"); expect(output).not.toContain("Resume Session (All)"); }); it("does not start redundant All loads when toggling scopes while All is already loading", async () => { const currentSessions = [makeSession({ id: "current" })]; const allDeferred = createDeferred(); let allLoadCalls = 0; const selector = new SessionSelectorComponent( async () => currentSessions, async () => { allLoadCalls++; return allDeferred.promise; }, () => {}, () => {}, () => {}, () => {}, ); await flushPromises(); const list = selector.getSessionList(); list.handleInput("\t"); // current -> all (starts async load) list.handleInput("\t"); // all -> current list.handleInput("\t"); // current -> all again while load pending expect(allLoadCalls).toBe(1); allDeferred.resolve([makeSession({ id: "all" })]); await flushPromises(); }); });