import { describe, test, expect, mock } from "bun:test"; import { __testing, decodeClientWsMessage, MAX_CLIENT_WS_PAYLOAD_BYTES, resolveNewSessionPermissionMode, type ServerConfig, } from "../server.js"; import { authTokensEqual, decodeWebSocketAuthProtocol, encodeWebSocketAuthProtocol, extractWebSocketAuthToken, } from "../ws-auth.js"; import { buildRcsWsUrl } from "../rcs-upstream.js"; function makeTestWs(sent: unknown[]) { type TestWs = Parameters[0]; return { readyState: 1, send: mock((message: string) => { sent.push(JSON.parse(message)); }), close: mock(() => {}), raw: null, isInner: false, url: "", origin: "", protocol: "", } as unknown as TestWs; } describe("Server HTTP endpoints", () => { test("package.json has correct bin and main entries", async () => { const pkg = await import("../../package.json", { with: { type: "json" } }); expect(pkg.default.name).toBe("acp-link"); expect(pkg.default.main).toBe("./dist/server.js"); expect(pkg.default.bin).toBeDefined(); expect(pkg.default.bin["acp-link"]).toBe("dist/cli/bin.js"); }); test("ServerConfig interface accepts all expected fields", () => { const config: ServerConfig = { port: 9315, host: "localhost", command: "echo", args: [], cwd: "/tmp", debug: false, token: "test-token", https: false, }; expect(config.port).toBe(9315); expect(config.token).toBe("test-token"); }); test("ServerConfig allows optional fields to be omitted", () => { const config: ServerConfig = { port: 9315, host: "localhost", command: "echo", args: [], cwd: "/tmp", }; expect(config.debug).toBeUndefined(); expect(config.token).toBeUndefined(); expect(config.https).toBeUndefined(); }); }); describe("WebSocket message types", () => { const clientMessageTypes = [ "connect", "disconnect", "new_session", "prompt", "permission_response", "cancel", "set_session_model", "list_sessions", "load_session", "resume_session", "ping", ]; test("all client message types are recognized", () => { expect(clientMessageTypes.length).toBe(11); expect(clientMessageTypes).toContain("ping"); expect(clientMessageTypes).toContain("connect"); expect(clientMessageTypes).toContain("cancel"); }); test("decodes supported client message payloads", () => { expect(decodeClientWsMessage('{"type":"ping"}')).toEqual({ type: "ping" }); expect( decodeClientWsMessage(Buffer.from('{"type":"prompt","payload":{"content":[]}}')), ).toEqual({ type: "prompt", payload: { content: [] } }); expect( decodeClientWsMessage(new TextEncoder().encode('{"type":"cancel"}').buffer), ).toEqual({ type: "cancel" }); expect( decodeClientWsMessage([ Buffer.from('{"type":"list_sessions","payload":{"cursor":"'), Buffer.from('next"}}'), ]), ).toEqual({ type: "list_sessions", payload: { cwd: undefined, cursor: "next" } }); }); test("rejects malformed typed client payloads", () => { expect(() => decodeClientWsMessage('{"type":"prompt"}')).toThrow( "Invalid prompt payload", ); expect(() => decodeClientWsMessage('{"type":"load_session","payload":{}}'), ).toThrow("Invalid load_session payload"); expect(() => decodeClientWsMessage('{"type":"unknown"}')).toThrow( "Unknown message type", ); expect(() => decodeClientWsMessage( '{"type":"new_session","payload":{"permissionMode":123}}', ), ).toThrow("Invalid new_session.permissionMode"); expect(() => decodeClientWsMessage( '{"type":"new_session","payload":{"permissionMode":{}}}', ), ).toThrow("Invalid new_session.permissionMode"); expect(() => decodeClientWsMessage( '{"type":"new_session","payload":{"permissionMode":null}}', ), ).toThrow("Invalid new_session.permissionMode"); }); test("rejects oversized client message payloads before decoding", () => { const payload = "x".repeat(MAX_CLIENT_WS_PAYLOAD_BYTES + 1); expect(() => decodeClientWsMessage(payload)).toThrow("WebSocket message too large"); }); }); describe("WebSocket auth protocol", () => { test("round-trips tokens through a WebSocket subprotocol token", () => { const protocol = encodeWebSocketAuthProtocol("secret/token+with=symbols"); expect(protocol).toStartWith("rcs.auth."); expect(protocol).not.toContain("secret/token"); expect(decodeWebSocketAuthProtocol(protocol)).toBe("secret/token+with=symbols"); }); test("ignores query-token style inputs", () => { expect(decodeWebSocketAuthProtocol(undefined)).toBeUndefined(); expect(decodeWebSocketAuthProtocol("token=secret")).toBeUndefined(); expect(decodeWebSocketAuthProtocol("other, rcs.auth.")).toBeUndefined(); }); test("prefers Authorization headers and supports protocol auth", () => { expect( extractWebSocketAuthToken({ authorization: "Bearer header-token", protocol: encodeWebSocketAuthProtocol("protocol-token"), }), ).toBe("header-token"); expect( extractWebSocketAuthToken({ protocol: encodeWebSocketAuthProtocol("protocol-token"), }), ).toBe("protocol-token"); }); test("compares auth tokens through the shared constant-time path", () => { expect(authTokensEqual("secret-token", "secret-token")).toBe(true); expect(authTokensEqual("secret-token", "wrong-token")).toBe(false); expect(authTokensEqual(undefined, "secret-token")).toBe(false); }); }); describe("RCS upstream URL normalization", () => { test("removes legacy token query params from WebSocket URLs", () => { expect( buildRcsWsUrl("http://example.test/acp/ws?token=old-secret&x=1"), ).toBe("ws://example.test/acp/ws?x=1"); }); test("adds /acp/ws for base URLs", () => { expect(buildRcsWsUrl("https://example.test/")).toBe( "wss://example.test/acp/ws", ); }); }); describe("permission mode resolution", () => { test("uses client requested non-bypass modes", () => { expect(resolveNewSessionPermissionMode("plan", "acceptEdits")).toBe("plan"); }); test("uses local default when client does not request a mode", () => { expect(resolveNewSessionPermissionMode(undefined, "acceptEdits")).toBe("acceptEdits"); }); test("rejects client requested bypassPermissions without local default", () => { expect(() => resolveNewSessionPermissionMode("bypassPermissions", "acceptEdits"), ).toThrow("bypassPermissions requires local ACP_PERMISSION_MODE"); expect(() => resolveNewSessionPermissionMode("bypass", "acceptEdits"), ).toThrow("bypassPermissions requires local ACP_PERMISSION_MODE"); expect(() => resolveNewSessionPermissionMode("bypasspermissions", "acceptEdits"), ).toThrow("bypassPermissions requires local ACP_PERMISSION_MODE"); expect(() => resolveNewSessionPermissionMode("bypassPermissions", undefined), ).toThrow("bypassPermissions requires local ACP_PERMISSION_MODE"); }); test("rejects unknown client permission modes before forwarding", () => { expect(() => resolveNewSessionPermissionMode("unknown-mode", "acceptEdits"), ).toThrow("Invalid permissionMode: unknown-mode"); }); test("allows bypassPermissions when local default already enables it", () => { expect(resolveNewSessionPermissionMode("bypassPermissions", "bypassPermissions")).toBe("bypassPermissions"); expect(resolveNewSessionPermissionMode("bypass", "bypassPermissions")).toBe("bypassPermissions"); expect(resolveNewSessionPermissionMode("bypassPermissions", "bypass")).toBe("bypassPermissions"); }); test("new_session rejects client bypass before forwarding to the agent", async () => { const sent: unknown[] = []; const ws = makeTestWs(sent); const originalTestInternals = process.env.ACP_LINK_TEST_INTERNALS; process.env.ACP_LINK_TEST_INTERNALS = "1"; let unregisterClient = () => {}; let restoreMode = () => {}; try { const newSession = mock(async () => ({ sessionId: "should-not-be-created", })); unregisterClient = __testing.registerClient(ws, { connection: { newSession }, }); restoreMode = __testing.setDefaultPermissionMode("acceptEdits"); await __testing.dispatchClientMessage(ws, { type: "new_session", payload: { cwd: "/tmp", permissionMode: "bypass", }, }); expect(newSession).not.toHaveBeenCalled(); expect(__testing.getClientSessionId(ws)).toBeNull(); expect(sent).toEqual([ { type: "error", payload: { message: expect.stringContaining( "bypassPermissions requires local ACP_PERMISSION_MODE", ), }, }, ]); } finally { restoreMode(); unregisterClient(); if (originalTestInternals === undefined) { delete process.env.ACP_LINK_TEST_INTERNALS; } else { process.env.ACP_LINK_TEST_INTERNALS = originalTestInternals; } } }); }); describe("Heartbeat constants", () => { test("PERMISSION_TIMEOUT_MS is 5 minutes", () => { const PERMISSION_TIMEOUT_MS = 5 * 60 * 1000; expect(PERMISSION_TIMEOUT_MS).toBe(300_000); }); test("HEARTBEAT_INTERVAL_MS is 30 seconds", () => { const HEARTBEAT_INTERVAL_MS = 30_000; expect(HEARTBEAT_INTERVAL_MS).toBe(30_000); }); });