import { beforeAll, describe, expect, test } from "bun:test"; import type { Model } from "@oh-my-pi/pi-ai"; import { buildModel } from "@oh-my-pi/pi-catalog/build"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import { buildBrowserItems, ModelBrowser, sortModelItems, } from "@oh-my-pi/pi-coding-agent/modes/components/model-browser"; import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; function makeModel(provider: string, id: string): Model { return buildModel({ id, name: id, api: "ollama-chat", provider, baseUrl: "https://example.com", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128_000, maxTokens: 1024, }); } /** Browser preloaded with `models`, MRU-sorted like the hub does on sync. */ function makeBrowser(models: Model[], mruOrder: string[]): ModelBrowser { const browser = new ModelBrowser(Settings.isolated({})); const items = buildBrowserItems(models); sortModelItems(items, { mruOrder }); browser.setMruOrder(mruOrder); browser.setItems(items); return browser; } describe("ModelBrowser search ranking", () => { test("an exact query match outranks the MRU model", () => { // Regression: with gpt-5.6-sol as the active (MRU) model, typing // "gpt-5.5" must select gpt-5.5, not keep the MRU pinned on top. const browser = makeBrowser( [ makeModel("openai-codex", "gpt-5.6-sol"), makeModel("openai-codex", "gpt-5.6-luna"), makeModel("openai-codex", "gpt-5.5"), makeModel("openai-codex", "gpt-5.4"), ], ["openai-codex/gpt-5.6-sol", "openai-codex/gpt-5.6-luna"], ); browser.setQuery("gpt-5.5"); expect(browser.getSelected()?.selector).toBe("openai-codex/gpt-5.5"); }); test("MRU breaks ties between equally good matches", () => { // Same model id under two providers: match quality is identical, so // the recently used provider must win over alphabetical order. const browser = makeBrowser([makeModel("g0i", "gpt-5.5"), makeModel("zenmux", "gpt-5.5")], ["zenmux/gpt-5.5"]); browser.setQuery("gpt-5.5"); expect(browser.getSelected()?.selector).toBe("zenmux/gpt-5.5"); }); }); describe("ModelBrowser perf display", () => { beforeAll(async () => { // render() reads the global theme singleton. await initTheme(false); }); function makePerfBrowser(): ModelBrowser { const browser = new ModelBrowser(Settings.isolated({})); browser.setItems(buildBrowserItems([makeModel("openai", "gpt-5")])); browser.setPerfStats(new Map([["openai/gpt-5", { samples: 12, tps: 118.4, ttftMs: 930 }]])); return browser; } function renderPlain(browser: ModelBrowser, width: number): string[] { return browser.render(width).map(line => Bun.stripANSI(line)); } test("row perf column scales with width: off, TPS-only, TTFT+TPS", () => { const browser = makePerfBrowser(); expect(renderPlain(browser, 70)[2]).not.toContain("t/s"); expect(renderPlain(browser, 80)[2]).toContain("118t/s"); const wideRow = renderPlain(browser, 120)[2]; expect(wideRow).toContain("0.9s 118t/s"); }); test("detail line shows measured perf regardless of width", () => { const browser = makePerfBrowser(); const lines = renderPlain(browser, 70); expect(lines[lines.length - 2]).toContain("~118t/s ยท 0.9s ttft"); }); test("models without measurements render no perf cell", () => { const browser = new ModelBrowser(Settings.isolated({})); browser.setItems(buildBrowserItems([makeModel("openai", "gpt-5")])); expect(renderPlain(browser, 120)[2]).not.toContain("t/s"); }); });