@alexkroman1/aai 1.7.0 → 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 +16 -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 +628 -642
  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 +56 -53
  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
package/host/s2s.test.ts CHANGED
@@ -3,7 +3,6 @@ import { silentLogger } from "./_test-utils.ts";
3
3
  import type { S2sCallbacks, S2sWebSocket } from "./s2s.ts";
4
4
  import { connectS2s } from "./s2s.ts";
5
5
 
6
- /** EventTarget-based WebSocket stub (standard API, no `.on()` adapter needed). */
7
6
  function createWebSocketStub() {
8
7
  const target = new EventTarget();
9
8
  return Object.assign(target, {
@@ -11,7 +10,6 @@ function createWebSocketStub() {
11
10
  send: vi.fn(),
12
11
  close: vi.fn(),
13
12
  addEventListener: target.addEventListener.bind(target) as S2sWebSocket["addEventListener"],
14
- /** Simulate a server-side event for testing. */
15
13
  emit(event: string, ...args: unknown[]) {
16
14
  const builders: Record<string, () => Event> = {
17
15
  open: () => new Event("open"),
@@ -79,6 +77,20 @@ async function setupHandle(callbacks?: S2sCallbacks) {
79
77
  return { raw, handle, logger };
80
78
  }
81
79
 
80
+ type WebSocketStub = ReturnType<typeof createWebSocketStub>;
81
+
82
+ function emitMessage(raw: WebSocketStub, payload: unknown): void {
83
+ raw.emit("message", Buffer.from(JSON.stringify(payload)));
84
+ }
85
+
86
+ function lastSent(raw: WebSocketStub): Record<string, unknown> {
87
+ return JSON.parse(raw.send.mock.calls[0]?.[0] as string);
88
+ }
89
+
90
+ function errorArg(callbacks: S2sCallbacks): Error {
91
+ return (callbacks.onError as ReturnType<typeof vi.fn>).mock.calls[0]?.[0];
92
+ }
93
+
82
94
  describe("connectS2s", () => {
83
95
  test("resolves with handle after open", async () => {
84
96
  const { handle } = await setupHandle();
@@ -114,17 +126,15 @@ describe("connectS2s", () => {
114
126
  ).rejects.toThrow("connection refused");
115
127
  });
116
128
 
117
- // ─── Handle methods ────────────────────────────────────────────────────
118
-
119
129
  test("updateSession sends session.update message", async () => {
120
130
  const { raw, handle } = await setupHandle();
121
131
 
122
132
  handle.updateSession({ systemPrompt: "test", tools: [] });
123
133
 
124
134
  expect(raw.send).toHaveBeenCalledOnce();
125
- const sent = JSON.parse(raw.send.mock.calls[0]?.[0] as string);
135
+ const sent = lastSent(raw) as { type: string; session: { system_prompt: string } };
126
136
  expect(sent.type).toBe("session.update");
127
- expect(sent.session.system_prompt).toBe("test"); // wire format stays snake_case
137
+ expect(sent.session.system_prompt).toBe("test");
128
138
  });
129
139
 
130
140
  test("sendAudio sends base64-encoded audio when open", async () => {
@@ -133,14 +143,14 @@ describe("connectS2s", () => {
133
143
  handle.sendAudio(new Uint8Array([1, 2, 3, 4]));
134
144
 
135
145
  expect(raw.send).toHaveBeenCalledOnce();
136
- const sent = JSON.parse(raw.send.mock.calls[0]?.[0] as string);
146
+ const sent = lastSent(raw);
137
147
  expect(sent.type).toBe("input.audio");
138
- expect(typeof sent.audio).toBe("string"); // base64
148
+ expect(typeof sent.audio).toBe("string");
139
149
  });
140
150
 
141
151
  test("sendAudio is no-op when ws is not open", async () => {
142
152
  const { raw, handle } = await setupHandle();
143
- raw.readyState = 3; // CLOSED
153
+ raw.readyState = 3;
144
154
 
145
155
  handle.sendAudio(new Uint8Array([1, 2, 3, 4]));
146
156
  expect(raw.send).not.toHaveBeenCalled();
@@ -158,7 +168,7 @@ describe("connectS2s", () => {
158
168
 
159
169
  test("sendAudioRaw is no-op when ws is not open", async () => {
160
170
  const { raw, handle } = await setupHandle();
161
- raw.readyState = 3; // CLOSED
171
+ raw.readyState = 3;
162
172
 
163
173
  handle.sendAudioRaw('{"type":"input.audio","audio":"abc"}');
164
174
  expect(raw.send).not.toHaveBeenCalled();
@@ -170,7 +180,7 @@ describe("connectS2s", () => {
170
180
  handle.sendToolResult("call-123", "result-text");
171
181
 
172
182
  expect(raw.send).toHaveBeenCalledOnce();
173
- const sent = JSON.parse(raw.send.mock.calls[0]?.[0] as string);
183
+ const sent = lastSent(raw);
174
184
  expect(sent.type).toBe("tool.result");
175
185
  expect(sent.call_id).toBe("call-123");
176
186
  expect(sent.result).toBe("result-text");
@@ -182,7 +192,7 @@ describe("connectS2s", () => {
182
192
  handle.resumeSession("session-abc");
183
193
 
184
194
  expect(raw.send).toHaveBeenCalledOnce();
185
- const sent = JSON.parse(raw.send.mock.calls[0]?.[0] as string);
195
+ const sent = lastSent(raw);
186
196
  expect(sent.type).toBe("session.resume");
187
197
  expect(sent.session_id).toBe("session-abc");
188
198
  });
@@ -196,27 +206,17 @@ describe("connectS2s", () => {
196
206
 
197
207
  test("send is no-op when ws is not open", async () => {
198
208
  const { raw, handle } = await setupHandle();
199
- raw.readyState = 3; // CLOSED
209
+ raw.readyState = 3;
200
210
 
201
211
  handle.updateSession({ systemPrompt: "test", tools: [] });
202
212
  expect(raw.send).not.toHaveBeenCalled();
203
213
  });
204
214
 
205
- // ─── Message dispatch ──────────────────────────────────────────────────
206
-
207
215
  test("session.ready dispatches 'onSessionReady' callback", async () => {
208
216
  const callbacks = makeMockCallbacks();
209
217
  const { raw } = await setupHandle(callbacks);
210
218
 
211
- raw.emit(
212
- "message",
213
- Buffer.from(
214
- JSON.stringify({
215
- type: "session.ready",
216
- session_id: "s123",
217
- }),
218
- ),
219
- );
219
+ emitMessage(raw, { type: "session.ready", session_id: "s123" });
220
220
 
221
221
  expect(callbacks.onSessionReady).toHaveBeenCalledOnce();
222
222
  expect(callbacks.onSessionReady).toHaveBeenCalledWith("s123");
@@ -226,7 +226,7 @@ describe("connectS2s", () => {
226
226
  const callbacks = makeMockCallbacks();
227
227
  const { raw } = await setupHandle(callbacks);
228
228
 
229
- raw.emit("message", Buffer.from(JSON.stringify({ type: "input.speech.started" })));
229
+ emitMessage(raw, { type: "input.speech.started" });
230
230
 
231
231
  expect(callbacks.onSpeechStarted).toHaveBeenCalledOnce();
232
232
  });
@@ -235,9 +235,9 @@ describe("connectS2s", () => {
235
235
  const callbacks = makeMockCallbacks();
236
236
  const { raw } = await setupHandle(callbacks);
237
237
 
238
- // Prime VAD state — speech_stopped is only forwarded after a speech_started.
239
- raw.emit("message", Buffer.from(JSON.stringify({ type: "input.speech.started" })));
240
- raw.emit("message", Buffer.from(JSON.stringify({ type: "input.speech.stopped" })));
238
+ // speech_stopped is only forwarded after a speech_started primes VAD state.
239
+ emitMessage(raw, { type: "input.speech.started" });
240
+ emitMessage(raw, { type: "input.speech.stopped" });
241
241
 
242
242
  expect(callbacks.onSpeechStarted).toHaveBeenCalledOnce();
243
243
  expect(callbacks.onSpeechStopped).toHaveBeenCalledOnce();
@@ -247,9 +247,9 @@ describe("connectS2s", () => {
247
247
  const callbacks = makeMockCallbacks();
248
248
  const { raw } = await setupHandle(callbacks);
249
249
 
250
- raw.emit("message", Buffer.from(JSON.stringify({ type: "input.speech.started" })));
251
- raw.emit("message", Buffer.from(JSON.stringify({ type: "input.speech.stopped" })));
252
- raw.emit("message", Buffer.from(JSON.stringify({ type: "input.speech.stopped" })));
250
+ emitMessage(raw, { type: "input.speech.started" });
251
+ emitMessage(raw, { type: "input.speech.stopped" });
252
+ emitMessage(raw, { type: "input.speech.stopped" });
253
253
 
254
254
  expect(callbacks.onSpeechStopped).toHaveBeenCalledOnce();
255
255
  });
@@ -258,16 +258,7 @@ describe("connectS2s", () => {
258
258
  const callbacks = makeMockCallbacks();
259
259
  const { raw } = await setupHandle(callbacks);
260
260
 
261
- raw.emit(
262
- "message",
263
- Buffer.from(
264
- JSON.stringify({
265
- type: "transcript.user",
266
- item_id: "item-1",
267
- text: "Hello world",
268
- }),
269
- ),
270
- );
261
+ emitMessage(raw, { type: "transcript.user", item_id: "item-1", text: "Hello world" });
271
262
 
272
263
  expect(callbacks.onUserTranscript).toHaveBeenCalledOnce();
273
264
  expect(callbacks.onUserTranscript).toHaveBeenCalledWith("Hello world");
@@ -277,15 +268,7 @@ describe("connectS2s", () => {
277
268
  const callbacks = makeMockCallbacks();
278
269
  const { raw } = await setupHandle(callbacks);
279
270
 
280
- raw.emit(
281
- "message",
282
- Buffer.from(
283
- JSON.stringify({
284
- type: "reply.started",
285
- reply_id: "r1",
286
- }),
287
- ),
288
- );
271
+ emitMessage(raw, { type: "reply.started", reply_id: "r1" });
289
272
 
290
273
  expect(callbacks.onReplyStarted).toHaveBeenCalledOnce();
291
274
  expect(callbacks.onReplyStarted).toHaveBeenCalledWith("r1");
@@ -295,18 +278,13 @@ describe("connectS2s", () => {
295
278
  const callbacks = makeMockCallbacks();
296
279
  const { raw } = await setupHandle(callbacks);
297
280
 
298
- raw.emit(
299
- "message",
300
- Buffer.from(
301
- JSON.stringify({
302
- type: "transcript.agent",
303
- text: "Full response",
304
- reply_id: "r1",
305
- item_id: "i1",
306
- interrupted: false,
307
- }),
308
- ),
309
- );
281
+ emitMessage(raw, {
282
+ type: "transcript.agent",
283
+ text: "Full response",
284
+ reply_id: "r1",
285
+ item_id: "i1",
286
+ interrupted: false,
287
+ });
310
288
 
311
289
  expect(callbacks.onAgentTranscript).toHaveBeenCalledOnce();
312
290
  expect(callbacks.onAgentTranscript).toHaveBeenCalledWith("Full response", false);
@@ -316,10 +294,7 @@ describe("connectS2s", () => {
316
294
  const callbacks = makeMockCallbacks();
317
295
  const { raw } = await setupHandle(callbacks);
318
296
 
319
- raw.emit(
320
- "message",
321
- Buffer.from(JSON.stringify({ type: "transcript.agent", text: "response" })),
322
- );
297
+ emitMessage(raw, { type: "transcript.agent", text: "response" });
323
298
 
324
299
  expect(callbacks.onAgentTranscript).toHaveBeenCalledWith("response", false);
325
300
  });
@@ -328,16 +303,11 @@ describe("connectS2s", () => {
328
303
  const callbacks = makeMockCallbacks();
329
304
  const { raw } = await setupHandle(callbacks);
330
305
 
331
- raw.emit(
332
- "message",
333
- Buffer.from(
334
- JSON.stringify({
335
- type: "transcript.agent",
336
- text: "Interrupted response",
337
- interrupted: true,
338
- }),
339
- ),
340
- );
306
+ emitMessage(raw, {
307
+ type: "transcript.agent",
308
+ text: "Interrupted response",
309
+ interrupted: true,
310
+ });
341
311
 
342
312
  expect(callbacks.onAgentTranscript).toHaveBeenCalledWith("Interrupted response", true);
343
313
  });
@@ -346,17 +316,12 @@ describe("connectS2s", () => {
346
316
  const callbacks = makeMockCallbacks();
347
317
  const { raw } = await setupHandle(callbacks);
348
318
 
349
- raw.emit(
350
- "message",
351
- Buffer.from(
352
- JSON.stringify({
353
- type: "tool.call",
354
- call_id: "c1",
355
- name: "web_search",
356
- args: { query: "test" },
357
- }),
358
- ),
359
- );
319
+ emitMessage(raw, {
320
+ type: "tool.call",
321
+ call_id: "c1",
322
+ name: "web_search",
323
+ args: { query: "test" },
324
+ });
360
325
 
361
326
  expect(callbacks.onToolCall).toHaveBeenCalledOnce();
362
327
  expect(callbacks.onToolCall).toHaveBeenCalledWith("c1", "web_search", { query: "test" });
@@ -366,15 +331,7 @@ describe("connectS2s", () => {
366
331
  const callbacks = makeMockCallbacks();
367
332
  const { raw } = await setupHandle(callbacks);
368
333
 
369
- raw.emit(
370
- "message",
371
- Buffer.from(
372
- JSON.stringify({
373
- type: "reply.done",
374
- status: "completed",
375
- }),
376
- ),
377
- );
334
+ emitMessage(raw, { type: "reply.done", status: "completed" });
378
335
 
379
336
  expect(callbacks.onReplyDone).toHaveBeenCalledOnce();
380
337
  expect(callbacks.onCancelled).not.toHaveBeenCalled();
@@ -384,15 +341,7 @@ describe("connectS2s", () => {
384
341
  const callbacks = makeMockCallbacks();
385
342
  const { raw } = await setupHandle(callbacks);
386
343
 
387
- raw.emit(
388
- "message",
389
- Buffer.from(
390
- JSON.stringify({
391
- type: "reply.done",
392
- status: "interrupted",
393
- }),
394
- ),
395
- );
344
+ emitMessage(raw, { type: "reply.done", status: "interrupted" });
396
345
 
397
346
  expect(callbacks.onCancelled).toHaveBeenCalledOnce();
398
347
  expect(callbacks.onReplyDone).not.toHaveBeenCalled();
@@ -411,7 +360,7 @@ describe("connectS2s", () => {
411
360
  sid: "sess-abc",
412
361
  });
413
362
 
414
- raw.emit("message", Buffer.from(JSON.stringify({ type: "reply.done", status: "completed" })));
363
+ emitMessage(raw, { type: "reply.done", status: "completed" });
415
364
 
416
365
  const arrivalCall = infoSpy.mock.calls.find((c) => c[0] === "S2S << reply.done");
417
366
  expect(arrivalCall).toBeDefined();
@@ -422,16 +371,11 @@ describe("connectS2s", () => {
422
371
  const callbacks = makeMockCallbacks();
423
372
  const { raw } = await setupHandle(callbacks);
424
373
 
425
- raw.emit(
426
- "message",
427
- Buffer.from(
428
- JSON.stringify({
429
- type: "session.error",
430
- code: "session_not_found",
431
- message: "Session not found",
432
- }),
433
- ),
434
- );
374
+ emitMessage(raw, {
375
+ type: "session.error",
376
+ code: "session_not_found",
377
+ message: "Session not found",
378
+ });
435
379
 
436
380
  expect(callbacks.onSessionExpired).toHaveBeenCalledOnce();
437
381
  });
@@ -440,16 +384,11 @@ describe("connectS2s", () => {
440
384
  const callbacks = makeMockCallbacks();
441
385
  const { raw } = await setupHandle(callbacks);
442
386
 
443
- raw.emit(
444
- "message",
445
- Buffer.from(
446
- JSON.stringify({
447
- type: "session.error",
448
- code: "session_forbidden",
449
- message: "Forbidden",
450
- }),
451
- ),
452
- );
387
+ emitMessage(raw, {
388
+ type: "session.error",
389
+ code: "session_forbidden",
390
+ message: "Forbidden",
391
+ });
453
392
 
454
393
  expect(callbacks.onSessionExpired).toHaveBeenCalledOnce();
455
394
  });
@@ -458,19 +397,14 @@ describe("connectS2s", () => {
458
397
  const callbacks = makeMockCallbacks();
459
398
  const { raw } = await setupHandle(callbacks);
460
399
 
461
- raw.emit(
462
- "message",
463
- Buffer.from(
464
- JSON.stringify({
465
- type: "session.error",
466
- code: "rate_limit",
467
- message: "Too many requests",
468
- }),
469
- ),
470
- );
400
+ emitMessage(raw, {
401
+ type: "session.error",
402
+ code: "rate_limit",
403
+ message: "Too many requests",
404
+ });
471
405
 
472
406
  expect(callbacks.onError).toHaveBeenCalledOnce();
473
- const err = (callbacks.onError as ReturnType<typeof vi.fn>).mock.calls[0]?.[0];
407
+ const err = errorArg(callbacks);
474
408
  expect(err).toBeInstanceOf(Error);
475
409
  expect(err.message).toBe("Too many requests");
476
410
  });
@@ -479,24 +413,14 @@ describe("connectS2s", () => {
479
413
  const callbacks = makeMockCallbacks();
480
414
  const { raw } = await setupHandle(callbacks);
481
415
 
482
- raw.emit(
483
- "message",
484
- Buffer.from(
485
- JSON.stringify({
486
- type: "error",
487
- message: "Bad gateway",
488
- }),
489
- ),
490
- );
416
+ emitMessage(raw, { type: "error", message: "Bad gateway" });
491
417
 
492
418
  expect(callbacks.onError).toHaveBeenCalledOnce();
493
- const err = (callbacks.onError as ReturnType<typeof vi.fn>).mock.calls[0]?.[0];
419
+ const err = errorArg(callbacks);
494
420
  expect(err).toBeInstanceOf(Error);
495
421
  expect(err.message).toBe("Bad gateway");
496
422
  });
497
423
 
498
- // ─── Audio fast path ───────────────────────────────────────────────────
499
-
500
424
  test("reply.audio dispatches 'onAudio' callback with decoded Uint8Array", async () => {
501
425
  const callbacks = makeMockCallbacks();
502
426
  const { raw } = await setupHandle(callbacks);
@@ -504,15 +428,7 @@ describe("connectS2s", () => {
504
428
  const audioBytes = new Uint8Array([10, 20, 30, 40]);
505
429
  const base64 = Buffer.from(audioBytes).toString("base64");
506
430
 
507
- raw.emit(
508
- "message",
509
- Buffer.from(
510
- JSON.stringify({
511
- type: "reply.audio",
512
- data: base64,
513
- }),
514
- ),
515
- );
431
+ emitMessage(raw, { type: "reply.audio", data: base64 });
516
432
 
517
433
  expect(callbacks.onAudio).toHaveBeenCalledOnce();
518
434
  const payload = (callbacks.onAudio as ReturnType<typeof vi.fn>).mock.calls[0]?.[0];
@@ -520,8 +436,6 @@ describe("connectS2s", () => {
520
436
  expect(Array.from(payload)).toEqual([10, 20, 30, 40]);
521
437
  });
522
438
 
523
- // ─── Edge cases ────────────────────────────────────────────────────────
524
-
525
439
  test("invalid JSON message is logged and ignored", async () => {
526
440
  const { raw, logger } = await setupHandle();
527
441
 
@@ -533,47 +447,23 @@ describe("connectS2s", () => {
533
447
  test("unrecognized message type is logged and ignored", async () => {
534
448
  const { raw, logger } = await setupHandle();
535
449
 
536
- raw.emit(
537
- "message",
538
- Buffer.from(
539
- JSON.stringify({
540
- type: "totally.unknown.type",
541
- }),
542
- ),
543
- );
450
+ emitMessage(raw, { type: "totally.unknown.type" });
544
451
 
545
452
  expect(logger.warn).toHaveBeenCalled();
546
453
  });
547
454
 
548
455
  test("reply.content_part events are silently ignored (no dispatch)", async () => {
549
456
  const { raw } = await setupHandle();
550
- // These types return undefined from S2S_DISPATCH — no event should fire.
551
- raw.emit(
552
- "message",
553
- Buffer.from(
554
- JSON.stringify({
555
- type: "reply.content_part.started",
556
- }),
557
- ),
558
- );
559
- raw.emit(
560
- "message",
561
- Buffer.from(
562
- JSON.stringify({
563
- type: "reply.content_part.done",
564
- }),
565
- ),
566
- );
567
- // No error thrown = pass
457
+ emitMessage(raw, { type: "reply.content_part.started" });
458
+ emitMessage(raw, { type: "reply.content_part.done" });
568
459
  });
569
460
 
570
- test("session.updated is silently ignored (no dispatch)", async () => {
461
+ test("session.updated without config.id is silently ignored (no dispatch)", async () => {
571
462
  const callbacks = makeMockCallbacks();
572
463
  const { raw } = await setupHandle(callbacks);
573
464
 
574
- raw.emit("message", Buffer.from(JSON.stringify({ type: "session.updated" })));
465
+ emitMessage(raw, { type: "session.updated" });
575
466
 
576
- // session.updated is dropped — no callbacks fired
577
467
  expect(callbacks.onSessionReady).not.toHaveBeenCalled();
578
468
  expect(callbacks.onReplyStarted).not.toHaveBeenCalled();
579
469
  expect(callbacks.onReplyDone).not.toHaveBeenCalled();
@@ -581,7 +471,18 @@ describe("connectS2s", () => {
581
471
  expect(callbacks.onSpeechStopped).not.toHaveBeenCalled();
582
472
  });
583
473
 
584
- // ─── Close and error events ────────────────────────────────────────────
474
+ test("session.updated with config.id dispatches 'onSessionReady' callback", async () => {
475
+ const callbacks = makeMockCallbacks();
476
+ const { raw } = await setupHandle(callbacks);
477
+
478
+ emitMessage(raw, {
479
+ type: "session.updated",
480
+ config: { id: "sess_from_updated", system_prompt: "x", tools: [] },
481
+ });
482
+
483
+ expect(callbacks.onSessionReady).toHaveBeenCalledOnce();
484
+ expect(callbacks.onSessionReady).toHaveBeenCalledWith("sess_from_updated");
485
+ });
585
486
 
586
487
  test("close event dispatches 'onClose' callback with code and reason", async () => {
587
488
  const callbacks = makeMockCallbacks();
@@ -600,7 +501,7 @@ describe("connectS2s", () => {
600
501
  raw.emit("error", new Error("ws transport error"));
601
502
 
602
503
  expect(callbacks.onError).toHaveBeenCalledOnce();
603
- const err = (callbacks.onError as ReturnType<typeof vi.fn>).mock.calls[0]?.[0];
504
+ const err = errorArg(callbacks);
604
505
  expect(err).toBeInstanceOf(Error);
605
506
  expect(err.message).toBe("ws transport error");
606
507
  });