| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238 |
- import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
- import { OpencodeClient } from "@kirincode-ai/sdk/v2"
- import { runInteractiveMode } from "@/cli/cmd/run/runtime"
- import type { FooterApi, RunProvider } from "@/cli/cmd/run/types"
- type SessionMessage = NonNullable<Awaited<ReturnType<OpencodeClient["session"]["messages"]>>["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<T>() {
- let resolve!: (value: T | PromiseLike<T>) => void
- const promise = new Promise<T>((done) => {
- resolve = done
- })
- return { promise, resolve }
- }
- function ok<T>(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<void>()
- const providers = defer<void>()
- 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]])
- })
- })
|