| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596 |
- import { expect, test } from "bun:test"
- import { isSessionNotFoundError, isUnauthorizedError, OpenCode } from "../src/promise/index"
- test("exposes every standard HTTP API group", () => {
- const client = OpenCode.make({ baseUrl: "http://localhost:3000" })
- expect(Object.keys(client)).toEqual([
- "health",
- "server",
- "location",
- "agent",
- "plugin",
- "session",
- "message",
- "model",
- "generate",
- "provider",
- "integration",
- "mcp",
- "credential",
- "project",
- "form",
- "permission",
- "file",
- "command",
- "skill",
- "event",
- "pty",
- "shell",
- "question",
- "reference",
- "projectCopy",
- "vcs",
- "debug",
- "websearch",
- ])
- expect(Object.keys(client.debug)).toEqual(["location"])
- expect(Object.keys(client.debug.location)).toEqual(["list", "evict"])
- expect(Object.keys(client.message)).toEqual(["list"])
- expect(Object.keys(client.integration)).toEqual(["list", "get", "wellknown", "connect", "oauth", "command"])
- expect(Object.keys(client.integration.wellknown)).toEqual(["add"])
- expect(Object.keys(client.integration.connect)).toEqual(["key"])
- expect(Object.keys(client.integration.oauth)).toEqual(["connect", "status", "complete", "cancel"])
- expect(Object.keys(client.integration.command)).toEqual(["connect", "status", "cancel"])
- expect(Object.keys(client.websearch)).toEqual(["providers", "query"])
- expect(Object.keys(client.file)).toEqual(["read", "list", "find"])
- expect(Object.keys(client.vcs)).toEqual(["get", "status", "diff"])
- expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"])
- expect(Object.keys(client.shell)).toEqual(["list", "create", "get", "timeout", "output", "remove"])
- expect(Object.keys(client.project)).toEqual(["list", "current", "directories"])
- })
- test("websearch.query uses the public HTTP contract", async () => {
- let request: Request | undefined
- const client = OpenCode.make({
- baseUrl: "http://localhost:3000",
- fetch: async (input, init) => {
- request = input instanceof Request ? input : new Request(input, init)
- return Response.json({
- location: { directory: "/tmp/project", project: { id: "proj_test", directory: "/tmp/project" } },
- data: {
- providerID: "exa",
- results: [{ url: "https://example.com", title: "Result", content: "result", time: {} }],
- },
- })
- },
- })
- const result = await client.websearch.query({
- query: "opencode",
- providerID: "exa",
- location: { directory: "/tmp/project" },
- })
- expect(result.data).toEqual({
- providerID: "exa",
- results: [{ url: "https://example.com", title: "Result", content: "result", time: {} }],
- })
- expect(request?.method).toBe("POST")
- expect(request?.url).toBe("http://localhost:3000/api/websearch?location%5Bdirectory%5D=%2Ftmp%2Fproject")
- expect(await request?.json()).toEqual({ query: "opencode", providerID: "exa" })
- })
- test("server.get uses the public HTTP contract", async () => {
- let request: Request | undefined
- const client = OpenCode.make({
- baseUrl: "http://localhost:3000",
- fetch: async (input) => {
- request = input instanceof Request ? input : new Request(input)
- return Response.json({ urls: ["http://192.168.1.10:4096"] })
- },
- })
- expect(await client.server.get()).toEqual({ urls: ["http://192.168.1.10:4096"] })
- expect(request?.method).toBe("GET")
- expect(request?.url).toBe("http://localhost:3000/api/server")
- })
- test("experimental wellknown integration add uses the public HTTP contract", async () => {
- let request: Request | undefined
- const client = OpenCode.make({
- baseUrl: "http://localhost:3000",
- fetch: async (input, init) => {
- request = input instanceof Request ? input : new Request(input, init)
- return new Response(null, { status: 204 })
- },
- })
- await client.integration.wellknown.add({
- url: "https://example.com",
- location: { directory: "/tmp/project" },
- })
- expect(request?.method).toBe("POST")
- expect(request?.url).toBe(
- "http://localhost:3000/api/experimental/integration/wellknown?location%5Bdirectory%5D=%2Ftmp%2Fproject",
- )
- expect(await request?.json()).toEqual({ url: "https://example.com" })
- })
- test("health.stop sends exact replacement identity", async () => {
- let request: Request | undefined
- const client = OpenCode.make({
- baseUrl: "http://localhost:3000",
- fetch: async (input, init) => {
- request = input instanceof Request ? input : new Request(input, init)
- return Response.json({ accepted: true })
- },
- })
- expect(await client.health.stop({ instanceID: "instance" })).toEqual({ accepted: true })
- expect(request?.method).toBe("POST")
- expect(request?.url).toBe("http://localhost:3000/api/service/stop")
- expect(await request?.json()).toEqual({ instanceID: "instance" })
- })
- test("MCP resource catalog uses the public HTTP contract", async () => {
- let request: Request | undefined
- const client = OpenCode.make({
- baseUrl: "http://localhost:3000",
- fetch: async (input) => {
- request = input instanceof Request ? input : new Request(input)
- return Response.json({
- location: { directory: "/tmp/project", project: { id: "proj_test", directory: "/tmp/project" } },
- data: {
- resources: [{ server: "docs", name: "Readme", uri: "docs://readme" }],
- templates: [{ server: "docs", name: "File", uriTemplate: "docs://{path}" }],
- },
- })
- },
- })
- const result = await client.mcp.resource.catalog({ location: { directory: "/tmp/project" } })
- expect(result.data.resources[0]?.uri).toBe("docs://readme")
- expect(request?.method).toBe("GET")
- expect(request?.url).toBe("http://localhost:3000/api/mcp/resource?location%5Bdirectory%5D=%2Ftmp%2Fproject")
- })
- test("file.read returns binary content from the public HTTP contract", async () => {
- let request: Request | undefined
- const client = OpenCode.make({
- baseUrl: "http://localhost:3000",
- fetch: async (input) => {
- request = input instanceof Request ? input : new Request(input)
- return new Response(new Uint8Array([104, 105]))
- },
- })
- const content = await client.file.read({
- path: "src/a b#c.ts",
- location: { directory: "/tmp/project" },
- })
- expect(Array.from(content)).toEqual([104, 105])
- expect(request?.url).toBe(
- "http://localhost:3000/api/fs/read/src/a%20b%23c.ts?location%5Bdirectory%5D=%2Ftmp%2Fproject",
- )
- })
- test("project methods use the public HTTP contract", async () => {
- const requests: string[] = []
- const client = OpenCode.make({
- baseUrl: "http://localhost:3000",
- fetch: async (input) => {
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url
- requests.push(url)
- if (url.includes("/directories")) return Response.json([])
- return Response.json({ id: "proj_test", directory: "/tmp/project" })
- },
- })
- const current = await client.project.current({ location: { workspace: "wrk_test" } })
- const directories = await client.project.directories({
- projectID: current.id,
- location: { directory: current.directory },
- })
- expect(current).toEqual({ id: "proj_test", directory: "/tmp/project" })
- expect(directories).toEqual([])
- expect(requests).toEqual([
- "http://localhost:3000/api/project/current?location%5Bworkspace%5D=wrk_test",
- "http://localhost:3000/api/project/proj_test/directories?location%5Bdirectory%5D=%2Ftmp%2Fproject",
- ])
- })
- test("shell list and remove use the public HTTP contract", async () => {
- const requests: Array<{ method: string; url: string }> = []
- const shell = {
- id: "sh_test",
- status: "running",
- command: "pwd",
- cwd: "/tmp/project",
- shell: "/bin/zsh",
- file: "/tmp/opencode-shell",
- metadata: { sessionID: "ses_test" },
- time: { started: 1_717_171_717_000 },
- }
- const client = OpenCode.make({
- baseUrl: "http://localhost:3000",
- fetch: async (input, init) => {
- const request = input instanceof Request ? input : new Request(input, init)
- requests.push({ method: request.method, url: request.url })
- if (request.method === "DELETE") return new Response(null, { status: 204 })
- return Response.json({
- location: { directory: "/tmp/project", project: { id: "proj_test", directory: "/tmp/project" } },
- data: [shell],
- })
- },
- })
- const result = await client.shell.list({ location: { directory: "/tmp/project" } })
- await client.shell.remove({ id: shell.id })
- expect(result.data).toEqual([shell])
- expect(requests).toEqual([
- { method: "GET", url: "http://localhost:3000/api/shell?location%5Bdirectory%5D=%2Ftmp%2Fproject" },
- { method: "DELETE", url: "http://localhost:3000/api/shell/sh_test" },
- ])
- })
- test("session.get returns the wire projection", async () => {
- const client = OpenCode.make({
- baseUrl: "http://localhost:3000",
- fetch: async (input) => {
- expect(typeof input === "string" ? input : input instanceof URL ? input.href : input.url).toBe(
- "http://localhost:3000/api/session/ses_test",
- )
- return Response.json(session)
- },
- })
- const result = await client.session.get({ sessionID: "ses_test" })
- expect(result.time.created).toBe(1_717_171_717_000)
- })
- test("session instructions methods use the public HTTP contract", async () => {
- const requests: Array<{ method: string; url: string; body?: unknown }> = []
- const instructions = [{ key: "review-notes", value: { text: "Check the diff", priority: 1 } }]
- const client = OpenCode.make({
- baseUrl: "http://localhost:3000",
- fetch: async (input, init) => {
- const request = input instanceof Request ? input : new Request(input, init)
- requests.push({
- method: request.method,
- url: request.url,
- body: request.method === "PUT" ? await request.json() : undefined,
- })
- if (request.method === "GET") return Response.json({ data: instructions })
- return new Response(null, { status: 204 })
- },
- })
- const result = await client.session.instructions.entry.list({ sessionID: "ses_test" })
- await client.session.instructions.entry.put({
- sessionID: "ses_test",
- key: "review-notes",
- value: instructions[0].value,
- })
- await client.session.instructions.entry.remove({ sessionID: "ses_test", key: "review-notes" })
- expect(result).toEqual(instructions)
- expect(requests).toEqual([
- {
- method: "GET",
- url: "http://localhost:3000/api/session/ses_test/instructions/entries",
- body: undefined,
- },
- {
- method: "PUT",
- url: "http://localhost:3000/api/session/ses_test/instructions/entries/review-notes",
- body: { value: { text: "Check the diff", priority: 1 } },
- },
- {
- method: "DELETE",
- url: "http://localhost:3000/api/session/ses_test/instructions/entries/review-notes",
- body: undefined,
- },
- ])
- })
- test("session.pending.list uses the public HTTP contract", async () => {
- const requests: Array<{ method: string; url: string }> = []
- const pending = [
- {
- id: "msg_pending",
- sessionID: "ses_test",
- timeCreated: 1_717_171_717_000,
- type: "user",
- data: { text: "Fix the failing tests" },
- delivery: "steer",
- },
- ]
- const client = OpenCode.make({
- baseUrl: "http://localhost:3000",
- fetch: async (input, init) => {
- const request = input instanceof Request ? input : new Request(input, init)
- requests.push({ method: request.method, url: request.url })
- return Response.json({ data: pending })
- },
- })
- const result = await client.session.pending.list({ sessionID: "ses_test" })
- expect(result).toEqual(pending)
- expect(requests).toEqual([{ method: "GET", url: "http://localhost:3000/api/session/ses_test/pending" }])
- })
- test("event.subscribe exposes the Promise event stream wire projection", async () => {
- const client = OpenCode.make({
- baseUrl: "http://localhost:3000",
- fetch: async () =>
- new Response(
- `: heartbeat\n\ndata: ${JSON.stringify({ id: "evt_connected", created: 0, type: "server.connected", data: {} })}\n\n` +
- `data: ${JSON.stringify(modelSwitchedEvent)}\n\n`,
- { headers: { "content-type": "text/event-stream" } },
- ),
- })
- const events = []
- for await (const event of client.event.subscribe()) events.push(event)
- expect(events).toEqual([{ id: "evt_connected", created: 0, type: "server.connected", data: {} }, modelSwitchedEvent])
- expect(events[1]?.type === "session.model.selected" && events[1].created).toBe(1_717_171_717_000)
- })
- test("event.subscribe terminates on malformed Promise SSE data", async () => {
- const client = OpenCode.make({
- baseUrl: "http://localhost:3000",
- fetch: async () => new Response("data: {not-json}\n\n", { headers: { "content-type": "text/event-stream" } }),
- })
- await expect(client.event.subscribe()[Symbol.asyncIterator]().next()).rejects.toMatchObject({
- name: "ClientError",
- reason: "MalformedResponse",
- })
- })
- test("event.subscribe accepts a fragmented SSE event below the size limit", async () => {
- const event = { id: "evt_large", type: "test.large", data: { output: "x".repeat(12 * 1024 * 1024) } }
- const encoded = new TextEncoder().encode(`data: ${JSON.stringify(event)}\n\n`)
- const client = OpenCode.make({
- baseUrl: "http://localhost:3000",
- fetch: async () =>
- new Response(
- new ReadableStream({
- start(controller) {
- for (let offset = 0; offset < encoded.length; offset += 64 * 1024) {
- controller.enqueue(encoded.slice(offset, offset + 64 * 1024))
- }
- controller.close()
- },
- }),
- { headers: { "content-type": "text/event-stream" } },
- ),
- })
- await expect(client.event.subscribe()[Symbol.asyncIterator]().next()).resolves.toEqual({ done: false, value: event })
- })
- test("event.subscribe rejects an SSE event above the size limit", async () => {
- const client = OpenCode.make({
- baseUrl: "http://localhost:3000",
- fetch: async () =>
- new Response(`data: ${JSON.stringify({ output: "x".repeat(16 * 1024 * 1024) })}`, {
- headers: { "content-type": "text/event-stream" },
- }),
- })
- await expect(client.event.subscribe()[Symbol.asyncIterator]().next()).rejects.toMatchObject({
- name: "ClientError",
- reason: "SseEventTooLarge",
- })
- })
- test("session methods use the public HTTP contract", async () => {
- const requests: Array<{ url: string; init?: RequestInit }> = []
- const client = OpenCode.make({
- baseUrl: "http://localhost:3000",
- fetch: async (input, init) => {
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url
- requests.push({ url, init })
- if (url.includes("/event")) {
- return new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\n`, {
- headers: { "content-type": "text/event-stream" },
- })
- }
- if (url.includes("/log")) {
- return new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\ndata: ${JSON.stringify(synced)}\n\n`, {
- headers: { "content-type": "text/event-stream" },
- })
- }
- if (url.includes("/prompt")) return Response.json(admission)
- if (url.includes("/generate")) return Response.json({ data: { text: "A transient answer" } })
- if (url.includes("/synthetic")) return Response.json(syntheticAdmission)
- if (url.endsWith("/compact")) return Response.json(compactionAdmission)
- if (url.includes("/context")) return Response.json({ data: [] })
- if (url.includes("/message/")) return Response.json({ data: modelSwitchedMessage })
- if (url.endsWith("/api/session/active")) return Response.json({ data: { ses_test: { type: "running" } } })
- if (init?.method === "POST" && url.endsWith("/api/session")) return Response.json(session)
- if (init?.method === "POST") return new Response(null, { status: 204 })
- return Response.json({ data: [session.data], cursor: { next: "next" } })
- },
- })
- const page = await client.session.list({ limit: 10, order: "desc", parentID: null })
- const active = await client.session.active()
- const created = await client.session.create({ location: { directory: "/tmp/project" } })
- await client.session.switchAgent({ sessionID: "ses_test", agent: "build" })
- await client.session.switchModel({
- sessionID: "ses_test",
- model: { id: "claude", providerID: "anthropic" },
- })
- const admitted = await client.session.prompt({
- sessionID: "ses_test",
- text: "Hello",
- resume: false,
- })
- const generated = await client.session.generate({ sessionID: "ses_test", prompt: "Summarize this session" })
- const synthetic = await client.session.synthetic({
- sessionID: "ses_test",
- text: "Completed",
- delivery: "queue",
- resume: false,
- })
- await client.session.compact({ sessionID: "ses_test" })
- await client.session.wait({ sessionID: "ses_test" })
- const context = await client.session.context({ sessionID: "ses_test" })
- const log = []
- for await (const item of client.session.log({ sessionID: "ses_test", after: 0 })) log.push(item)
- await client.session.interrupt({ sessionID: "ses_test" })
- const message = await client.session.message({ sessionID: "ses_test", messageID: "msg_model" })
- expect(page.cursor.next).toBe("next")
- expect(active).toEqual({ ses_test: { type: "running" } })
- expect(created.id).toBe("ses_test")
- expect(admitted.id).toBe("msg_test")
- expect(generated.text).toBe("A transient answer")
- expect(synthetic).toMatchObject({ type: "synthetic", data: { text: "Completed" }, delivery: "queue" })
- expect(context).toEqual([])
- expect(log).toEqual([modelSwitchedEvent, synced])
- expect(message).toEqual(modelSwitchedMessage)
- expect(requests.map((request) => [request.init?.method, request.url])).toEqual([
- ["GET", "http://localhost:3000/api/session?limit=10&order=desc&parentID=null"],
- ["GET", "http://localhost:3000/api/session/active"],
- ["POST", "http://localhost:3000/api/session"],
- ["POST", "http://localhost:3000/api/session/ses_test/agent"],
- ["POST", "http://localhost:3000/api/session/ses_test/model"],
- ["POST", "http://localhost:3000/api/session/ses_test/prompt"],
- ["POST", "http://localhost:3000/api/session/ses_test/generate"],
- ["POST", "http://localhost:3000/api/session/ses_test/synthetic"],
- ["POST", "http://localhost:3000/api/session/ses_test/compact"],
- ["POST", "http://localhost:3000/api/session/ses_test/wait"],
- ["GET", "http://localhost:3000/api/session/ses_test/context"],
- ["GET", "http://localhost:3000/api/experimental/session/ses_test/log?after=0"],
- ["POST", "http://localhost:3000/api/session/ses_test/interrupt"],
- ["GET", "http://localhost:3000/api/session/ses_test/message/msg_model"],
- ])
- const body = requests.find((request) => request.url.endsWith("/api/session/ses_test/prompt"))?.init?.body
- if (typeof body !== "string") throw new Error("Expected JSON request body")
- expect(JSON.parse(body)).toEqual({
- text: "Hello",
- resume: false,
- })
- const syntheticBody = requests.find((request) => request.url.endsWith("/synthetic"))?.init?.body
- if (typeof syntheticBody !== "string") throw new Error("Expected JSON synthetic request body")
- expect(JSON.parse(syntheticBody)).toEqual({
- text: "Completed",
- delivery: "queue",
- resume: false,
- })
- })
- test("middleware errors remain declared client errors", async () => {
- const client = OpenCode.make({
- baseUrl: "http://localhost:3000",
- fetch: async () =>
- Response.json({ _tag: "UnauthorizedError", message: "Authentication required" }, { status: 401 }),
- })
- try {
- await client.session.create({})
- throw new Error("Expected request to fail")
- } catch (error) {
- expect(isUnauthorizedError(error)).toBe(true)
- }
- })
- test("session.log decodes SessionNotFoundError", async () => {
- const client = OpenCode.make({
- baseUrl: "http://localhost:3000",
- fetch: async () =>
- Response.json(
- { _tag: "SessionNotFoundError", sessionID: "ses_missing", message: "Session not found" },
- { status: 404 },
- ),
- })
- try {
- await client.session.log({ sessionID: "ses_missing" })[Symbol.asyncIterator]().next()
- throw new Error("Expected request to fail")
- } catch (error) {
- expect(isSessionNotFoundError(error)).toBe(true)
- }
- })
- const session = {
- data: {
- id: "ses_test",
- projectID: "project",
- cost: 0,
- tokens: {
- input: 1,
- output: 2,
- reasoning: 3,
- cache: { read: 4, write: 5 },
- },
- time: {
- created: 1_717_171_717_000,
- updated: 1_717_171_717_000,
- },
- title: "Test",
- location: { directory: "/tmp/project" },
- },
- }
- const admission = {
- data: {
- id: "msg_test",
- sessionID: "ses_test",
- type: "user",
- data: { text: "Hello" },
- delivery: "steer",
- timeCreated: 1_717_171_717_000,
- },
- }
- const syntheticAdmission = {
- data: {
- id: "msg_synthetic",
- sessionID: "ses_test",
- type: "synthetic",
- data: { text: "Completed" },
- delivery: "queue",
- timeCreated: 1_717_171_717_000,
- },
- }
- const compactionAdmission = {
- data: {
- type: "compaction",
- id: "msg_compaction",
- sessionID: "ses_test",
- timeCreated: 1_717_171_717_000,
- },
- }
- const modelSwitchedMessage = {
- id: "msg_model",
- type: "model-switched",
- time: { created: 1_717_171_717_000 },
- model: { id: "claude", providerID: "anthropic" },
- }
- const synced = { type: "log.synced", aggregateID: "ses_test", seq: 1 }
- const modelSwitchedEvent = {
- id: "evt_model",
- created: 1_717_171_717_000,
- type: "session.model.selected",
- durable: { aggregateID: "ses_test", seq: 1, version: 1 },
- data: {
- sessionID: "ses_test",
- model: { id: "claude", providerID: "anthropic" },
- },
- }
|