From b03d06412750aa2eca85d277acba851732affadb Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 28 Apr 2026 18:27:15 -0400 Subject: [PATCH] test(httpapi): cover sdk prompt and stream parity --- .../opencode/test/server/httpapi-sdk.test.ts | 205 +++++++++++++++++- 1 file changed, 200 insertions(+), 5 deletions(-) diff --git a/packages/opencode/test/server/httpapi-sdk.test.ts b/packages/opencode/test/server/httpapi-sdk.test.ts index 4c5113c21f..c0984170be 100644 --- a/packages/opencode/test/server/httpapi-sdk.test.ts +++ b/packages/opencode/test/server/httpapi-sdk.test.ts @@ -8,22 +8,33 @@ import { MessageID, PartID, SessionID } from "../../src/session/schema" import { MessageV2 } from "../../src/session/message-v2" import { ModelID, ProviderID } from "../../src/provider/schema" import { Session as SessionNs } from "@/session/session" +import { TestLLMServer } from "../lib/llm-server" import path from "path" import { resetDatabase } from "../fixture/db" import { tmpdir } from "../fixture/fixture" -const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI +const original = { + OPENCODE_EXPERIMENTAL_HTTPAPI: Flag.OPENCODE_EXPERIMENTAL_HTTPAPI, + OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD, + OPENCODE_SERVER_USERNAME: Flag.OPENCODE_SERVER_USERNAME, +} type Backend = "legacy" | "httpapi" type Sdk = ReturnType type SdkResult = { response: Response; data?: unknown; error?: unknown } -function app(backend: Backend) { +function app(backend: Backend, input?: { password?: string; username?: string }) { Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = backend === "httpapi" + Flag.OPENCODE_SERVER_PASSWORD = input?.password + Flag.OPENCODE_SERVER_USERNAME = input?.username return backend === "httpapi" ? Server.Default().app : Server.Legacy().app } -function client(backend: Backend, directory?: string) { - const serverApp = app(backend) +function client( + backend: Backend, + directory?: string, + input?: { password?: string; username?: string; headers?: Record }, +) { + const serverApp = app(backend, input) const fetch = Object.assign( async (request: RequestInfo | URL, init?: RequestInit) => await serverApp.fetch(request instanceof Request ? request : new Request(request, init)), @@ -32,10 +43,48 @@ function client(backend: Backend, directory?: string) { return createOpencodeClient({ baseUrl: "http://localhost", directory, + headers: input?.headers, fetch, }) } +function authorization(username: string, password: string) { + return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}` +} + +function providerConfig(url: string) { + return { + formatter: false, + lsp: false, + provider: { + test: { + name: "Test", + id: "test", + env: [], + npm: "@ai-sdk/openai-compatible", + models: { + "test-model": { + id: "test-model", + name: "Test Model", + attachment: false, + reasoning: false, + temperature: false, + tool_call: true, + release_date: "2025-01-01", + limit: { context: 100000, output: 10000 }, + cost: { input: 0, output: 0 }, + options: {}, + }, + }, + options: { + apiKey: "test-key", + baseURL: url, + }, + }, + }, + } +} + async function expectStatus(result: Promise<{ response: Response }>, status: number) { expect((await result).response.status).toBe(status) } @@ -128,8 +177,26 @@ async function withTmp(backend: Backend, fn: (input: { sdk: Sdk; directory: s return fn({ sdk: client(backend, tmp.path), directory: tmp.path }) } +async function withFakeLlm( + backend: Backend, + fn: (input: { sdk: Sdk; directory: string; llm: TestLLMServer["Service"] }) => Promise, +) { + return Effect.runPromise( + Effect.gen(function* () { + const llm = yield* TestLLMServer + const tmp = yield* Effect.acquireRelease( + Effect.promise(() => tmpdir({ git: true, config: providerConfig(llm.url) })), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ) + return yield* Effect.promise(() => fn({ sdk: client(backend, tmp.path), directory: tmp.path, llm })) + }).pipe(Effect.scoped, Effect.provide(TestLLMServer.layer)), + ) +} + afterEach(async () => { - Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original.OPENCODE_EXPERIMENTAL_HTTPAPI + Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD + Flag.OPENCODE_SERVER_USERNAME = original.OPENCODE_SERVER_USERNAME await Instance.disposeAll() await resetDatabase() }) @@ -199,6 +266,63 @@ describe("HttpApi SDK", () => { }) }) + test("matches generated SDK global event stream across backends", async () => { + await compareBackends(async (backend) => { + const events = await client(backend).global.event({ signal: AbortSignal.timeout(1_000) }) + try { + const first = await events.stream.next() + return { + type: record(record(first.value).payload).type, + } + } finally { + await events.stream.return(undefined) + } + }) + }) + + test("matches generated SDK instance event stream across backends", async () => { + await compareBackends((backend) => + withTmp(backend, async ({ sdk }) => { + const events = await sdk.event.subscribe(undefined, { signal: AbortSignal.timeout(1_000) }) + try { + const first = await events.stream.next() + return { + type: record(record(first.value).payload).type, + } + } finally { + await events.stream.return(undefined) + } + }), + ) + }) + + test("matches generated SDK basic auth behavior across backends", async () => { + await compareBackends((backend) => + withTmp(backend, async ({ directory }) => { + const missing = await capture( + client(backend, directory, { password: "secret" }).file.read({ path: "hello.txt" }), + ) + const bad = await capture( + client(backend, directory, { + password: "secret", + headers: { authorization: authorization("opencode", "wrong") }, + }).file.read({ path: "hello.txt" }), + ) + const good = await capture( + client(backend, directory, { + password: "secret", + headers: { authorization: authorization("opencode", "secret") }, + }).file.read({ path: "hello.txt" }), + ) + + return { + statuses: statuses({ missing, bad, good }), + content: record(good.data).content, + } + }), + ) + }) + test("matches generated SDK instance read routes across backends", async () => { await compareBackends((backend) => withTmp(backend, async ({ sdk, directory }) => { @@ -356,6 +480,77 @@ describe("HttpApi SDK", () => { ) }) + test("matches generated SDK prompt no-reply routes across backends", async () => { + await compareBackends((backend) => + withTmp(backend, async ({ sdk }) => { + const session = await capture(sdk.session.create({ title: "prompt" })) + const sessionID = String(record(session.data).id) + const prompt = await capture( + sdk.session.prompt({ + sessionID, + agent: "build", + noReply: true, + parts: [{ type: "text", text: "hello" }], + }), + ) + const asyncPrompt = await capture( + sdk.session.promptAsync({ + sessionID, + agent: "build", + noReply: true, + parts: [{ type: "text", text: "async hello" }], + }), + ) + const messages = await capture(sdk.session.messages({ sessionID })) + + return { + statuses: statuses({ session, prompt, asyncPrompt, messages }), + promptRole: record(record(prompt.data).info).role, + messageCount: array(messages.data).length, + messageTexts: array(messages.data) + .flatMap((item) => array(record(item).parts)) + .map((part) => record(part).text) + .filter((text): text is string => typeof text === "string") + .sort(), + } + }), + ) + }) + + test("matches generated SDK prompt streaming through fake LLM across backends", async () => { + await compareBackends((backend) => + withFakeLlm(backend, async ({ sdk, llm }) => { + await Effect.runPromise(llm.text("fake world", { usage: { input: 11, output: 7 } })) + const session = await capture( + sdk.session.create({ + title: "llm prompt", + permission: [{ permission: "*", pattern: "*", action: "allow" }], + }), + ) + const sessionID = String(record(session.data).id) + const prompt = await capture( + sdk.session.prompt({ + sessionID, + agent: "build", + model: { providerID: "test", modelID: "test-model" }, + parts: [{ type: "text", text: "hello llm" }], + }), + ) + const messages = await capture(sdk.session.messages({ sessionID })) + const inputs = await Effect.runPromise(llm.inputs) + + return { + statuses: statuses({ session, prompt, messages }), + calls: inputs.length, + requestedModel: inputs[0]?.model, + responseText: JSON.stringify(prompt.data).includes("fake world"), + persistedText: JSON.stringify(messages.data).includes("fake world"), + userText: JSON.stringify(messages.data).includes("hello llm"), + } + }), + ) + }) + test("matches generated SDK TUI validation and command routes across backends", async () => { await compareBackends((backend) => withTmp(backend, async ({ sdk }) => {