diff --git a/src/agents/utils.test.ts b/src/agents/utils.test.ts index 5b4462ba..7b9b40b6 100644 --- a/src/agents/utils.test.ts +++ b/src/agents/utils.test.ts @@ -45,17 +45,17 @@ describe("createBuiltinAgents with model overrides", () => { expect(agents.Sisyphus.reasoningEffort).toBeUndefined() }) - test("Oracle uses system default when no availableModels provided", async () => { - // #given - no available models, falls back to system default + test("Oracle uses first fallback entry when no availableModels provided (no cache scenario)", async () => { + // #given - no available models simulates CI without model cache // #when const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL) - // #then - falls back to system default (anthropic/claude-opus-4-5) - expect(agents.oracle.model).toBe("anthropic/claude-opus-4-5") - expect(agents.oracle.thinking).toEqual({ type: "enabled", budgetTokens: 32000 }) - expect(agents.oracle.reasoningEffort).toBeUndefined() - expect(agents.oracle.textVerbosity).toBeUndefined() + // #then - uses first fallback entry (openai/gpt-5.2) instead of system default + expect(agents.oracle.model).toBe("openai/gpt-5.2") + expect(agents.oracle.reasoningEffort).toBe("medium") + expect(agents.oracle.textVerbosity).toBe("high") + expect(agents.oracle.thinking).toBeUndefined() }) test("Oracle with GPT model override has reasoningEffort, no thinking", async () => { diff --git a/src/cli/doctor/checks/model-resolution.test.ts b/src/cli/doctor/checks/model-resolution.test.ts index 83ebb6f9..e5d6e1f6 100644 --- a/src/cli/doctor/checks/model-resolution.test.ts +++ b/src/cli/doctor/checks/model-resolution.test.ts @@ -97,13 +97,14 @@ describe("model-resolution check", () => { // #when: Running the model resolution check // #then: Returns pass with details showing resolution flow - it("returns pass status with agent and category counts", async () => { + it("returns pass or warn status with agent and category counts", async () => { const { checkModelResolution } = await import("./model-resolution") const result = await checkModelResolution() - // #then: Should pass and show counts - expect(result.status).toBe("pass") + // #then: Should pass (with cache) or warn (no cache) and show counts + // In CI without model cache, status is "warn"; locally with cache, status is "pass" + expect(["pass", "warn"]).toContain(result.status) expect(result.message).toMatch(/\d+ agents?, \d+ categories?/) }) diff --git a/src/shared/model-resolver.test.ts b/src/shared/model-resolver.test.ts index 299210fa..f8d168ac 100644 --- a/src/shared/model-resolver.test.ts +++ b/src/shared/model-resolver.test.ts @@ -336,8 +336,8 @@ describe("resolveModelWithFallback", () => { expect(logSpy).toHaveBeenCalledWith("No available model found in fallback chain, falling through to system default") }) - test("returns system default when availableModels is empty", () => { - // #given + test("uses first fallback entry when availableModels is empty (no cache scenario)", () => { + // #given - empty availableModels simulates CI environment without model cache const input: ExtendedModelResolutionInput = { fallbackChain: [ { providers: ["anthropic"], model: "claude-opus-4-5" }, @@ -349,9 +349,9 @@ describe("resolveModelWithFallback", () => { // #when const result = resolveModelWithFallback(input) - // #then - expect(result.model).toBe("google/gemini-3-pro") - expect(result.source).toBe("system-default") + // #then - should use first fallback entry, not system default + expect(result.model).toBe("anthropic/claude-opus-4-5") + expect(result.source).toBe("provider-fallback") }) test("returns system default when fallbackChain is not provided", () => { diff --git a/src/shared/model-resolver.ts b/src/shared/model-resolver.ts index 9881e883..89d35f8e 100644 --- a/src/shared/model-resolver.ts +++ b/src/shared/model-resolver.ts @@ -53,6 +53,15 @@ export function resolveModelWithFallback( // Step 2: Provider fallback chain (with availability check) if (fallbackChain && fallbackChain.length > 0) { + // If availableModels is empty (no cache), use first fallback entry directly without availability check + if (availableModels.size === 0) { + const firstEntry = fallbackChain[0] + const firstProvider = firstEntry.providers[0] + const model = `${firstProvider}/${firstEntry.model}` + log("Model resolved via fallback chain (no cache, using first entry)", { provider: firstProvider, model: firstEntry.model, variant: firstEntry.variant }) + return { model, source: "provider-fallback", variant: firstEntry.variant } + } + for (const entry of fallbackChain) { for (const provider of entry.providers) { const fullModel = `${provider}/${entry.model}`