import assert from "node:assert/strict"; import test from "node:test"; import { scheduleWorkspaceSearch } from "../src/lib/workspace-search.ts"; import { retainSelectedHistoryItem, resolveSidebarSelectionAfterRefresh } from "../src/lib/sidebar-selection.ts"; const flush = async () => { await Promise.resolve(); await Promise.resolve(); }; test("typing is debounced and superseded requests never start", async (t) => { t.mock.timers.enable({ apis: ["setTimeout"] }); const queries = []; const results = []; const request = async (query) => { queries.push(query); return { items: [], hasMore: false }; }; const reportError = (error) => assert.fail(String(error)); const cancelFirst = scheduleWorkspaceSearch("ne", request, (result) => results.push(result), reportError); t.mock.timers.tick(299); assert.deepEqual(queries, []); cancelFirst(); const cancelLast = scheduleWorkspaceSearch("nebula", request, (result) => results.push(result), reportError); t.mock.timers.tick(300); await flush(); assert.deepEqual(queries, ["nebula"]); assert.deepEqual(results, [{ items: [], hasMore: false }]); cancelLast(); }); test("cancellation aborts the transport and discards stale results and errors", async (t) => { t.mock.timers.enable({ apis: ["setTimeout"] }); let resolveFirst; let rejectSecond; let signal; const results = []; const errors = []; const first = scheduleWorkspaceSearch("first", (_query, value) => { signal = value; return new Promise((resolve) => { resolveFirst = resolve; }); }, (result) => results.push(result), (error) => errors.push(error)); t.mock.timers.tick(300); first(); assert.equal(signal.aborted, true); const second = scheduleWorkspaceSearch("second", () => new Promise((_resolve, reject) => { rejectSecond = reject; }), (result) => results.push(result), (error) => errors.push(error)); t.mock.timers.tick(300); second(); resolveFirst({ items: [{ id: "stale" }], hasMore: true }); rejectSecond(new Error("stale failure")); await flush(); assert.deepEqual(results, []); assert.deepEqual(errors, []); }); test("search results retain excerpts and truncation info; failures reach the UI", async (t) => { t.mock.timers.enable({ apis: ["setTimeout"] }); const expected = { items: [{ type: "chat", id: "old", searchSnippet: "A nebula in the sky" }], hasMore: true }; let received; let error; const cancel = scheduleWorkspaceSearch("nebula", async () => expected, (result) => { received = result; }, assert.fail); t.mock.timers.tick(300); await flush(); assert.deepEqual(received, expected); cancel(); const cancelFailure = scheduleWorkspaceSearch("nebula", async () => { throw new Error("Offline"); }, assert.fail, (result) => { error = result; }); t.mock.timers.tick(300); await flush(); assert.equal(error.message, "Offline"); cancelFailure(); }); test("opening older chat/search results survives collection refresh without restoring deleted or unrelated items", () => { const recent = [{ type: "chat", id: "recent" }]; for (const kind of ["chat", "search"]) { const selection = { kind, id: "old" }; const retained = { type: kind, id: "old" }; const refreshed = retainSelectedHistoryItem(recent, selection, retained); assert.deepEqual(resolveSidebarSelectionAfterRefresh(selection, refreshed), selection); assert.equal(retainSelectedHistoryItem(refreshed, selection, retained), refreshed); assert.equal(retainSelectedHistoryItem(recent, { kind, id: "different" }, retained), recent); assert.equal(retainSelectedHistoryItem(recent, selection, null), recent); assert.equal(retainSelectedHistoryItem(recent, null, retained), recent); } });