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"));
});
});