import type { WebSocketFactory, WebSocketLike } from "../src/app-server-client.js"; import { isRecord } from "../src/domain.js"; export type FakeRequestHandler = ( message: Record, server: FakeAppServer, ) => void; class FakeWebSocket extends EventTarget implements WebSocketLike { readyState = 0; readonly #server: FakeAppServer; constructor(server: FakeAppServer) { super(); this.#server = server; } open(): void { if (this.readyState !== 0) return; this.readyState = 1; this.dispatchEvent(new Event("open")); } send(data: string): void { if (this.readyState !== 1) throw new Error("fake socket is not open"); const parsed: unknown = JSON.parse(data); if (!isRecord(parsed)) throw new Error("expected request object"); this.#server.receive(parsed); } deliver(value: unknown): void { if (this.readyState !== 1) return; const data = typeof value === "string" ? value : JSON.stringify(value); this.dispatchEvent(new MessageEvent("message", { data })); } close(): void { if (this.readyState === 3) return; this.readyState = 3; this.dispatchEvent(new Event("close")); } } export class FakeAppServer { readonly frames: Record[] = []; readonly authorizationHeaders: Array = []; readonly #handler: FakeRequestHandler; readonly #autoOpen: boolean; socket: FakeWebSocket | null = null; constructor(handler: FakeRequestHandler, autoOpen = true) { this.#handler = handler; this.#autoOpen = autoOpen; } readonly factory: WebSocketFactory = (_url, authorizationHeader) => { this.authorizationHeaders.push(authorizationHeader); const socket = new FakeWebSocket(this); this.socket = socket; if (this.#autoOpen) queueMicrotask(() => socket.open()); return socket; }; receive(message: Record): void { this.frames.push(message); this.#handler(message, this); } respond(request: Record, result: unknown): void { this.socket?.deliver({ id: request.id, result }); } fail(request: Record, code: number, message: string): void { this.socket?.deliver({ id: request.id, error: { code, message } }); } notify(method: string, params: unknown): void { this.socket?.deliver({ method, params }); } deliverRaw(value: string): void { this.socket?.deliver(value); } disconnect(): void { this.socket?.close(); } } export function successfulHandshake( handler: FakeRequestHandler = (request, server) => server.respond(request, {}), ): FakeRequestHandler { return (request, server) => { if (request.method === "initialize") { server.respond(request, { userAgent: "fake", platformFamily: "unix", platformOs: "macos" }); return; } if (request.method === "initialized") return; handler(request, server); }; } export function rawThread( id: string, overrides: Record = {}, ): Record { return { id, sessionId: id, parentThreadId: null, forkedFromId: null, preview: `preview-${id}`, ephemeral: false, modelProvider: "openai", createdAt: 10, updatedAt: 20, recencyAt: 20, cwd: "/work/project", source: "cli", status: { type: "notLoaded" }, name: null, agentNickname: null, agentRole: null, turns: [], ...overrides, }; }