import { t } from "./lib/console-text"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { GlobalRegistrator } from "@happy-dom/global-registrator"; import type { FirstPartyMcpToolName } from "@opengeni/contracts"; import { projectPickerRows } from "../model-policy"; import type { WorkspaceModelCatalogModel } from "@opengeni/sdk"; import { act, useState } from "react"; import { createRoot } from "react-dom/client"; import { ModelPicker, ModelPickerMenu, SessionToolPicker, SessionToolsMenuBody, visibleSessionToolSelection, type PickerModelRow, type SessionToolSelection, } from "./components/pickers"; import { DropdownMenu } from "../ui/dropdown-menu"; import { BillingClassMark } from "./components/billing-class-mark"; beforeAll(() => { GlobalRegistrator.register(); ( globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean } ).IS_REACT_ACT_ENVIRONMENT = true; // Instant page swaps in tests (no slide exit delay). window.matchMedia = ((query: string) => ({ matches: query.includes("prefers-reduced-motion"), media: query, onchange: null, addListener() {}, removeListener() {}, addEventListener() {}, removeEventListener() {}, dispatchEvent() { return false; }, }) as MediaQueryList) as typeof window.matchMedia; }); afterAll(() => { GlobalRegistrator.unregister(); }); const FIRST_PARTY = [ { id: "session_get" as FirstPartyMcpToolName, name: "Get session" }, { id: "session_steer" as FirstPartyMcpToolName, name: "Steer session" }, ]; describe("unified session tool picker", () => { test("dialog connectors toggle without a roving-focus context and preserve hidden builtins", async () => { let latest: SessionToolSelection = { mcpServerIds: new Set(["files"]), firstPartyToolIds: new Set(["session_get"]), }; const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); function Harness() { const [selection, setSelection] = useState(latest); const [customizing, setCustomizing] = useState(false); return ( { latest = next; setSelection(next); }} /> ); } try { await act(async () => root.render()); // Connector controls are deliberately outside the eager composer graph. await act(async () => { await import("./components/session-connectors-menu-body"); }); const customize = container.querySelector( 'button[role="switch"][aria-label="Customize connectors"]', )!; expect(container.querySelector('button[role="switch"][aria-label="Linear"]')).toBeNull(); // The read-only state is spoken text, not an aria-label on a plain span. expect(container.textContent).toContain("Linear, off for this session"); await act(async () => customize.click()); const linear = container.querySelector( 'button[role="switch"][aria-label="Linear"]', )!; expect(linear.getAttribute("aria-checked")).toBe("false"); await act(async () => linear.click()); expect(latest.mcpServerIds.has("linear")).toBe(true); expect(linear.getAttribute("aria-checked")).toBe("true"); await act(async () => linear.click()); expect(latest.mcpServerIds.has("linear")).toBe(false); expect(latest.mcpServerIds.has("files")).toBe(true); expect([...latest.firstPartyToolIds]).toEqual(["session_get"]); expect( container.querySelectorAll('button[role="switch"][aria-label="Customize connectors"]'), ).toHaveLength(1); } finally { await act(async () => root.unmount()); container.remove(); } }); test("shows one durable selection for connected and Opengeni tools", async () => { let latest: SessionToolSelection = { mcpServerIds: new Set(["linear"]), firstPartyToolIds: new Set(FIRST_PARTY.map((tool) => tool.id)), }; const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); function Harness() { const [selection, setSelection] = useState(latest); return ( { latest = next; setSelection(next); }} /> ); } try { await act(async () => root.render()); const trigger = container.querySelector( 'button[aria-label="Session tools"]', ); expect(trigger?.textContent).toContain("Tools · All"); expect(container.querySelectorAll('button[aria-label="Session tools"]')).toHaveLength(1); expect(container.textContent).not.toContain("Tools for this turn"); expect(latest.mcpServerIds).toEqual(new Set(["linear"])); expect(latest.firstPartyToolIds).toEqual(new Set(FIRST_PARTY.map((tool) => tool.id))); } finally { await act(async () => root.unmount()); container.remove(); } }); test("never counts or preserves non-rendered runtime infrastructure", async () => { let latest: SessionToolSelection = { mcpServerIds: new Set(["docs", "opengeni", "files"]), firstPartyToolIds: new Set(FIRST_PARTY.map((tool) => tool.id)), }; const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); function Harness() { const [selection, setSelection] = useState(latest); return ( { latest = next; setSelection(next); }} /> ); } try { await act(async () => root.render()); const trigger = container.querySelector( 'button[aria-label="Session tools"]', ); expect(trigger?.textContent).toContain("Tools · All"); expect(trigger?.textContent).not.toContain("5/3"); expect(visibleSessionToolSelection(latest, [{ id: "docs" }], FIRST_PARTY)).toEqual({ mcpServerIds: new Set(["docs"]), firstPartyToolIds: new Set(FIRST_PARTY.map((tool) => tool.id)), }); } finally { await act(async () => root.unmount()); container.remove(); } }); }); function catalogModel( overrides: Partial & Pick, ): WorkspaceModelCatalogModel { return { provider: "openai", providerLabel: "OpenAI", api: "responses", credentialReadiness: { status: "ready", reason: null, basis: "configuration", checkedAt: null, }, policyAllowed: true, availability: { status: "available", selectable: true, reason: null, checkedAt: null, }, capabilities: { reasoning: { upstream: "supported", runnable: true, efforts: ["low", "high", "xhigh"], defaultEffort: "low", required: false, }, functionCalling: { upstream: "supported", runnable: true }, structuredOutput: { upstream: "supported", runnable: true }, hostedTools: { webSearch: { upstream: "unsupported", runnable: false }, xSearch: { upstream: "unsupported", runnable: false }, codeExecution: { upstream: "unsupported", runnable: false }, }, inputModalities: ["text"], outputModalities: ["text"], transports: { sse: { upstream: "supported", runnable: true }, responsesWebSocket: { upstream: "unsupported", runnable: false }, realtimeAudio: { upstream: "unsupported", runnable: false }, }, latencyModes: [ { id: "standard", upstream: "supported", runnable: true }, { id: "fast", upstream: "supported", runnable: true }, ], }, billing: { upstreamPayer: "deployment", metering: "opengeni_credits" }, ...overrides, }; } describe("catalog-backed ModelPicker", () => { test("first-party branding stays explicit while custom presentation and provider identities survive", async () => { const rows = projectPickerRows(t, [ catalogModel({ id: "deployment/free", label: "Free deployment", cost: "free" }), catalogModel({ id: "codex/example", label: "Codex model", source: "codex", cost: "subscription", }), ]); const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); try { for (const mode of ["default", "custom", "undefined"] as const) { const customized = mode === "custom"; await act(async () => root.render( <> {}} onEffortChange={() => {}} onLatencyModeChange={() => {}} /> , ), ); const group = container.querySelector( `section[aria-label="${customized ? "Console models" : "Opengeni"}"]`, )!; expect(group).not.toBeNull(); expect( group.querySelector('[data-testid="billing-class-icon-opengeni_credits"]') === null, ).toBe(customized); if (!customized) expect(group.querySelector('svg[viewBox="0 0 176 138.73"]')).not.toBeNull(); expect(group.textContent).toContain("Free"); expect( container.querySelector( '[role="img"][aria-label="Opengeni"] svg[viewBox="0 0 176 138.73"]', ), ).not.toBeNull(); expect( container.querySelector('[role="img"][aria-label="Codex"] svg[viewBox="0 0 24 24"]'), ).not.toBeNull(); expect(container.querySelector('section[aria-label="Codex"]')).not.toBeNull(); } } finally { await act(async () => root.unmount()); container.remove(); } }); test("renders selected product model and effort on the trigger", async () => { const rows: PickerModelRow[] = projectPickerRows(t, [ catalogModel({ id: "gpt-5.6-sol", label: "Sol" }), catalogModel({ id: "blocked", label: "Blocked", availability: { status: "unavailable", selectable: false, reason: "policy_blocked", checkedAt: null, }, }), ]); expect(rows.find((row) => row.id === "blocked")?.selectable).toBe(false); expect(rows.find((row) => row.id === "blocked")?.unavailableReason).toBe( "Blocked by workspace policy", ); expect(rows.some((row) => row.billingClass === "opengeni_credits")).toBe(true); const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); try { await act(async () => root.render( {}} onEffortChange={() => {}} onLatencyModeChange={() => {}} />, ), ); const trigger = container.querySelector( 'button[aria-label="Model and effort"]', ); expect(trigger?.textContent).toContain("Sol"); expect(trigger?.textContent).toContain("Extra high"); expect(trigger?.textContent).not.toContain("Fast"); expect(container.querySelector('[data-testid="model-picker-fast-icon"]')).toBeNull(); expect( trigger?.querySelector('[data-testid="billing-class-icon-opengeni_credits"]'), ).toBeTruthy(); expect( trigger?.querySelector('[aria-label="Opengeni"] svg[viewBox="0 0 176 138.73"]'), ).not.toBeNull(); expect(trigger?.querySelector(".lucide-sparkles")).toBeNull(); } finally { await act(async () => root.unmount()); container.remove(); } }); test("collapsed Fast uses a lightning icon, not the word Fast", async () => { const rows = projectPickerRows(t, [catalogModel({ id: "gpt-5.6-sol", label: "Sol" })]); const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); try { await act(async () => root.render( {}} onEffortChange={() => {}} onLatencyModeChange={() => {}} />, ), ); const trigger = container.querySelector( 'button[aria-label="Model and effort"]', ); expect(trigger?.textContent).not.toContain("Fast"); expect(container.querySelector('[data-testid="model-picker-fast-icon"]')).toBeTruthy(); } finally { await act(async () => root.unmount()); container.remove(); } }); test("shows every billing section and separate thinking controls", async () => { const rows = projectPickerRows(t, [ catalogModel({ id: "gpt-5.6-sol", label: "Sol" }), catalogModel({ id: "codex/gpt-5.6-luna", label: "Luna", provider: "codex-subscription", providerLabel: "Codex", credentialSource: { kind: "connected_subscription", provider: "codex" }, billing: { upstreamPayer: "connected_subscription", metering: "external", }, }), ]); const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); try { await act(async () => root.render( {}} onEffortChange={() => {}} onLatencyModeChange={() => {}} />, ), ); expect(container.querySelector('[data-testid="model-picker-reasoning"]')).toBeTruthy(); expect(container.textContent).toContain("Thinking"); expect( container.querySelector('[data-testid="model-picker-choice-gpt-5.6-sol"]'), ).toBeTruthy(); expect( container.querySelector('[data-testid="model-picker-choice-codex/gpt-5.6-luna"]'), ).toBeTruthy(); expect( container.querySelector('[role="radiogroup"][aria-label="Thinking effort"]'), ).toBeTruthy(); expect(container.querySelector('[data-testid="model-picker-back"]')).toBeNull(); } finally { await act(async () => root.unmount()); container.remove(); } }); test("one usable rail collapses the provider root", async () => { const rows = projectPickerRows(t, [ catalogModel({ id: "gpt-5.6-sol", label: "Sol", source: "opengeni" }), catalogModel({ id: "deepseek-v4-flash-0731", label: "DeepSeek", source: "opengeni", }), ]); const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); try { await act(async () => root.render( {}} onEffortChange={() => {}} onLatencyModeChange={() => {}} />, ), ); await act(async () => { container.querySelector('[data-testid="model-picker-back"]')?.click(); }); expect(container.querySelector('[data-testid="model-picker-models"]')).toBeTruthy(); expect(container.querySelector('[data-testid="model-picker-providers"]')).toBeNull(); expect(container.querySelector('[data-testid="model-picker-back"]')).toBeNull(); expect(container.textContent).toContain("DeepSeek"); } finally { await act(async () => root.unmount()); container.remove(); } }); test("changes model directly and adjusts thinking without navigation", async () => { const rows = projectPickerRows(t, [ catalogModel({ id: "gpt-5.6-sol", label: "Sol" }), catalogModel({ id: "codex/gpt-5.6-luna", label: "Luna", provider: "codex-subscription", providerLabel: "Codex", credentialSource: { kind: "connected_subscription", provider: "codex" }, billing: { upstreamPayer: "connected_subscription", metering: "external", }, }), ]); let selected = "gpt-5.6-sol"; const selection = { effort: "low" as "low" | "high" | "xhigh", latency: "standard" as "standard" | "fast", }; const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); function Harness() { const [model, setModel] = useState(selected); const [effortState, setEffortState] = useState(selection.effort); const [latencyMode, setLatencyMode] = useState(selection.latency); return ( { selected = id; setModel(id); }} onEffortChange={(value) => { selection.effort = value as typeof selection.effort; setEffortState(selection.effort); }} onLatencyModeChange={(mode) => { selection.latency = mode === "fast" ? "fast" : "standard"; setLatencyMode(selection.latency); }} /> ); } try { await act(async () => root.render()); await act(async () => container .querySelector( '[data-testid="model-picker-choice-codex/gpt-5.6-luna"]', )! .click(), ); expect(selected).toBe("codex/gpt-5.6-luna"); expect(selection.effort).toBe("low"); await act(async () => { container.querySelector('[role="radio"][aria-label="High"]')!.click(); }); expect(selection.effort).toBe("high"); await act(async () => container.querySelector('[data-testid="model-picker-fast"]')!.click(), ); expect(selection.latency).toBe("fast"); expect( container.querySelector('[data-testid="model-picker-choice-gpt-5.6-sol"]'), ).toBeTruthy(); } finally { await act(async () => root.unmount()); container.remove(); } }); test("hides Fast toggle when the focused model cannot run it", async () => { const baseCapabilities = catalogModel({ id: "x", label: "x", }).capabilities!; const rows = projectPickerRows(t, [ catalogModel({ id: "slow-only", label: "Slow", capabilities: { ...baseCapabilities, latencyModes: [{ id: "standard", upstream: "supported", runnable: true }], }, }), ]); const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); try { await act(async () => root.render( {}} onEffortChange={() => {}} onLatencyModeChange={() => {}} />, ), ); expect(container.querySelector('[data-testid="model-picker-reasoning"]')).toBeTruthy(); expect(container.querySelector('[data-testid="model-picker-fast"]')).toBeNull(); } finally { await act(async () => root.unmount()); container.remove(); } }); });