cc-libs/tools/mcp-bridge/test/opencode-proxy.test.ts

164 lines
5.8 KiB
TypeScript

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<HttpResponse> {
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<void> }> {
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<void>((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<void>((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<void> {
return new Promise((resolve, reject) => {
if (server.address()) {
resolve();
return;
}
server.once("listening", () => resolve());
server.once("error", reject);
});
}
function closeWss(proxy: WebSocketServer): Promise<void> {
for (const client of proxy.clients) {
client.terminate();
}
return new Promise<void>((resolve) => proxy.close(() => resolve()));
}