import { describe, expect, test } from "bun:test" import { createComputed, createRoot, createSignal } from "solid-js" import type { CommandPaletteEntry } from "@/shell/commands/palette" import { createCommandPaletteSearch } from "@/shell/commands/search" const copy: CommandPaletteEntry = { id: "command:session.copyID", type: "command", title: "Copy Session ID", category: "Commands", } const file: CommandPaletteEntry = { id: "file:copy.txt", type: "file", title: "copy.txt", category: "Files" } const setting: CommandPaletteEntry = { id: "setting:copy", type: "setting", title: "Copy on select", category: "Settings", } const session: CommandPaletteEntry = { id: "session:copy", type: "session", title: "Copy session", category: "Sessions", updated: 2, } const tab: CommandPaletteEntry = { ...session, id: "session:tab", title: "Copy tab", updated: 1 } describe("command palette search", () => { test("matches commands synchronously and cancels obsolete requests", () => { const signals: AbortSignal[] = [] const root = createRoot((dispose) => { const [query, setQuery] = createSignal("") const search = createCommandPaletteSearch({ query, items: (text) => (text === "copy session" ? [copy] : []), sources: [ (_text, signal) => { signals.push(signal) return new Promise(() => {}) }, ], }) return { search, setQuery, dispose } }) root.setQuery(" copy session ") expect(root.search.items()).toEqual([copy]) expect(root.search.loading()).toBe(true) expect(signals[0].aborted).toBe(true) root.setQuery("no match") expect(root.search.items()).toEqual([]) expect(signals[1].aborted).toBe(true) root.dispose() expect(signals[2].aborted).toBe(true) }) test("lists commands first, then sessions with open tabs leading, settings and files, publishes each source independently, and drops stale results on a new query", async () => { const files = Promise.withResolvers() const sessions = Promise.withResolvers() const fileVisible = Promise.withResolvers() const sessionVisible = Promise.withResolvers() const root = createRoot((dispose) => { const [query, setQuery] = createSignal("copy") const search = createCommandPaletteSearch({ query, items: () => [copy, setting], sources: [() => sessions.promise, () => files.promise], open: (entry) => entry.id === tab.id, }) createComputed(() => { if (search.items().some((entry) => entry.id === file.id)) fileVisible.resolve() if (search.items().some((entry) => entry.id === session.id)) sessionVisible.resolve() }) return { search, setQuery, dispose } }) expect(root.search.items()).toEqual([copy, setting]) files.resolve([file]) await fileVisible.promise expect(root.search.items()).toEqual([copy, setting, file]) expect(root.search.loading()).toBe(true) sessions.resolve([session, tab]) await sessionVisible.promise expect(root.search.items()).toEqual([copy, tab, session, setting, file]) expect(root.search.loading()).toBe(false) root.setQuery("new query") expect(root.search.items()).toEqual([copy, setting]) root.dispose() }) test("failed searches do not hide commands or successful sources", async () => { const settled = Promise.withResolvers() const root = createRoot((dispose) => { const search = createCommandPaletteSearch({ query: () => "copy", items: () => [copy], sources: [() => Promise.reject(new Error("offline")), () => Promise.resolve([file])], }) createComputed(() => { if (!search.loading()) settled.resolve() }) return { search, dispose } }) await settled.promise expect(root.search.items()).toEqual([copy, file]) root.dispose() }) test("filters previous remote results synchronously while a refined query is pending", async () => { const first = Promise.withResolvers() const second = Promise.withResolvers() const otherFile: CommandPaletteEntry = { id: "file: readme.md", type: "file", title: "readme.md", category: "Files" } const ready = Promise.withResolvers() const root = createRoot((dispose) => { const [query, setQuery] = createSignal("c") const search = createCommandPaletteSearch({ query, items: (q) => (q === "c" ? [copy] : []), sources: [(q) => (q === "c" ? first.promise : second.promise)], }) createComputed(() => { if (search.items().some((entry) => entry.id === file.id)) ready.resolve() }) return { search, setQuery, dispose } }) first.resolve([file, otherFile]) await ready.promise expect(root.search.items()).toEqual([copy, file, otherFile]) root.setQuery("copy") expect(root.search.loading()).toBe(true) expect(root.search.items()).toEqual([file]) root.dispose() }) })