@alexkroman1/aai 1.4.5 → 1.5.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.
- package/.turbo/turbo-build.log +9 -9
- package/CHANGELOG.md +13 -0
- package/dist/assemblyai-C969QGi4.js +35 -0
- package/dist/cartesia-BfQPOQ7Y.js +37 -0
- package/dist/host/_pipeline-test-fakes.d.ts +3 -1
- package/dist/host/providers/stt/deepgram.d.ts +28 -0
- package/dist/host/providers/tts/cartesia.d.ts +1 -1
- package/dist/host/providers/tts/rime.d.ts +44 -0
- package/dist/host/runtime-barrel.d.ts +4 -2
- package/dist/host/runtime-barrel.js +1432 -1208
- package/dist/host/runtime.d.ts +2 -2
- package/dist/host/s2s.d.ts +16 -16
- package/dist/host/session-core.d.ts +37 -0
- package/dist/host/transports/pipeline-transport.d.ts +48 -0
- package/dist/host/transports/s2s-transport.d.ts +19 -0
- package/dist/host/transports/types.d.ts +45 -0
- package/dist/host/ws-handler.d.ts +14 -10
- package/dist/sdk/protocol.d.ts +6 -5
- package/dist/sdk/providers/llm-barrel.js +1 -1
- package/dist/sdk/providers/stt/deepgram.d.ts +35 -0
- package/dist/sdk/providers/stt-barrel.d.ts +1 -0
- package/dist/sdk/providers/stt-barrel.js +2 -2
- package/dist/sdk/providers/tts/cartesia.d.ts +12 -4
- package/dist/sdk/providers/tts/rime.d.ts +42 -0
- package/dist/sdk/providers/tts-barrel.d.ts +1 -0
- package/dist/sdk/providers/tts-barrel.js +2 -2
- package/host/_pipeline-test-fakes.ts +6 -3
- package/host/_test-utils.ts +209 -128
- package/host/cleanup.test.ts +25 -298
- package/host/integration/pipeline-reference.integration.test.ts +30 -35
- package/host/providers/resolve.ts +10 -2
- package/host/providers/stt/deepgram.test.ts +229 -0
- package/host/providers/stt/deepgram.ts +172 -0
- package/host/providers/tts/cartesia.ts +7 -3
- package/host/providers/tts/rime.test.ts +251 -0
- package/host/providers/tts/rime.ts +322 -0
- package/host/runtime-barrel.ts +4 -2
- package/host/runtime.test.ts +13 -46
- package/host/runtime.ts +131 -23
- package/host/s2s.test.ts +122 -131
- package/host/s2s.ts +44 -52
- package/host/session-core.test.ts +257 -0
- package/host/session-core.ts +262 -0
- package/host/transports/pipeline-transport.test.ts +651 -0
- package/host/transports/pipeline-transport.ts +532 -0
- package/host/{fixture-replay.test.ts → transports/s2s-transport-fixtures.test.ts} +76 -106
- package/host/transports/s2s-transport.test.ts +56 -0
- package/host/transports/s2s-transport.ts +116 -0
- package/host/transports/types.test.ts +22 -0
- package/host/transports/types.ts +51 -0
- package/host/ws-handler.test.ts +324 -242
- package/host/ws-handler.ts +56 -59
- package/package.json +2 -1
- package/sdk/__snapshots__/exports.test.ts.snap +3 -3
- package/sdk/protocol-compat.test.ts +8 -0
- package/sdk/protocol.ts +6 -5
- package/sdk/providers/stt/deepgram.ts +43 -0
- package/sdk/providers/stt-barrel.ts +2 -0
- package/sdk/providers/tts/cartesia.ts +15 -5
- package/sdk/providers/tts/rime.ts +52 -0
- package/sdk/providers/tts-barrel.ts +2 -0
- package/dist/assemblyai-Cxg9eobY.js +0 -18
- package/dist/cartesia-DwDk2tEu.js +0 -10
- package/dist/host/pipeline-session-ctx.d.ts +0 -24
- package/dist/host/pipeline-session.d.ts +0 -52
- package/dist/host/session-ctx.d.ts +0 -73
- package/dist/host/session.d.ts +0 -62
- package/host/pipeline-session-ctx.test.ts +0 -31
- package/host/pipeline-session-ctx.ts +0 -36
- package/host/pipeline-session.test.ts +0 -672
- package/host/pipeline-session.ts +0 -533
- package/host/s2s-fixtures.test.ts +0 -237
- package/host/session-ctx.test.ts +0 -387
- package/host/session-ctx.ts +0 -134
- package/host/session-fixture-replay.test.ts +0 -128
- package/host/session.test.ts +0 -634
- package/host/session.ts +0 -412
- /package/dist/{anthropic-BrUCPKUc.js → anthropic-CcLZygAr.js} +0 -0
package/host/ws-handler.test.ts
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { describe, expect, test, vi } from "vitest";
|
|
2
2
|
import type { ClientSink } from "../sdk/protocol.ts";
|
|
3
3
|
import { MockWebSocket } from "./_mock-ws.ts";
|
|
4
|
-
import {
|
|
5
|
-
import type {
|
|
4
|
+
import { makeLogger, makeMockCore, silentLogger } from "./_test-utils.ts";
|
|
5
|
+
import type { SessionCore } from "./session-core.ts";
|
|
6
6
|
import { wireSessionSocket } from "./ws-handler.ts";
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
// ─── Test helpers ────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const defaultConfig = { audioFormat: "pcm16" as const, sampleRate: 16_000, ttsSampleRate: 24_000 };
|
|
11
|
+
|
|
12
|
+
/** Simulate a binary frame arriving on the WebSocket. */
|
|
13
|
+
function simulateBinaryFrame(ws: MockWebSocket, frame: Uint8Array): void {
|
|
14
|
+
ws.dispatchEvent(new MessageEvent("message", { data: frame }));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Simulate a string (text) frame arriving on the WebSocket. */
|
|
18
|
+
function simulateTextFrame(ws: MockWebSocket, text: string): void {
|
|
19
|
+
ws.dispatchEvent(new MessageEvent("message", { data: text }));
|
|
10
20
|
}
|
|
11
21
|
|
|
12
22
|
/** Wait until wireSessionSocket has fully initialized (sessionReady = true). */
|
|
@@ -17,9 +27,11 @@ async function waitForSessionReady(logger: { info: ReturnType<typeof vi.fn> }):
|
|
|
17
27
|
});
|
|
18
28
|
}
|
|
19
29
|
|
|
20
|
-
|
|
30
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
21
31
|
|
|
22
32
|
describe("wireSessionSocket", () => {
|
|
33
|
+
// ─── Lifecycle: startup ──────────────────────────────────────────────────
|
|
34
|
+
|
|
23
35
|
test("'Session ready' is not logged until session.start() resolves", async () => {
|
|
24
36
|
const logs: string[] = [];
|
|
25
37
|
const logger = {
|
|
@@ -30,25 +42,26 @@ describe("wireSessionSocket", () => {
|
|
|
30
42
|
};
|
|
31
43
|
|
|
32
44
|
let resolveStart!: () => void;
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
const core = makeMockCore({
|
|
46
|
+
start: vi.fn(
|
|
47
|
+
() =>
|
|
48
|
+
new Promise<void>((r) => {
|
|
49
|
+
resolveStart = r;
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
});
|
|
40
53
|
|
|
41
54
|
const ws = new MockWebSocket("ws://test");
|
|
42
55
|
ws.readyState = MockWebSocket.OPEN;
|
|
43
56
|
|
|
44
57
|
wireSessionSocket(ws, {
|
|
45
58
|
sessions: new Map(),
|
|
46
|
-
createSession: () =>
|
|
59
|
+
createSession: () => core,
|
|
47
60
|
readyConfig: defaultConfig,
|
|
48
61
|
logger,
|
|
49
62
|
});
|
|
50
63
|
|
|
51
|
-
expect(
|
|
64
|
+
expect(core.start).toHaveBeenCalled();
|
|
52
65
|
expect(logs).toContain("Session connected");
|
|
53
66
|
expect(logs).not.toContain("Session ready");
|
|
54
67
|
|
|
@@ -67,15 +80,14 @@ describe("wireSessionSocket", () => {
|
|
|
67
80
|
debug: (msg: string, meta?: Record<string, unknown>) => logs.push({ msg, meta }),
|
|
68
81
|
};
|
|
69
82
|
|
|
70
|
-
const
|
|
71
|
-
session.start = vi.fn(() => Promise.reject(new Error("boom")));
|
|
83
|
+
const core = makeMockCore({ start: vi.fn(() => Promise.reject(new Error("boom"))) });
|
|
72
84
|
|
|
73
85
|
const ws = new MockWebSocket("ws://test");
|
|
74
86
|
ws.readyState = MockWebSocket.OPEN;
|
|
75
87
|
|
|
76
88
|
wireSessionSocket(ws, {
|
|
77
89
|
sessions: new Map(),
|
|
78
|
-
createSession: () =>
|
|
90
|
+
createSession: () => core,
|
|
79
91
|
readyConfig: defaultConfig,
|
|
80
92
|
logger,
|
|
81
93
|
});
|
|
@@ -87,32 +99,31 @@ describe("wireSessionSocket", () => {
|
|
|
87
99
|
});
|
|
88
100
|
|
|
89
101
|
test("session is added to sessions map on open", () => {
|
|
90
|
-
const sessions = new Map<string,
|
|
91
|
-
const
|
|
102
|
+
const sessions = new Map<string, SessionCore>();
|
|
103
|
+
const core = makeMockCore();
|
|
92
104
|
|
|
93
105
|
const ws = new MockWebSocket("ws://test");
|
|
94
106
|
ws.readyState = MockWebSocket.OPEN;
|
|
95
107
|
|
|
96
108
|
wireSessionSocket(ws, {
|
|
97
109
|
sessions,
|
|
98
|
-
createSession: () =>
|
|
110
|
+
createSession: () => core,
|
|
99
111
|
readyConfig: defaultConfig,
|
|
100
112
|
});
|
|
101
113
|
|
|
102
114
|
expect(sessions.size).toBe(1);
|
|
103
|
-
expect([...sessions.values()][0]).toBe(
|
|
115
|
+
expect([...sessions.values()][0]).toBe(core);
|
|
104
116
|
});
|
|
105
117
|
|
|
106
118
|
test("session is removed from sessions map on close", async () => {
|
|
107
|
-
const sessions = new Map<string,
|
|
108
|
-
const session = makeStubSession();
|
|
119
|
+
const sessions = new Map<string, SessionCore>();
|
|
109
120
|
|
|
110
121
|
const ws = new MockWebSocket("ws://test");
|
|
111
122
|
ws.readyState = MockWebSocket.OPEN;
|
|
112
123
|
|
|
113
124
|
wireSessionSocket(ws, {
|
|
114
125
|
sessions,
|
|
115
|
-
createSession: () =>
|
|
126
|
+
createSession: () => makeMockCore(),
|
|
116
127
|
readyConfig: defaultConfig,
|
|
117
128
|
});
|
|
118
129
|
|
|
@@ -124,134 +135,156 @@ describe("wireSessionSocket", () => {
|
|
|
124
135
|
});
|
|
125
136
|
});
|
|
126
137
|
|
|
127
|
-
|
|
138
|
+
// ─── CONFIG frame on open ────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
test("sends CONFIG JSON frame as first message on open", () => {
|
|
128
141
|
const ws = new MockWebSocket("ws://test");
|
|
129
142
|
ws.readyState = MockWebSocket.OPEN;
|
|
130
143
|
|
|
131
144
|
wireSessionSocket(ws, {
|
|
132
145
|
sessions: new Map(),
|
|
133
|
-
createSession: () =>
|
|
146
|
+
createSession: () => makeMockCore(),
|
|
134
147
|
readyConfig: defaultConfig,
|
|
148
|
+
logger: silentLogger,
|
|
135
149
|
});
|
|
136
150
|
|
|
137
|
-
|
|
138
|
-
|
|
151
|
+
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
156
|
});
|
|
140
157
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
test("Uint8Array binary data is forwarded to session.onAudio", async () => {
|
|
144
|
-
const session = makeStubSession();
|
|
158
|
+
test("CONFIG frame contains correct sampleRate and ttsSampleRate", () => {
|
|
145
159
|
const ws = new MockWebSocket("ws://test");
|
|
146
160
|
ws.readyState = MockWebSocket.OPEN;
|
|
147
|
-
const logger = makeLogger();
|
|
148
161
|
|
|
149
162
|
wireSessionSocket(ws, {
|
|
150
163
|
sessions: new Map(),
|
|
151
|
-
createSession: () =>
|
|
164
|
+
createSession: () => makeMockCore(),
|
|
152
165
|
readyConfig: defaultConfig,
|
|
153
|
-
logger,
|
|
166
|
+
logger: silentLogger,
|
|
154
167
|
});
|
|
155
168
|
|
|
156
|
-
|
|
169
|
+
const firstFrame = ws.sent[0];
|
|
170
|
+
const msg = JSON.parse(firstFrame as string);
|
|
171
|
+
expect(msg.type).toBe("config");
|
|
172
|
+
expect(msg.audioFormat).toBe("pcm16");
|
|
173
|
+
expect(msg.sampleRate).toBe(16_000);
|
|
174
|
+
expect(msg.ttsSampleRate).toBe(24_000);
|
|
175
|
+
});
|
|
157
176
|
|
|
158
|
-
|
|
159
|
-
ws
|
|
177
|
+
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>();
|
|
181
|
+
let capturedId: string | undefined;
|
|
160
182
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
183
|
+
wireSessionSocket(ws, {
|
|
184
|
+
sessions,
|
|
185
|
+
createSession: (sid) => {
|
|
186
|
+
capturedId = sid;
|
|
187
|
+
return makeMockCore();
|
|
188
|
+
},
|
|
189
|
+
readyConfig: defaultConfig,
|
|
190
|
+
logger: silentLogger,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const firstFrame = ws.sent[0];
|
|
194
|
+
const msg = JSON.parse(firstFrame as string);
|
|
195
|
+
expect(msg.type).toBe("config");
|
|
196
|
+
expect(msg.sessionId).toBeTruthy();
|
|
197
|
+
expect(msg.sessionId).toBe(capturedId);
|
|
164
198
|
});
|
|
165
199
|
|
|
166
|
-
|
|
167
|
-
|
|
200
|
+
// ─── Inbound C2S frame routing ───────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
test("raw binary Uint8Array routes to session.onAudio", async () => {
|
|
203
|
+
const core = makeMockCore();
|
|
168
204
|
const ws = new MockWebSocket("ws://test");
|
|
169
205
|
ws.readyState = MockWebSocket.OPEN;
|
|
170
206
|
const logger = makeLogger();
|
|
171
207
|
|
|
172
208
|
wireSessionSocket(ws, {
|
|
173
209
|
sessions: new Map(),
|
|
174
|
-
createSession: () =>
|
|
210
|
+
createSession: () => core,
|
|
175
211
|
readyConfig: defaultConfig,
|
|
176
212
|
logger,
|
|
177
213
|
});
|
|
178
214
|
|
|
179
215
|
await waitForSessionReady(logger);
|
|
180
216
|
|
|
181
|
-
const
|
|
182
|
-
ws
|
|
217
|
+
const pcm = new Uint8Array([1, 2, 3, 4]);
|
|
218
|
+
simulateBinaryFrame(ws, pcm);
|
|
183
219
|
|
|
184
|
-
expect(
|
|
220
|
+
expect(core.onAudio).toHaveBeenCalledOnce();
|
|
221
|
+
const passed = (core.onAudio as ReturnType<typeof vi.fn>).mock.calls[0]?.[0];
|
|
222
|
+
expect(passed).toBeInstanceOf(Uint8Array);
|
|
185
223
|
});
|
|
186
224
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
test("audio_ready message calls session.onAudioReady", async () => {
|
|
190
|
-
const session = makeStubSession();
|
|
225
|
+
test("audio_ready JSON text frame routes to session.onAudioReady", async () => {
|
|
226
|
+
const core = makeMockCore();
|
|
191
227
|
const ws = new MockWebSocket("ws://test");
|
|
192
228
|
ws.readyState = MockWebSocket.OPEN;
|
|
193
229
|
const logger = makeLogger();
|
|
194
230
|
|
|
195
231
|
wireSessionSocket(ws, {
|
|
196
232
|
sessions: new Map(),
|
|
197
|
-
createSession: () =>
|
|
233
|
+
createSession: () => core,
|
|
198
234
|
readyConfig: defaultConfig,
|
|
199
235
|
logger,
|
|
200
236
|
});
|
|
201
237
|
|
|
202
238
|
await waitForSessionReady(logger);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
expect(session.onAudioReady).toHaveBeenCalledOnce();
|
|
239
|
+
simulateTextFrame(ws, JSON.stringify({ type: "audio_ready" }));
|
|
240
|
+
expect(core.onAudioReady).toHaveBeenCalledOnce();
|
|
206
241
|
});
|
|
207
242
|
|
|
208
|
-
test("cancel
|
|
209
|
-
const
|
|
243
|
+
test("cancel JSON text frame routes to session.onCancel", async () => {
|
|
244
|
+
const core = makeMockCore();
|
|
210
245
|
const ws = new MockWebSocket("ws://test");
|
|
211
246
|
ws.readyState = MockWebSocket.OPEN;
|
|
212
247
|
const logger = makeLogger();
|
|
213
248
|
|
|
214
249
|
wireSessionSocket(ws, {
|
|
215
250
|
sessions: new Map(),
|
|
216
|
-
createSession: () =>
|
|
251
|
+
createSession: () => core,
|
|
217
252
|
readyConfig: defaultConfig,
|
|
218
253
|
logger,
|
|
219
254
|
});
|
|
220
255
|
|
|
221
256
|
await waitForSessionReady(logger);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
expect(session.onCancel).toHaveBeenCalledOnce();
|
|
257
|
+
simulateTextFrame(ws, JSON.stringify({ type: "cancel" }));
|
|
258
|
+
expect(core.onCancel).toHaveBeenCalledOnce();
|
|
225
259
|
});
|
|
226
260
|
|
|
227
|
-
test("reset
|
|
228
|
-
const
|
|
261
|
+
test("reset JSON text frame routes to session.onReset", async () => {
|
|
262
|
+
const core = makeMockCore();
|
|
229
263
|
const ws = new MockWebSocket("ws://test");
|
|
230
264
|
ws.readyState = MockWebSocket.OPEN;
|
|
231
265
|
const logger = makeLogger();
|
|
232
266
|
|
|
233
267
|
wireSessionSocket(ws, {
|
|
234
268
|
sessions: new Map(),
|
|
235
|
-
createSession: () =>
|
|
269
|
+
createSession: () => core,
|
|
236
270
|
readyConfig: defaultConfig,
|
|
237
271
|
logger,
|
|
238
272
|
});
|
|
239
273
|
|
|
240
274
|
await waitForSessionReady(logger);
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
expect(session.onReset).toHaveBeenCalledOnce();
|
|
275
|
+
simulateTextFrame(ws, JSON.stringify({ type: "reset" }));
|
|
276
|
+
expect(core.onReset).toHaveBeenCalledOnce();
|
|
244
277
|
});
|
|
245
278
|
|
|
246
|
-
test("history
|
|
247
|
-
const
|
|
279
|
+
test("history JSON text frame routes to session.onHistory with decoded messages", async () => {
|
|
280
|
+
const core = makeMockCore();
|
|
248
281
|
const ws = new MockWebSocket("ws://test");
|
|
249
282
|
ws.readyState = MockWebSocket.OPEN;
|
|
250
283
|
const logger = makeLogger();
|
|
251
284
|
|
|
252
285
|
wireSessionSocket(ws, {
|
|
253
286
|
sessions: new Map(),
|
|
254
|
-
createSession: () =>
|
|
287
|
+
createSession: () => core,
|
|
255
288
|
readyConfig: defaultConfig,
|
|
256
289
|
logger,
|
|
257
290
|
});
|
|
@@ -260,169 +293,123 @@ describe("wireSessionSocket", () => {
|
|
|
260
293
|
|
|
261
294
|
const messages = [
|
|
262
295
|
{ role: "user" as const, content: "Hello" },
|
|
263
|
-
{ role: "assistant" as const, content: "Hi" },
|
|
296
|
+
{ role: "assistant" as const, content: "Hi there" },
|
|
264
297
|
];
|
|
265
|
-
ws
|
|
266
|
-
expect(
|
|
298
|
+
simulateTextFrame(ws, JSON.stringify({ type: "history", messages }));
|
|
299
|
+
expect(core.onHistory).toHaveBeenCalledOnce();
|
|
300
|
+
const passed = (core.onHistory as ReturnType<typeof vi.fn>).mock.calls[0]?.[0];
|
|
301
|
+
expect(passed).toEqual(messages);
|
|
267
302
|
});
|
|
268
303
|
|
|
269
|
-
|
|
270
|
-
|
|
304
|
+
// ─── Text message error handling ─────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
test("invalid JSON text frame is dropped with warning, session not closed", async () => {
|
|
307
|
+
const core = makeMockCore();
|
|
271
308
|
const ws = new MockWebSocket("ws://test");
|
|
272
309
|
ws.readyState = MockWebSocket.OPEN;
|
|
273
|
-
const logger =
|
|
310
|
+
const logger = makeLogger();
|
|
274
311
|
|
|
275
312
|
wireSessionSocket(ws, {
|
|
276
313
|
sessions: new Map(),
|
|
277
|
-
createSession: () =>
|
|
314
|
+
createSession: () => core,
|
|
278
315
|
readyConfig: defaultConfig,
|
|
279
316
|
logger,
|
|
280
317
|
});
|
|
281
318
|
|
|
282
319
|
await waitForSessionReady(logger);
|
|
283
320
|
|
|
284
|
-
ws
|
|
285
|
-
expect(logger.warn).toHaveBeenCalledWith("
|
|
321
|
+
simulateTextFrame(ws, "this is not json{{{");
|
|
322
|
+
expect(logger.warn).toHaveBeenCalledWith("ws: invalid JSON; dropping", expect.any(Object));
|
|
323
|
+
// Session methods must not be called
|
|
324
|
+
expect(core.onAudioReady).not.toHaveBeenCalled();
|
|
325
|
+
// Socket must still be open (not closed)
|
|
326
|
+
expect(ws.readyState).toBe(MockWebSocket.OPEN);
|
|
286
327
|
});
|
|
287
328
|
|
|
288
|
-
test("
|
|
289
|
-
const
|
|
329
|
+
test("unknown client message type is silently dropped", async () => {
|
|
330
|
+
const core = makeMockCore();
|
|
290
331
|
const ws = new MockWebSocket("ws://test");
|
|
291
332
|
ws.readyState = MockWebSocket.OPEN;
|
|
292
|
-
const logger =
|
|
333
|
+
const logger = makeLogger();
|
|
293
334
|
|
|
294
335
|
wireSessionSocket(ws, {
|
|
295
336
|
sessions: new Map(),
|
|
296
|
-
createSession: () =>
|
|
337
|
+
createSession: () => core,
|
|
297
338
|
readyConfig: defaultConfig,
|
|
298
339
|
logger,
|
|
299
340
|
});
|
|
300
341
|
|
|
301
342
|
await waitForSessionReady(logger);
|
|
302
343
|
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
|
|
344
|
+
// Valid JSON with a valid { type } envelope but unknown type — lenientParse returns ok:false, malformed:false
|
|
345
|
+
simulateTextFrame(ws, JSON.stringify({ type: "some_future_message_type" }));
|
|
346
|
+
// Must NOT warn — rolling-upgrade tolerance
|
|
306
347
|
expect(logger.warn).not.toHaveBeenCalled();
|
|
348
|
+
expect(core.onAudioReady).not.toHaveBeenCalled();
|
|
349
|
+
expect(ws.readyState).toBe(MockWebSocket.OPEN);
|
|
307
350
|
});
|
|
308
351
|
|
|
309
|
-
// ───
|
|
310
|
-
|
|
311
|
-
test("ClientSink.event sends JSON text via ws.send", () => {
|
|
312
|
-
let capturedClient!: ClientSink;
|
|
313
|
-
const ws = new MockWebSocket("ws://test");
|
|
314
|
-
ws.readyState = MockWebSocket.OPEN;
|
|
315
|
-
|
|
316
|
-
wireSessionSocket(ws, {
|
|
317
|
-
sessions: new Map(),
|
|
318
|
-
createSession: (_sid, client) => {
|
|
319
|
-
capturedClient = client;
|
|
320
|
-
return makeStubSession();
|
|
321
|
-
},
|
|
322
|
-
readyConfig: defaultConfig,
|
|
323
|
-
logger: silentLogger,
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
capturedClient.event({ type: "speech_started" });
|
|
327
|
-
const sentStrings = ws.sent.filter((d): d is string => typeof d === "string");
|
|
328
|
-
expect(sentStrings).toContainEqual(expect.stringContaining('"speech_started"'));
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
test("ClientSink.playAudioChunk sends binary data", () => {
|
|
332
|
-
let capturedClient!: ClientSink;
|
|
333
|
-
const ws = new MockWebSocket("ws://test");
|
|
334
|
-
ws.readyState = MockWebSocket.OPEN;
|
|
352
|
+
// ─── Message buffering ───────────────────────────────────────────────────
|
|
335
353
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
354
|
+
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
|
+
),
|
|
344
363
|
});
|
|
345
364
|
|
|
346
|
-
const chunk = new Uint8Array([10, 20, 30]);
|
|
347
|
-
capturedClient.playAudioChunk(chunk);
|
|
348
|
-
expect(ws.sent).toContain(chunk);
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
test("ClientSink.playAudioDone sends audio_done JSON", () => {
|
|
352
|
-
let capturedClient!: ClientSink;
|
|
353
365
|
const ws = new MockWebSocket("ws://test");
|
|
354
366
|
ws.readyState = MockWebSocket.OPEN;
|
|
367
|
+
const logger = makeLogger();
|
|
355
368
|
|
|
356
369
|
wireSessionSocket(ws, {
|
|
357
370
|
sessions: new Map(),
|
|
358
|
-
createSession: (
|
|
359
|
-
capturedClient = client;
|
|
360
|
-
return makeStubSession();
|
|
361
|
-
},
|
|
371
|
+
createSession: () => core,
|
|
362
372
|
readyConfig: defaultConfig,
|
|
363
|
-
logger
|
|
373
|
+
logger,
|
|
364
374
|
});
|
|
365
375
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
expect(
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
test("ClientSink.open reflects ws.readyState", () => {
|
|
372
|
-
let capturedClient!: ClientSink;
|
|
373
|
-
const ws = new MockWebSocket("ws://test");
|
|
374
|
-
ws.readyState = MockWebSocket.OPEN;
|
|
376
|
+
// Session not ready yet — send a cancel text frame
|
|
377
|
+
simulateTextFrame(ws, JSON.stringify({ type: "cancel" }));
|
|
378
|
+
expect(core.onCancel).not.toHaveBeenCalled();
|
|
375
379
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
capturedClient = client;
|
|
380
|
-
return makeStubSession();
|
|
381
|
-
},
|
|
382
|
-
readyConfig: defaultConfig,
|
|
383
|
-
logger: silentLogger,
|
|
384
|
-
});
|
|
380
|
+
// Now let start() resolve
|
|
381
|
+
resolveStart();
|
|
382
|
+
await waitForSessionReady(logger);
|
|
385
383
|
|
|
386
|
-
expect(
|
|
387
|
-
ws.readyState = MockWebSocket.CLOSED;
|
|
388
|
-
expect(capturedClient.open).toBe(false);
|
|
384
|
+
expect(core.onCancel).toHaveBeenCalledOnce();
|
|
389
385
|
});
|
|
390
386
|
|
|
391
|
-
test("
|
|
392
|
-
let capturedClient!: ClientSink;
|
|
387
|
+
test("messages before session is created (no open yet) are ignored", () => {
|
|
393
388
|
const ws = new MockWebSocket("ws://test");
|
|
394
|
-
ws.readyState = MockWebSocket.
|
|
389
|
+
ws.readyState = MockWebSocket.CONNECTING;
|
|
395
390
|
|
|
396
391
|
wireSessionSocket(ws, {
|
|
397
392
|
sessions: new Map(),
|
|
398
|
-
createSession: (
|
|
399
|
-
capturedClient = client;
|
|
400
|
-
return makeStubSession();
|
|
401
|
-
},
|
|
393
|
+
createSession: () => makeMockCore(),
|
|
402
394
|
readyConfig: defaultConfig,
|
|
403
395
|
logger: silentLogger,
|
|
404
396
|
});
|
|
405
397
|
|
|
406
|
-
//
|
|
407
|
-
ws.
|
|
408
|
-
|
|
409
|
-
};
|
|
410
|
-
// Should not throw
|
|
411
|
-
capturedClient.event({ type: "speech_started" });
|
|
412
|
-
capturedClient.playAudioChunk(new Uint8Array([1]));
|
|
413
|
-
capturedClient.playAudioDone();
|
|
398
|
+
// No open yet — session is null, should be silently ignored
|
|
399
|
+
simulateTextFrame(ws, JSON.stringify({ type: "audio_ready" }));
|
|
400
|
+
// No error thrown
|
|
414
401
|
});
|
|
415
402
|
|
|
416
|
-
// ─── Close handler
|
|
403
|
+
// ─── Close handler ───────────────────────────────────────────────────────
|
|
417
404
|
|
|
418
405
|
test("close handler calls session.stop", async () => {
|
|
419
|
-
const
|
|
406
|
+
const core = makeMockCore();
|
|
420
407
|
const ws = new MockWebSocket("ws://test");
|
|
421
408
|
ws.readyState = MockWebSocket.OPEN;
|
|
422
409
|
|
|
423
410
|
wireSessionSocket(ws, {
|
|
424
411
|
sessions: new Map(),
|
|
425
|
-
createSession: () =>
|
|
412
|
+
createSession: () => core,
|
|
426
413
|
readyConfig: defaultConfig,
|
|
427
414
|
logger: silentLogger,
|
|
428
415
|
});
|
|
@@ -430,20 +417,20 @@ describe("wireSessionSocket", () => {
|
|
|
430
417
|
ws.close();
|
|
431
418
|
|
|
432
419
|
await vi.waitFor(() => {
|
|
433
|
-
expect(
|
|
420
|
+
expect(core.stop).toHaveBeenCalledOnce();
|
|
434
421
|
});
|
|
435
422
|
});
|
|
436
423
|
|
|
437
|
-
// ─── Error handler
|
|
424
|
+
// ─── Error handler ───────────────────────────────────────────────────────
|
|
438
425
|
|
|
439
426
|
test("error event is logged", () => {
|
|
440
427
|
const ws = new MockWebSocket("ws://test");
|
|
441
428
|
ws.readyState = MockWebSocket.OPEN;
|
|
442
|
-
const logger =
|
|
429
|
+
const logger = makeLogger();
|
|
443
430
|
|
|
444
431
|
wireSessionSocket(ws, {
|
|
445
432
|
sessions: new Map(),
|
|
446
|
-
createSession: () =>
|
|
433
|
+
createSession: () => makeMockCore(),
|
|
447
434
|
readyConfig: defaultConfig,
|
|
448
435
|
logger,
|
|
449
436
|
});
|
|
@@ -461,11 +448,11 @@ describe("wireSessionSocket", () => {
|
|
|
461
448
|
test("generic error event logs default message", () => {
|
|
462
449
|
const ws = new MockWebSocket("ws://test");
|
|
463
450
|
ws.readyState = MockWebSocket.OPEN;
|
|
464
|
-
const logger =
|
|
451
|
+
const logger = makeLogger();
|
|
465
452
|
|
|
466
453
|
wireSessionSocket(ws, {
|
|
467
454
|
sessions: new Map(),
|
|
468
|
-
createSession: () =>
|
|
455
|
+
createSession: () => makeMockCore(),
|
|
469
456
|
readyConfig: defaultConfig,
|
|
470
457
|
logger,
|
|
471
458
|
});
|
|
@@ -478,7 +465,7 @@ describe("wireSessionSocket", () => {
|
|
|
478
465
|
);
|
|
479
466
|
});
|
|
480
467
|
|
|
481
|
-
// ─── Callbacks
|
|
468
|
+
// ─── Callbacks ───────────────────────────────────────────────────────────
|
|
482
469
|
|
|
483
470
|
test("onOpen callback is invoked when socket opens", () => {
|
|
484
471
|
const onOpen = vi.fn();
|
|
@@ -487,7 +474,7 @@ describe("wireSessionSocket", () => {
|
|
|
487
474
|
|
|
488
475
|
wireSessionSocket(ws, {
|
|
489
476
|
sessions: new Map(),
|
|
490
|
-
createSession: () =>
|
|
477
|
+
createSession: () => makeMockCore(),
|
|
491
478
|
readyConfig: defaultConfig,
|
|
492
479
|
onOpen,
|
|
493
480
|
logger: silentLogger,
|
|
@@ -503,7 +490,7 @@ describe("wireSessionSocket", () => {
|
|
|
503
490
|
|
|
504
491
|
wireSessionSocket(ws, {
|
|
505
492
|
sessions: new Map(),
|
|
506
|
-
createSession: () =>
|
|
493
|
+
createSession: () => makeMockCore(),
|
|
507
494
|
readyConfig: defaultConfig,
|
|
508
495
|
onClose,
|
|
509
496
|
logger: silentLogger,
|
|
@@ -517,17 +504,16 @@ describe("wireSessionSocket", () => {
|
|
|
517
504
|
const onSessionEnd = vi.fn();
|
|
518
505
|
const ws = new MockWebSocket("ws://test");
|
|
519
506
|
ws.readyState = MockWebSocket.OPEN;
|
|
520
|
-
const sessions = new Map<string,
|
|
507
|
+
const sessions = new Map<string, SessionCore>();
|
|
521
508
|
|
|
522
509
|
wireSessionSocket(ws, {
|
|
523
510
|
sessions,
|
|
524
|
-
createSession: () =>
|
|
511
|
+
createSession: () => makeMockCore(),
|
|
525
512
|
readyConfig: defaultConfig,
|
|
526
513
|
onSessionEnd,
|
|
527
514
|
logger: silentLogger,
|
|
528
515
|
});
|
|
529
516
|
|
|
530
|
-
// Session is now in the map
|
|
531
517
|
expect(sessions.size).toBe(1);
|
|
532
518
|
const sessionId = [...sessions.keys()][0] ?? "";
|
|
533
519
|
|
|
@@ -540,24 +526,138 @@ describe("wireSessionSocket", () => {
|
|
|
540
526
|
expect(sessions.size).toBe(0);
|
|
541
527
|
});
|
|
542
528
|
|
|
543
|
-
|
|
529
|
+
test("onSinkCreated callback is invoked with sessionId and ClientSink", () => {
|
|
530
|
+
const onSinkCreated = vi.fn();
|
|
531
|
+
const ws = new MockWebSocket("ws://test");
|
|
532
|
+
ws.readyState = MockWebSocket.OPEN;
|
|
533
|
+
|
|
534
|
+
wireSessionSocket(ws, {
|
|
535
|
+
sessions: new Map(),
|
|
536
|
+
createSession: () => makeMockCore(),
|
|
537
|
+
readyConfig: defaultConfig,
|
|
538
|
+
onSinkCreated,
|
|
539
|
+
logger: silentLogger,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
expect(onSinkCreated).toHaveBeenCalledOnce();
|
|
543
|
+
expect(typeof onSinkCreated.mock.calls[0]?.[0]).toBe("string");
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// ─── ClientSink (indirect testing via createSession capture) ─────────────
|
|
547
|
+
|
|
548
|
+
test("ClientSink.open reflects ws.readyState", () => {
|
|
549
|
+
let capturedClient!: ClientSink;
|
|
550
|
+
const ws = new MockWebSocket("ws://test");
|
|
551
|
+
ws.readyState = MockWebSocket.OPEN;
|
|
552
|
+
|
|
553
|
+
wireSessionSocket(ws, {
|
|
554
|
+
sessions: new Map(),
|
|
555
|
+
createSession: (_sid, client) => {
|
|
556
|
+
capturedClient = client;
|
|
557
|
+
return makeMockCore();
|
|
558
|
+
},
|
|
559
|
+
readyConfig: defaultConfig,
|
|
560
|
+
logger: silentLogger,
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
expect(capturedClient.open).toBe(true);
|
|
564
|
+
ws.readyState = MockWebSocket.CLOSED;
|
|
565
|
+
expect(capturedClient.open).toBe(false);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
test("ClientSink.playAudioChunk sends raw binary Uint8Array", () => {
|
|
569
|
+
let capturedClient!: ClientSink;
|
|
570
|
+
const ws = new MockWebSocket("ws://test");
|
|
571
|
+
ws.readyState = MockWebSocket.OPEN;
|
|
572
|
+
|
|
573
|
+
wireSessionSocket(ws, {
|
|
574
|
+
sessions: new Map(),
|
|
575
|
+
createSession: (_sid, client) => {
|
|
576
|
+
capturedClient = client;
|
|
577
|
+
return makeMockCore();
|
|
578
|
+
},
|
|
579
|
+
readyConfig: defaultConfig,
|
|
580
|
+
logger: silentLogger,
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
const chunk = new Uint8Array([10, 20, 30]);
|
|
584
|
+
capturedClient.playAudioChunk(chunk);
|
|
585
|
+
|
|
586
|
+
// Find binary frames in sent (skip the initial config JSON string)
|
|
587
|
+
const binaryFrames = (ws.sent as unknown[]).filter((d) => d instanceof Uint8Array);
|
|
588
|
+
expect(binaryFrames.length).toBeGreaterThanOrEqual(1);
|
|
589
|
+
expect(binaryFrames[0]).toBe(chunk);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
test("ClientSink.playAudioDone sends audio_done JSON text frame", () => {
|
|
593
|
+
let capturedClient!: ClientSink;
|
|
594
|
+
const ws = new MockWebSocket("ws://test");
|
|
595
|
+
ws.readyState = MockWebSocket.OPEN;
|
|
596
|
+
|
|
597
|
+
wireSessionSocket(ws, {
|
|
598
|
+
sessions: new Map(),
|
|
599
|
+
createSession: (_sid, client) => {
|
|
600
|
+
capturedClient = client;
|
|
601
|
+
return makeMockCore();
|
|
602
|
+
},
|
|
603
|
+
readyConfig: defaultConfig,
|
|
604
|
+
logger: silentLogger,
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
capturedClient.playAudioDone();
|
|
608
|
+
|
|
609
|
+
// Find JSON string frames after the initial config
|
|
610
|
+
const textFrames = (ws.sent as unknown[])
|
|
611
|
+
.filter((d): d is string => typeof d === "string")
|
|
612
|
+
.map((s) => JSON.parse(s));
|
|
613
|
+
const audioDoneFrame = textFrames.find((m) => m.type === "audio_done");
|
|
614
|
+
expect(audioDoneFrame).toBeDefined();
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test("ClientSink tolerates ws.send throwing (closed socket)", () => {
|
|
618
|
+
let capturedClient!: ClientSink;
|
|
619
|
+
const ws = new MockWebSocket("ws://test");
|
|
620
|
+
ws.readyState = MockWebSocket.OPEN;
|
|
621
|
+
|
|
622
|
+
wireSessionSocket(ws, {
|
|
623
|
+
sessions: new Map(),
|
|
624
|
+
createSession: (_sid, client) => {
|
|
625
|
+
capturedClient = client;
|
|
626
|
+
return makeMockCore();
|
|
627
|
+
},
|
|
628
|
+
readyConfig: defaultConfig,
|
|
629
|
+
logger: silentLogger,
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// Override send to throw
|
|
633
|
+
ws.send = () => {
|
|
634
|
+
throw new Error("socket closed");
|
|
635
|
+
};
|
|
636
|
+
// Should not throw
|
|
637
|
+
capturedClient.event({ type: "speech_started" });
|
|
638
|
+
capturedClient.playAudioChunk(new Uint8Array([1]));
|
|
639
|
+
capturedClient.playAudioDone();
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
// ─── Concurrency regression tests ────────────────────────────────────────
|
|
544
643
|
|
|
545
644
|
test("close during start() does not double-stop or throw", async () => {
|
|
546
645
|
let resolveStart!: () => void;
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
646
|
+
const core = makeMockCore({
|
|
647
|
+
start: vi.fn(
|
|
648
|
+
() =>
|
|
649
|
+
new Promise<void>((r) => {
|
|
650
|
+
resolveStart = r;
|
|
651
|
+
}),
|
|
652
|
+
),
|
|
653
|
+
});
|
|
554
654
|
const ws = new MockWebSocket("ws://test");
|
|
555
655
|
ws.readyState = MockWebSocket.OPEN;
|
|
556
|
-
const sessions = new Map<string,
|
|
656
|
+
const sessions = new Map<string, SessionCore>();
|
|
557
657
|
|
|
558
658
|
wireSessionSocket(ws, {
|
|
559
659
|
sessions,
|
|
560
|
-
createSession: () =>
|
|
660
|
+
createSession: () => core,
|
|
561
661
|
readyConfig: defaultConfig,
|
|
562
662
|
logger: silentLogger,
|
|
563
663
|
});
|
|
@@ -568,25 +668,23 @@ describe("wireSessionSocket", () => {
|
|
|
568
668
|
// Now start() resolves
|
|
569
669
|
resolveStart();
|
|
570
670
|
await vi.waitFor(() => {
|
|
571
|
-
expect(
|
|
671
|
+
expect(core.stop).toHaveBeenCalledOnce();
|
|
572
672
|
});
|
|
573
673
|
});
|
|
574
674
|
|
|
575
675
|
test("start() failure removes session from map before close", async () => {
|
|
576
|
-
const
|
|
577
|
-
session.start = vi.fn(() => Promise.reject(new Error("boom")));
|
|
676
|
+
const core = makeMockCore({ start: vi.fn(() => Promise.reject(new Error("boom"))) });
|
|
578
677
|
const ws = new MockWebSocket("ws://test");
|
|
579
678
|
ws.readyState = MockWebSocket.OPEN;
|
|
580
|
-
const sessions = new Map<string,
|
|
679
|
+
const sessions = new Map<string, SessionCore>();
|
|
581
680
|
|
|
582
681
|
wireSessionSocket(ws, {
|
|
583
682
|
sessions,
|
|
584
|
-
createSession: () =>
|
|
683
|
+
createSession: () => core,
|
|
585
684
|
readyConfig: defaultConfig,
|
|
586
685
|
logger: silentLogger,
|
|
587
686
|
});
|
|
588
687
|
|
|
589
|
-
// Wait for start() rejection to propagate
|
|
590
688
|
await vi.waitFor(() => {
|
|
591
689
|
expect(sessions.size).toBe(0);
|
|
592
690
|
});
|
|
@@ -595,25 +693,25 @@ describe("wireSessionSocket", () => {
|
|
|
595
693
|
ws.close();
|
|
596
694
|
});
|
|
597
695
|
|
|
598
|
-
// ─── Session start timeout
|
|
696
|
+
// ─── Session start timeout ────────────────────────────────────────────────
|
|
599
697
|
|
|
600
698
|
test("session.start() timeout triggers 'Session start failed'", async () => {
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
);
|
|
699
|
+
const core = makeMockCore({
|
|
700
|
+
start: vi.fn(
|
|
701
|
+
() =>
|
|
702
|
+
new Promise<void>(() => {
|
|
703
|
+
/* intentionally never resolves */
|
|
704
|
+
}),
|
|
705
|
+
),
|
|
706
|
+
});
|
|
609
707
|
|
|
610
708
|
const ws = new MockWebSocket("ws://test");
|
|
611
709
|
ws.readyState = MockWebSocket.OPEN;
|
|
612
|
-
const sessions = new Map<string,
|
|
710
|
+
const sessions = new Map<string, SessionCore>();
|
|
613
711
|
|
|
614
712
|
wireSessionSocket(ws, {
|
|
615
713
|
sessions,
|
|
616
|
-
createSession: () =>
|
|
714
|
+
createSession: () => core,
|
|
617
715
|
readyConfig: defaultConfig,
|
|
618
716
|
logger: silentLogger,
|
|
619
717
|
sessionStartTimeoutMs: 50,
|
|
@@ -634,52 +732,34 @@ describe("wireSessionSocket", () => {
|
|
|
634
732
|
);
|
|
635
733
|
});
|
|
636
734
|
|
|
637
|
-
// ─── Socket not yet open
|
|
735
|
+
// ─── Socket not yet open ──────────────────────────────────────────────────
|
|
638
736
|
|
|
639
737
|
test("waits for open event when readyState is not OPEN", async () => {
|
|
640
|
-
const
|
|
738
|
+
const core = makeMockCore();
|
|
641
739
|
const ws = new MockWebSocket("ws://test");
|
|
642
740
|
ws.readyState = MockWebSocket.CONNECTING;
|
|
643
741
|
|
|
644
742
|
wireSessionSocket(ws, {
|
|
645
743
|
sessions: new Map(),
|
|
646
|
-
createSession: () =>
|
|
744
|
+
createSession: () => core,
|
|
647
745
|
readyConfig: defaultConfig,
|
|
648
746
|
logger: silentLogger,
|
|
649
747
|
});
|
|
650
748
|
|
|
651
749
|
// Session not started yet — waiting for open
|
|
652
|
-
expect(
|
|
750
|
+
expect(core.start).not.toHaveBeenCalled();
|
|
653
751
|
|
|
654
752
|
// Simulate open
|
|
655
753
|
ws.readyState = MockWebSocket.OPEN;
|
|
656
754
|
ws.dispatchEvent(new Event("open"));
|
|
657
755
|
|
|
658
|
-
expect(
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
// ─── No session ignores messages ───────────────────────────────────────
|
|
662
|
-
|
|
663
|
-
test("messages before session is created are ignored", () => {
|
|
664
|
-
const ws = new MockWebSocket("ws://test");
|
|
665
|
-
ws.readyState = MockWebSocket.CONNECTING;
|
|
666
|
-
|
|
667
|
-
wireSessionSocket(ws, {
|
|
668
|
-
sessions: new Map(),
|
|
669
|
-
createSession: () => makeStubSession(),
|
|
670
|
-
readyConfig: defaultConfig,
|
|
671
|
-
logger: silentLogger,
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
// Send message before open — session is null, should be ignored
|
|
675
|
-
ws.simulateMessage(JSON.stringify({ type: "audio_ready" }));
|
|
676
|
-
// No error thrown
|
|
756
|
+
expect(core.start).toHaveBeenCalledOnce();
|
|
677
757
|
});
|
|
678
758
|
|
|
679
|
-
// ─── Session resume
|
|
759
|
+
// ─── Session resume ───────────────────────────────────────────────────────
|
|
680
760
|
|
|
681
761
|
test("resumeFrom reuses old session ID instead of generating new UUID", () => {
|
|
682
|
-
const sessions = new Map<string,
|
|
762
|
+
const sessions = new Map<string, SessionCore>();
|
|
683
763
|
const ws = new MockWebSocket("ws://test");
|
|
684
764
|
ws.readyState = MockWebSocket.OPEN;
|
|
685
765
|
let capturedId: string | undefined;
|
|
@@ -688,7 +768,7 @@ describe("wireSessionSocket", () => {
|
|
|
688
768
|
sessions,
|
|
689
769
|
createSession: (sid) => {
|
|
690
770
|
capturedId = sid;
|
|
691
|
-
return
|
|
771
|
+
return makeMockCore();
|
|
692
772
|
},
|
|
693
773
|
readyConfig: defaultConfig,
|
|
694
774
|
logger: silentLogger,
|
|
@@ -699,24 +779,26 @@ describe("wireSessionSocket", () => {
|
|
|
699
779
|
expect(sessions.has("old-session-abc")).toBeTruthy();
|
|
700
780
|
});
|
|
701
781
|
|
|
702
|
-
test("
|
|
782
|
+
test("CONFIG frame contains resumed session ID as sessionId", () => {
|
|
703
783
|
const ws = new MockWebSocket("ws://test");
|
|
704
784
|
ws.readyState = MockWebSocket.OPEN;
|
|
705
785
|
|
|
706
786
|
wireSessionSocket(ws, {
|
|
707
787
|
sessions: new Map(),
|
|
708
|
-
createSession: () =>
|
|
788
|
+
createSession: () => makeMockCore(),
|
|
709
789
|
readyConfig: defaultConfig,
|
|
710
790
|
logger: silentLogger,
|
|
711
791
|
resumeFrom: "resume-id-123",
|
|
712
792
|
});
|
|
713
793
|
|
|
714
|
-
const
|
|
715
|
-
|
|
794
|
+
const firstFrame = ws.sent[0];
|
|
795
|
+
const msg = JSON.parse(firstFrame as string);
|
|
796
|
+
expect(msg.type).toBe("config");
|
|
797
|
+
expect(msg.sessionId).toBe("resume-id-123");
|
|
716
798
|
});
|
|
717
799
|
|
|
718
800
|
test("without resumeFrom, generates a new UUID session ID", () => {
|
|
719
|
-
const sessions = new Map<string,
|
|
801
|
+
const sessions = new Map<string, SessionCore>();
|
|
720
802
|
const ws = new MockWebSocket("ws://test");
|
|
721
803
|
ws.readyState = MockWebSocket.OPEN;
|
|
722
804
|
let capturedId: string | undefined;
|
|
@@ -725,7 +807,7 @@ describe("wireSessionSocket", () => {
|
|
|
725
807
|
sessions,
|
|
726
808
|
createSession: (sid) => {
|
|
727
809
|
capturedId = sid;
|
|
728
|
-
return
|
|
810
|
+
return makeMockCore();
|
|
729
811
|
},
|
|
730
812
|
readyConfig: defaultConfig,
|
|
731
813
|
logger: silentLogger,
|