1import {
2 ANTIGRAVITY_DEFAULT_MODEL,
3 ProviderDriverKind,
4 ProviderInstanceId,
5 type ServerProvider,
6} from "@t3tools/contracts";
7import { renderToStaticMarkup } from "react-dom/server";
8import { describe, expect, it } from "vite-plus/test";
9
10import { deriveProviderInstanceEntries } from "../../providerInstances";
11import { ProviderModelPicker } from "./ProviderModelPicker";
12import type { ModelEsque } from "./providerIconUtils";
13
14function 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
31function 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
55describe("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});