@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.
- package/.turbo/turbo-build.log +11 -9
- package/CHANGELOG.md +10 -0
- package/dist/{_internal-types-CrnTi9Ew.js → _internal-types-CfOAbK6V.js} +22 -35
- package/dist/constants-y68COEGj.js +29 -0
- package/dist/host/_base64.d.ts +2 -0
- package/dist/host/_mock-ws.d.ts +0 -61
- package/dist/host/_pipeline-test-fakes.d.ts +7 -4
- package/dist/host/_run-code.d.ts +0 -25
- package/dist/host/_runtime-conformance.d.ts +3 -34
- package/dist/host/memory-vector.d.ts +0 -11
- package/dist/host/providers/resolve-kv.d.ts +0 -7
- package/dist/host/providers/resolve-vector.d.ts +0 -8
- package/dist/host/providers/stt/assemblyai.d.ts +0 -14
- package/dist/host/providers/stt/deepgram.d.ts +2 -14
- package/dist/host/providers/stt/soniox.d.ts +0 -22
- package/dist/host/providers/tts/rime.d.ts +10 -31
- package/dist/host/runtime-barrel.js +619 -630
- package/dist/host/runtime-config.d.ts +9 -6
- package/dist/host/runtime.d.ts +3 -0
- package/dist/host/to-vercel-tools.d.ts +3 -33
- package/dist/host/transports/openai-realtime-transport.d.ts +43 -0
- package/dist/host/unstorage-kv.d.ts +0 -26
- package/dist/index.js +3 -3
- package/dist/openai-realtime-cjPAHMMx.js +10 -0
- package/dist/sdk/_internal-types.d.ts +6 -55
- package/dist/sdk/allowed-hosts.d.ts +4 -3
- package/dist/sdk/constants.d.ts +4 -29
- package/dist/sdk/define.d.ts +7 -4
- package/dist/sdk/kv.d.ts +13 -37
- package/dist/sdk/manifest-barrel.js +1 -1
- package/dist/sdk/manifest.d.ts +8 -2
- package/dist/sdk/protocol.js +1 -1
- package/dist/sdk/providers/s2s/openai-realtime.d.ts +17 -0
- package/dist/sdk/providers/s2s-barrel.d.ts +9 -0
- package/dist/sdk/providers/s2s-barrel.js +2 -0
- package/dist/sdk/providers/tts/rime.d.ts +1 -1
- package/dist/sdk/providers.d.ts +6 -2
- package/dist/sdk/types.d.ts +7 -1
- package/dist/{types-KUgezM6u.js → types-DOWVZhb9.js} +1 -7
- package/dist/{ws-upgrade-BeOQ7fXL.js → ws-upgrade-CG8-by1n.js} +2 -3
- package/host/_base64.ts +9 -0
- package/host/_mock-ws.ts +0 -65
- package/host/_pipeline-test-fakes.ts +19 -31
- package/host/_run-code.ts +10 -53
- package/host/_runtime-conformance.ts +3 -44
- package/host/_test-utils.ts +20 -42
- package/host/builtin-tools.test.ts +127 -222
- package/host/builtin-tools.ts +6 -10
- package/host/cleanup.test.ts +30 -73
- package/host/integration/pipeline-reference.integration.test.ts +12 -17
- package/host/integration.test.ts +0 -7
- package/host/memory-vector.test.ts +3 -1
- package/host/memory-vector.ts +16 -21
- package/host/pinecone-vector.test.ts +14 -17
- package/host/pinecone-vector.ts +10 -19
- package/host/providers/providers.test-d.ts +5 -3
- package/host/providers/resolve-kv.ts +23 -41
- package/host/providers/resolve-vector.ts +3 -12
- package/host/providers/resolve.test.ts +15 -28
- package/host/providers/resolve.ts +24 -24
- package/host/providers/stt/assemblyai.test.ts +2 -14
- package/host/providers/stt/assemblyai.ts +12 -35
- package/host/providers/stt/deepgram.test.ts +23 -83
- package/host/providers/stt/deepgram.ts +15 -40
- package/host/providers/stt/elevenlabs.test.ts +26 -38
- package/host/providers/stt/elevenlabs.ts +10 -9
- package/host/providers/stt/soniox.test.ts +35 -85
- package/host/providers/stt/soniox.ts +8 -53
- package/host/providers/tts/cartesia.test.ts +19 -58
- package/host/providers/tts/cartesia.ts +36 -66
- package/host/providers/tts/rime.test.ts +12 -38
- package/host/providers/tts/rime.ts +23 -86
- package/host/runtime-config.test.ts +9 -9
- package/host/runtime-config.ts +16 -22
- package/host/runtime.test.ts +111 -73
- package/host/runtime.ts +138 -86
- package/host/s2s.test.ts +92 -191
- package/host/s2s.ts +55 -49
- package/host/server-shutdown.test.ts +9 -30
- package/host/server.test.ts +2 -13
- package/host/server.ts +85 -100
- package/host/session-core.test.ts +15 -30
- package/host/session-core.ts +10 -13
- package/host/session-prompt.test.ts +1 -5
- package/host/to-vercel-tools.test.ts +53 -72
- package/host/to-vercel-tools.ts +9 -39
- package/host/tool-executor.test.ts +25 -51
- package/host/tool-executor.ts +18 -12
- package/host/transports/openai-realtime-transport.test.ts +371 -0
- package/host/transports/openai-realtime-transport.ts +319 -0
- package/host/transports/pipeline-transport.test.ts +125 -298
- package/host/transports/pipeline-transport.ts +20 -68
- package/host/transports/s2s-transport-fixtures.test.ts +31 -92
- package/host/transports/s2s-transport.test.ts +65 -134
- package/host/transports/s2s-transport.ts +15 -43
- package/host/transports/types.test.ts +4 -8
- package/host/unstorage-kv.test.ts +3 -2
- package/host/unstorage-kv.ts +5 -35
- package/host/ws-handler.test.ts +72 -176
- package/host/ws-handler.ts +6 -12
- package/package.json +6 -1
- package/sdk/__snapshots__/exports.test.ts.snap +7 -0
- package/sdk/__snapshots__/schema-shapes.test.ts.snap +1 -0
- package/sdk/_internal-types.test.ts +6 -9
- package/sdk/_internal-types.ts +16 -57
- package/sdk/_test-matchers.ts +25 -15
- package/sdk/allowed-hosts.test.ts +50 -114
- package/sdk/allowed-hosts.ts +8 -14
- package/sdk/constants.ts +5 -52
- package/sdk/define.test.ts +7 -6
- package/sdk/define.ts +7 -3
- package/sdk/exports.test.ts +6 -1
- package/sdk/kv.ts +13 -37
- package/sdk/manifest.test-d.ts +5 -0
- package/sdk/manifest.test.ts +61 -9
- package/sdk/manifest.ts +11 -11
- package/sdk/protocol-compat.test.ts +66 -98
- package/sdk/protocol-snapshot.test.ts +2 -16
- package/sdk/protocol.test.ts +13 -22
- package/sdk/providers/s2s/openai-realtime.ts +36 -0
- package/sdk/providers/s2s-barrel.ts +12 -0
- package/sdk/providers/tts/rime.ts +1 -1
- package/sdk/providers.ts +24 -5
- package/sdk/schema-alignment.test.ts +25 -73
- package/sdk/schema-shapes.test.ts +1 -29
- package/sdk/system-prompt.test.ts +0 -1
- package/sdk/system-prompt.ts +17 -19
- package/sdk/types-inference.test.ts +10 -36
- package/sdk/types.ts +7 -0
- package/sdk/ws-upgrade.test.ts +24 -23
- package/sdk/ws-upgrade.ts +2 -3
- package/tsdown.config.ts +8 -11
- package/dist/constants-C2nirZUI.js +0 -54
package/host/ws-handler.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
31
|
+
function parseFirstFrame(ws: MockWebSocket): Record<string, unknown> {
|
|
32
|
+
return JSON.parse(ws.sent[0] as string);
|
|
33
|
+
}
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
45
|
-
const core = makeMockCore({
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
77
|
-
|
|
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 =
|
|
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
|
-
|
|
153
|
-
expect(
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
356
|
-
const core = makeMockCore({
|
|
357
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
646
|
-
const core = makeMockCore({
|
|
647
|
-
|
|
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 =
|
|
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
|
-
/*
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
});
|
package/host/ws-handler.ts
CHANGED
|
@@ -93,15 +93,11 @@ function createClientSink(ws: SessionWebSocket, log: Logger): ClientSink {
|
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
function
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
232
|
-
handleTextMessage(event.data, session, log, sid);
|
|
226
|
+
dispatchMessage(event.data, session, log, sid);
|
|
233
227
|
});
|
|
234
228
|
|
|
235
229
|
ws.addEventListener("close", () => {
|