import { afterEach, describe, expect, mock, spyOn, test } from "bun:test" import { OpencodeClient } from "@opencode-ai/sdk/v2" import { runInteractiveMode } from "@/cli/cmd/run/runtime" import type { FooterApi, RunProvider } from "@/cli/cmd/run/types" type SessionMessage = NonNullable>["data"]>[number] const provider: RunProvider = { id: "openai", name: "OpenAI", source: "api", env: [], options: {}, models: { "gpt-5": { id: "gpt-5", providerID: "openai", api: { id: "openai", url: "https://openai.test", npm: "@ai-sdk/openai", }, name: "Little Frank", capabilities: { temperature: true, reasoning: true, attachment: true, toolcall: true, input: { text: true, audio: false, image: false, video: false, pdf: false, }, output: { text: true, audio: false, image: false, video: false, pdf: false, }, interleaved: false, }, cost: { input: 0, output: 0, cache: { read: 0, write: 0, }, }, limit: { context: 128000, output: 8192, }, status: "active", options: {}, headers: {}, release_date: "2026-01-01", }, }, } const transportProviders: RunProvider[][] = [] function defer() { let resolve!: (value: T | PromiseLike) => void const promise = new Promise((done) => { resolve = done }) return { promise, resolve } } function ok(data: T) { return Promise.resolve({ data, error: undefined, request: new Request("https://opencode.test"), response: new Response(), }) } function footer(): FooterApi { let closed = false const closes = new Set<() => void>() const notify = () => { for (const fn of closes) fn() } return { get isClosed() { return closed }, onPrompt: () => () => {}, onQueuedRemove: () => () => {}, onClose(fn) { if (closed) { fn() return () => {} } closes.add(fn) return () => { closes.delete(fn) } }, event() {}, append() {}, idle() { return Promise.resolve() }, close() { if (closed) { return } closed = true notify() }, destroy() { if (closed) { return } closed = true notify() }, } } afterEach(() => { mock.restore() transportProviders.length = 0 }) describe("run interactive runtime", () => { test("waits for provider metadata before eager replay transport bootstrap", async () => { const providersStarted = defer() const providers = defer() const sdk = new OpencodeClient() spyOn(sdk.config, "providers").mockImplementation(async () => { providersStarted.resolve() await providers.promise return ok({ providers: [provider], default: {} }) }) spyOn(sdk.session, "messages").mockImplementation(() => ok([ { info: { id: "msg-user-1", sessionID: "ses-1", role: "user", time: { created: 1, }, agent: "build", model: { providerID: "openai", modelID: "gpt-5", variant: undefined, }, }, parts: [ { id: "part-user-1", sessionID: "ses-1", messageID: "msg-user-1", type: "text", text: "hello", }, ], } satisfies SessionMessage, ]), ) spyOn(sdk.session, "get").mockRejectedValue(new Error("not needed")) spyOn(sdk.app, "agents").mockImplementation(() => ok([])) spyOn(sdk.experimental.resource, "list").mockImplementation(() => ok({})) spyOn(sdk.command, "list").mockImplementation(() => ok([])) const task = runInteractiveMode( { sdk, directory: "/tmp", sessionID: "ses-1", sessionTitle: "Session", resume: true, replay: true, replayLimit: 100, agent: "build", model: { providerID: "openai", modelID: "gpt-5", }, variant: undefined, files: [], thinking: true, backgroundSubagents: false, }, { createRuntimeLifecycle: async () => ({ footer: footer(), onResize: () => () => {}, refreshTheme: () => {}, resetForReplay: () => Promise.resolve(), close: () => Promise.resolve(), }), streamTransport: Promise.resolve({ createSessionTransport: async (input: { providers?: () => RunProvider[]; footer: FooterApi }) => { transportProviders.push(input.providers?.() ?? []) setTimeout(() => { input.footer.close() }, 0) return { runPromptTurn: async () => {}, selectSubagent: () => {}, replayOnResize: async () => false, close: async () => {}, } }, formatUnknownError: (error: unknown) => (error instanceof Error ? error.message : String(error)), }), }, ) await providersStarted.promise expect(transportProviders).toEqual([]) providers.resolve() await task expect(transportProviders).toEqual([[provider]]) }) })