mini: fix default model (#35515)

This commit is contained in:
Simon Klee
2026-07-06 09:20:23 +02:00
committed by GitHub
parent 147da5d278
commit fb75ea2cf6
17 changed files with 398 additions and 118 deletions
@@ -1,12 +1,37 @@
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
import { OpencodeClient } from "@opencode-ai/sdk/v2"
import { loadRunReferences, runProviders } from "@/cli/cmd/run/catalog.shared"
import { loadRunReferences, runProviders, waitForDefaultModel } from "@/cli/cmd/run/catalog.shared"
afterEach(() => {
mock.restore()
})
describe("run catalog shared", () => {
test("resolves the catalog-selected model for the footer", async () => {
const client = new OpencodeClient()
const selected = spyOn(client.v2.model, "default").mockImplementation(
() =>
Promise.resolve({
data: {
location: { directory: "/tmp", project: { id: "proj_1", directory: "/tmp" } },
data: {
id: "gpt-5",
providerID: "openai",
},
},
error: undefined,
request: new Request("https://opencode.test"),
response: new Response(),
}) as never,
)
await expect(waitForDefaultModel({ sdk: client, directory: "/tmp" })).resolves.toEqual({
providerID: "openai",
modelID: "gpt-5",
})
expect(selected).toHaveBeenCalledWith({ location: { directory: "/tmp" } }, { throwOnError: true })
})
test("loads visible project references from the current reference catalog", async () => {
const client = new OpencodeClient()
const list = spyOn(client.v2.reference, "list").mockImplementation(
+156 -11
View File
@@ -113,6 +113,7 @@ describe("run interactive runtime", () => {
test("waits for provider metadata before eager replay transport bootstrap", async () => {
const providersStarted = defer<void>()
const providers = defer<void>()
const lifecycleModels: unknown[] = []
const sdk = new OpencodeClient()
const legacyProviders = spyOn(sdk.config, "providers").mockRejectedValue(new Error("legacy providers should stay unused"))
@@ -249,23 +250,23 @@ describe("run interactive runtime", () => {
replay: true,
replayLimit: 100,
agent: "build",
model: {
providerID: "openai",
modelID: "gpt-5",
},
model: undefined,
variant: undefined,
files: [],
thinking: true,
backgroundSubagents: false,
},
{
createRuntimeLifecycle: async () => ({
footer: footer(),
onResize: () => () => {},
refreshTheme: () => {},
resetForReplay: () => Promise.resolve(),
close: () => Promise.resolve(),
}),
createRuntimeLifecycle: async (input) => {
lifecycleModels.push(input.model)
return {
footer: footer(),
onResize: () => () => {},
refreshTheme: () => {},
resetForReplay: () => Promise.resolve(),
close: () => Promise.resolve(),
}
},
streamTransport: Promise.resolve({
createSessionTransport: async (input: { providers?: () => RunProvider[]; footer: FooterApi }) => {
transportProviders.push(input.providers?.() ?? [])
@@ -293,12 +294,156 @@ describe("run interactive runtime", () => {
await task
expect(lifecycleModels).toEqual([{ providerID: "openai", modelID: "gpt-5" }])
expect(transportProviders).toEqual([[provider]])
expect(legacyProviders).not.toHaveBeenCalled()
expect(legacyAgents).not.toHaveBeenCalled()
expect(legacyCommands).not.toHaveBeenCalled()
})
test("defers catalog-selected model resolution until after first paint", async () => {
const sdk = new OpencodeClient()
const defaultStarted = defer<void>()
const releaseDefault = defer<void>()
const lifecycleStarted = defer<void>()
const painted = defer<void>()
const modelShown = defer<void>()
let defaultRequested = false
const events: FooterEvent[] = []
const api = footer(events)
api.idle = () => painted.promise
const event = api.event
api.event = (value) => {
event(value)
if (value.type !== "model") return
modelShown.resolve()
api.close()
}
spyOn(sdk.v2.model, "default").mockImplementation(async () => {
defaultRequested = true
defaultStarted.resolve()
await releaseDefault.promise
return ok({
location: { directory: "/tmp" },
data: { id: "gpt-5", providerID: "openai" },
}) as never
})
spyOn(sdk.v2.provider, "list").mockImplementation(() =>
ok({ location: { directory: "/tmp" }, data: [] }) as never,
)
spyOn(sdk.v2.model, "list").mockImplementation(() =>
ok({ location: { directory: "/tmp" }, data: [] }) as never,
)
spyOn(sdk.v2.agent, "list").mockImplementation(() =>
ok({ location: { directory: "/tmp" }, data: [] }) as never,
)
spyOn(sdk.v2.reference, "list").mockImplementation(() =>
ok({ location: { directory: "/tmp" }, data: [] }) as never,
)
spyOn(sdk.v2.command, "list").mockImplementation(() =>
ok({ location: { directory: "/tmp" }, data: [] }) as never,
)
spyOn(sdk.v2.skill, "list").mockImplementation(() =>
ok({ location: { directory: "/tmp" }, data: [] }) as never,
)
const task = runInteractiveMode(
{
sdk,
directory: "/tmp",
sessionID: "ses-fresh",
resume: false,
agent: "build",
model: undefined,
variant: undefined,
files: [],
thinking: false,
backgroundSubagents: false,
},
{
createRuntimeLifecycle: async (input) => {
expect(input.model).toBeUndefined()
lifecycleStarted.resolve()
return {
footer: api,
onResize: () => () => {},
refreshTheme: () => {},
resetForReplay: () => Promise.resolve(),
close: () => Promise.resolve(),
}
},
streamTransport: Promise.resolve({
createSessionTransport: async () => ({
runPromptTurn: async () => {},
interruptActiveTurn: async () => {},
selectSubagent: () => {},
replayOnResize: async () => false,
close: async () => {},
}),
formatUnknownError: (error: unknown) => (error instanceof Error ? error.message : String(error)),
}),
},
)
await lifecycleStarted.promise
expect(defaultRequested).toBe(false)
painted.resolve()
await defaultStarted.promise
releaseDefault.resolve()
await modelShown.promise
await task
expect(events.find((event) => event.type === "model")).toEqual({
type: "model",
model: "gpt-5 · openai",
selection: { providerID: "openai", modelID: "gpt-5" },
})
})
test("does not start deferred work after the footer closes", async () => {
const sdk = new OpencodeClient()
const lifecycleStarted = defer<void>()
const painted = defer<void>()
const api = footer()
api.idle = () => painted.promise
const defaultModel = spyOn(sdk.v2.model, "default")
const task = runInteractiveMode(
{
sdk,
directory: "/tmp",
sessionID: "ses-closed",
resume: false,
agent: "build",
model: undefined,
variant: undefined,
files: [],
thinking: false,
backgroundSubagents: false,
},
{
createRuntimeLifecycle: async () => {
lifecycleStarted.resolve()
return {
footer: api,
onResize: () => () => {},
refreshTheme: () => {},
resetForReplay: () => Promise.resolve(),
close: () => Promise.resolve(),
}
},
},
)
await lifecycleStarted.promise
api.close()
painted.resolve()
await task
expect(defaultModel).not.toHaveBeenCalled()
})
test("retains last-known-good state across failed coalesced refreshes and retries later", async () => {
const sdk = new OpencodeClient()
const refreshGate = defer<void>()
@@ -303,6 +303,8 @@ describe("run session shared", () => {
const out = await resolveCurrentSession(client, "ses_1")
expect(out.model).toEqual({ providerID: "openai", modelID: "gpt-5" })
expect(out.variant).toBe("high")
expect(out.turns[0]?.prompt).toEqual({
text: "Review @note.ts",
parts: [