| 1 | import { |
| 2 | ANTIGRAVITY_DEFAULT_MODEL, |
| 3 | ProviderDriverKind, |
| 4 | ProviderInstanceId, |
| 5 | type ServerProvider, |
| 6 | } from "@t3tools/contracts"; |
| 7 | import { renderToStaticMarkup } from "react-dom/server"; |
| 8 | import { describe, expect, it } from "vite-plus/test"; |
| 9 | |
| 10 | import { deriveProviderInstanceEntries } from "../../providerInstances"; |
| 11 | import { ProviderModelPicker } from "./ProviderModelPicker"; |
| 12 | import type { ModelEsque } from "./providerIconUtils"; |
| 13 | |
| 14 | function providerEntry(instanceId: string, driver: string) { |
| 15 | const provider: ServerProvider = { |
| 16 | instanceId: ProviderInstanceId.make(instanceId), |
| 17 | driver: ProviderDriverKind.make(driver), |
| 18 | enabled: true, |
| 19 | installed: true, |
| 20 | version: null, |
| 21 | status: "ready", |
| 22 | auth: { status: "authenticated" }, |
| 23 | checkedAt: "2026-08-28T00:00:00.000Z", |
| 24 | models: [], |
| 25 | slashCommands: [], |
| 26 | skills: [], |
| 27 | }; |
| 28 | return deriveProviderInstanceEntries([provider])[0]!; |
| 29 | } |
| 30 | |
| 31 | function renderPicker(input: { |
| 32 | instanceId: string; |
| 33 | driver: string; |
| 34 | model: string; |
| 35 | options: ReadonlyArray<ModelEsque>; |
| 36 | includeEntry?: boolean; |
| 37 | triggerLabel?: string; |
| 38 | }) { |
| 39 | const instanceId = ProviderInstanceId.make(input.instanceId); |
| 40 | const entry = providerEntry(input.instanceId, input.driver); |
| 41 | return renderToStaticMarkup( |
| 42 | <ProviderModelPicker |
| 43 | environmentId={null} |
| 44 | activeInstanceId={instanceId} |
| 45 | model={input.model} |
| 46 | lockedProvider={null} |
| 47 | instanceEntries={input.includeEntry === false ? [] : [entry]} |
| 48 | modelOptionsByInstance={new Map([[instanceId, input.options]])} |
| 49 | onInstanceModelChange={() => {}} |
| 50 | {...(input.triggerLabel ? { triggerLabel: input.triggerLabel } : {})} |
| 51 | />, |
| 52 | ); |
| 53 | } |
| 54 | |
| 55 | describe("ProviderModelPicker", () => { |
| 56 | it("shows a neutral aggregate value without a representative model or availability badge", () => { |
| 57 | const markup = renderPicker({ |
| 58 | instanceId: "codex_personal", |
| 59 | driver: "codex", |
| 60 | model: "gpt-5", |
| 61 | options: [{ slug: "gpt-5", name: "GPT 5", isUnavailable: true }], |
| 62 | triggerLabel: "Mixed values", |
| 63 | }); |
| 64 | expect(markup).toContain("Mixed values"); |
| 65 | expect(markup).not.toContain("GPT 5"); |
| 66 | expect(markup).not.toContain("Unavailable"); |
| 67 | }); |
| 68 | |
| 69 | it.each(["", ANTIGRAVITY_DEFAULT_MODEL])( |
| 70 | "shows a choice prompt before Antigravity has an account catalog for %s", |
| 71 | (model) => { |
| 72 | const markup = renderPicker({ |
| 73 | instanceId: "antigravity", |
| 74 | driver: "antigravity", |
| 75 | model, |
| 76 | options: [], |
| 77 | }); |
| 78 | |
| 79 | expect(markup).toContain("Choose model"); |
| 80 | expect(markup).not.toContain(ANTIGRAVITY_DEFAULT_MODEL); |
| 81 | }, |
| 82 | ); |
| 83 | |
| 84 | it.each([{ aliases: [ANTIGRAVITY_DEFAULT_MODEL] }, { isDefault: true }])( |
| 85 | "shows the actual default model for an Antigravity marker with %j", |
| 86 | (defaultMetadata) => { |
| 87 | const markup = renderPicker({ |
| 88 | instanceId: "google_work", |
| 89 | driver: "antigravity", |
| 90 | model: ANTIGRAVITY_DEFAULT_MODEL, |
| 91 | options: [ |
| 92 | { slug: "gemini-fast", name: "Gemini Fast" }, |
| 93 | { slug: "gemini-pro", name: "Gemini Pro", ...defaultMetadata }, |
| 94 | ], |
| 95 | }); |
| 96 | |
| 97 | expect(markup).toContain("Gemini Pro"); |
| 98 | expect(markup).not.toContain("Gemini Fast"); |
| 99 | expect(markup).not.toContain(ANTIGRAVITY_DEFAULT_MODEL); |
| 100 | }, |
| 101 | ); |
| 102 | |
| 103 | it.each(["opencode", "antigravity"])( |
| 104 | "keeps the selected model label when the %s account catalog does not contain it", |
| 105 | (driver) => { |
| 106 | const markup = renderPicker({ |
| 107 | instanceId: "team_runtime", |
| 108 | driver, |
| 109 | model: "missing-model", |
| 110 | options: [{ slug: "fallback", name: "Fallback model" }], |
| 111 | }); |
| 112 | |
| 113 | expect(markup).toContain("missing-model"); |
| 114 | expect(markup).not.toContain("Fallback model"); |
| 115 | }, |
| 116 | ); |
| 117 | |
| 118 | it.each(["codex", "claudeAgent", "cursor", "grok"])( |
| 119 | "uses the first option label for a missing %s model", |
| 120 | (driver) => { |
| 121 | const markup = renderPicker({ |
| 122 | instanceId: `${driver}_work`, |
| 123 | driver, |
| 124 | model: "missing-model", |
| 125 | options: [{ slug: "fallback-model", name: "Fallback model" }], |
| 126 | }); |
| 127 | |
| 128 | expect(markup).toContain("Fallback model"); |
| 129 | expect(markup).not.toContain(">missing-model<"); |
| 130 | }, |
| 131 | ); |
| 132 | |
| 133 | it("prefers a matching model for OpenCode", () => { |
| 134 | const markup = renderPicker({ |
| 135 | instanceId: "custom_runtime", |
| 136 | driver: "opencode", |
| 137 | model: "openrouter/selected", |
| 138 | options: [ |
| 139 | { slug: "openrouter/fallback", name: "Fallback model" }, |
| 140 | { slug: "openrouter/selected", name: "Selected model" }, |
| 141 | ], |
| 142 | }); |
| 143 | |
| 144 | expect(markup).toContain("Selected model"); |
| 145 | expect(markup).not.toContain("Fallback model"); |
| 146 | }); |
| 147 | |
| 148 | it("uses the first option when the active instance entry is missing", () => { |
| 149 | const markup = renderPicker({ |
| 150 | instanceId: "missing_instance", |
| 151 | driver: "opencode", |
| 152 | model: "missing-model", |
| 153 | options: [{ slug: "fallback-model", name: "Fallback model" }], |
| 154 | includeEntry: false, |
| 155 | }); |
| 156 | |
| 157 | expect(markup).toContain("Fallback model"); |
| 158 | expect(markup).not.toContain(">missing-model<"); |
| 159 | }); |
| 160 | |
| 161 | it("keeps instance initials visible in the resting trigger", () => { |
| 162 | const activeEntry = providerEntry("codex_personal", "codex"); |
| 163 | const markup = renderToStaticMarkup( |
| 164 | <ProviderModelPicker |
| 165 | environmentId={null} |
| 166 | activeInstanceId={activeEntry.instanceId} |
| 167 | model="gpt-5" |
| 168 | lockedProvider={null} |
| 169 | instanceEntries={[providerEntry("codex", "codex"), activeEntry]} |
| 170 | modelOptionsByInstance={new Map()} |
| 171 | size="xs" |
| 172 | onInstanceModelChange={() => {}} |
| 173 | />, |
| 174 | ); |
| 175 | |
| 176 | expect(markup).toContain(">CP</span>"); |
| 177 | }); |
| 178 | }); |