import assert from "node:assert/strict"; import test from "node:test"; import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; import type { AddressInfo } from "node:net"; import { WebSocket, type RawData, type WebSocketServer } from "ws"; import { parseHttpRequestFrame, parseJsonFrame } from "../src/protocol.js"; import { startOpencodeProxy } from "../src/opencode-proxy.js"; test("parseHttpRequestFrame accepts a valid frame", () => { const frame = parseJsonFrame( JSON.stringify({ type: "http", id: "req_1", method: "POST", path: "/session/x/message", body: "{}" }), ); assert.deepEqual(parseHttpRequestFrame(frame), { type: "http", id: "req_1", method: "POST", path: "/session/x/message", body: "{}", }); }); test("parseHttpRequestFrame omits absent body", () => { const frame = parseHttpRequestFrame({ type: "http", id: "req_1", method: "GET", path: "/session" }); assert.deepEqual(frame, { type: "http", id: "req_1", method: "GET", path: "/session", body: undefined }); }); test("parseHttpRequestFrame rejects invalid frames", () => { assert.equal(parseHttpRequestFrame({ type: "http", id: "", method: "GET", path: "/x" }), null); assert.equal(parseHttpRequestFrame({ type: "http", id: "a", method: "DELETE", path: "/x" }), null); assert.equal(parseHttpRequestFrame({ type: "http", id: "a", method: "GET", path: "no-slash" }), null); assert.equal(parseHttpRequestFrame({ type: "http", id: "a", method: "GET", path: "/x", body: 5 }), null); assert.equal(parseHttpRequestFrame({ type: "other", id: "a", method: "GET", path: "/x" }), null); }); test("proxy forwards a request to opencode and returns the raw response", async () => { const seen: { method?: string; url?: string; auth?: string; body?: string } = {}; const opencode = await startFakeOpencode((req, res, body) => { seen.method = req.method; seen.url = req.url; seen.auth = req.headers.authorization; seen.body = body; res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ info: { finish: "stop" }, parts: [{ type: "text", text: "pong" }] })); }); const proxy = startOpencodeProxy({ host: "127.0.0.1", port: 0, opencodeUrl: opencode.url, username: "opencode", password: "secret", }); await waitForListening(proxy); try { const response = await roundtrip(proxyUrl(proxy), { type: "http", id: "req_42", method: "POST", path: "/session/ses_1/message", body: JSON.stringify({ parts: [{ type: "text", text: "ping" }] }), }); assert.equal(response.type, "http-response"); assert.equal(response.id, "req_42"); assert.equal(response.status, 200); const parsed = JSON.parse(response.body as string) as { parts: { text: string }[] }; assert.equal(parsed.parts[0].text, "pong"); assert.equal(seen.method, "POST"); assert.equal(seen.url, "/session/ses_1/message"); assert.equal(seen.auth, "Basic " + Buffer.from("opencode:secret").toString("base64")); assert.equal(seen.body, JSON.stringify({ parts: [{ type: "text", text: "ping" }] })); } finally { await closeWss(proxy); await opencode.close(); } }); test("proxy maps a fetch failure to status 0", async () => { // Point at a port with nothing listening so fetch rejects. const proxy = startOpencodeProxy({ host: "127.0.0.1", port: 0, opencodeUrl: "http://127.0.0.1:1" }); await waitForListening(proxy); try { const response = await roundtrip(proxyUrl(proxy), { type: "http", id: "req_err", method: "GET", path: "/session", }); assert.equal(response.id, "req_err"); assert.equal(response.status, 0); assert.equal(typeof response.error, "string"); } finally { await closeWss(proxy); } }); type HttpResponse = { type: string; id: string; status: number; body?: string; error?: string }; function rawToString(data: RawData): string { if (Array.isArray(data)) { return Buffer.concat(data).toString("utf8"); } if (Buffer.isBuffer(data)) { return data.toString("utf8"); } return Buffer.from(data).toString("utf8"); } function roundtrip(url: string, frame: unknown): Promise { return new Promise((resolve, reject) => { const ws = new WebSocket(url); ws.on("open", () => ws.send(JSON.stringify(frame))); ws.on("message", (data: RawData) => { ws.close(); resolve(JSON.parse(rawToString(data)) as HttpResponse); }); ws.on("error", reject); }); } async function startFakeOpencode( handler: (req: IncomingMessage, res: ServerResponse, body: string) => void, ): Promise<{ url: string; close: () => Promise }> { const server = createServer((req, res) => { const chunks: Buffer[] = []; req.on("data", (c: Buffer) => chunks.push(c)); req.on("end", () => handler(req, res, Buffer.concat(chunks).toString("utf8"))); }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const port = (server.address() as AddressInfo).port; return { url: `http://127.0.0.1:${port}`, close: () => new Promise((resolve) => server.close(() => resolve())), }; } function proxyUrl(proxy: WebSocketServer): string { const address = proxy.address(); if (!address || typeof address === "string") { throw new Error("proxy has no TCP address"); } return `ws://127.0.0.1:${address.port}`; } function waitForListening(server: WebSocketServer | Server): Promise { return new Promise((resolve, reject) => { if (server.address()) { resolve(); return; } server.once("listening", () => resolve()); server.once("error", reject); }); } function closeWss(proxy: WebSocketServer): Promise { for (const client of proxy.clients) { client.terminate(); } return new Promise((resolve) => proxy.close(() => resolve())); }