import React from "react"; import { describe, expect, it } from "vitest"; import { render } from "ink-testing-library"; import { CommandPalette } from "./CommandPalette"; import { TerminalContextProvider, type TTerminalContextValue } from "../app/TerminalContext"; import { resolveTerminalTheme } from "../services/terminal-theme"; import type { TInteractiveCommandDefinition } from "../services/command-registry"; const testContextValue: TTerminalContextValue = { theme: resolveTerminalTheme({ noColor: true }), capabilities: { isTTY: true, isCI: false, noColor: true, supportsUnicode: true, columns: 80, rows: 24 }, registry: [], projectName: "test-project", }; function makeCommand(id: string, path: string[], description: string, category: string, keywords: string[] = []): TInteractiveCommandDefinition { return { id, path, title: path.join(" "), description, category, aliases: [], keywords, interactiveCapability: "native", command: {} as TInteractiveCommandDefinition["command"], }; } const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); describe("CommandPalette", () => { it("lists every command when no query has been typed yet", () => { const commands = [ makeCommand("git.move-code", ["git", "move-code"], "Guided workflow", "Git", ["move code"]), makeCommand("db.studio", ["db", "studio"], "Open Database Studio", "Database", ["open db"]), ]; const { lastFrame } = render( undefined} onCancel={() => undefined} /> , ); const frame = lastFrame() ?? ""; expect(frame).toContain("git move-code"); expect(frame).toContain("db studio"); }); it("fuzzy-filters commands as the user types", async () => { const commands = [ makeCommand("git.move-code", ["git", "move-code"], "Guided workflow", "Git", ["move code"]), makeCommand("db.studio", ["db", "studio"], "Open Database Studio", "Database", ["open db"]), ]; const { lastFrame, stdin } = render( undefined} onCancel={() => undefined} /> , ); // Ink attaches its raw-mode stdin listener from a useEffect, which only // flushes after this initial tick — writing synchronously right after // render() would be dropped before any listener exists. await wait(50); stdin.write("db"); await wait(50); const frame = lastFrame() ?? ""; expect(frame).toContain("db studio"); expect(frame).not.toContain("git move-code"); }); it("ranks an exact command-name match above a same-category command whose description merely mentions the query", async () => { // Regression test: the palette used to score its own "/"-prefixed title // and "{Category} — description" string, so a query like "ai sessions" // scored a false 80 (startsWith) against every AI-Sessions-category // command's description, beating the real 100 (exact match) that "ai // sessions" itself could only ever get through its unprefixed searchText. const commands = [ makeCommand("ai.studio", ["ai", "studio"], "Open the local AI Studio (browser UI) for analyzing Claude Code / Codex sessions", "AI Sessions", ["ai studio"]), makeCommand("ai.sessions", ["ai", "sessions"], "List recent AI sessions in the terminal", "AI Sessions", []), ]; const { lastFrame, stdin } = render( undefined} onCancel={() => undefined} /> , ); await wait(50); stdin.write("ai sessions"); await wait(50); const frame = lastFrame() ?? ""; expect(frame.indexOf("/ai sessions")).toBeGreaterThanOrEqual(0); expect(frame.indexOf("/ai sessions")).toBeLessThan(frame.indexOf("/ai studio")); }); });