fix(doctor): align auto-update and doctor config paths

This commit is contained in:
YeonGyu-Kim
2026-03-17 15:56:02 +09:00
parent 0cf386ec52
commit e9887dd82f
7 changed files with 288 additions and 21 deletions
@@ -7,16 +7,22 @@ import * as connectedProvidersCache from "../../shared/connected-providers-cache
describe("resolveCategoryExecution", () => {
let connectedProvidersSpy: ReturnType<typeof spyOn> | undefined
let providerModelsSpy: ReturnType<typeof spyOn> | undefined
let hasConnectedProvidersSpy: ReturnType<typeof spyOn> | undefined
let hasProviderModelsSpy: ReturnType<typeof spyOn> | undefined
beforeEach(() => {
mock.restore()
connectedProvidersSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
providerModelsSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue(null)
hasConnectedProvidersSpy = spyOn(connectedProvidersCache, "hasConnectedProvidersCache").mockReturnValue(false)
hasProviderModelsSpy = spyOn(connectedProvidersCache, "hasProviderModelsCache").mockReturnValue(false)
})
afterEach(() => {
connectedProvidersSpy?.mockRestore()
providerModelsSpy?.mockRestore()
hasConnectedProvidersSpy?.mockRestore()
hasProviderModelsSpy?.mockRestore()
})
const createMockExecutorContext = (): ExecutorContext => ({
@@ -27,7 +33,7 @@ describe("resolveCategoryExecution", () => {
sisyphusJuniorModel: undefined,
})
test("returns clear error when category exists but required model is not available", async () => {
test("returns unpinned resolution when category cache is not ready on first run", async () => {
//#given
const args = {
category: "deep",
@@ -39,6 +45,9 @@ describe("resolveCategoryExecution", () => {
enableSkillTools: false,
}
const executorCtx = createMockExecutorContext()
executorCtx.userCategories = {
deep: {},
}
const inheritedModel = undefined
const systemDefaultModel = "anthropic/claude-sonnet-4-6"
@@ -46,10 +55,10 @@ describe("resolveCategoryExecution", () => {
const result = await resolveCategoryExecution(args, executorCtx, inheritedModel, systemDefaultModel)
//#then
expect(result.error).toBeDefined()
expect(result.error).toContain("deep")
expect(result.error).toMatch(/model.*not.*available|requires.*model/i)
expect(result.error).not.toContain("Unknown category")
expect(result.error).toBeUndefined()
expect(result.actualModel).toBeUndefined()
expect(result.categoryModel).toBeUndefined()
expect(result.agentToUse).toBeDefined()
})
test("returns 'unknown category' error for truly unknown categories", async () => {
+5 -2
View File
@@ -85,6 +85,7 @@ Available categories: ${allCategoryNames}`,
let actualModel: string | undefined
let modelInfo: ModelFallbackInfo | undefined
let categoryModel: { providerID: string; modelID: string; variant?: string } | undefined
let isModelResolutionSkipped = false
const overrideModel = sisyphusJuniorModel
const explicitCategoryModel = userCategories?.[args.category!]?.model
@@ -109,7 +110,9 @@ Available categories: ${allCategoryNames}`,
systemDefaultModel,
})
if (resolution) {
if (resolution && "skipped" in resolution) {
isModelResolutionSkipped = true
} else if (resolution) {
const { model: resolvedModel, variant: resolvedVariant } = resolution
actualModel = resolvedModel
@@ -156,7 +159,7 @@ Available categories: ${allCategoryNames}`,
}
const categoryPromptAppend = resolved.promptAppend || undefined
if (!categoryModel && !actualModel) {
if (!categoryModel && !actualModel && !isModelResolutionSkipped) {
const categoryNames = Object.keys(enabledCategories)
return {
agentToUse: "",
@@ -1,4 +1,5 @@
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test"
declare const require: (name: string) => any
const { afterEach, beforeEach, describe, expect, mock, spyOn, test } = require("bun:test")
import { resolveModelForDelegateTask } from "./model-selection"
import * as connectedProvidersCache from "../../shared/connected-providers-cache"
@@ -22,7 +23,7 @@ describe("resolveModelForDelegateTask", () => {
})
describe("#when availableModels is empty and no user model override", () => {
test("#then returns undefined to let OpenCode use system default", () => {
test("#then returns skipped sentinel to leave model unpinned", () => {
const result = resolveModelForDelegateTask({
categoryDefaultModel: "anthropic/claude-sonnet-4-6",
fallbackChain: [
@@ -32,7 +33,7 @@ describe("resolveModelForDelegateTask", () => {
systemDefaultModel: "anthropic/claude-sonnet-4-6",
})
expect(result).toBeUndefined()
expect(result).toEqual({ skipped: true })
})
})
@@ -53,7 +54,7 @@ describe("resolveModelForDelegateTask", () => {
})
describe("#when user set fallback_models but no cache exists", () => {
test("#then returns undefined (skip fallback resolution without cache)", () => {
test("#then returns skipped sentinel (skip fallback resolution without cache)", () => {
const result = resolveModelForDelegateTask({
userFallbackModels: ["openai/gpt-5.4", "google/gemini-3.1-pro"],
categoryDefaultModel: "anthropic/claude-sonnet-4-6",
@@ -63,7 +64,7 @@ describe("resolveModelForDelegateTask", () => {
availableModels: new Set(),
})
expect(result).toBeUndefined()
expect(result).toEqual({ skipped: true })
})
})
})
@@ -85,8 +86,7 @@ describe("resolveModelForDelegateTask", () => {
systemDefaultModel: "anthropic/claude-sonnet-4-6",
})
expect(result).toBeDefined()
expect(result!.model).toBe("anthropic/claude-sonnet-4-6")
expect(result).toEqual({ model: "anthropic/claude-sonnet-4-6" })
})
})
@@ -100,8 +100,7 @@ describe("resolveModelForDelegateTask", () => {
availableModels: new Set(["anthropic/claude-sonnet-4-6"]),
})
expect(result).toBeDefined()
expect(result!.model).toBe("anthropic/claude-sonnet-4-6")
expect(result).toEqual({ model: "anthropic/claude-sonnet-4-6" })
})
})
+2 -2
View File
@@ -51,7 +51,7 @@ export function resolveModelForDelegateTask(input: {
fallbackChain?: FallbackEntry[]
availableModels: Set<string>
systemDefaultModel?: string
}): { model: string; variant?: string } | undefined {
}): { model: string; variant?: string } | { skipped: true } | undefined {
const userModel = normalizeModel(input.userModel)
if (userModel) {
return { model: userModel }
@@ -60,7 +60,7 @@ export function resolveModelForDelegateTask(input: {
// Before provider cache is created (first run), skip model resolution entirely.
// OpenCode will use its system default model when no model is specified in the prompt.
if (input.availableModels.size === 0 && !hasProviderModelsCache() && !hasConnectedProvidersCache()) {
return undefined
return { skipped: true }
}
const categoryDefault = normalizeModel(input.categoryDefaultModel)