@alexkroman1/aai 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/.turbo/turbo-build.log +11 -9
  2. package/CHANGELOG.md +10 -0
  3. package/dist/{_internal-types-CrnTi9Ew.js → _internal-types-CfOAbK6V.js} +22 -35
  4. package/dist/constants-y68COEGj.js +29 -0
  5. package/dist/host/_base64.d.ts +2 -0
  6. package/dist/host/_mock-ws.d.ts +0 -61
  7. package/dist/host/_pipeline-test-fakes.d.ts +7 -4
  8. package/dist/host/_run-code.d.ts +0 -25
  9. package/dist/host/_runtime-conformance.d.ts +3 -34
  10. package/dist/host/memory-vector.d.ts +0 -11
  11. package/dist/host/providers/resolve-kv.d.ts +0 -7
  12. package/dist/host/providers/resolve-vector.d.ts +0 -8
  13. package/dist/host/providers/stt/assemblyai.d.ts +0 -14
  14. package/dist/host/providers/stt/deepgram.d.ts +2 -14
  15. package/dist/host/providers/stt/soniox.d.ts +0 -22
  16. package/dist/host/providers/tts/rime.d.ts +10 -31
  17. package/dist/host/runtime-barrel.js +619 -630
  18. package/dist/host/runtime-config.d.ts +9 -6
  19. package/dist/host/runtime.d.ts +3 -0
  20. package/dist/host/to-vercel-tools.d.ts +3 -33
  21. package/dist/host/transports/openai-realtime-transport.d.ts +43 -0
  22. package/dist/host/unstorage-kv.d.ts +0 -26
  23. package/dist/index.js +3 -3
  24. package/dist/openai-realtime-cjPAHMMx.js +10 -0
  25. package/dist/sdk/_internal-types.d.ts +6 -55
  26. package/dist/sdk/allowed-hosts.d.ts +4 -3
  27. package/dist/sdk/constants.d.ts +4 -29
  28. package/dist/sdk/define.d.ts +7 -4
  29. package/dist/sdk/kv.d.ts +13 -37
  30. package/dist/sdk/manifest-barrel.js +1 -1
  31. package/dist/sdk/manifest.d.ts +8 -2
  32. package/dist/sdk/protocol.js +1 -1
  33. package/dist/sdk/providers/s2s/openai-realtime.d.ts +17 -0
  34. package/dist/sdk/providers/s2s-barrel.d.ts +9 -0
  35. package/dist/sdk/providers/s2s-barrel.js +2 -0
  36. package/dist/sdk/providers/tts/rime.d.ts +1 -1
  37. package/dist/sdk/providers.d.ts +6 -2
  38. package/dist/sdk/types.d.ts +7 -1
  39. package/dist/{types-KUgezM6u.js → types-DOWVZhb9.js} +1 -7
  40. package/dist/{ws-upgrade-BeOQ7fXL.js → ws-upgrade-CG8-by1n.js} +2 -3
  41. package/host/_base64.ts +9 -0
  42. package/host/_mock-ws.ts +0 -65
  43. package/host/_pipeline-test-fakes.ts +19 -31
  44. package/host/_run-code.ts +10 -53
  45. package/host/_runtime-conformance.ts +3 -44
  46. package/host/_test-utils.ts +20 -42
  47. package/host/builtin-tools.test.ts +127 -222
  48. package/host/builtin-tools.ts +6 -10
  49. package/host/cleanup.test.ts +30 -73
  50. package/host/integration/pipeline-reference.integration.test.ts +12 -17
  51. package/host/integration.test.ts +0 -7
  52. package/host/memory-vector.test.ts +3 -1
  53. package/host/memory-vector.ts +16 -21
  54. package/host/pinecone-vector.test.ts +14 -17
  55. package/host/pinecone-vector.ts +10 -19
  56. package/host/providers/providers.test-d.ts +5 -3
  57. package/host/providers/resolve-kv.ts +23 -41
  58. package/host/providers/resolve-vector.ts +3 -12
  59. package/host/providers/resolve.test.ts +15 -28
  60. package/host/providers/resolve.ts +24 -24
  61. package/host/providers/stt/assemblyai.test.ts +2 -14
  62. package/host/providers/stt/assemblyai.ts +12 -35
  63. package/host/providers/stt/deepgram.test.ts +23 -83
  64. package/host/providers/stt/deepgram.ts +15 -40
  65. package/host/providers/stt/elevenlabs.test.ts +26 -38
  66. package/host/providers/stt/elevenlabs.ts +10 -9
  67. package/host/providers/stt/soniox.test.ts +35 -85
  68. package/host/providers/stt/soniox.ts +8 -53
  69. package/host/providers/tts/cartesia.test.ts +19 -58
  70. package/host/providers/tts/cartesia.ts +36 -66
  71. package/host/providers/tts/rime.test.ts +12 -38
  72. package/host/providers/tts/rime.ts +23 -86
  73. package/host/runtime-config.test.ts +9 -9
  74. package/host/runtime-config.ts +16 -22
  75. package/host/runtime.test.ts +111 -73
  76. package/host/runtime.ts +138 -86
  77. package/host/s2s.test.ts +92 -191
  78. package/host/s2s.ts +55 -49
  79. package/host/server-shutdown.test.ts +9 -30
  80. package/host/server.test.ts +2 -13
  81. package/host/server.ts +85 -100
  82. package/host/session-core.test.ts +15 -30
  83. package/host/session-core.ts +10 -13
  84. package/host/session-prompt.test.ts +1 -5
  85. package/host/to-vercel-tools.test.ts +53 -72
  86. package/host/to-vercel-tools.ts +9 -39
  87. package/host/tool-executor.test.ts +25 -51
  88. package/host/tool-executor.ts +18 -12
  89. package/host/transports/openai-realtime-transport.test.ts +371 -0
  90. package/host/transports/openai-realtime-transport.ts +319 -0
  91. package/host/transports/pipeline-transport.test.ts +125 -298
  92. package/host/transports/pipeline-transport.ts +20 -68
  93. package/host/transports/s2s-transport-fixtures.test.ts +31 -92
  94. package/host/transports/s2s-transport.test.ts +65 -134
  95. package/host/transports/s2s-transport.ts +15 -43
  96. package/host/transports/types.test.ts +4 -8
  97. package/host/unstorage-kv.test.ts +3 -2
  98. package/host/unstorage-kv.ts +5 -35
  99. package/host/ws-handler.test.ts +72 -176
  100. package/host/ws-handler.ts +6 -12
  101. package/package.json +6 -1
  102. package/sdk/__snapshots__/exports.test.ts.snap +7 -0
  103. package/sdk/__snapshots__/schema-shapes.test.ts.snap +1 -0
  104. package/sdk/_internal-types.test.ts +6 -9
  105. package/sdk/_internal-types.ts +16 -57
  106. package/sdk/_test-matchers.ts +25 -15
  107. package/sdk/allowed-hosts.test.ts +50 -114
  108. package/sdk/allowed-hosts.ts +8 -14
  109. package/sdk/constants.ts +5 -52
  110. package/sdk/define.test.ts +7 -6
  111. package/sdk/define.ts +7 -3
  112. package/sdk/exports.test.ts +6 -1
  113. package/sdk/kv.ts +13 -37
  114. package/sdk/manifest.test-d.ts +5 -0
  115. package/sdk/manifest.test.ts +61 -9
  116. package/sdk/manifest.ts +11 -11
  117. package/sdk/protocol-compat.test.ts +66 -98
  118. package/sdk/protocol-snapshot.test.ts +2 -16
  119. package/sdk/protocol.test.ts +13 -22
  120. package/sdk/providers/s2s/openai-realtime.ts +36 -0
  121. package/sdk/providers/s2s-barrel.ts +12 -0
  122. package/sdk/providers/tts/rime.ts +1 -1
  123. package/sdk/providers.ts +24 -5
  124. package/sdk/schema-alignment.test.ts +25 -73
  125. package/sdk/schema-shapes.test.ts +1 -29
  126. package/sdk/system-prompt.test.ts +0 -1
  127. package/sdk/system-prompt.ts +17 -19
  128. package/sdk/types-inference.test.ts +10 -36
  129. package/sdk/types.ts +7 -0
  130. package/sdk/ws-upgrade.test.ts +24 -23
  131. package/sdk/ws-upgrade.ts +2 -3
  132. package/tsdown.config.ts +8 -11
  133. package/dist/constants-C2nirZUI.js +0 -54
@@ -5,21 +5,22 @@ import { makeLogger, makeMockCore, silentLogger } from "./_test-utils.ts";
5
5
  import type { SessionCore } from "./session-core.ts";
6
6
  import { wireSessionSocket } from "./ws-handler.ts";
7
7
 
8
- // ─── Test helpers ────────────────────────────────────────────────────────────
9
-
10
8
  const defaultConfig = { audioFormat: "pcm16" as const, sampleRate: 16_000, ttsSampleRate: 24_000 };
11
9
 
12
- /** Simulate a binary frame arriving on the WebSocket. */
10
+ function openSocket(readyState: number = MockWebSocket.OPEN): MockWebSocket {
11
+ const ws = new MockWebSocket("ws://test");
12
+ ws.readyState = readyState;
13
+ return ws;
14
+ }
15
+
13
16
  function simulateBinaryFrame(ws: MockWebSocket, frame: Uint8Array): void {
14
17
  ws.dispatchEvent(new MessageEvent("message", { data: frame }));
15
18
  }
16
19
 
17
- /** Simulate a string (text) frame arriving on the WebSocket. */
18
20
  function simulateTextFrame(ws: MockWebSocket, text: string): void {
19
21
  ws.dispatchEvent(new MessageEvent("message", { data: text }));
20
22
  }
21
23
 
22
- /** Wait until wireSessionSocket has fully initialized (sessionReady = true). */
23
24
  async function waitForSessionReady(logger: { info: ReturnType<typeof vi.fn> }): Promise<void> {
24
25
  await vi.waitFor(() => {
25
26
  const calls = logger.info.mock.calls.map((c: unknown[]) => c[0]);
@@ -27,11 +28,19 @@ async function waitForSessionReady(logger: { info: ReturnType<typeof vi.fn> }):
27
28
  });
28
29
  }
29
30
 
30
- // ─── Tests ───────────────────────────────────────────────────────────────────
31
+ function parseFirstFrame(ws: MockWebSocket): Record<string, unknown> {
32
+ return JSON.parse(ws.sent[0] as string);
33
+ }
31
34
 
32
- describe("wireSessionSocket", () => {
33
- // ─── Lifecycle: startup ──────────────────────────────────────────────────
35
+ function deferred<T = void>(): { promise: Promise<T>; resolve: (v: T) => void } {
36
+ let resolve!: (v: T) => void;
37
+ const promise = new Promise<T>((r) => {
38
+ resolve = r;
39
+ });
40
+ return { promise, resolve };
41
+ }
34
42
 
43
+ describe("wireSessionSocket", () => {
35
44
  test("'Session ready' is not logged until session.start() resolves", async () => {
36
45
  const logs: string[] = [];
37
46
  const logger = {
@@ -41,18 +50,9 @@ describe("wireSessionSocket", () => {
41
50
  debug: (msg: string) => logs.push(msg),
42
51
  };
43
52
 
44
- let resolveStart!: () => void;
45
- const core = makeMockCore({
46
- start: vi.fn(
47
- () =>
48
- new Promise<void>((r) => {
49
- resolveStart = r;
50
- }),
51
- ),
52
- });
53
-
54
- const ws = new MockWebSocket("ws://test");
55
- ws.readyState = MockWebSocket.OPEN;
53
+ const startGate = deferred();
54
+ const core = makeMockCore({ start: vi.fn(() => startGate.promise) });
55
+ const ws = openSocket();
56
56
 
57
57
  wireSessionSocket(ws, {
58
58
  sessions: new Map(),
@@ -65,7 +65,7 @@ describe("wireSessionSocket", () => {
65
65
  expect(logs).toContain("Session connected");
66
66
  expect(logs).not.toContain("Session ready");
67
67
 
68
- resolveStart();
68
+ startGate.resolve();
69
69
  await vi.waitFor(() => {
70
70
  expect(logs).toContain("Session ready");
71
71
  });
@@ -73,17 +73,11 @@ describe("wireSessionSocket", () => {
73
73
 
74
74
  test("logs 'Session start failed' when start() rejects", async () => {
75
75
  const logs: { msg: string; meta: Record<string, unknown> | undefined }[] = [];
76
- const logger = {
77
- info: (msg: string, meta?: Record<string, unknown>) => logs.push({ msg, meta }),
78
- warn: (msg: string, meta?: Record<string, unknown>) => logs.push({ msg, meta }),
79
- error: (msg: string, meta?: Record<string, unknown>) => logs.push({ msg, meta }),
80
- debug: (msg: string, meta?: Record<string, unknown>) => logs.push({ msg, meta }),
81
- };
76
+ const record = (msg: string, meta?: Record<string, unknown>) => logs.push({ msg, meta });
77
+ const logger = { info: record, warn: record, error: record, debug: record };
82
78
 
83
79
  const core = makeMockCore({ start: vi.fn(() => Promise.reject(new Error("boom"))) });
84
-
85
- const ws = new MockWebSocket("ws://test");
86
- ws.readyState = MockWebSocket.OPEN;
80
+ const ws = openSocket();
87
81
 
88
82
  wireSessionSocket(ws, {
89
83
  sessions: new Map(),
@@ -101,9 +95,7 @@ describe("wireSessionSocket", () => {
101
95
  test("session is added to sessions map on open", () => {
102
96
  const sessions = new Map<string, SessionCore>();
103
97
  const core = makeMockCore();
104
-
105
- const ws = new MockWebSocket("ws://test");
106
- ws.readyState = MockWebSocket.OPEN;
98
+ const ws = openSocket();
107
99
 
108
100
  wireSessionSocket(ws, {
109
101
  sessions,
@@ -117,9 +109,7 @@ describe("wireSessionSocket", () => {
117
109
 
118
110
  test("session is removed from sessions map on close", async () => {
119
111
  const sessions = new Map<string, SessionCore>();
120
-
121
- const ws = new MockWebSocket("ws://test");
122
- ws.readyState = MockWebSocket.OPEN;
112
+ const ws = openSocket();
123
113
 
124
114
  wireSessionSocket(ws, {
125
115
  sessions,
@@ -135,11 +125,8 @@ describe("wireSessionSocket", () => {
135
125
  });
136
126
  });
137
127
 
138
- // ─── CONFIG frame on open ────────────────────────────────────────────────
139
-
140
128
  test("sends CONFIG JSON frame as first message on open", () => {
141
- const ws = new MockWebSocket("ws://test");
142
- ws.readyState = MockWebSocket.OPEN;
129
+ const ws = openSocket();
143
130
 
144
131
  wireSessionSocket(ws, {
145
132
  sessions: new Map(),
@@ -149,15 +136,12 @@ describe("wireSessionSocket", () => {
149
136
  });
150
137
 
151
138
  expect(ws.sent.length).toBeGreaterThanOrEqual(1);
152
- const firstFrame = ws.sent[0];
153
- expect(typeof firstFrame).toBe("string");
154
- const msg = JSON.parse(firstFrame as string);
155
- expect(msg.type).toBe("config");
139
+ expect(typeof ws.sent[0]).toBe("string");
140
+ expect(parseFirstFrame(ws).type).toBe("config");
156
141
  });
157
142
 
158
143
  test("CONFIG frame contains correct sampleRate and ttsSampleRate", () => {
159
- const ws = new MockWebSocket("ws://test");
160
- ws.readyState = MockWebSocket.OPEN;
144
+ const ws = openSocket();
161
145
 
162
146
  wireSessionSocket(ws, {
163
147
  sessions: new Map(),
@@ -166,8 +150,7 @@ describe("wireSessionSocket", () => {
166
150
  logger: silentLogger,
167
151
  });
168
152
 
169
- const firstFrame = ws.sent[0];
170
- const msg = JSON.parse(firstFrame as string);
153
+ const msg = parseFirstFrame(ws);
171
154
  expect(msg.type).toBe("config");
172
155
  expect(msg.audioFormat).toBe("pcm16");
173
156
  expect(msg.sampleRate).toBe(16_000);
@@ -175,13 +158,11 @@ describe("wireSessionSocket", () => {
175
158
  });
176
159
 
177
160
  test("CONFIG frame includes the session ID as sessionId", () => {
178
- const ws = new MockWebSocket("ws://test");
179
- ws.readyState = MockWebSocket.OPEN;
180
- const sessions = new Map<string, SessionCore>();
161
+ const ws = openSocket();
181
162
  let capturedId: string | undefined;
182
163
 
183
164
  wireSessionSocket(ws, {
184
- sessions,
165
+ sessions: new Map(),
185
166
  createSession: (sid) => {
186
167
  capturedId = sid;
187
168
  return makeMockCore();
@@ -190,19 +171,15 @@ describe("wireSessionSocket", () => {
190
171
  logger: silentLogger,
191
172
  });
192
173
 
193
- const firstFrame = ws.sent[0];
194
- const msg = JSON.parse(firstFrame as string);
174
+ const msg = parseFirstFrame(ws);
195
175
  expect(msg.type).toBe("config");
196
176
  expect(msg.sessionId).toBeTruthy();
197
177
  expect(msg.sessionId).toBe(capturedId);
198
178
  });
199
179
 
200
- // ─── Inbound C2S frame routing ───────────────────────────────────────────
201
-
202
180
  test("raw binary Uint8Array routes to session.onAudio", async () => {
203
181
  const core = makeMockCore();
204
- const ws = new MockWebSocket("ws://test");
205
- ws.readyState = MockWebSocket.OPEN;
182
+ const ws = openSocket();
206
183
  const logger = makeLogger();
207
184
 
208
185
  wireSessionSocket(ws, {
@@ -224,8 +201,7 @@ describe("wireSessionSocket", () => {
224
201
 
225
202
  test("audio_ready JSON text frame routes to session.onAudioReady", async () => {
226
203
  const core = makeMockCore();
227
- const ws = new MockWebSocket("ws://test");
228
- ws.readyState = MockWebSocket.OPEN;
204
+ const ws = openSocket();
229
205
  const logger = makeLogger();
230
206
 
231
207
  wireSessionSocket(ws, {
@@ -242,8 +218,7 @@ describe("wireSessionSocket", () => {
242
218
 
243
219
  test("cancel JSON text frame routes to session.onCancel", async () => {
244
220
  const core = makeMockCore();
245
- const ws = new MockWebSocket("ws://test");
246
- ws.readyState = MockWebSocket.OPEN;
221
+ const ws = openSocket();
247
222
  const logger = makeLogger();
248
223
 
249
224
  wireSessionSocket(ws, {
@@ -260,8 +235,7 @@ describe("wireSessionSocket", () => {
260
235
 
261
236
  test("reset JSON text frame routes to session.onReset", async () => {
262
237
  const core = makeMockCore();
263
- const ws = new MockWebSocket("ws://test");
264
- ws.readyState = MockWebSocket.OPEN;
238
+ const ws = openSocket();
265
239
  const logger = makeLogger();
266
240
 
267
241
  wireSessionSocket(ws, {
@@ -278,8 +252,7 @@ describe("wireSessionSocket", () => {
278
252
 
279
253
  test("history JSON text frame routes to session.onHistory with decoded messages", async () => {
280
254
  const core = makeMockCore();
281
- const ws = new MockWebSocket("ws://test");
282
- ws.readyState = MockWebSocket.OPEN;
255
+ const ws = openSocket();
283
256
  const logger = makeLogger();
284
257
 
285
258
  wireSessionSocket(ws, {
@@ -301,12 +274,9 @@ describe("wireSessionSocket", () => {
301
274
  expect(passed).toEqual(messages);
302
275
  });
303
276
 
304
- // ─── Text message error handling ─────────────────────────────────────────
305
-
306
277
  test("invalid JSON text frame is dropped with warning, session not closed", async () => {
307
278
  const core = makeMockCore();
308
- const ws = new MockWebSocket("ws://test");
309
- ws.readyState = MockWebSocket.OPEN;
279
+ const ws = openSocket();
310
280
  const logger = makeLogger();
311
281
 
312
282
  wireSessionSocket(ws, {
@@ -320,16 +290,13 @@ describe("wireSessionSocket", () => {
320
290
 
321
291
  simulateTextFrame(ws, "this is not json{{{");
322
292
  expect(logger.warn).toHaveBeenCalledWith("ws: invalid JSON; dropping", expect.any(Object));
323
- // Session methods must not be called
324
293
  expect(core.onAudioReady).not.toHaveBeenCalled();
325
- // Socket must still be open (not closed)
326
294
  expect(ws.readyState).toBe(MockWebSocket.OPEN);
327
295
  });
328
296
 
329
297
  test("unknown client message type is silently dropped", async () => {
330
298
  const core = makeMockCore();
331
- const ws = new MockWebSocket("ws://test");
332
- ws.readyState = MockWebSocket.OPEN;
299
+ const ws = openSocket();
333
300
  const logger = makeLogger();
334
301
 
335
302
  wireSessionSocket(ws, {
@@ -341,29 +308,17 @@ describe("wireSessionSocket", () => {
341
308
 
342
309
  await waitForSessionReady(logger);
343
310
 
344
- // Valid JSON with a valid { type } envelope but unknown type — lenientParse returns ok:false, malformed:false
311
+ // Valid envelope but unknown type — lenientParse returns ok:false, malformed:false; must NOT warn (rolling-upgrade tolerance)
345
312
  simulateTextFrame(ws, JSON.stringify({ type: "some_future_message_type" }));
346
- // Must NOT warn — rolling-upgrade tolerance
347
313
  expect(logger.warn).not.toHaveBeenCalled();
348
314
  expect(core.onAudioReady).not.toHaveBeenCalled();
349
315
  expect(ws.readyState).toBe(MockWebSocket.OPEN);
350
316
  });
351
317
 
352
- // ─── Message buffering ───────────────────────────────────────────────────
353
-
354
318
  test("frames before session is ready are buffered and replayed after start()", async () => {
355
- let resolveStart!: () => void;
356
- const core = makeMockCore({
357
- start: vi.fn(
358
- () =>
359
- new Promise<void>((r) => {
360
- resolveStart = r;
361
- }),
362
- ),
363
- });
364
-
365
- const ws = new MockWebSocket("ws://test");
366
- ws.readyState = MockWebSocket.OPEN;
319
+ const startGate = deferred();
320
+ const core = makeMockCore({ start: vi.fn(() => startGate.promise) });
321
+ const ws = openSocket();
367
322
  const logger = makeLogger();
368
323
 
369
324
  wireSessionSocket(ws, {
@@ -373,20 +328,17 @@ describe("wireSessionSocket", () => {
373
328
  logger,
374
329
  });
375
330
 
376
- // Session not ready yet — send a cancel text frame
377
331
  simulateTextFrame(ws, JSON.stringify({ type: "cancel" }));
378
332
  expect(core.onCancel).not.toHaveBeenCalled();
379
333
 
380
- // Now let start() resolve
381
- resolveStart();
334
+ startGate.resolve();
382
335
  await waitForSessionReady(logger);
383
336
 
384
337
  expect(core.onCancel).toHaveBeenCalledOnce();
385
338
  });
386
339
 
387
340
  test("messages before session is created (no open yet) are ignored", () => {
388
- const ws = new MockWebSocket("ws://test");
389
- ws.readyState = MockWebSocket.CONNECTING;
341
+ const ws = openSocket(MockWebSocket.CONNECTING);
390
342
 
391
343
  wireSessionSocket(ws, {
392
344
  sessions: new Map(),
@@ -395,17 +347,12 @@ describe("wireSessionSocket", () => {
395
347
  logger: silentLogger,
396
348
  });
397
349
 
398
- // No open yet — session is null, should be silently ignored
399
350
  simulateTextFrame(ws, JSON.stringify({ type: "audio_ready" }));
400
- // No error thrown
401
351
  });
402
352
 
403
- // ─── Close handler ───────────────────────────────────────────────────────
404
-
405
353
  test("close handler calls session.stop", async () => {
406
354
  const core = makeMockCore();
407
- const ws = new MockWebSocket("ws://test");
408
- ws.readyState = MockWebSocket.OPEN;
355
+ const ws = openSocket();
409
356
 
410
357
  wireSessionSocket(ws, {
411
358
  sessions: new Map(),
@@ -421,11 +368,8 @@ describe("wireSessionSocket", () => {
421
368
  });
422
369
  });
423
370
 
424
- // ─── Error handler ───────────────────────────────────────────────────────
425
-
426
371
  test("error event is logged", () => {
427
- const ws = new MockWebSocket("ws://test");
428
- ws.readyState = MockWebSocket.OPEN;
372
+ const ws = openSocket();
429
373
  const logger = makeLogger();
430
374
 
431
375
  wireSessionSocket(ws, {
@@ -446,8 +390,7 @@ describe("wireSessionSocket", () => {
446
390
  });
447
391
 
448
392
  test("generic error event logs default message", () => {
449
- const ws = new MockWebSocket("ws://test");
450
- ws.readyState = MockWebSocket.OPEN;
393
+ const ws = openSocket();
451
394
  const logger = makeLogger();
452
395
 
453
396
  wireSessionSocket(ws, {
@@ -465,12 +408,9 @@ describe("wireSessionSocket", () => {
465
408
  );
466
409
  });
467
410
 
468
- // ─── Callbacks ───────────────────────────────────────────────────────────
469
-
470
411
  test("onOpen callback is invoked when socket opens", () => {
471
412
  const onOpen = vi.fn();
472
- const ws = new MockWebSocket("ws://test");
473
- ws.readyState = MockWebSocket.OPEN;
413
+ const ws = openSocket();
474
414
 
475
415
  wireSessionSocket(ws, {
476
416
  sessions: new Map(),
@@ -485,8 +425,7 @@ describe("wireSessionSocket", () => {
485
425
 
486
426
  test("onClose callback is invoked when socket closes", () => {
487
427
  const onClose = vi.fn();
488
- const ws = new MockWebSocket("ws://test");
489
- ws.readyState = MockWebSocket.OPEN;
428
+ const ws = openSocket();
490
429
 
491
430
  wireSessionSocket(ws, {
492
431
  sessions: new Map(),
@@ -502,8 +441,7 @@ describe("wireSessionSocket", () => {
502
441
 
503
442
  test("onSessionEnd is called with sessionId after session cleanup", async () => {
504
443
  const onSessionEnd = vi.fn();
505
- const ws = new MockWebSocket("ws://test");
506
- ws.readyState = MockWebSocket.OPEN;
444
+ const ws = openSocket();
507
445
  const sessions = new Map<string, SessionCore>();
508
446
 
509
447
  wireSessionSocket(ws, {
@@ -528,8 +466,7 @@ describe("wireSessionSocket", () => {
528
466
 
529
467
  test("onSinkCreated callback is invoked with sessionId and ClientSink", () => {
530
468
  const onSinkCreated = vi.fn();
531
- const ws = new MockWebSocket("ws://test");
532
- ws.readyState = MockWebSocket.OPEN;
469
+ const ws = openSocket();
533
470
 
534
471
  wireSessionSocket(ws, {
535
472
  sessions: new Map(),
@@ -543,12 +480,9 @@ describe("wireSessionSocket", () => {
543
480
  expect(typeof onSinkCreated.mock.calls[0]?.[0]).toBe("string");
544
481
  });
545
482
 
546
- // ─── ClientSink (indirect testing via createSession capture) ─────────────
547
-
548
483
  test("ClientSink.open reflects ws.readyState", () => {
549
484
  let capturedClient!: ClientSink;
550
- const ws = new MockWebSocket("ws://test");
551
- ws.readyState = MockWebSocket.OPEN;
485
+ const ws = openSocket();
552
486
 
553
487
  wireSessionSocket(ws, {
554
488
  sessions: new Map(),
@@ -567,8 +501,7 @@ describe("wireSessionSocket", () => {
567
501
 
568
502
  test("ClientSink.playAudioChunk sends raw binary Uint8Array", () => {
569
503
  let capturedClient!: ClientSink;
570
- const ws = new MockWebSocket("ws://test");
571
- ws.readyState = MockWebSocket.OPEN;
504
+ const ws = openSocket();
572
505
 
573
506
  wireSessionSocket(ws, {
574
507
  sessions: new Map(),
@@ -583,7 +516,6 @@ describe("wireSessionSocket", () => {
583
516
  const chunk = new Uint8Array([10, 20, 30]);
584
517
  capturedClient.playAudioChunk(chunk);
585
518
 
586
- // Find binary frames in sent (skip the initial config JSON string)
587
519
  const binaryFrames = (ws.sent as unknown[]).filter((d) => d instanceof Uint8Array);
588
520
  expect(binaryFrames.length).toBeGreaterThanOrEqual(1);
589
521
  expect(binaryFrames[0]).toBe(chunk);
@@ -591,8 +523,7 @@ describe("wireSessionSocket", () => {
591
523
 
592
524
  test("ClientSink.playAudioDone sends audio_done JSON text frame", () => {
593
525
  let capturedClient!: ClientSink;
594
- const ws = new MockWebSocket("ws://test");
595
- ws.readyState = MockWebSocket.OPEN;
526
+ const ws = openSocket();
596
527
 
597
528
  wireSessionSocket(ws, {
598
529
  sessions: new Map(),
@@ -606,18 +537,15 @@ describe("wireSessionSocket", () => {
606
537
 
607
538
  capturedClient.playAudioDone();
608
539
 
609
- // Find JSON string frames after the initial config
610
540
  const textFrames = (ws.sent as unknown[])
611
541
  .filter((d): d is string => typeof d === "string")
612
542
  .map((s) => JSON.parse(s));
613
- const audioDoneFrame = textFrames.find((m) => m.type === "audio_done");
614
- expect(audioDoneFrame).toBeDefined();
543
+ expect(textFrames.find((m) => m.type === "audio_done")).toBeDefined();
615
544
  });
616
545
 
617
546
  test("ClientSink tolerates ws.send throwing (closed socket)", () => {
618
547
  let capturedClient!: ClientSink;
619
- const ws = new MockWebSocket("ws://test");
620
- ws.readyState = MockWebSocket.OPEN;
548
+ const ws = openSocket();
621
549
 
622
550
  wireSessionSocket(ws, {
623
551
  sessions: new Map(),
@@ -629,30 +557,18 @@ describe("wireSessionSocket", () => {
629
557
  logger: silentLogger,
630
558
  });
631
559
 
632
- // Override send to throw
633
560
  ws.send = () => {
634
561
  throw new Error("socket closed");
635
562
  };
636
- // Should not throw
637
563
  capturedClient.event({ type: "speech_started" });
638
564
  capturedClient.playAudioChunk(new Uint8Array([1]));
639
565
  capturedClient.playAudioDone();
640
566
  });
641
567
 
642
- // ─── Concurrency regression tests ────────────────────────────────────────
643
-
644
568
  test("close during start() does not double-stop or throw", async () => {
645
- let resolveStart!: () => void;
646
- const core = makeMockCore({
647
- start: vi.fn(
648
- () =>
649
- new Promise<void>((r) => {
650
- resolveStart = r;
651
- }),
652
- ),
653
- });
654
- const ws = new MockWebSocket("ws://test");
655
- ws.readyState = MockWebSocket.OPEN;
569
+ const startGate = deferred();
570
+ const core = makeMockCore({ start: vi.fn(() => startGate.promise) });
571
+ const ws = openSocket();
656
572
  const sessions = new Map<string, SessionCore>();
657
573
 
658
574
  wireSessionSocket(ws, {
@@ -662,11 +578,9 @@ describe("wireSessionSocket", () => {
662
578
  logger: silentLogger,
663
579
  });
664
580
 
665
- // Close while start() is pending
666
581
  ws.close();
582
+ startGate.resolve();
667
583
 
668
- // Now start() resolves
669
- resolveStart();
670
584
  await vi.waitFor(() => {
671
585
  expect(core.stop).toHaveBeenCalledOnce();
672
586
  });
@@ -674,8 +588,7 @@ describe("wireSessionSocket", () => {
674
588
 
675
589
  test("start() failure removes session from map before close", async () => {
676
590
  const core = makeMockCore({ start: vi.fn(() => Promise.reject(new Error("boom"))) });
677
- const ws = new MockWebSocket("ws://test");
678
- ws.readyState = MockWebSocket.OPEN;
591
+ const ws = openSocket();
679
592
  const sessions = new Map<string, SessionCore>();
680
593
 
681
594
  wireSessionSocket(ws, {
@@ -689,24 +602,19 @@ describe("wireSessionSocket", () => {
689
602
  expect(sessions.size).toBe(0);
690
603
  });
691
604
 
692
- // Close should not throw — session is null
693
605
  ws.close();
694
606
  });
695
607
 
696
- // ─── Session start timeout ────────────────────────────────────────────────
697
-
698
608
  test("session.start() timeout triggers 'Session start failed'", async () => {
699
609
  const core = makeMockCore({
700
610
  start: vi.fn(
701
611
  () =>
702
612
  new Promise<void>(() => {
703
- /* intentionally never resolves */
613
+ /* never resolves */
704
614
  }),
705
615
  ),
706
616
  });
707
-
708
- const ws = new MockWebSocket("ws://test");
709
- ws.readyState = MockWebSocket.OPEN;
617
+ const ws = openSocket();
710
618
  const sessions = new Map<string, SessionCore>();
711
619
 
712
620
  wireSessionSocket(ws, {
@@ -732,12 +640,9 @@ describe("wireSessionSocket", () => {
732
640
  );
733
641
  });
734
642
 
735
- // ─── Socket not yet open ──────────────────────────────────────────────────
736
-
737
- test("waits for open event when readyState is not OPEN", async () => {
643
+ test("waits for open event when readyState is not OPEN", () => {
738
644
  const core = makeMockCore();
739
- const ws = new MockWebSocket("ws://test");
740
- ws.readyState = MockWebSocket.CONNECTING;
645
+ const ws = openSocket(MockWebSocket.CONNECTING);
741
646
 
742
647
  wireSessionSocket(ws, {
743
648
  sessions: new Map(),
@@ -746,22 +651,17 @@ describe("wireSessionSocket", () => {
746
651
  logger: silentLogger,
747
652
  });
748
653
 
749
- // Session not started yet — waiting for open
750
654
  expect(core.start).not.toHaveBeenCalled();
751
655
 
752
- // Simulate open
753
656
  ws.readyState = MockWebSocket.OPEN;
754
657
  ws.dispatchEvent(new Event("open"));
755
658
 
756
659
  expect(core.start).toHaveBeenCalledOnce();
757
660
  });
758
661
 
759
- // ─── Session resume ───────────────────────────────────────────────────────
760
-
761
662
  test("resumeFrom reuses old session ID instead of generating new UUID", () => {
762
663
  const sessions = new Map<string, SessionCore>();
763
- const ws = new MockWebSocket("ws://test");
764
- ws.readyState = MockWebSocket.OPEN;
664
+ const ws = openSocket();
765
665
  let capturedId: string | undefined;
766
666
 
767
667
  wireSessionSocket(ws, {
@@ -780,8 +680,7 @@ describe("wireSessionSocket", () => {
780
680
  });
781
681
 
782
682
  test("CONFIG frame contains resumed session ID as sessionId", () => {
783
- const ws = new MockWebSocket("ws://test");
784
- ws.readyState = MockWebSocket.OPEN;
683
+ const ws = openSocket();
785
684
 
786
685
  wireSessionSocket(ws, {
787
686
  sessions: new Map(),
@@ -791,16 +690,14 @@ describe("wireSessionSocket", () => {
791
690
  resumeFrom: "resume-id-123",
792
691
  });
793
692
 
794
- const firstFrame = ws.sent[0];
795
- const msg = JSON.parse(firstFrame as string);
693
+ const msg = parseFirstFrame(ws);
796
694
  expect(msg.type).toBe("config");
797
695
  expect(msg.sessionId).toBe("resume-id-123");
798
696
  });
799
697
 
800
698
  test("without resumeFrom, generates a new UUID session ID", () => {
801
699
  const sessions = new Map<string, SessionCore>();
802
- const ws = new MockWebSocket("ws://test");
803
- ws.readyState = MockWebSocket.OPEN;
700
+ const ws = openSocket();
804
701
  let capturedId: string | undefined;
805
702
 
806
703
  wireSessionSocket(ws, {
@@ -815,7 +712,6 @@ describe("wireSessionSocket", () => {
815
712
 
816
713
  expect(capturedId).toBeDefined();
817
714
  expect(capturedId).not.toBe("");
818
- // UUID format: 8-4-4-4-12
819
715
  expect(capturedId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
820
716
  });
821
717
  });
@@ -93,15 +93,11 @@ function createClientSink(ws: SessionWebSocket, log: Logger): ClientSink {
93
93
  };
94
94
  }
95
95
 
96
- function handleBinaryAudio(data: unknown, session: SessionCore): boolean {
96
+ function dispatchMessage(data: unknown, session: SessionCore, log: Logger, sid: string): void {
97
97
  if (data instanceof Uint8Array) {
98
98
  session.onAudio(data);
99
- return true;
99
+ return;
100
100
  }
101
- return false;
102
- }
103
-
104
- function handleTextMessage(data: unknown, session: SessionCore, log: Logger, sid: string): void {
105
101
  if (typeof data !== "string") {
106
102
  log.warn("ws: non-string, non-binary frame received; dropping", { sid });
107
103
  return;
@@ -118,7 +114,6 @@ function handleTextMessage(data: unknown, session: SessionCore, log: Logger, sid
118
114
  if (result.malformed) {
119
115
  log.warn("ws: malformed client message", { sid, error: result.error });
120
116
  }
121
- // else: unrecognised type — silently drop (rolling-upgrade tolerance)
122
117
  return;
123
118
  }
124
119
  switch (result.data.type) {
@@ -166,8 +161,7 @@ export function wireSessionSocket(ws: SessionWebSocket, opts: WsSessionOptions):
166
161
  const buf = messageBuffer;
167
162
  messageBuffer = null;
168
163
  for (const event of buf) {
169
- if (handleBinaryAudio(event.data, session)) continue;
170
- handleTextMessage(event.data, session, log, sid);
164
+ dispatchMessage(event.data, session, log, sid);
171
165
  }
172
166
  }
173
167
 
@@ -224,12 +218,12 @@ export function wireSessionSocket(ws: SessionWebSocket, opts: WsSessionOptions):
224
218
  // Buffer messages until session.start() completes to avoid dispatching
225
219
  // to a session whose transport connection isn't established yet.
226
220
  if (!sessionReady) {
227
- if (messageBuffer && messageBuffer.length < MAX_MESSAGE_BUFFER_SIZE)
221
+ if (messageBuffer && messageBuffer.length < MAX_MESSAGE_BUFFER_SIZE) {
228
222
  messageBuffer.push(event);
223
+ }
229
224
  return;
230
225
  }
231
- if (handleBinaryAudio(event.data, session)) return;
232
- handleTextMessage(event.data, session, log, sid);
226
+ dispatchMessage(event.data, session, log, sid);
233
227
  });
234
228
 
235
229
  ws.addEventListener("close", () => {