| 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | |
| 3 | import { |
| 4 | DEFAULT_SERVER_SETTINGS, |
| 5 | ProjectId, |
| 6 | ProviderDriverKind, |
| 7 | ProviderInstanceId, |
| 8 | type ModelSelection, |
| 9 | type ServerConfig, |
| 10 | type ServerSettings, |
| 11 | } from "@t3tools/contracts"; |
| 12 | |
| 13 | import { |
| 14 | buildModelOptions, |
| 15 | describeUnavailableModelSelection, |
| 16 | groupByProvider, |
| 17 | isModelSelectionUnavailable, |
| 18 | resolveDefaultableModelSelection, |
| 19 | resolveNewTaskModelSelection, |
| 20 | resolveSelectableModelSelection, |
| 21 | type ModelOption, |
| 22 | } from "./modelOptions"; |
| 23 | |
| 24 | describe("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 | |
| 751 | describe("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 | }); |