@alexkroman1/aai 0.12.2 → 1.0.2
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 +20 -0
- package/CHANGELOG.md +174 -0
- package/dist/constants-VTFoymJ-.js +47 -0
- package/dist/host/_run-code.d.ts +4 -2
- package/dist/host/_runtime-conformance.d.ts +4 -5
- package/dist/host/builtin-tools.d.ts +11 -7
- package/dist/host/runtime-barrel.d.ts +15 -0
- package/dist/{direct-executor-ZUU0Ke4j.js → host/runtime-barrel.js} +463 -345
- package/dist/host/runtime-config.d.ts +42 -0
- package/dist/host/runtime.d.ts +119 -35
- package/dist/host/s2s.d.ts +14 -38
- package/dist/host/server.d.ts +16 -8
- package/dist/host/session-ctx.d.ts +55 -0
- package/dist/host/session.d.ts +21 -70
- package/dist/host/tool-executor.d.ts +20 -0
- package/dist/host/unstorage-kv.d.ts +1 -1
- package/dist/host/ws-handler.d.ts +4 -2
- package/dist/index.d.ts +9 -20
- package/dist/index.js +63 -2
- package/dist/{isolate → sdk}/_internal-types.d.ts +6 -10
- package/dist/{isolate → sdk}/constants.d.ts +6 -4
- package/dist/sdk/define.d.ts +66 -0
- package/dist/{isolate → sdk}/kv.d.ts +1 -49
- package/dist/sdk/manifest-barrel.d.ts +8 -0
- package/dist/sdk/manifest-barrel.js +52 -0
- package/dist/sdk/manifest.d.ts +50 -0
- package/dist/{isolate → sdk}/protocol.d.ts +59 -36
- package/dist/sdk/protocol.js +163 -0
- package/dist/{isolate → sdk}/system-prompt.d.ts +3 -2
- package/dist/sdk/types.d.ts +201 -0
- package/dist/sdk/ws-upgrade.d.ts +5 -0
- package/dist/{system-prompt-CVJSQJiA.js → system-prompt-nik_iavo.js} +11 -10
- package/dist/types-Cfx_4QDK.js +39 -0
- package/dist/ws-upgrade-BeOQ7fXL.js +30 -0
- package/exports-no-dev-deps.test.ts +62 -0
- package/host/_mock-ws.ts +185 -0
- package/host/_run-code.ts +217 -0
- package/host/_runtime-conformance.ts +143 -0
- package/host/_test-utils.ts +276 -0
- package/host/builtin-tools.test.ts +774 -0
- package/host/builtin-tools.ts +255 -0
- package/host/cleanup.test.ts +422 -0
- package/host/fixture-replay.test.ts +463 -0
- package/host/fixtures/README.md +40 -0
- package/host/fixtures/greeting-session-sequence.json +40 -0
- package/host/fixtures/reply-audio-samples.json +42 -0
- package/host/fixtures/reply-lifecycle.json +21 -0
- package/host/fixtures/session-ready.json +48 -0
- package/host/fixtures/session-updated.json +45 -0
- package/host/fixtures/simple-question-sequence.json +73 -0
- package/host/fixtures/tool-call-sequence.json +114 -0
- package/host/fixtures/tool-calls.json +11 -0
- package/host/fixtures/tool-config-session-sequence.json +51 -0
- package/host/fixtures/user-speech-recognition.json +30 -0
- package/host/fixtures/web-search-sequence.json +122 -0
- package/host/integration.test.ts +222 -0
- package/host/runtime-barrel.ts +25 -0
- package/host/runtime-config.test.ts +71 -0
- package/host/runtime-config.ts +99 -0
- package/host/runtime.test.ts +641 -0
- package/host/runtime.ts +308 -0
- package/host/s2s-fixtures.test.ts +237 -0
- package/host/s2s.test.ts +562 -0
- package/host/s2s.ts +310 -0
- package/host/server-shutdown.test.ts +76 -0
- package/host/server.test.ts +116 -0
- package/host/server.ts +223 -0
- package/host/session-ctx.ts +107 -0
- package/host/session-fixture-replay.test.ts +136 -0
- package/host/session-prompt.test.ts +77 -0
- package/host/session.test.ts +590 -0
- package/host/session.ts +370 -0
- package/host/tool-executor.test.ts +124 -0
- package/host/tool-executor.ts +80 -0
- package/host/unstorage-kv.test.ts +99 -0
- package/host/unstorage-kv.ts +69 -0
- package/host/ws-handler.test.ts +739 -0
- package/host/ws-handler.ts +255 -0
- package/index.ts +16 -0
- package/package.json +28 -72
- package/sdk/_internal-types.test.ts +34 -0
- package/sdk/_internal-types.ts +115 -0
- package/sdk/compat-fixtures/README.md +26 -0
- package/sdk/compat-fixtures/v1.json +68 -0
- package/sdk/constants.ts +77 -0
- package/sdk/define.test.ts +57 -0
- package/sdk/define.ts +88 -0
- package/sdk/kv.ts +60 -0
- package/sdk/manifest-barrel.ts +12 -0
- package/sdk/manifest.test.ts +56 -0
- package/sdk/manifest.ts +89 -0
- package/sdk/protocol-compat.test.ts +187 -0
- package/sdk/protocol-snapshot.test.ts +199 -0
- package/sdk/protocol.test.ts +170 -0
- package/sdk/protocol.ts +223 -0
- package/sdk/schema-alignment.test.ts +191 -0
- package/sdk/system-prompt.test.ts +111 -0
- package/sdk/system-prompt.ts +74 -0
- package/sdk/tsconfig.json +12 -0
- package/sdk/types-inference.test.ts +122 -0
- package/sdk/types.test.ts +14 -0
- package/sdk/types.ts +226 -0
- package/sdk/utils.test.ts +52 -0
- package/sdk/utils.ts +20 -0
- package/sdk/ws-upgrade.test.ts +48 -0
- package/sdk/ws-upgrade.ts +13 -0
- package/tsconfig.build.json +14 -0
- package/tsconfig.json +10 -0
- package/tsdown.config.ts +26 -0
- package/vitest.config.ts +17 -0
- package/dist/host/_test-utils.d.ts +0 -73
- package/dist/host/direct-executor.d.ts +0 -128
- package/dist/host/index.d.ts +0 -18
- package/dist/host/index.js +0 -165
- package/dist/host/matchers.d.ts +0 -20
- package/dist/host/matchers.js +0 -41
- package/dist/host/server.js +0 -164
- package/dist/host/testing.d.ts +0 -294
- package/dist/host/testing.js +0 -2
- package/dist/host/vite-plugin.d.ts +0 -15
- package/dist/host/vite-plugin.js +0 -83
- package/dist/isolate/_kv-utils.d.ts +0 -10
- package/dist/isolate/_utils.js +0 -17
- package/dist/isolate/hooks.d.ts +0 -44
- package/dist/isolate/hooks.js +0 -58
- package/dist/isolate/index.d.ts +0 -18
- package/dist/isolate/index.js +0 -6
- package/dist/isolate/kv.js +0 -1
- package/dist/isolate/protocol.js +0 -2
- package/dist/isolate/types.d.ts +0 -418
- package/dist/isolate/types.js +0 -175
- package/dist/protocol-rcOrz7T3.js +0 -183
- package/dist/testing-Bb2B5Uob.js +0 -513
- package/dist/types.test-d.d.ts +0 -7
- /package/dist/{isolate/_utils.d.ts → sdk/utils.d.ts} +0 -0
package/host/s2s.test.ts
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { silentLogger } from "./_test-utils.ts";
|
|
3
|
+
import type { S2sWebSocket } from "./s2s.ts";
|
|
4
|
+
import { connectS2s } from "./s2s.ts";
|
|
5
|
+
|
|
6
|
+
/** EventTarget-based WebSocket stub (standard API, no `.on()` adapter needed). */
|
|
7
|
+
function createWebSocketStub() {
|
|
8
|
+
const target = new EventTarget();
|
|
9
|
+
return Object.assign(target, {
|
|
10
|
+
readyState: 0,
|
|
11
|
+
send: vi.fn(),
|
|
12
|
+
close: vi.fn(),
|
|
13
|
+
addEventListener: target.addEventListener.bind(target) as S2sWebSocket["addEventListener"],
|
|
14
|
+
/** Simulate a server-side event for testing. */
|
|
15
|
+
emit(event: string, ...args: unknown[]) {
|
|
16
|
+
const builders: Record<string, () => Event> = {
|
|
17
|
+
open: () => new Event("open"),
|
|
18
|
+
message: () => new MessageEvent("message", { data: args[0] }),
|
|
19
|
+
close: () => {
|
|
20
|
+
const ev = new Event("close");
|
|
21
|
+
if (typeof args[0] === "number") Object.assign(ev, { code: args[0] });
|
|
22
|
+
if (typeof args[1] === "string") Object.assign(ev, { reason: args[1] });
|
|
23
|
+
return ev;
|
|
24
|
+
},
|
|
25
|
+
error: () => {
|
|
26
|
+
const msg = args[0] instanceof Error ? args[0].message : String(args[0]);
|
|
27
|
+
const ev = new Event("error");
|
|
28
|
+
Object.defineProperty(ev, "message", { value: msg });
|
|
29
|
+
return ev;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
const build = builders[event];
|
|
33
|
+
if (build) target.dispatchEvent(build());
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const s2sConfig = { wssUrl: "wss://fake", inputSampleRate: 16_000, outputSampleRate: 16_000 };
|
|
39
|
+
|
|
40
|
+
function createTestS2s() {
|
|
41
|
+
const raw = createWebSocketStub();
|
|
42
|
+
const createWebSocket = () => {
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
raw.readyState = 1;
|
|
45
|
+
raw.emit("open");
|
|
46
|
+
}, 0);
|
|
47
|
+
return raw;
|
|
48
|
+
};
|
|
49
|
+
return { raw, createWebSocket, logger: { ...silentLogger } };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function setupHandle() {
|
|
53
|
+
const { raw, createWebSocket, logger } = createTestS2s();
|
|
54
|
+
const handle = await connectS2s({
|
|
55
|
+
apiKey: "test-key",
|
|
56
|
+
config: s2sConfig,
|
|
57
|
+
createWebSocket,
|
|
58
|
+
logger,
|
|
59
|
+
});
|
|
60
|
+
return { raw, handle, logger };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe("connectS2s", () => {
|
|
64
|
+
test("resolves with handle after open", async () => {
|
|
65
|
+
const { handle } = await setupHandle();
|
|
66
|
+
expect(handle).toEqual(
|
|
67
|
+
expect.objectContaining({
|
|
68
|
+
sendAudio: expect.any(Function),
|
|
69
|
+
sendToolResult: expect.any(Function),
|
|
70
|
+
updateSession: expect.any(Function),
|
|
71
|
+
resumeSession: expect.any(Function),
|
|
72
|
+
close: expect.any(Function),
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("rejects when error fires before open", async () => {
|
|
78
|
+
const raw = createWebSocketStub();
|
|
79
|
+
const createWebSocket = () => {
|
|
80
|
+
setTimeout(() => {
|
|
81
|
+
raw.emit("error", new Error("connection refused"));
|
|
82
|
+
}, 0);
|
|
83
|
+
return raw;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
await expect(
|
|
87
|
+
connectS2s({
|
|
88
|
+
apiKey: "test-key",
|
|
89
|
+
config: s2sConfig,
|
|
90
|
+
createWebSocket,
|
|
91
|
+
logger: silentLogger,
|
|
92
|
+
}),
|
|
93
|
+
).rejects.toThrow("connection refused");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ─── Handle methods ────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
test("updateSession sends session.update message", async () => {
|
|
99
|
+
const { raw, handle } = await setupHandle();
|
|
100
|
+
|
|
101
|
+
handle.updateSession({ systemPrompt: "test", tools: [] });
|
|
102
|
+
|
|
103
|
+
expect(raw.send).toHaveBeenCalledOnce();
|
|
104
|
+
const sent = JSON.parse(raw.send.mock.calls[0]?.[0] as string);
|
|
105
|
+
expect(sent.type).toBe("session.update");
|
|
106
|
+
expect(sent.session.system_prompt).toBe("test"); // wire format stays snake_case
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("sendAudio sends base64-encoded audio when open", async () => {
|
|
110
|
+
const { raw, handle } = await setupHandle();
|
|
111
|
+
|
|
112
|
+
handle.sendAudio(new Uint8Array([1, 2, 3, 4]));
|
|
113
|
+
|
|
114
|
+
expect(raw.send).toHaveBeenCalledOnce();
|
|
115
|
+
const sent = JSON.parse(raw.send.mock.calls[0]?.[0] as string);
|
|
116
|
+
expect(sent.type).toBe("input.audio");
|
|
117
|
+
expect(typeof sent.audio).toBe("string"); // base64
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("sendAudio is no-op when ws is not open", async () => {
|
|
121
|
+
const { raw, handle } = await setupHandle();
|
|
122
|
+
raw.readyState = 3; // CLOSED
|
|
123
|
+
|
|
124
|
+
handle.sendAudio(new Uint8Array([1, 2, 3, 4]));
|
|
125
|
+
expect(raw.send).not.toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("sendToolResult sends tool.result message", async () => {
|
|
129
|
+
const { raw, handle } = await setupHandle();
|
|
130
|
+
|
|
131
|
+
handle.sendToolResult("call-123", "result-text");
|
|
132
|
+
|
|
133
|
+
expect(raw.send).toHaveBeenCalledOnce();
|
|
134
|
+
const sent = JSON.parse(raw.send.mock.calls[0]?.[0] as string);
|
|
135
|
+
expect(sent.type).toBe("tool.result");
|
|
136
|
+
expect(sent.call_id).toBe("call-123");
|
|
137
|
+
expect(sent.result).toBe("result-text");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("resumeSession sends session.resume message", async () => {
|
|
141
|
+
const { raw, handle } = await setupHandle();
|
|
142
|
+
|
|
143
|
+
handle.resumeSession("session-abc");
|
|
144
|
+
|
|
145
|
+
expect(raw.send).toHaveBeenCalledOnce();
|
|
146
|
+
const sent = JSON.parse(raw.send.mock.calls[0]?.[0] as string);
|
|
147
|
+
expect(sent.type).toBe("session.resume");
|
|
148
|
+
expect(sent.session_id).toBe("session-abc");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("close() closes the underlying ws", async () => {
|
|
152
|
+
const { raw, handle } = await setupHandle();
|
|
153
|
+
|
|
154
|
+
handle.close();
|
|
155
|
+
expect(raw.close).toHaveBeenCalledOnce();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("send is no-op when ws is not open", async () => {
|
|
159
|
+
const { raw, handle } = await setupHandle();
|
|
160
|
+
raw.readyState = 3; // CLOSED
|
|
161
|
+
|
|
162
|
+
handle.updateSession({ systemPrompt: "test", tools: [] });
|
|
163
|
+
expect(raw.send).not.toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ─── Message dispatch ──────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
test("session.ready dispatches 'ready' event", async () => {
|
|
169
|
+
const { raw, handle } = await setupHandle();
|
|
170
|
+
const onReady = vi.fn();
|
|
171
|
+
handle.on("ready", onReady);
|
|
172
|
+
|
|
173
|
+
raw.emit(
|
|
174
|
+
"message",
|
|
175
|
+
Buffer.from(
|
|
176
|
+
JSON.stringify({
|
|
177
|
+
type: "session.ready",
|
|
178
|
+
session_id: "s123",
|
|
179
|
+
}),
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
expect(onReady).toHaveBeenCalledOnce();
|
|
184
|
+
expect(onReady.mock.calls[0]?.[0].sessionId).toBe("s123");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("input.speech.started dispatches 'event' with type 'speech_started'", async () => {
|
|
188
|
+
const { raw, handle } = await setupHandle();
|
|
189
|
+
const handler = vi.fn();
|
|
190
|
+
handle.on("event", handler);
|
|
191
|
+
|
|
192
|
+
raw.emit("message", Buffer.from(JSON.stringify({ type: "input.speech.started" })));
|
|
193
|
+
|
|
194
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
195
|
+
expect(handler.mock.calls[0]?.[0]).toEqual({ type: "speech_started" });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("input.speech.stopped dispatches 'event' with type 'speech_stopped'", async () => {
|
|
199
|
+
const { raw, handle } = await setupHandle();
|
|
200
|
+
const handler = vi.fn();
|
|
201
|
+
handle.on("event", handler);
|
|
202
|
+
|
|
203
|
+
raw.emit("message", Buffer.from(JSON.stringify({ type: "input.speech.stopped" })));
|
|
204
|
+
|
|
205
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
206
|
+
expect(handler.mock.calls[0]?.[0]).toEqual({ type: "speech_stopped" });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("transcript.user dispatches 'event' with user_transcript", async () => {
|
|
210
|
+
const { raw, handle } = await setupHandle();
|
|
211
|
+
const handler = vi.fn();
|
|
212
|
+
handle.on("event", handler);
|
|
213
|
+
|
|
214
|
+
raw.emit(
|
|
215
|
+
"message",
|
|
216
|
+
Buffer.from(
|
|
217
|
+
JSON.stringify({
|
|
218
|
+
type: "transcript.user",
|
|
219
|
+
item_id: "item-1",
|
|
220
|
+
text: "Hello world",
|
|
221
|
+
}),
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
226
|
+
expect(handler.mock.calls[0]?.[0]).toEqual({
|
|
227
|
+
type: "user_transcript",
|
|
228
|
+
text: "Hello world",
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("reply.started dispatches 'replyStarted' event", async () => {
|
|
233
|
+
const { raw, handle } = await setupHandle();
|
|
234
|
+
const handler = vi.fn();
|
|
235
|
+
handle.on("replyStarted", handler);
|
|
236
|
+
|
|
237
|
+
raw.emit(
|
|
238
|
+
"message",
|
|
239
|
+
Buffer.from(
|
|
240
|
+
JSON.stringify({
|
|
241
|
+
type: "reply.started",
|
|
242
|
+
reply_id: "r1",
|
|
243
|
+
}),
|
|
244
|
+
),
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
248
|
+
expect(handler.mock.calls[0]?.[0].replyId).toBe("r1");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("transcript.agent dispatches 'event' with agent_transcript", async () => {
|
|
252
|
+
const { raw, handle } = await setupHandle();
|
|
253
|
+
const handler = vi.fn();
|
|
254
|
+
handle.on("event", handler);
|
|
255
|
+
|
|
256
|
+
raw.emit(
|
|
257
|
+
"message",
|
|
258
|
+
Buffer.from(
|
|
259
|
+
JSON.stringify({
|
|
260
|
+
type: "transcript.agent",
|
|
261
|
+
text: "Full response",
|
|
262
|
+
reply_id: "r1",
|
|
263
|
+
item_id: "i1",
|
|
264
|
+
interrupted: false,
|
|
265
|
+
}),
|
|
266
|
+
),
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
270
|
+
const payload = handler.mock.calls[0]?.[0];
|
|
271
|
+
expect(payload.type).toBe("agent_transcript");
|
|
272
|
+
expect(payload.text).toBe("Full response");
|
|
273
|
+
expect(payload._interrupted).toBe(false);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("transcript.agent defaults _interrupted to false when missing", async () => {
|
|
277
|
+
const { raw, handle } = await setupHandle();
|
|
278
|
+
const handler = vi.fn();
|
|
279
|
+
handle.on("event", handler);
|
|
280
|
+
|
|
281
|
+
raw.emit(
|
|
282
|
+
"message",
|
|
283
|
+
Buffer.from(JSON.stringify({ type: "transcript.agent", text: "response" })),
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
expect(handler.mock.calls[0]?.[0]._interrupted).toBe(false);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("transcript.agent with interrupted:true sets _interrupted:true", async () => {
|
|
290
|
+
const { raw, handle } = await setupHandle();
|
|
291
|
+
const handler = vi.fn();
|
|
292
|
+
handle.on("event", handler);
|
|
293
|
+
|
|
294
|
+
raw.emit(
|
|
295
|
+
"message",
|
|
296
|
+
Buffer.from(
|
|
297
|
+
JSON.stringify({
|
|
298
|
+
type: "transcript.agent",
|
|
299
|
+
text: "Interrupted response",
|
|
300
|
+
interrupted: true,
|
|
301
|
+
}),
|
|
302
|
+
),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
expect(handler.mock.calls[0]?.[0]._interrupted).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("tool.call dispatches 'event' with tool_call shape", async () => {
|
|
309
|
+
const { raw, handle } = await setupHandle();
|
|
310
|
+
const handler = vi.fn();
|
|
311
|
+
handle.on("event", handler);
|
|
312
|
+
|
|
313
|
+
raw.emit(
|
|
314
|
+
"message",
|
|
315
|
+
Buffer.from(
|
|
316
|
+
JSON.stringify({
|
|
317
|
+
type: "tool.call",
|
|
318
|
+
call_id: "c1",
|
|
319
|
+
name: "web_search",
|
|
320
|
+
args: { query: "test" },
|
|
321
|
+
}),
|
|
322
|
+
),
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
326
|
+
const payload = handler.mock.calls[0]?.[0];
|
|
327
|
+
expect(payload.type).toBe("tool_call");
|
|
328
|
+
expect(payload.toolCallId).toBe("c1");
|
|
329
|
+
expect(payload.toolName).toBe("web_search");
|
|
330
|
+
expect(payload.args).toEqual({ query: "test" });
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("reply.done (non-interrupted) dispatches 'event' with type 'reply_done'", async () => {
|
|
334
|
+
const { raw, handle } = await setupHandle();
|
|
335
|
+
const handler = vi.fn();
|
|
336
|
+
handle.on("event", handler);
|
|
337
|
+
|
|
338
|
+
raw.emit(
|
|
339
|
+
"message",
|
|
340
|
+
Buffer.from(
|
|
341
|
+
JSON.stringify({
|
|
342
|
+
type: "reply.done",
|
|
343
|
+
status: "completed",
|
|
344
|
+
}),
|
|
345
|
+
),
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
349
|
+
expect(handler.mock.calls[0]?.[0]).toEqual({ type: "reply_done" });
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("reply.done with status 'interrupted' dispatches 'event' with type 'cancelled'", async () => {
|
|
353
|
+
const { raw, handle } = await setupHandle();
|
|
354
|
+
const handler = vi.fn();
|
|
355
|
+
handle.on("event", handler);
|
|
356
|
+
|
|
357
|
+
raw.emit(
|
|
358
|
+
"message",
|
|
359
|
+
Buffer.from(
|
|
360
|
+
JSON.stringify({
|
|
361
|
+
type: "reply.done",
|
|
362
|
+
status: "interrupted",
|
|
363
|
+
}),
|
|
364
|
+
),
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
368
|
+
expect(handler.mock.calls[0]?.[0]).toEqual({ type: "cancelled" });
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("session.error with session_not_found dispatches 'sessionExpired'", async () => {
|
|
372
|
+
const { raw, handle } = await setupHandle();
|
|
373
|
+
const handler = vi.fn();
|
|
374
|
+
handle.on("sessionExpired", handler);
|
|
375
|
+
|
|
376
|
+
raw.emit(
|
|
377
|
+
"message",
|
|
378
|
+
Buffer.from(
|
|
379
|
+
JSON.stringify({
|
|
380
|
+
type: "session.error",
|
|
381
|
+
code: "session_not_found",
|
|
382
|
+
message: "Session not found",
|
|
383
|
+
}),
|
|
384
|
+
),
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("session.error with session_forbidden dispatches 'sessionExpired'", async () => {
|
|
391
|
+
const { raw, handle } = await setupHandle();
|
|
392
|
+
const handler = vi.fn();
|
|
393
|
+
handle.on("sessionExpired", handler);
|
|
394
|
+
|
|
395
|
+
raw.emit(
|
|
396
|
+
"message",
|
|
397
|
+
Buffer.from(
|
|
398
|
+
JSON.stringify({
|
|
399
|
+
type: "session.error",
|
|
400
|
+
code: "session_forbidden",
|
|
401
|
+
message: "Forbidden",
|
|
402
|
+
}),
|
|
403
|
+
),
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("session.error with other code dispatches 'error' with Error object", async () => {
|
|
410
|
+
const { raw, handle } = await setupHandle();
|
|
411
|
+
const handler = vi.fn();
|
|
412
|
+
handle.on("error", handler);
|
|
413
|
+
|
|
414
|
+
raw.emit(
|
|
415
|
+
"message",
|
|
416
|
+
Buffer.from(
|
|
417
|
+
JSON.stringify({
|
|
418
|
+
type: "session.error",
|
|
419
|
+
code: "rate_limit",
|
|
420
|
+
message: "Too many requests",
|
|
421
|
+
}),
|
|
422
|
+
),
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
426
|
+
const err = handler.mock.calls[0]?.[0];
|
|
427
|
+
expect(err).toBeInstanceOf(Error);
|
|
428
|
+
expect(err.message).toBe("Too many requests");
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("bare error dispatches 'error' with Error object", async () => {
|
|
432
|
+
const { raw, handle } = await setupHandle();
|
|
433
|
+
const handler = vi.fn();
|
|
434
|
+
handle.on("error", handler);
|
|
435
|
+
|
|
436
|
+
raw.emit(
|
|
437
|
+
"message",
|
|
438
|
+
Buffer.from(
|
|
439
|
+
JSON.stringify({
|
|
440
|
+
type: "error",
|
|
441
|
+
message: "Bad gateway",
|
|
442
|
+
}),
|
|
443
|
+
),
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
447
|
+
const err = handler.mock.calls[0]?.[0];
|
|
448
|
+
expect(err).toBeInstanceOf(Error);
|
|
449
|
+
expect(err.message).toBe("Bad gateway");
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// ─── Audio fast path ───────────────────────────────────────────────────
|
|
453
|
+
|
|
454
|
+
test("reply.audio dispatches 'audio' with decoded Uint8Array", async () => {
|
|
455
|
+
const { raw, handle } = await setupHandle();
|
|
456
|
+
const handler = vi.fn();
|
|
457
|
+
handle.on("audio", handler);
|
|
458
|
+
|
|
459
|
+
const audioBytes = new Uint8Array([10, 20, 30, 40]);
|
|
460
|
+
const base64 = Buffer.from(audioBytes).toString("base64");
|
|
461
|
+
|
|
462
|
+
raw.emit(
|
|
463
|
+
"message",
|
|
464
|
+
Buffer.from(
|
|
465
|
+
JSON.stringify({
|
|
466
|
+
type: "reply.audio",
|
|
467
|
+
data: base64,
|
|
468
|
+
}),
|
|
469
|
+
),
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
473
|
+
const payload = handler.mock.calls[0]?.[0];
|
|
474
|
+
expect(payload.audio).toBeInstanceOf(Uint8Array);
|
|
475
|
+
expect(Array.from(payload.audio)).toEqual([10, 20, 30, 40]);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// ─── Edge cases ────────────────────────────────────────────────────────
|
|
479
|
+
|
|
480
|
+
test("invalid JSON message is logged and ignored", async () => {
|
|
481
|
+
const { raw, logger } = await setupHandle();
|
|
482
|
+
|
|
483
|
+
raw.emit("message", Buffer.from("not-valid-json{{{"));
|
|
484
|
+
|
|
485
|
+
expect(logger.warn).toHaveBeenCalledWith("S2S << invalid JSON", expect.any(Object));
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test("unrecognized message type is logged and ignored", async () => {
|
|
489
|
+
const { raw, logger } = await setupHandle();
|
|
490
|
+
|
|
491
|
+
raw.emit(
|
|
492
|
+
"message",
|
|
493
|
+
Buffer.from(
|
|
494
|
+
JSON.stringify({
|
|
495
|
+
type: "totally.unknown.type",
|
|
496
|
+
}),
|
|
497
|
+
),
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
expect(logger.warn).toHaveBeenCalled();
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("reply.content_part events are silently ignored (no dispatch)", async () => {
|
|
504
|
+
const { raw } = await setupHandle();
|
|
505
|
+
// These types return undefined from S2S_DISPATCH — no event should fire.
|
|
506
|
+
raw.emit(
|
|
507
|
+
"message",
|
|
508
|
+
Buffer.from(
|
|
509
|
+
JSON.stringify({
|
|
510
|
+
type: "reply.content_part.started",
|
|
511
|
+
}),
|
|
512
|
+
),
|
|
513
|
+
);
|
|
514
|
+
raw.emit(
|
|
515
|
+
"message",
|
|
516
|
+
Buffer.from(
|
|
517
|
+
JSON.stringify({
|
|
518
|
+
type: "reply.content_part.done",
|
|
519
|
+
}),
|
|
520
|
+
),
|
|
521
|
+
);
|
|
522
|
+
// No error thrown = pass
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test("session.updated is silently ignored (no dispatch)", async () => {
|
|
526
|
+
const { raw, handle } = await setupHandle();
|
|
527
|
+
const eventHandler = vi.fn();
|
|
528
|
+
handle.on("event", eventHandler);
|
|
529
|
+
|
|
530
|
+
raw.emit("message", Buffer.from(JSON.stringify({ type: "session.updated" })));
|
|
531
|
+
|
|
532
|
+
// session.updated is dropped — no event emitted
|
|
533
|
+
expect(eventHandler).not.toHaveBeenCalled();
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// ─── Close and error events ────────────────────────────────────────────
|
|
537
|
+
|
|
538
|
+
test("close event dispatches 'close' with code and reason", async () => {
|
|
539
|
+
const { raw, handle } = await setupHandle();
|
|
540
|
+
const handler = vi.fn();
|
|
541
|
+
handle.on("close", handler);
|
|
542
|
+
|
|
543
|
+
raw.emit("close", 1000, "normal");
|
|
544
|
+
|
|
545
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
546
|
+
expect(handler.mock.calls[0]?.[0]).toBe(1000);
|
|
547
|
+
expect(handler.mock.calls[0]?.[1]).toBe("normal");
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
test("error after open dispatches 'error' with Error object", async () => {
|
|
551
|
+
const { raw, handle } = await setupHandle();
|
|
552
|
+
const handler = vi.fn();
|
|
553
|
+
handle.on("error", handler);
|
|
554
|
+
|
|
555
|
+
raw.emit("error", new Error("ws transport error"));
|
|
556
|
+
|
|
557
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
558
|
+
const err = handler.mock.calls[0]?.[0];
|
|
559
|
+
expect(err).toBeInstanceOf(Error);
|
|
560
|
+
expect(err.message).toBe("ws transport error");
|
|
561
|
+
});
|
|
562
|
+
});
|