import { describe, expect, test } from "bun:test"; import { openExecSession, type ExecWebSocket } from "../../lib/exec-api"; class FakeSocket implements ExecWebSocket { binaryType: "arraybuffer" | "blob" = "blob"; readyState = 0; sent: string[] = []; closes: Array<[number | undefined, string | undefined]> = []; private listeners = new Map void>>(); send(data: string) { this.sent.push(data); } close(code?: number, reason?: string) { this.closes.push([code, reason]); this.readyState = 3; } addEventListener(type: string, listener: (event: any) => void) { let values = this.listeners.get(type); if (!values) { values = new Set(); this.listeners.set(type, values); } values.add(listener); } removeEventListener(type: string, listener: (event: any) => void) { this.listeners.get(type)?.delete(listener); } emit(type: string, event: any = {}) { if (type === "open") this.readyState = 1; for (const listener of [...(this.listeners.get(type) ?? [])]) listener(event); } } const session = { token: "secret-token", expiresAt: "2099-01-01T00:00:00.000Z", user: { username: "user", roles: [] }, }; describe("exec WebSocket API", () => { test("authenticates the upgrade and maps protocol byte frames", async () => { const socket = new FakeSocket(); let connection: | { url: string; headers: Readonly> } | undefined; const opening = openExecSession( "shop/demo", { deployment: "api", command: ["sh", "-c", "echo ok"], tty: true, columns: 120, rows: 40, }, new AbortController().signal, { baseUrl: "https://api.test/api/v2", session, socketFactory(url, headers) { connection = { url, headers }; return socket; }, }, ); socket.emit("open"); const client = await opening; expect(connection).toEqual({ url: "wss://api.test/api/v2/workspaces/shop%2Fdemo/exec", headers: { authorization: "Bearer secret-token", }, }); expect(JSON.parse(socket.sent[0]!)).toEqual({ type: "start", version: 1, deployment: "api", command: ["sh", "-c", "echo ok"], tty: true, columns: 120, rows: 40, }); client.sendStdin(new TextEncoder().encode("hello"), true); client.resize(80, 24); const iterator = client[Symbol.asyncIterator](); socket.emit("message", { data: JSON.stringify({ type: "stdout", data: "b3V0", encoding: "base64", }), }); expect(await iterator.next()).toEqual({ done: false, value: { type: "stdout", data: new TextEncoder().encode("out") }, }); expect(JSON.parse(socket.sent[1]!)).toEqual({ type: "stdin", data: "aGVsbG8=", encoding: "base64", eof: true, }); expect(JSON.parse(socket.sent[2]!)).toEqual({ type: "resize", columns: 80, rows: 24, }); socket.emit("message", { data: JSON.stringify({ type: "exit", exitCode: 7 }), }); expect((await iterator.next()).value).toEqual({ type: "exit", exitCode: 7, }); socket.emit("close"); expect((await iterator.next()).done).toBe(true); }); test("closes an active connection when cancelled", async () => { const socket = new FakeSocket(); const controller = new AbortController(); const opening = openExecSession( "shop", { deployment: "api", command: ["sh"], tty: false }, controller.signal, { session, socketFactory: () => socket }, ); socket.emit("open"); const client = await opening; const next = client[Symbol.asyncIterator]().next(); controller.abort(); expect((await next).done).toBe(true); expect(socket.closes).toContainEqual([1000, "aborted"]); }); test("rejects malformed server frames and closes with protocol error", async () => { const socket = new FakeSocket(); const opening = openExecSession( "shop", { deployment: "api", command: ["sh"], tty: false }, new AbortController().signal, { session, socketFactory: () => socket }, ); socket.emit("open"); const client = await opening; const next = client[Symbol.asyncIterator]().next(); socket.emit("message", { data: JSON.stringify({ type: "stdout", data: 1 }), }); await expect(next).rejects.toThrow("Invalid exec output frame"); expect(socket.closes).toContainEqual([1002, "invalid frame"]); }); });