mini: fix default model (#35515)
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user