1import { describe, expect, it } from "vite-plus/test";
2
3import {
4 DEFAULT_SERVER_SETTINGS,
5 ProjectId,
6 ProviderDriverKind,
7 ProviderInstanceId,
8 type ModelSelection,
9 type ServerConfig,
10 type ServerSettings,
11} from "@t3tools/contracts";
12
13import {
14 buildModelOptions,
15 describeUnavailableModelSelection,
16 groupByProvider,
17 isModelSelectionUnavailable,
18 resolveDefaultableModelSelection,
19 resolveNewTaskModelSelection,
20 resolveSelectableModelSelection,
21 type ModelOption,
22} from "./modelOptions";
23
24describe("mobile model options", () => {
25 it("groups models by provider and flags legacy entries", () => {
26 const config = {
27 providers: [
28 {
29 instanceId: "codex",
30 driver: "codex",
31 displayName: "Codex",
32 enabled: true,
33 installed: true,
34 auth: { status: "authenticated" },
35 models: [
36 {
37 slug: "gpt-5.6-sol",
38 name: "GPT-5.6 Sol",
39 isCustom: false,
40 capabilities: null,
41 },
42 {
43 slug: "gpt-5.4",
44 name: "GPT-5.4",
45 isCustom: false,
46 isLegacy: true,
47 capabilities: null,
48 },
49 ],
50 },
51 ],
52 } as unknown as ServerConfig;
53
54 expect(groupByProvider(buildModelOptions(config, null, undefined, null))).toMatchObject([
55 {
56 providerKey: "codex",
57 providerLabel: "Codex",
58 models: [
59 { key: "codex:gpt-5.6-sol", label: "GPT-5.6 Sol", subtitle: "", isLegacy: false },
60 { key: "codex:gpt-5.4", label: "GPT-5.4", isLegacy: true },
61 ],
62 },
63 ]);
64 });
65
66 it("carries configured ACP identity into model and provider catalogs", () => {
67 const iconUrl = "https://cdn.agentclientprotocol.com/registry/v1/latest/antigravity-acp.svg";
68 const config = {
69 providers: [
70 {
71 instanceId: "acpRegistry_antigravity",
72 driver: "acpRegistry",
73 displayName: "Antigravity",
74 iconUrl,
75 enabled: true,
76 installed: true,
77 auth: { status: "authenticated" },
78 models: [
79 {
80 slug: "default",
81 name: "Default",
82 isCustom: false,
83 capabilities: null,
84 },
85 ],
86 },
87 ],
88 } as unknown as ServerConfig;
89
90 const [group] = groupByProvider(buildModelOptions(config, null, undefined, null));
91
92 expect(group).toMatchObject({
93 providerKey: "acpRegistry_antigravity",
94 providerLabel: "Antigravity",
95 models: [
96 {
97 providerDriver: "acpRegistry",
98 providerIconUrl: iconUrl,
99 },
100 ],
101 });
102 });
103
104 it("distinguishes same-name OpenCode models without changing their routing", () => {
105 const sources = [
106 { id: "anthropic", label: "Anthropic" },
107 { id: "github-copilot", label: "GitHub Copilot" },
108 { id: "opencode", label: "OpenCode Zen" },
109 ];
110 const config = {
111 providers: [
112 {
113 instanceId: "opencode_work",
114 driver: "opencode",
115 displayName: "OpenCode Work",
116 enabled: true,
117 installed: true,
118 auth: { status: "authenticated" },
119 models: sources.map((source) => ({
120 slug: `${source.id}/claude-fable-5`,
121 name: "Claude Fable 5",
122 subProvider: source.label,
123 isCustom: false,
124 capabilities: null,
125 })),
126 },
127 ],
128 } as unknown as ServerConfig;
129 const selection = {
130 instanceId: ProviderInstanceId.make("opencode_work"),
131 model: "github-copilot/claude-fable-5",
132 };
133
134 const options = buildModelOptions(config, selection, undefined, null);
135
136 expect(options).toMatchObject(
137 sources.map((source) => ({
138 key: `opencode_work:${source.id}/claude-fable-5`,
139 label: "Claude Fable 5",
140 subtitle: source.label,
141 providerLabel: "OpenCode Work",
142 selection: {
143 instanceId: "opencode_work",
144 model: `${source.id}/claude-fable-5`,
145 },
146 })),
147 );
148 expect(groupByProvider(options)).toEqual([
149 { providerKey: "opencode_work", providerLabel: "OpenCode Work", models: options },
150 ]);
151 });
152
153 it("does not materialize catalog defaults for missing stored options", () => {
154 const config = {
155 providers: [
156 {
157 instanceId: "codex",
158 driver: "codex",
159 displayName: "Codex",
160 enabled: true,
161 installed: true,
162 auth: { status: "authenticated" },
163 models: [
164 {
165 slug: "gpt-test",
166 name: "GPT Test",
167 isCustom: false,
168 capabilities: {
169 optionDescriptors: [
170 {
171 id: "serviceTier",
172 label: "Service Tier",
173 type: "select",
174 options: [
175 { id: "default", label: "Standard", isDefault: true },
176 { id: "priority", label: "Fast" },
177 ],
178 currentValue: "default",
179 },
180 ],
181 },
182 },
183 ],
184 },
185 ],
186 } as unknown as ServerConfig;
187
188 const [option] = buildModelOptions(
189 config,
190 {
191 instanceId: ProviderInstanceId.make("codex"),
192 model: "gpt-test",
193 },
194 undefined,
195 null,
196 );
197
198 expect(option?.capabilities?.optionDescriptors?.[0]?.id).toBe("serviceTier");
199 expect(option?.selection.options).toBeUndefined();
200
201 const [emptyOption] = buildModelOptions(
202 config,
203 {
204 instanceId: ProviderInstanceId.make("codex"),
205 model: "gpt-test",
206 options: [],
207 },
208 undefined,
209 null,
210 );
211 expect(emptyOption?.selection).toEqual(option?.selection);
212
213 const [explicitOption] = buildModelOptions(
214 config,
215 {
216 instanceId: ProviderInstanceId.make("codex"),
217 model: "gpt-test",
218 options: [{ id: "serviceTier", value: "priority" }],
219 },
220 undefined,
221 null,
222 );
223 expect(explicitOption?.selection.options).toEqual([{ id: "serviceTier", value: "priority" }]);
224 });
225
226 it("limits existing threads to their provider while new tasks keep every provider", () => {
227 const providers = ["codex", "claudeAgent"].map((instanceId) => ({
228 instanceId,
229 driver: instanceId,
230 enabled: true,
231 installed: true,
232 auth: { status: "authenticated" },
233 models: [{ slug: "test", name: instanceId, capabilities: null }],
234 }));
235 const config = { providers } as unknown as ServerConfig;
236 const selection = { instanceId: ProviderInstanceId.make("codex"), model: "test" };
237
238 expect(
239 buildModelOptions(config, selection, undefined, null).map((option) => option.providerKey),
240 ).toEqual(["codex", "claudeAgent"]);
241 expect(buildModelOptions(config, selection, selection.instanceId, null)).toEqual(
242 buildModelOptions(config, selection, undefined, null).filter(
243 (option) => option.providerKey === "codex",
244 ),
245 );
246 });
247
248 it.each(["disabled", "unavailable", "missing"] as const)(
249 "retains the selected %s provider's fallback in a filtered catalog",
250 (state) => {
251 const selection = {
252 instanceId: ProviderInstanceId.make("google_work"),
253 model: "saved-model",
254 options: [{ id: "native-option", value: "saved-choice" }],
255 };
256 const provider = {
257 instanceId: selection.instanceId,
258 driver: "antigravity",
259 displayName: "Google Work",
260 enabled: state !== "disabled",
261 installed: true,
262 availability: state === "unavailable" ? "unavailable" : "available",
263 auth: { status: "authenticated" },
264 models: [{ slug: selection.model, name: "Saved model", capabilities: null }],
265 };
266 const config = {
267 providers: state === "missing" ? [] : [provider],
268 settings: { providerInstances: { google_work: { driver: "antigravity" } } },
269 } as unknown as ServerConfig;
270 const options = buildModelOptions(config, selection, selection.instanceId, null);
271 expect(options).toEqual(buildModelOptions(config, selection, undefined, null));
272 expect(options).toHaveLength(1);
273 expect(options[0]).toMatchObject({ selection, isUnavailable: true });
274 },
275 );
276
277 it("rejects stored selections whose provider is not usable", () => {
278 const config = {
279 providers: [
280 {
281 instanceId: "codex",
282 driver: "codex",
283 enabled: true,
284 installed: true,
285 auth: { status: "authenticated" },
286 models: [],
287 },
288 {
289 instanceId: "claudeAgent",
290 driver: "claudeAgent",
291 enabled: false,
292 installed: true,
293 auth: { status: "authenticated" },
294 models: [],
295 },
296 ],
297 } as unknown as ServerConfig;
298
299 const usable = {
300 instanceId: ProviderInstanceId.make("codex"),
301 model: "gpt-5.6-sol",
302 };
303 const disabled = {
304 instanceId: ProviderInstanceId.make("claudeAgent"),
305 model: "claude-sonnet-5",
306 };
307 const removed = {
308 instanceId: ProviderInstanceId.make("codex_personal"),
309 model: "gpt-5.6-sol",
310 };
311
312 expect(resolveSelectableModelSelection(config, usable, null)).toBe(usable);
313 expect(resolveSelectableModelSelection(config, disabled, null)).toBeNull();
314 expect(resolveSelectableModelSelection(config, removed, null)).toBeNull();
315 // Disabled is disabled even with no settings to consult; only a provider
316 // that is merely missing or unauthenticated falls through silently.
317 expect(isModelSelectionUnavailable(config, disabled, null)).toBe(true);
318 expect(isModelSelectionUnavailable(config, removed, null)).toBe(false);
319 // An offline environment has no config to validate.
320 expect(resolveSelectableModelSelection(null, disabled, null)).toBe(disabled);
321 });
322
323 describe("Antigravity selections", () => {
324 const selection = {
325 instanceId: ProviderInstanceId.make("google_work"),
326 model: "gemini-3.1-pro-high",
327 options: [{ id: "native-option", value: "saved/opaque-choice" }],
328 };
329 const model = {
330 slug: selection.model,
331 name: "Gemini 3.1 Pro High",
332 subProvider: "Google",
333 isCustom: false,
334 isDefault: true,
335 isLegacy: true,
336 capabilities: {
337 optionDescriptors: [
338 {
339 id: "native-option",
340 label: "Native option",
341 type: "select",
342 options: [{ id: "current/default", label: "Default", isDefault: true }],
343 currentValue: "current/default",
344 },
345 ],
346 },
347 };
348 const config = {
349 providers: [
350 {
351 instanceId: selection.instanceId,
352 driver: "antigravity",
353 displayName: "Google Work",
354 enabled: true,
355 installed: true,
356 auth: { status: "authenticated" },
357 models: [model],
358 },
359 ],
360 } as unknown as ServerConfig;
361
362 it.each([
363 ["disabled", { enabled: false }],
364 ["uninstalled", { installed: false }],
365 ["signed out", { auth: { status: "unauthenticated" } }],
366 ["unavailable", { availability: "unavailable" }],
367 ] as const)("keeps a %s provider's selection and known model details", (_state, update) => {
368 const unavailableConfig = {
369 ...config,
370 providers: config.providers.map((provider) => ({ ...provider, ...update })),
371 };
372
373 expect(resolveSelectableModelSelection(unavailableConfig, selection, null)).toBe(selection);
374 expect(resolveDefaultableModelSelection(unavailableConfig, selection, null)).toBe(selection);
375 expect(isModelSelectionUnavailable(unavailableConfig, selection, null)).toBe(true);
376 expect(buildModelOptions(unavailableConfig, null, undefined, null)).toEqual([]);
377 const [option] = buildModelOptions(unavailableConfig, selection, undefined, null);
378 expect(option).toMatchObject({
379 key: `google_work:${selection.model}`,
380 label: model.name,
381 subtitle: "Google",
382 providerKey: "google_work",
383 providerLabel: "Google Work",
384 providerDriver: "antigravity",
385 isDefault: false,
386 isLegacy: true,
387 isUnavailable: true,
388 capabilities: model.capabilities,
389 });
390 expect(option?.selection).toBe(selection);
391 });
392
393 it("keeps an exact selection when its model leaves and returns to the catalog", () => {
394 const changedConfig = {
395 ...config,
396 providers: config.providers.map((provider) => ({
397 ...provider,
398 models: provider.models.map((model) => ({ ...model, slug: "gemini-3.1-pro-low" })),
399 })),
400 };
401
402 expect(resolveDefaultableModelSelection(changedConfig, selection, null)).toBe(selection);
403 expect(isModelSelectionUnavailable(changedConfig, selection, null)).toBe(true);
404 const options = buildModelOptions(changedConfig, selection, undefined, null);
405 const missing = options.find((option) => option.selection.model === selection.model);
406 expect(missing).toMatchObject({
407 label: selection.model,
408 providerLabel: "Google Work",
409 providerDriver: "antigravity",
410 isUnavailable: true,
411 capabilities: null,
412 });
413 expect(missing?.selection).toBe(selection);
414 expect(
415 resolveNewTaskModelSelection({
416 draftSelection: null,
417 projectDefaultSelection: resolveDefaultableModelSelection(changedConfig, selection, null),
418 stickySelection: null,
419 modelOptions: options,
420 }),
421 ).toBe(selection);
422
423 const [restored] = buildModelOptions(config, selection, undefined, null);
424 expect(isModelSelectionUnavailable(config, selection, null)).toBe(false);
425 expect(restored?.isUnavailable).not.toBe(true);
426 expect(restored?.selection).toBe(selection);
427 expect(resolveDefaultableModelSelection(config, selection, null)).toBe(selection);
428 expect(
429 buildModelOptions(config, null, undefined, null)[0]?.selection.options,
430 ).toBeUndefined();
431 });
432
433 it("uses configured instance metadata when provider status is missing", () => {
434 const missingStatusConfig = {
435 providers: [],
436 settings: {
437 providerInstances: {
438 [selection.instanceId]: { driver: "antigravity", displayName: "Google Work" },
439 },
440 },
441 } as unknown as ServerConfig;
442
443 expect(resolveDefaultableModelSelection(missingStatusConfig, selection, null)).toBe(
444 selection,
445 );
446 expect(isModelSelectionUnavailable(missingStatusConfig, selection, null)).toBe(true);
447 expect(buildModelOptions(missingStatusConfig, selection, undefined, null)).toMatchObject([
448 {
449 providerDriver: "antigravity",
450 providerLabel: "Google Work",
451 isUnavailable: true,
452 selection,
453 },
454 ]);
455 });
456
457 it("keeps offline selections without assuming that an unknown instance is Antigravity", () => {
458 const unknownConfig = { ...config, providers: [] };
459
460 expect(resolveDefaultableModelSelection(null, selection, null)).toBe(selection);
461 expect(isModelSelectionUnavailable(null, selection, null)).toBe(false);
462 expect(buildModelOptions(null, selection, undefined, null)[0]?.selection).toBe(selection);
463 expect(buildModelOptions(null, selection, undefined, null)[0]?.isUnavailable).not.toBe(true);
464 expect(isModelSelectionUnavailable(unknownConfig, selection, null)).toBe(false);
465 expect(resolveSelectableModelSelection(unknownConfig, selection, null)).toBeNull();
466 });
467 });
468
469 it("keeps legacy models out of implicit defaults", () => {
470 const config = {
471 providers: [
472 {
473 instanceId: "codex",
474 driver: "codex",
475 displayName: "Codex",
476 enabled: true,
477 installed: true,
478 auth: { status: "authenticated" },
479 models: [
480 { slug: "gpt-5.6-sol", name: "GPT-5.6 Sol", isCustom: false, capabilities: null },
481 {
482 slug: "gpt-5.4",
483 name: "GPT-5.4",
484 isCustom: false,
485 isLegacy: true,
486 capabilities: null,
487 },
488 ],
489 },
490 ],
491 } as unknown as ServerConfig;
492
493 const current = { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.6-sol" };
494 const legacy = { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" };
495
496 expect(resolveDefaultableModelSelection(config, current, null)).toBe(current);
497 // A legacy last-used selection falls through to the provider default.
498 expect(resolveDefaultableModelSelection(config, legacy, null)).toBeNull();
499 // Offline: nothing to validate against, selection passes through.
500 expect(resolveDefaultableModelSelection(null, legacy, null)).toBe(legacy);
501 });
502
503 it("resolves new tasks from draft, project, sticky, then provider defaults", () => {
504 const draft = { instanceId: ProviderInstanceId.make("codex"), model: "draft" };
505 const project = { instanceId: ProviderInstanceId.make("codex"), model: "project" };
506 const sticky = { instanceId: ProviderInstanceId.make("codex"), model: "sticky" };
507 const providerDefault = {
508 selection: { instanceId: ProviderInstanceId.make("codex"), model: "default" },
509 isDefault: true,
510 } as ModelOption;
511 const resolve = (
512 draftSelection: ModelSelection | null,
513 projectDefaultSelection: ModelSelection | null,
514 stickySelection: ModelSelection | null,
515 ) =>
516 resolveNewTaskModelSelection({
517 draftSelection,
518 projectDefaultSelection,
519 stickySelection,
520 modelOptions: [providerDefault],
521 });
522
523 expect(resolve(draft, project, sticky)).toBe(draft);
524 expect(resolve(null, project, sticky)).toBe(project);
525 expect(resolve(null, null, sticky)).toBe(sticky);
526 expect(resolve(null, null, null)).toBe(providerDefault.selection);
527
528 const unavailable = { ...providerDefault, isUnavailable: true };
529 expect(
530 resolveNewTaskModelSelection({
531 draftSelection: null,
532 projectDefaultSelection: null,
533 stickySelection: null,
534 modelOptions: [unavailable],
535 }),
536 ).toBeNull();
537 });
538
539 describe("project-aware enablement", () => {
540 const projectId = ProjectId.make("project-a");
541 const instanceId = ProviderInstanceId.make("codex_work");
542
543 function configWith(settings: ServerSettings): ServerConfig {
544 return {
545 providers: [
546 {
547 instanceId,
548 driver: "codex",
549 displayName: "Codex Work",
550 enabled: true,
551 installed: true,
552 auth: { status: "authenticated" },
553 models: [{ slug: "model-a", name: "Model A", isCustom: false, capabilities: null }],
554 },
555 ],
556 settings,
557 } as unknown as ServerConfig;
558 }
559
560 it("words a project's own turn-off apart from missing setup", () => {
561 const settings: ServerSettings = {
562 ...DEFAULT_SERVER_SETTINGS,
563 providerInstances: {
564 [instanceId]: { driver: ProviderDriverKind.make("codex"), enabled: true },
565 },
566 projectSettingsOverrides: {
567 [projectId]: { providerInstances: { [instanceId]: { enabled: false } } },
568 },
569 };
570 const selection = { instanceId, model: "model-a" };
571
572 expect(
573 describeUnavailableModelSelection(configWith(settings), selection, projectId).title,
574 ).toBe("Model turned off for this project");
575 expect(describeUnavailableModelSelection(configWith(settings), selection, null).title).toBe(
576 "Antigravity model unavailable",
577 );
578 });
579
580 it("refuses to send a non-Antigravity model the project turned off", () => {
581 const settings: ServerSettings = {
582 ...DEFAULT_SERVER_SETTINGS,
583 providerInstances: {
584 [instanceId]: { driver: ProviderDriverKind.make("codex"), enabled: true },
585 },
586 projectSettingsOverrides: {
587 [projectId]: { providerInstances: { [instanceId]: { enabled: false } } },
588 },
589 };
590 const selection = { instanceId, model: "model-a" };
591
592 expect(isModelSelectionUnavailable(configWith(settings), selection, projectId)).toBe(true);
593 expect(isModelSelectionUnavailable(configWith(settings), selection, null)).toBe(false);
594 });
595
596 it("hides an instance the project disables even though the machine enables it", () => {
597 const settings: ServerSettings = {
598 ...DEFAULT_SERVER_SETTINGS,
599 providerInstances: {
600 [instanceId]: { driver: ProviderDriverKind.make("codex"), enabled: true },
601 },
602 projectSettingsOverrides: {
603 [projectId]: { providerInstances: { [instanceId]: { enabled: false } } },
604 },
605 };
606
607 expect(buildModelOptions(configWith(settings), null, undefined, projectId)).toEqual([]);
608 });
609
610 it("shows an instance the project enables even though the machine disables it", () => {
611 const settings: ServerSettings = {
612 ...DEFAULT_SERVER_SETTINGS,
613 providerInstances: {
614 [instanceId]: { driver: ProviderDriverKind.make("codex"), enabled: false },
615 },
616 projectSettingsOverrides: {
617 [projectId]: { providerInstances: { [instanceId]: { enabled: true } } },
618 },
619 };
620
621 expect(buildModelOptions(configWith(settings), null, undefined, projectId)).toHaveLength(1);
622 });
623
624 it("falls back to the machine value with no project in scope", () => {
625 const settings: ServerSettings = {
626 ...DEFAULT_SERVER_SETTINGS,
627 providerInstances: {
628 [instanceId]: { driver: ProviderDriverKind.make("codex"), enabled: true },
629 },
630 projectSettingsOverrides: {
631 [projectId]: { providerInstances: { [instanceId]: { enabled: false } } },
632 },
633 };
634
635 expect(buildModelOptions(configWith(settings), null, undefined, null)).toHaveLength(1);
636 });
637
638 it("hides a machine-disabled instance for a project with no override, even though another project's keep-alive left it running", () => {
639 const otherProjectId = ProjectId.make("project-b");
640 const settings: ServerSettings = {
641 ...DEFAULT_SERVER_SETTINGS,
642 providerInstances: {
643 [instanceId]: { driver: ProviderDriverKind.make("codex"), enabled: false },
644 },
645 projectSettingsOverrides: {
646 [otherProjectId]: { providerInstances: { [instanceId]: { enabled: true } } },
647 },
648 };
649
650 // configWith's snapshot always reports enabled: true, standing in for
651 // the server keeping the instance alive to serve project-b's override.
652 expect(buildModelOptions(configWith(settings), null, undefined, projectId)).toEqual([]);
653 });
654
655 it("refuses to send an instance the project merely inherits as disabled, not just an explicit override", () => {
656 const settings: ServerSettings = {
657 ...DEFAULT_SERVER_SETTINGS,
658 providerInstances: {
659 [instanceId]: { driver: ProviderDriverKind.make("codex"), enabled: false },
660 },
661 };
662 const selection = { instanceId, model: "model-a" };
663
664 // configWith's snapshot always reports enabled: true, standing in for
665 // the server keeping the instance alive for another project's keep-alive.
666 expect(isModelSelectionUnavailable(configWith(settings), selection, projectId)).toBe(true);
667 expect(
668 describeUnavailableModelSelection(configWith(settings), selection, projectId).title,
669 ).toBe("Model turned off for this project");
670 // With no project in scope, the machine value applies and is disabled.
671 expect(isModelSelectionUnavailable(configWith(settings), selection, null)).toBe(true);
672 });
673
674 it("marks a project-disabled instance's fallback selection unavailable instead of reviving it", () => {
675 const settings: ServerSettings = {
676 ...DEFAULT_SERVER_SETTINGS,
677 providerInstances: {
678 [instanceId]: { driver: ProviderDriverKind.make("codex"), enabled: true },
679 },
680 projectSettingsOverrides: {
681 [projectId]: { providerInstances: { [instanceId]: { enabled: false } } },
682 },
683 };
684 const fallback = { instanceId, model: "model-a" };
685
686 const options = buildModelOptions(configWith(settings), fallback, undefined, projectId);
687
688 expect(options).toHaveLength(1);
689 expect(options[0]).toMatchObject({ selection: fallback, isUnavailable: true });
690 });
691
692 it("rejects a project-disabled instance's selection via resolveSelectableModelSelection", () => {
693 const settings: ServerSettings = {
694 ...DEFAULT_SERVER_SETTINGS,
695 providerInstances: {
696 [instanceId]: { driver: ProviderDriverKind.make("codex"), enabled: true },
697 },
698 projectSettingsOverrides: {
699 [projectId]: { providerInstances: { [instanceId]: { enabled: false } } },
700 },
701 };
702 const selection = { instanceId, model: "model-a" };
703
704 expect(
705 resolveSelectableModelSelection(configWith(settings), selection, projectId),
706 ).toBeNull();
707 expect(resolveSelectableModelSelection(configWith(settings), selection, null)).toBe(
708 selection,
709 );
710 });
711
712 it("rejects a project-disabled antigravity instance's selection instead of keeping it", () => {
713 const antigravityInstanceId = ProviderInstanceId.make("antigravity_work");
714 const config = {
715 providers: [
716 {
717 instanceId: antigravityInstanceId,
718 driver: "antigravity",
719 displayName: "Antigravity Work",
720 enabled: true,
721 installed: true,
722 auth: { status: "authenticated" },
723 availability: "available",
724 models: [{ slug: "model-a", name: "Model A", isCustom: false, capabilities: null }],
725 },
726 ],
727 settings: {
728 ...DEFAULT_SERVER_SETTINGS,
729 providerInstances: {
730 [antigravityInstanceId]: {
731 driver: ProviderDriverKind.make("antigravity"),
732 enabled: true,
733 },
734 },
735 projectSettingsOverrides: {
736 [projectId]: { providerInstances: { [antigravityInstanceId]: { enabled: false } } },
737 },
738 },
739 } as unknown as ServerConfig;
740 const selection = { instanceId: antigravityInstanceId, model: "model-a" };
741
742 expect(resolveSelectableModelSelection(config, selection, projectId)).toBeNull();
743 expect(resolveSelectableModelSelection(config, selection, null)).toBe(selection);
744 expect(
745 resolveSelectableModelSelection({ ...config, providers: [] }, selection, projectId),
746 ).toBeNull();
747 });
748 });
749});
750
751describe("synchronized mobile model visibility", () => {
752 const provider = ProviderInstanceId.make("codex_work");
753 const other = ProviderInstanceId.make("codex_personal");
754 const models = [
755 { slug: "hidden", name: "Hidden", isCustom: false, capabilities: null },
756 { slug: "first", name: "First", isCustom: false, capabilities: null },
757 { slug: "second", name: "Second", isCustom: false, capabilities: null },
758 { slug: "custom", name: "Custom", isCustom: true, capabilities: null },
759 ];
760 const config = {
761 settings: {
762 ...DEFAULT_SERVER_SETTINGS,
763 // A live snapshot entry always has a matching configured instance (or
764 // is a driver's own default id); these are stand-ins for neither, so
765 // the enablement fixture needs its own entry to read as enabled.
766 providerInstances: {
767 [provider]: { driver: ProviderDriverKind.make("codex"), enabled: true },
768 [other]: { driver: ProviderDriverKind.make("codex"), enabled: true },
769 },
770 providerModelPreferences: {
771 [provider]: {
772 hiddenModels: ["hidden", "custom", "missing"],
773 modelOrder: ["second", "first"],
774 },
775 },
776 },
777 providers: [provider, other].map((instanceId) => ({
778 instanceId,
779 driver: "codex",
780 enabled: true,
781 installed: true,
782 auth: { status: "authenticated" },
783 models,
784 })),
785 } as unknown as ServerConfig;
786
787 it("applies server visibility and ordering independently to each provider instance", () => {
788 const options = buildModelOptions(config, null, undefined, null);
789 expect(
790 options
791 .filter((option) => option.providerKey === provider)
792 .map((option) => option.selection.model),
793 ).toEqual(["second", "first", "custom"]);
794 expect(
795 options
796 .filter((option) => option.providerKey === other)
797 .map((option) => option.selection.model),
798 ).toEqual(["hidden", "first", "second", "custom"]);
799 });
800
801 it("exposes hidden built-ins for management without offering custom visibility controls", () => {
802 const options = buildModelOptions(
803 config,
804 { instanceId: provider, model: "hidden" },
805 provider,
806 null,
807 true,
808 );
809 expect(options.map((option) => option.selection.model)).toEqual([
810 "second",
811 "first",
812 "hidden",
813 "custom",
814 ]);
815 expect(options.find((option) => option.selection.model === "hidden")).toMatchObject({
816 isHidden: true,
817 canHide: true,
818 });
819 expect(options.find((option) => option.selection.model === "custom")).toMatchObject({
820 isHidden: false,
821 canHide: false,
822 });
823 });
824
825 it.each(["hidden", "missing"])("does not resurrect the hidden fallback %s", (model) => {
826 expect(
827 buildModelOptions(config, { instanceId: provider, model }, provider, null).map(
828 (option) => option.selection.model,
829 ),
830 ).toEqual(["second", "first", "custom"]);
831 });
832
833 it("keeps custom models visible and restores built-in models when the server clears hidden models", () => {
834 expect(
835 buildModelOptions(config, { instanceId: provider, model: "custom" }, provider, null).map(
836 (option) => option.selection.model,
837 ),
838 ).toContain("custom");
839 const cleared = { ...config, settings: { ...config.settings, providerModelPreferences: {} } };
840 expect(
841 buildModelOptions(cleared, null, provider, null).map((option) => option.selection.model),
842 ).toEqual(["hidden", "first", "second", "custom"]);
843 });
844});