mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-11 11:28:27 +00:00
255 lines
9 KiB
TypeScript
255 lines
9 KiB
TypeScript
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" },
|
|
},
|
|
}
|