| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255 |
- import { expect, test } from "bun:test"
- import { isSessionNotFoundError, isUnauthorizedError, OpenCode } from "../src"
- test("exposes every standard HTTP API group", () => {
- const client = OpenCode.make({ baseUrl: "http://localhost:3000" })
- expect(Object.keys(client)).toEqual([
- "health",
- "location",
- "agents",
- "sessions",
- "messages",
- "models",
- "providers",
- "integrations",
- "credentials",
- "permissions",
- "files",
- "commands",
- "skills",
- "events",
- "ptys",
- "questions",
- "references",
- "projectCopies",
- ])
- expect(Object.keys(client.messages)).toEqual(["list"])
- expect(Object.keys(client.integrations)).toEqual([
- "list",
- "get",
- "connectKey",
- "connectOauth",
- "attemptStatus",
- "attemptComplete",
- "attemptCancel",
- ])
- expect(Object.keys(client.files)).toEqual(["list", "find"])
- expect(Object.keys(client.ptys)).toEqual(["list", "create", "get", "update", "remove"])
- })
- test("sessions.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.sessions.get({ sessionID: "ses_test" })
- expect(result.time.created).toBe(1_717_171_717_000)
- })
- test("events.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", 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.events.subscribe()) events.push(event)
- expect(events).toEqual([{ id: "evt_connected", type: "server.connected", data: {} }, modelSwitchedEvent])
- expect(events[1]?.type === "session.next.model.switched" && events[1].data.timestamp).toBe(1_717_171_717_000)
- })
- test("events.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.events.subscribe()[Symbol.asyncIterator]().next()).rejects.toMatchObject({
- name: "ClientError",
- reason: "MalformedResponse",
- })
- })
- test("session methods use the public HTTP contract", async () => {
- const requests: Array<{ url: string; init?: RequestInit }> = []
- let historyPage = 0
- 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("/history")) {
- historyPage++
- return Response.json(
- historyPage === 1 ? { data: [modelSwitchedEvent], hasMore: true } : { data: [], hasMore: false },
- )
- }
- if (url.includes("/prompt")) return Response.json(admission)
- 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.sessions.list({ limit: 10, order: "desc" })
- const active = await client.sessions.active()
- const created = await client.sessions.create({ location: { directory: "/tmp/project" } })
- await client.sessions.switchAgent({ sessionID: "ses_test", agent: "build" })
- await client.sessions.switchModel({
- sessionID: "ses_test",
- model: { id: "claude", providerID: "anthropic" },
- })
- const admitted = await client.sessions.prompt({
- sessionID: "ses_test",
- prompt: { text: "Hello" },
- resume: false,
- })
- await client.sessions.compact({ sessionID: "ses_test" })
- await client.sessions.wait({ sessionID: "ses_test" })
- const context = await client.sessions.context({ sessionID: "ses_test" })
- const history = await client.sessions.history({ sessionID: "ses_test", after: 0, limit: 1 })
- const historyAfter = history.data.at(-1)?.durable?.seq
- const historyNext = history.hasMore
- ? await client.sessions.history({ sessionID: "ses_test", after: historyAfter, limit: 2 })
- : undefined
- const events = []
- for await (const event of client.sessions.events({ sessionID: "ses_test", after: 0 })) events.push(event)
- await client.sessions.interrupt({ sessionID: "ses_test" })
- const message = await client.sessions.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(context).toEqual([])
- expect(history).toEqual({ data: [modelSwitchedEvent], hasMore: true })
- expect(historyNext).toEqual({ data: [], hasMore: false })
- expect(events).toEqual([modelSwitchedEvent])
- expect(message).toEqual(modelSwitchedMessage)
- expect(requests.map((request) => [request.init?.method, request.url])).toEqual([
- ["GET", "http://localhost:3000/api/session?limit=10&order=desc"],
- ["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/compact"],
- ["POST", "http://localhost:3000/api/session/ses_test/wait"],
- ["GET", "http://localhost:3000/api/session/ses_test/context"],
- ["GET", "http://localhost:3000/api/session/ses_test/history?limit=1&after=0"],
- ["GET", "http://localhost:3000/api/session/ses_test/history?limit=2&after=1"],
- ["GET", "http://localhost:3000/api/session/ses_test/event?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({
- prompt: { text: "Hello" },
- 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.sessions.create({})
- throw new Error("Expected request to fail")
- } catch (error) {
- expect(isUnauthorizedError(error)).toBe(true)
- }
- })
- test("sessions.history 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.sessions.history({ sessionID: "ses_missing" })
- 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: {
- admittedSeq: 0,
- id: "msg_test",
- sessionID: "ses_test",
- prompt: { text: "Hello" },
- delivery: "steer",
- 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 modelSwitchedEvent = {
- id: "evt_model",
- type: "session.next.model.switched",
- durable: { aggregateID: "ses_test", seq: 1, version: 1 },
- data: {
- timestamp: 1_717_171_717_000,
- sessionID: "ses_test",
- messageID: "msg_model",
- model: { id: "claude", providerID: "anthropic" },
- },
- }
|