@alexkroman1/aai 1.0.6 → 1.2.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 -11
- package/CHANGELOG.md +22 -0
- package/dist/_internal-types-CoDTiBd1.js +61 -0
- package/dist/host/_mock-ws.d.ts +0 -24
- package/dist/host/runtime-barrel.d.ts +0 -1
- package/dist/host/runtime-barrel.js +55 -5
- package/dist/host/runtime.d.ts +2 -0
- package/dist/host/tool-executor.d.ts +1 -0
- package/dist/host/ws-handler.d.ts +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +90 -1
- package/dist/sdk/allowed-hosts.d.ts +34 -0
- package/dist/sdk/manifest-barrel.d.ts +3 -5
- package/dist/sdk/manifest-barrel.js +2 -52
- package/dist/sdk/manifest.d.ts +2 -0
- package/dist/sdk/protocol.d.ts +11 -28
- package/dist/sdk/protocol.js +6 -3
- package/dist/sdk/types.d.ts +2 -0
- package/host/_mock-ws.ts +0 -50
- package/host/_test-utils.ts +1 -0
- package/host/runtime-barrel.ts +0 -1
- package/host/runtime.ts +13 -1
- package/host/session-ctx.test.ts +387 -0
- package/host/session-fixture-replay.test.ts +2 -10
- package/host/session.test.ts +19 -41
- package/host/tool-executor.test.ts +36 -0
- package/host/tool-executor.ts +4 -0
- package/host/ws-handler.ts +3 -0
- package/index.ts +1 -0
- package/package.json +1 -1
- package/sdk/__snapshots__/exports.test.ts.snap +79 -0
- package/sdk/__snapshots__/schema-shapes.test.ts.snap +187 -0
- package/sdk/_test-matchers.test.ts +75 -0
- package/sdk/_test-matchers.ts +73 -0
- package/sdk/allowed-hosts.test.ts +236 -0
- package/sdk/allowed-hosts.ts +113 -0
- package/sdk/exports.test.ts +31 -0
- package/sdk/manifest-barrel.ts +13 -7
- package/sdk/manifest.test.ts +103 -2
- package/sdk/manifest.ts +19 -0
- package/sdk/protocol-compat.test.ts +0 -6
- package/sdk/protocol-snapshot.test.ts +7 -5
- package/sdk/protocol.test.ts +107 -21
- package/sdk/protocol.ts +7 -15
- package/sdk/schema-alignment.test.ts +1 -27
- package/sdk/schema-shapes.test.ts +103 -0
- package/sdk/tsconfig.json +1 -1
- package/sdk/types.test.ts +56 -1
- package/sdk/types.ts +2 -0
- package/sdk/ws-upgrade.test.ts +8 -8
- package/tsconfig.build.json +8 -1
- package/tsconfig.json +1 -1
- package/vitest.config.ts +1 -0
- package/dist/system-prompt-nik_iavo.js +0 -92
package/host/session.test.ts
CHANGED
|
@@ -83,14 +83,14 @@ describe("createS2sSession", () => {
|
|
|
83
83
|
const { session, client } = setup();
|
|
84
84
|
await session.start();
|
|
85
85
|
session.onCancel();
|
|
86
|
-
expect(client.events).
|
|
86
|
+
expect(client.events).toContainEvent("cancelled");
|
|
87
87
|
});
|
|
88
88
|
|
|
89
89
|
test("onReset clears state and emits reset event", async () => {
|
|
90
90
|
const { session, client, mockHandle } = setup();
|
|
91
91
|
await session.start();
|
|
92
92
|
session.onReset();
|
|
93
|
-
expect(client.events).
|
|
93
|
+
expect(client.events).toContainEvent("reset");
|
|
94
94
|
expect(mockHandle.close).toHaveBeenCalled();
|
|
95
95
|
});
|
|
96
96
|
|
|
@@ -119,10 +119,7 @@ describe("createS2sSession", () => {
|
|
|
119
119
|
mockHandle._fire("event", { type: "user_transcript", text: "Hello there" });
|
|
120
120
|
await flush();
|
|
121
121
|
|
|
122
|
-
expect(client.events).
|
|
123
|
-
type: "user_transcript",
|
|
124
|
-
text: "Hello there",
|
|
125
|
-
});
|
|
122
|
+
expect(client.events).toContainEvent("user_transcript", { text: "Hello there" });
|
|
126
123
|
});
|
|
127
124
|
|
|
128
125
|
test("audio event forwards audio to client", async () => {
|
|
@@ -145,10 +142,7 @@ describe("createS2sSession", () => {
|
|
|
145
142
|
_interrupted: false,
|
|
146
143
|
});
|
|
147
144
|
|
|
148
|
-
expect(client.events).
|
|
149
|
-
type: "agent_transcript",
|
|
150
|
-
text: "Full response",
|
|
151
|
-
});
|
|
145
|
+
expect(client.events).toContainEvent("agent_transcript", { text: "Full response" });
|
|
152
146
|
});
|
|
153
147
|
|
|
154
148
|
test("speech_started and speech_stopped events are forwarded", async () => {
|
|
@@ -158,8 +152,8 @@ describe("createS2sSession", () => {
|
|
|
158
152
|
mockHandle._fire("event", { type: "speech_started" });
|
|
159
153
|
mockHandle._fire("event", { type: "speech_stopped" });
|
|
160
154
|
|
|
161
|
-
expect(client.events).
|
|
162
|
-
expect(client.events).
|
|
155
|
+
expect(client.events).toContainEvent("speech_started");
|
|
156
|
+
expect(client.events).toContainEvent("speech_stopped");
|
|
163
157
|
});
|
|
164
158
|
|
|
165
159
|
test("reply_started resets tool call count", async () => {
|
|
@@ -177,7 +171,7 @@ describe("createS2sSession", () => {
|
|
|
177
171
|
mockHandle._fire("event", { type: "reply_done" });
|
|
178
172
|
|
|
179
173
|
expect(client.audioDoneCount).toBe(1);
|
|
180
|
-
expect(client.events).
|
|
174
|
+
expect(client.events).toContainEvent("reply_done");
|
|
181
175
|
});
|
|
182
176
|
|
|
183
177
|
test("cancelled event emits cancelled", async () => {
|
|
@@ -186,7 +180,7 @@ describe("createS2sSession", () => {
|
|
|
186
180
|
|
|
187
181
|
mockHandle._fire("event", { type: "cancelled" });
|
|
188
182
|
|
|
189
|
-
expect(client.events).
|
|
183
|
+
expect(client.events).toContainEvent("cancelled");
|
|
190
184
|
});
|
|
191
185
|
|
|
192
186
|
test("error event emits error to client and closes handle", async () => {
|
|
@@ -195,11 +189,7 @@ describe("createS2sSession", () => {
|
|
|
195
189
|
|
|
196
190
|
mockHandle._fire("error", new Error("Something broke"));
|
|
197
191
|
|
|
198
|
-
expect(client.events).
|
|
199
|
-
type: "error",
|
|
200
|
-
code: "internal",
|
|
201
|
-
message: "Something broke",
|
|
202
|
-
});
|
|
192
|
+
expect(client.events).toContainEvent("error", { code: "internal", message: "Something broke" });
|
|
203
193
|
expect(mockHandle.close).toHaveBeenCalled();
|
|
204
194
|
});
|
|
205
195
|
|
|
@@ -227,14 +217,12 @@ describe("createS2sSession", () => {
|
|
|
227
217
|
"session-1",
|
|
228
218
|
expect.any(Array),
|
|
229
219
|
);
|
|
230
|
-
expect(client.events).
|
|
231
|
-
type: "tool_call",
|
|
220
|
+
expect(client.events).toContainEvent("tool_call", {
|
|
232
221
|
toolCallId: "call-1",
|
|
233
222
|
toolName: "my_tool",
|
|
234
223
|
args: { key: "value" },
|
|
235
224
|
});
|
|
236
|
-
expect(client.events).
|
|
237
|
-
type: "tool_call_done",
|
|
225
|
+
expect(client.events).toContainEvent("tool_call_done", {
|
|
238
226
|
toolCallId: "call-1",
|
|
239
227
|
result: "tool-output",
|
|
240
228
|
});
|
|
@@ -318,13 +306,7 @@ describe("createS2sSession", () => {
|
|
|
318
306
|
|
|
319
307
|
await session.start();
|
|
320
308
|
|
|
321
|
-
expect(client.events).
|
|
322
|
-
expect.objectContaining({
|
|
323
|
-
type: "error",
|
|
324
|
-
code: "internal",
|
|
325
|
-
message: "connect failed",
|
|
326
|
-
}),
|
|
327
|
-
);
|
|
309
|
+
expect(client.events).toContainEvent("error", { code: "internal", message: "connect failed" });
|
|
328
310
|
|
|
329
311
|
spy.mockRestore();
|
|
330
312
|
});
|
|
@@ -503,10 +485,6 @@ describe("createS2sSession", () => {
|
|
|
503
485
|
|
|
504
486
|
// ─── Idle timeout tests ──────────────────────────────────────────────
|
|
505
487
|
|
|
506
|
-
function hasIdleTimeout(events: unknown[]): boolean {
|
|
507
|
-
return events.some((e) => (e as Record<string, unknown>).type === "idle_timeout");
|
|
508
|
-
}
|
|
509
|
-
|
|
510
488
|
test("idle timeout fires after configured period of inactivity", async () => {
|
|
511
489
|
vi.useFakeTimers();
|
|
512
490
|
const { session, client, mockHandle } = setup({
|
|
@@ -519,7 +497,7 @@ describe("createS2sSession", () => {
|
|
|
519
497
|
});
|
|
520
498
|
await session.start();
|
|
521
499
|
vi.advanceTimersByTime(10_000);
|
|
522
|
-
expect(
|
|
500
|
+
expect(client.events).toContainEvent("idle_timeout");
|
|
523
501
|
expect(mockHandle.close).toHaveBeenCalled();
|
|
524
502
|
vi.useRealTimers();
|
|
525
503
|
});
|
|
@@ -538,9 +516,9 @@ describe("createS2sSession", () => {
|
|
|
538
516
|
vi.advanceTimersByTime(8000);
|
|
539
517
|
session.onAudio(new Uint8Array([1, 2, 3]));
|
|
540
518
|
vi.advanceTimersByTime(8000);
|
|
541
|
-
expect(
|
|
519
|
+
expect(client.events).not.toContainEvent("idle_timeout");
|
|
542
520
|
vi.advanceTimersByTime(2000);
|
|
543
|
-
expect(
|
|
521
|
+
expect(client.events).toContainEvent("idle_timeout");
|
|
544
522
|
vi.useRealTimers();
|
|
545
523
|
});
|
|
546
524
|
|
|
@@ -556,7 +534,7 @@ describe("createS2sSession", () => {
|
|
|
556
534
|
});
|
|
557
535
|
await session.start();
|
|
558
536
|
vi.advanceTimersByTime(600_000);
|
|
559
|
-
expect(
|
|
537
|
+
expect(client.events).not.toContainEvent("idle_timeout");
|
|
560
538
|
vi.useRealTimers();
|
|
561
539
|
});
|
|
562
540
|
|
|
@@ -573,7 +551,7 @@ describe("createS2sSession", () => {
|
|
|
573
551
|
await session.start();
|
|
574
552
|
await session.stop();
|
|
575
553
|
vi.advanceTimersByTime(20_000);
|
|
576
|
-
expect(
|
|
554
|
+
expect(client.events).not.toContainEvent("idle_timeout");
|
|
577
555
|
vi.useRealTimers();
|
|
578
556
|
});
|
|
579
557
|
|
|
@@ -582,9 +560,9 @@ describe("createS2sSession", () => {
|
|
|
582
560
|
const { session, client } = setup();
|
|
583
561
|
await session.start();
|
|
584
562
|
vi.advanceTimersByTime(240_000);
|
|
585
|
-
expect(
|
|
563
|
+
expect(client.events).not.toContainEvent("idle_timeout");
|
|
586
564
|
vi.advanceTimersByTime(60_000);
|
|
587
|
-
expect(
|
|
565
|
+
expect(client.events).toContainEvent("idle_timeout");
|
|
588
566
|
vi.useRealTimers();
|
|
589
567
|
});
|
|
590
568
|
});
|
|
@@ -121,4 +121,40 @@ describe("executeToolCall", () => {
|
|
|
121
121
|
expect(result).toBe(JSON.stringify({ error: 'Tool "slow" timed out after 30000ms' }));
|
|
122
122
|
vi.useRealTimers();
|
|
123
123
|
});
|
|
124
|
+
|
|
125
|
+
test("ctx.send calls the send callback", async () => {
|
|
126
|
+
const sends: Array<{ event: string; data: unknown }> = [];
|
|
127
|
+
const tool: ToolDef = {
|
|
128
|
+
description: "sends an event",
|
|
129
|
+
parameters: z.object({}),
|
|
130
|
+
execute: (_args, ctx) => {
|
|
131
|
+
ctx.send("game_state", { hp: 10 });
|
|
132
|
+
return "ok";
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
const result = await executeToolCall(
|
|
136
|
+
"sender",
|
|
137
|
+
{},
|
|
138
|
+
{
|
|
139
|
+
tool,
|
|
140
|
+
env: {},
|
|
141
|
+
send: (event, data) => sends.push({ event, data }),
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
expect(result).toBe("ok");
|
|
145
|
+
expect(sends).toEqual([{ event: "game_state", data: { hp: 10 } }]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("ctx.send is a no-op when no send callback provided", async () => {
|
|
149
|
+
const tool: ToolDef = {
|
|
150
|
+
description: "sends an event",
|
|
151
|
+
parameters: z.object({}),
|
|
152
|
+
execute: (_args, ctx) => {
|
|
153
|
+
ctx.send("test", {});
|
|
154
|
+
return "ok";
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
const result = await executeToolCall("sender", {}, { tool, env: {} });
|
|
158
|
+
expect(result).toBe("ok");
|
|
159
|
+
});
|
|
124
160
|
});
|
package/host/tool-executor.ts
CHANGED
|
@@ -27,6 +27,7 @@ export type ExecuteToolCallOptions = {
|
|
|
27
27
|
kv?: Kv | undefined;
|
|
28
28
|
messages?: readonly Message[] | undefined;
|
|
29
29
|
logger?: Logger | undefined;
|
|
30
|
+
send?: ((event: string, data: unknown) => void) | undefined;
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
function buildToolContext(opts: ExecuteToolCallOptions): ToolContext {
|
|
@@ -40,6 +41,9 @@ function buildToolContext(opts: ExecuteToolCallOptions): ToolContext {
|
|
|
40
41
|
},
|
|
41
42
|
messages: messages ?? [],
|
|
42
43
|
sessionId: sessionId ?? "",
|
|
44
|
+
send(event: string, data: unknown): void {
|
|
45
|
+
opts.send?.(event, data);
|
|
46
|
+
},
|
|
43
47
|
};
|
|
44
48
|
}
|
|
45
49
|
|
package/host/ws-handler.ts
CHANGED
|
@@ -47,6 +47,8 @@ export type WsSessionOptions = {
|
|
|
47
47
|
onClose?: () => void;
|
|
48
48
|
/** Callback invoked with the session ID after session cleanup. */
|
|
49
49
|
onSessionEnd?: (sessionId: string) => void;
|
|
50
|
+
/** Callback invoked with the session ID and client sink after session setup. */
|
|
51
|
+
onSinkCreated?: (sessionId: string, sink: ClientSink) => void;
|
|
50
52
|
/** Logger instance. Defaults to console. */
|
|
51
53
|
logger?: Logger;
|
|
52
54
|
/** Timeout in ms for session.start(). Defaults to 10 000 (10s). */
|
|
@@ -184,6 +186,7 @@ export function wireSessionSocket(ws: SessionWebSocket, opts: WsSessionOptions):
|
|
|
184
186
|
const client = createClientSink(ws, log);
|
|
185
187
|
session = opts.createSession(sessionId, client);
|
|
186
188
|
sessions.set(sessionId, session);
|
|
189
|
+
opts.onSinkCreated?.(sessionId, client);
|
|
187
190
|
|
|
188
191
|
// Send config immediately — zero RTT. Include sessionId so the client
|
|
189
192
|
// can reconnect with ?sessionId=<id> to resume a persisted session.
|
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`export surface stability > @alexkroman1/aai main export 1`] = `
|
|
4
|
+
[
|
|
5
|
+
"AGENT_CSP",
|
|
6
|
+
"BuiltinToolSchema",
|
|
7
|
+
"DEFAULT_GREETING",
|
|
8
|
+
"DEFAULT_IDLE_TIMEOUT_MS",
|
|
9
|
+
"DEFAULT_MAX_HISTORY",
|
|
10
|
+
"DEFAULT_SESSION_START_TIMEOUT_MS",
|
|
11
|
+
"DEFAULT_SHUTDOWN_TIMEOUT_MS",
|
|
12
|
+
"DEFAULT_STT_SAMPLE_RATE",
|
|
13
|
+
"DEFAULT_SYSTEM_PROMPT",
|
|
14
|
+
"DEFAULT_TTS_SAMPLE_RATE",
|
|
15
|
+
"FETCH_TIMEOUT_MS",
|
|
16
|
+
"MAX_HTML_BYTES",
|
|
17
|
+
"MAX_MESSAGE_BUFFER_SIZE",
|
|
18
|
+
"MAX_PAGE_CHARS",
|
|
19
|
+
"MAX_TOOL_RESULT_CHARS",
|
|
20
|
+
"MAX_VALUE_SIZE",
|
|
21
|
+
"MAX_WS_PAYLOAD_BYTES",
|
|
22
|
+
"RUN_CODE_TIMEOUT_MS",
|
|
23
|
+
"TOOL_EXECUTION_TIMEOUT_MS",
|
|
24
|
+
"ToolChoiceSchema",
|
|
25
|
+
"WS_OPEN",
|
|
26
|
+
"agent",
|
|
27
|
+
"errorDetail",
|
|
28
|
+
"errorMessage",
|
|
29
|
+
"matchesAllowedHost",
|
|
30
|
+
"parseWsUpgradeParams",
|
|
31
|
+
"tool",
|
|
32
|
+
"toolError",
|
|
33
|
+
"validateAllowedHostPattern",
|
|
34
|
+
]
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
exports[`export surface stability > @alexkroman1/aai/manifest export 1`] = `
|
|
38
|
+
[
|
|
39
|
+
"AgentConfigSchema",
|
|
40
|
+
"EMPTY_PARAMS",
|
|
41
|
+
"ToolSchemaSchema",
|
|
42
|
+
"agentToolsToSchemas",
|
|
43
|
+
"toAgentConfig",
|
|
44
|
+
]
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
exports[`export surface stability > @alexkroman1/aai/protocol export 1`] = `
|
|
48
|
+
[
|
|
49
|
+
"ClientEventSchema",
|
|
50
|
+
"ClientMessageSchema",
|
|
51
|
+
"KvDelSchema",
|
|
52
|
+
"KvGetSchema",
|
|
53
|
+
"KvRequestSchema",
|
|
54
|
+
"KvSetSchema",
|
|
55
|
+
"ReadyConfigSchema",
|
|
56
|
+
"ServerMessageSchema",
|
|
57
|
+
"SessionErrorCodeSchema",
|
|
58
|
+
"buildReadyConfig",
|
|
59
|
+
"lenientParse",
|
|
60
|
+
]
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
exports[`export surface stability > @alexkroman1/aai/runtime export 1`] = `
|
|
64
|
+
[
|
|
65
|
+
"DEFAULT_S2S_CONFIG",
|
|
66
|
+
"_internals",
|
|
67
|
+
"buildCtx",
|
|
68
|
+
"consoleLogger",
|
|
69
|
+
"createRuntime",
|
|
70
|
+
"createS2sSession",
|
|
71
|
+
"createServer",
|
|
72
|
+
"createUnstorageKv",
|
|
73
|
+
"executeInIsolate",
|
|
74
|
+
"executeToolCall",
|
|
75
|
+
"jsonLogger",
|
|
76
|
+
"resolveAllBuiltins",
|
|
77
|
+
"wireSessionSocket",
|
|
78
|
+
]
|
|
79
|
+
`;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`manifest schema shapes > AgentConfigSchema shape 1`] = `
|
|
4
|
+
[
|
|
5
|
+
"builtinTools",
|
|
6
|
+
"greeting",
|
|
7
|
+
"idleTimeoutMs",
|
|
8
|
+
"maxSteps",
|
|
9
|
+
"name",
|
|
10
|
+
"sttPrompt",
|
|
11
|
+
"systemPrompt",
|
|
12
|
+
"toolChoice",
|
|
13
|
+
]
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
exports[`manifest schema shapes > ToolSchemaSchema shape 1`] = `
|
|
17
|
+
[
|
|
18
|
+
"description",
|
|
19
|
+
"name",
|
|
20
|
+
"parameters",
|
|
21
|
+
]
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
exports[`protocol schema shapes > ClientEventSchema option shapes 1`] = `
|
|
25
|
+
{
|
|
26
|
+
"agent_transcript": [
|
|
27
|
+
"text",
|
|
28
|
+
"type",
|
|
29
|
+
],
|
|
30
|
+
"cancelled": [
|
|
31
|
+
"type",
|
|
32
|
+
],
|
|
33
|
+
"custom_event": [
|
|
34
|
+
"data",
|
|
35
|
+
"event",
|
|
36
|
+
"type",
|
|
37
|
+
],
|
|
38
|
+
"error": [
|
|
39
|
+
"code",
|
|
40
|
+
"message",
|
|
41
|
+
"type",
|
|
42
|
+
],
|
|
43
|
+
"idle_timeout": [
|
|
44
|
+
"type",
|
|
45
|
+
],
|
|
46
|
+
"reply_done": [
|
|
47
|
+
"type",
|
|
48
|
+
],
|
|
49
|
+
"reset": [
|
|
50
|
+
"type",
|
|
51
|
+
],
|
|
52
|
+
"speech_started": [
|
|
53
|
+
"type",
|
|
54
|
+
],
|
|
55
|
+
"speech_stopped": [
|
|
56
|
+
"type",
|
|
57
|
+
],
|
|
58
|
+
"tool_call": [
|
|
59
|
+
"args",
|
|
60
|
+
"toolCallId",
|
|
61
|
+
"toolName",
|
|
62
|
+
"type",
|
|
63
|
+
],
|
|
64
|
+
"tool_call_done": [
|
|
65
|
+
"result",
|
|
66
|
+
"toolCallId",
|
|
67
|
+
"type",
|
|
68
|
+
],
|
|
69
|
+
"user_transcript": [
|
|
70
|
+
"text",
|
|
71
|
+
"turnOrder",
|
|
72
|
+
"type",
|
|
73
|
+
],
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
exports[`protocol schema shapes > ClientMessageSchema option shapes 1`] = `
|
|
78
|
+
{
|
|
79
|
+
"audio_ready": [
|
|
80
|
+
"type",
|
|
81
|
+
],
|
|
82
|
+
"cancel": [
|
|
83
|
+
"type",
|
|
84
|
+
],
|
|
85
|
+
"history": [
|
|
86
|
+
"messages",
|
|
87
|
+
"type",
|
|
88
|
+
],
|
|
89
|
+
"reset": [
|
|
90
|
+
"type",
|
|
91
|
+
],
|
|
92
|
+
}
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
exports[`protocol schema shapes > KvDelSchema shape 1`] = `
|
|
96
|
+
[
|
|
97
|
+
"key",
|
|
98
|
+
"op",
|
|
99
|
+
]
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
exports[`protocol schema shapes > KvGetSchema shape 1`] = `
|
|
103
|
+
[
|
|
104
|
+
"key",
|
|
105
|
+
"op",
|
|
106
|
+
]
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
exports[`protocol schema shapes > KvSetSchema shape 1`] = `
|
|
110
|
+
[
|
|
111
|
+
"expireIn",
|
|
112
|
+
"key",
|
|
113
|
+
"op",
|
|
114
|
+
"value",
|
|
115
|
+
]
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
exports[`protocol schema shapes > ReadyConfigSchema shape 1`] = `
|
|
119
|
+
[
|
|
120
|
+
"audioFormat",
|
|
121
|
+
"sampleRate",
|
|
122
|
+
"ttsSampleRate",
|
|
123
|
+
]
|
|
124
|
+
`;
|
|
125
|
+
|
|
126
|
+
exports[`protocol schema shapes > ServerMessageSchema option shapes 1`] = `
|
|
127
|
+
{
|
|
128
|
+
"agent_transcript": [
|
|
129
|
+
"text",
|
|
130
|
+
"type",
|
|
131
|
+
],
|
|
132
|
+
"audio_done": [
|
|
133
|
+
"type",
|
|
134
|
+
],
|
|
135
|
+
"cancelled": [
|
|
136
|
+
"type",
|
|
137
|
+
],
|
|
138
|
+
"config": [
|
|
139
|
+
"audioFormat",
|
|
140
|
+
"sampleRate",
|
|
141
|
+
"sessionId",
|
|
142
|
+
"ttsSampleRate",
|
|
143
|
+
"type",
|
|
144
|
+
],
|
|
145
|
+
"custom_event": [
|
|
146
|
+
"data",
|
|
147
|
+
"event",
|
|
148
|
+
"type",
|
|
149
|
+
],
|
|
150
|
+
"error": [
|
|
151
|
+
"code",
|
|
152
|
+
"message",
|
|
153
|
+
"type",
|
|
154
|
+
],
|
|
155
|
+
"idle_timeout": [
|
|
156
|
+
"type",
|
|
157
|
+
],
|
|
158
|
+
"reply_done": [
|
|
159
|
+
"type",
|
|
160
|
+
],
|
|
161
|
+
"reset": [
|
|
162
|
+
"type",
|
|
163
|
+
],
|
|
164
|
+
"speech_started": [
|
|
165
|
+
"type",
|
|
166
|
+
],
|
|
167
|
+
"speech_stopped": [
|
|
168
|
+
"type",
|
|
169
|
+
],
|
|
170
|
+
"tool_call": [
|
|
171
|
+
"args",
|
|
172
|
+
"toolCallId",
|
|
173
|
+
"toolName",
|
|
174
|
+
"type",
|
|
175
|
+
],
|
|
176
|
+
"tool_call_done": [
|
|
177
|
+
"result",
|
|
178
|
+
"toolCallId",
|
|
179
|
+
"type",
|
|
180
|
+
],
|
|
181
|
+
"user_transcript": [
|
|
182
|
+
"text",
|
|
183
|
+
"turnOrder",
|
|
184
|
+
"type",
|
|
185
|
+
],
|
|
186
|
+
}
|
|
187
|
+
`;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import "./_test-matchers.ts";
|
|
5
|
+
|
|
6
|
+
describe("toBeValidClientEvent", () => {
|
|
7
|
+
it("passes for a valid event", () => {
|
|
8
|
+
expect({ type: "speech_started" }).toBeValidClientEvent();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("passes for a valid event with fields", () => {
|
|
12
|
+
expect({
|
|
13
|
+
type: "user_transcript",
|
|
14
|
+
text: "hello world",
|
|
15
|
+
}).toBeValidClientEvent();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("fails for an invalid event", () => {
|
|
19
|
+
expect(() => {
|
|
20
|
+
expect({ type: "not_a_real_event" }).toBeValidClientEvent();
|
|
21
|
+
}).toThrow(/expected value to be a valid ClientEvent/);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("fails for a missing type field", () => {
|
|
25
|
+
expect(() => {
|
|
26
|
+
expect({ text: "no type" }).toBeValidClientEvent();
|
|
27
|
+
}).toThrow(/expected value to be a valid ClientEvent/);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("toContainEvent", () => {
|
|
32
|
+
const events = [
|
|
33
|
+
{ type: "speech_started" },
|
|
34
|
+
{ type: "user_transcript", text: "hello" },
|
|
35
|
+
{ type: "tool_call", toolCallId: "tc1", toolName: "search", args: { q: "test" } },
|
|
36
|
+
{ type: "reply_done" },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
it("finds a matching event by type", () => {
|
|
40
|
+
expect(events).toContainEvent("speech_started");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("finds a matching event with fields", () => {
|
|
44
|
+
expect(events).toContainEvent("tool_call", { toolName: "search" });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("matches a subset of fields", () => {
|
|
48
|
+
expect(events).toContainEvent("tool_call", {
|
|
49
|
+
toolName: "search",
|
|
50
|
+
args: { q: "test" },
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("fails when event type is not found", () => {
|
|
55
|
+
expect(() => {
|
|
56
|
+
expect(events).toContainEvent("cancelled");
|
|
57
|
+
}).toThrow(/expected array to contain event of type "cancelled"/);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("fails when fields do not match", () => {
|
|
61
|
+
expect(() => {
|
|
62
|
+
expect(events).toContainEvent("tool_call", { toolName: "visit_webpage" });
|
|
63
|
+
}).toThrow(/expected array to contain event of type "tool_call"/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("fails when received value is not an array", () => {
|
|
67
|
+
expect(() => {
|
|
68
|
+
expect("not-an-array").toContainEvent("speech_started");
|
|
69
|
+
}).toThrow(/expected an array of events/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("supports .not negation", () => {
|
|
73
|
+
expect(events).not.toContainEvent("cancelled");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* Custom Vitest matchers for AAI domain types.
|
|
4
|
+
*
|
|
5
|
+
* Registered via `expect.extend()` — add this file to the vitest `setupFiles`
|
|
6
|
+
* for the `aai` project so matchers are available in every test.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { isDeepStrictEqual } from "node:util";
|
|
10
|
+
import { expect } from "vitest";
|
|
11
|
+
import { ClientEventSchema } from "./protocol.ts";
|
|
12
|
+
|
|
13
|
+
type MatcherResult = { pass: boolean; message: () => string };
|
|
14
|
+
|
|
15
|
+
// ─── Matcher implementations ────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function toBeValidClientEvent(received: unknown): MatcherResult {
|
|
18
|
+
const result = ClientEventSchema.safeParse(received);
|
|
19
|
+
return {
|
|
20
|
+
pass: result.success,
|
|
21
|
+
message: () =>
|
|
22
|
+
result.success
|
|
23
|
+
? "expected value NOT to be a valid ClientEvent, but it parsed successfully"
|
|
24
|
+
: `expected value to be a valid ClientEvent\n\nZod errors:\n${result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function toContainEvent(
|
|
29
|
+
received: unknown,
|
|
30
|
+
type: string,
|
|
31
|
+
fields?: Record<string, unknown>,
|
|
32
|
+
): MatcherResult {
|
|
33
|
+
if (!Array.isArray(received)) {
|
|
34
|
+
return {
|
|
35
|
+
pass: false,
|
|
36
|
+
message: () => `expected an array of events, but received ${typeof received}`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const match = received.some((event: Record<string, unknown>) => {
|
|
41
|
+
if (event?.type !== type) return false;
|
|
42
|
+
if (!fields) return true;
|
|
43
|
+
return Object.entries(fields).every(([key, value]) => isDeepStrictEqual(event[key], value));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
pass: match,
|
|
48
|
+
message: () =>
|
|
49
|
+
match
|
|
50
|
+
? `expected array NOT to contain event of type "${type}"${fields ? ` with fields ${JSON.stringify(fields)}` : ""}`
|
|
51
|
+
: `expected array to contain event of type "${type}"${fields ? ` with fields ${JSON.stringify(fields)}` : ""}\n\nReceived event types: [${received.map((e: Record<string, unknown>) => `"${e?.type}"`).join(", ")}]`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Register matchers ──────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
expect.extend({
|
|
58
|
+
toBeValidClientEvent,
|
|
59
|
+
toContainEvent,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ─── Type augmentation ──────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
declare module "vitest" {
|
|
65
|
+
interface Assertion<T> {
|
|
66
|
+
toBeValidClientEvent(): void;
|
|
67
|
+
toContainEvent(type: string, fields?: Record<string, unknown>): void;
|
|
68
|
+
}
|
|
69
|
+
interface AsymmetricMatchersContaining {
|
|
70
|
+
toBeValidClientEvent(): void;
|
|
71
|
+
toContainEvent(type: string, fields?: Record<string, unknown>): void;
|
|
72
|
+
}
|
|
73
|
+
}
|