import { describe, expect, test } from "bun:test" import { generateModelConfig } from "./model-fallback" import type { InstallConfig } from "./types" function createConfig(overrides: Partial = {}): InstallConfig { return { hasClaude: false, isMax20: false, hasOpenAI: false, hasGemini: false, hasCopilot: false, hasOpencodeZen: false, hasZaiCodingPlan: false, ...overrides, } } describe("generateModelConfig", () => { describe("no providers available", () => { test("returns ULTIMATE_FALLBACK for all agents and categories when no providers", () => { // #given no providers are available const config = createConfig() // #when generateModelConfig is called const result = generateModelConfig(config) // #then should use ULTIMATE_FALLBACK for everything expect(result).toMatchSnapshot() }) }) describe("single native provider", () => { test("uses Claude models when only Claude is available", () => { // #given only Claude is available const config = createConfig({ hasClaude: true }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then should use Claude models per NATIVE_FALLBACK_CHAINS expect(result).toMatchSnapshot() }) test("uses Claude models with isMax20 flag", () => { // #given Claude is available with Max 20 plan const config = createConfig({ hasClaude: true, isMax20: true }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then should use higher capability models for Sisyphus expect(result).toMatchSnapshot() }) test("uses OpenAI models when only OpenAI is available", () => { // #given only OpenAI is available const config = createConfig({ hasOpenAI: true }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then should use OpenAI models expect(result).toMatchSnapshot() }) test("uses OpenAI models with isMax20 flag", () => { // #given OpenAI is available with Max 20 plan const config = createConfig({ hasOpenAI: true, isMax20: true }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then should use higher capability models expect(result).toMatchSnapshot() }) test("uses Gemini models when only Gemini is available", () => { // #given only Gemini is available const config = createConfig({ hasGemini: true }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then should use Gemini models expect(result).toMatchSnapshot() }) test("uses Gemini models with isMax20 flag", () => { // #given Gemini is available with Max 20 plan const config = createConfig({ hasGemini: true, isMax20: true }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then should use higher capability models expect(result).toMatchSnapshot() }) }) describe("all native providers", () => { test("uses preferred models from fallback chains when all natives available", () => { // #given all native providers are available const config = createConfig({ hasClaude: true, hasOpenAI: true, hasGemini: true, }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then should use first provider in each fallback chain expect(result).toMatchSnapshot() }) test("uses preferred models with isMax20 flag when all natives available", () => { // #given all native providers are available with Max 20 plan const config = createConfig({ hasClaude: true, hasOpenAI: true, hasGemini: true, isMax20: true, }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then should use higher capability models expect(result).toMatchSnapshot() }) }) describe("fallback providers", () => { test("uses OpenCode Zen models when only OpenCode Zen is available", () => { // #given only OpenCode Zen is available const config = createConfig({ hasOpencodeZen: true }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then should use OPENCODE_ZEN_MODELS expect(result).toMatchSnapshot() }) test("uses OpenCode Zen models with isMax20 flag", () => { // #given OpenCode Zen is available with Max 20 plan const config = createConfig({ hasOpencodeZen: true, isMax20: true }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then should use higher capability models expect(result).toMatchSnapshot() }) test("uses GitHub Copilot models when only Copilot is available", () => { // #given only GitHub Copilot is available const config = createConfig({ hasCopilot: true }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then should use GITHUB_COPILOT_MODELS expect(result).toMatchSnapshot() }) test("uses GitHub Copilot models with isMax20 flag", () => { // #given GitHub Copilot is available with Max 20 plan const config = createConfig({ hasCopilot: true, isMax20: true }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then should use higher capability models expect(result).toMatchSnapshot() }) test("uses ZAI model for librarian when only ZAI is available", () => { // #given only ZAI is available const config = createConfig({ hasZaiCodingPlan: true }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then should use ZAI_MODEL for librarian expect(result).toMatchSnapshot() }) test("uses ZAI model for librarian with isMax20 flag", () => { // #given ZAI is available with Max 20 plan const config = createConfig({ hasZaiCodingPlan: true, isMax20: true }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then should use ZAI_MODEL for librarian expect(result).toMatchSnapshot() }) }) describe("mixed provider scenarios", () => { test("uses Claude + OpenCode Zen combination", () => { // #given Claude and OpenCode Zen are available const config = createConfig({ hasClaude: true, hasOpencodeZen: true, }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then should prefer Claude (native) over OpenCode Zen expect(result).toMatchSnapshot() }) test("uses OpenAI + Copilot combination", () => { // #given OpenAI and Copilot are available const config = createConfig({ hasOpenAI: true, hasCopilot: true, }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then should prefer OpenAI (native) over Copilot expect(result).toMatchSnapshot() }) test("uses Claude + ZAI combination (librarian uses ZAI)", () => { // #given Claude and ZAI are available const config = createConfig({ hasClaude: true, hasZaiCodingPlan: true, }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then librarian should use ZAI, others use Claude expect(result).toMatchSnapshot() }) test("uses Gemini + Claude combination (explore uses Gemini)", () => { // #given Gemini and Claude are available const config = createConfig({ hasGemini: true, hasClaude: true, }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then explore should use Gemini flash expect(result).toMatchSnapshot() }) test("uses all fallback providers together", () => { // #given all fallback providers are available const config = createConfig({ hasOpencodeZen: true, hasCopilot: true, hasZaiCodingPlan: true, }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then should prefer OpenCode Zen, but librarian uses ZAI expect(result).toMatchSnapshot() }) test("uses all providers together", () => { // #given all providers are available const config = createConfig({ hasClaude: true, hasOpenAI: true, hasGemini: true, hasOpencodeZen: true, hasCopilot: true, hasZaiCodingPlan: true, }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then should prefer native providers, librarian uses ZAI expect(result).toMatchSnapshot() }) test("uses all providers with isMax20 flag", () => { // #given all providers are available with Max 20 plan const config = createConfig({ hasClaude: true, hasOpenAI: true, hasGemini: true, hasOpencodeZen: true, hasCopilot: true, hasZaiCodingPlan: true, isMax20: true, }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then should use higher capability models expect(result).toMatchSnapshot() }) }) describe("explore agent special cases", () => { test("explore uses Gemini flash when Gemini available", () => { // #given Gemini is available const config = createConfig({ hasGemini: true }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then explore should use gemini-3-flash-preview expect(result.agents?.explore?.model).toBe("google/gemini-3-flash-preview") }) test("explore uses Claude haiku when Claude + isMax20 but no Gemini", () => { // #given Claude is available with Max 20 plan but no Gemini const config = createConfig({ hasClaude: true, isMax20: true }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then explore should use claude-haiku-4-5 expect(result.agents?.explore?.model).toBe("anthropic/claude-haiku-4-5") }) test("explore uses grok-code when Claude without isMax20 and no Gemini", () => { // #given Claude is available without Max 20 plan and no Gemini const config = createConfig({ hasClaude: true, isMax20: false }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then explore should use grok-code expect(result.agents?.explore?.model).toBe("opencode/grok-code") }) test("explore uses grok-code when only OpenAI available", () => { // #given only OpenAI is available const config = createConfig({ hasOpenAI: true }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then explore should use grok-code (fallback) expect(result.agents?.explore?.model).toBe("opencode/grok-code") }) }) describe("Sisyphus agent special cases", () => { test("Sisyphus uses sisyphus-high capability when isMax20 is true", () => { // #given Claude is available with Max 20 plan const config = createConfig({ hasClaude: true, isMax20: true }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then Sisyphus should use opus (sisyphus-high) expect(result.agents?.Sisyphus?.model).toBe("anthropic/claude-opus-4-5") }) test("Sisyphus uses sisyphus-low capability when isMax20 is false", () => { // #given Claude is available without Max 20 plan const config = createConfig({ hasClaude: true, isMax20: false }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then Sisyphus should use sonnet (sisyphus-low) expect(result.agents?.Sisyphus?.model).toBe("anthropic/claude-sonnet-4-5") }) }) describe("librarian agent special cases", () => { test("librarian uses ZAI when ZAI is available regardless of other providers", () => { // #given ZAI and Claude are available const config = createConfig({ hasClaude: true, hasZaiCodingPlan: true, }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then librarian should use ZAI_MODEL expect(result.agents?.librarian?.model).toBe("zai-coding-plan/glm-4.7") }) test("librarian uses claude-sonnet when ZAI not available but Claude is", () => { // #given only Claude is available (no ZAI) const config = createConfig({ hasClaude: true }) // #when generateModelConfig is called const result = generateModelConfig(config) // #then librarian should use claude-sonnet-4-5 (third in fallback chain after ZAI and opencode/glm) expect(result.agents?.librarian?.model).toBe("anthropic/claude-sonnet-4-5") }) }) describe("schema URL", () => { test("always includes correct schema URL", () => { // #given any config const config = createConfig() // #when generateModelConfig is called const result = generateModelConfig(config) // #then should include correct schema URL expect(result.$schema).toBe( "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json" ) }) }) })