fake-app-server.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import type { WebSocketFactory, WebSocketLike } from "../src/app-server-client.js";
  2. import { isRecord } from "../src/domain.js";
  3. export type FakeRequestHandler = (
  4. message: Record<string, unknown>,
  5. server: FakeAppServer,
  6. ) => void;
  7. class FakeWebSocket extends EventTarget implements WebSocketLike {
  8. readyState = 0;
  9. readonly #server: FakeAppServer;
  10. constructor(server: FakeAppServer) {
  11. super();
  12. this.#server = server;
  13. }
  14. open(): void {
  15. if (this.readyState !== 0) return;
  16. this.readyState = 1;
  17. this.dispatchEvent(new Event("open"));
  18. }
  19. send(data: string): void {
  20. if (this.readyState !== 1) throw new Error("fake socket is not open");
  21. const parsed: unknown = JSON.parse(data);
  22. if (!isRecord(parsed)) throw new Error("expected request object");
  23. this.#server.receive(parsed);
  24. }
  25. deliver(value: unknown): void {
  26. if (this.readyState !== 1) return;
  27. const data = typeof value === "string" ? value : JSON.stringify(value);
  28. this.dispatchEvent(new MessageEvent("message", { data }));
  29. }
  30. close(): void {
  31. if (this.readyState === 3) return;
  32. this.readyState = 3;
  33. this.dispatchEvent(new Event("close"));
  34. }
  35. }
  36. export class FakeAppServer {
  37. readonly frames: Record<string, unknown>[] = [];
  38. readonly authorizationHeaders: Array<string | null> = [];
  39. readonly #handler: FakeRequestHandler;
  40. readonly #autoOpen: boolean;
  41. socket: FakeWebSocket | null = null;
  42. constructor(handler: FakeRequestHandler, autoOpen = true) {
  43. this.#handler = handler;
  44. this.#autoOpen = autoOpen;
  45. }
  46. readonly factory: WebSocketFactory = (_url, authorizationHeader) => {
  47. this.authorizationHeaders.push(authorizationHeader);
  48. const socket = new FakeWebSocket(this);
  49. this.socket = socket;
  50. if (this.#autoOpen) queueMicrotask(() => socket.open());
  51. return socket;
  52. };
  53. receive(message: Record<string, unknown>): void {
  54. this.frames.push(message);
  55. this.#handler(message, this);
  56. }
  57. respond(request: Record<string, unknown>, result: unknown): void {
  58. this.socket?.deliver({ id: request.id, result });
  59. }
  60. fail(request: Record<string, unknown>, code: number, message: string): void {
  61. this.socket?.deliver({ id: request.id, error: { code, message } });
  62. }
  63. notify(method: string, params: unknown): void {
  64. this.socket?.deliver({ method, params });
  65. }
  66. deliverRaw(value: string): void {
  67. this.socket?.deliver(value);
  68. }
  69. disconnect(): void {
  70. this.socket?.close();
  71. }
  72. }
  73. export function successfulHandshake(
  74. handler: FakeRequestHandler = (request, server) => server.respond(request, {}),
  75. ): FakeRequestHandler {
  76. return (request, server) => {
  77. if (request.method === "initialize") {
  78. server.respond(request, { userAgent: "fake", platformFamily: "unix", platformOs: "macos" });
  79. return;
  80. }
  81. if (request.method === "initialized") return;
  82. handler(request, server);
  83. };
  84. }
  85. export function rawThread(
  86. id: string,
  87. overrides: Record<string, unknown> = {},
  88. ): Record<string, unknown> {
  89. return {
  90. id,
  91. sessionId: id,
  92. parentThreadId: null,
  93. forkedFromId: null,
  94. preview: `preview-${id}`,
  95. ephemeral: false,
  96. modelProvider: "openai",
  97. createdAt: 10,
  98. updatedAt: 20,
  99. recencyAt: 20,
  100. cwd: "/work/project",
  101. source: "cli",
  102. status: { type: "notLoaded" },
  103. name: null,
  104. agentNickname: null,
  105. agentRole: null,
  106. turns: [],
  107. ...overrides,
  108. };
  109. }