@kognitivedev/cloud-voice 0.2.29
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 +2 -0
- package/.turbo/turbo-test.log +13 -0
- package/CHANGELOG.md +10 -0
- package/README.md +226 -0
- package/dist/browser.d.ts +7 -0
- package/dist/browser.js +301 -0
- package/dist/client.d.ts +219 -0
- package/dist/client.js +535 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +15 -0
- package/dist/server.d.ts +58 -0
- package/dist/server.js +92 -0
- package/dist/server.test.d.ts +1 -0
- package/dist/server.test.js +78 -0
- package/dist/sse.d.ts +7 -0
- package/dist/sse.js +75 -0
- package/dist/types.d.ts +865 -0
- package/dist/types.js +2 -0
- package/package.json +52 -0
- package/src/__tests__/browser.test.ts +196 -0
- package/src/__tests__/client.test.ts +482 -0
- package/src/__tests__/server.test.ts +84 -0
- package/src/browser.ts +342 -0
- package/src/client.ts +610 -0
- package/src/index.ts +100 -0
- package/src/server.ts +140 -0
- package/src/sse.ts +57 -0
- package/src/types.ts +927 -0
- package/tsconfig.json +14 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { KognitiveApiError } from "@kognitivedev/client-core";
|
|
3
|
+
import { KognitiveCloudVoiceClient } from "../client";
|
|
4
|
+
|
|
5
|
+
function createTransport(
|
|
6
|
+
json: (path: string, init?: RequestInit) => Promise<unknown> = async (path: string) => {
|
|
7
|
+
if (path === "/api/cloud/voice/phone/connections") return { connections: [{ id: "conn_1" }] };
|
|
8
|
+
if (path === "/api/cloud/voice/phone/numbers") return { numbers: [{ id: "num_1" }] };
|
|
9
|
+
if (path === "/api/cloud/voice/phone/bridge") return { bridge: { online: true, status: "online" } };
|
|
10
|
+
if (path === "/api/cloud/voice/phone/sip") return { sip: { bridge: { online: true }, diagnostics: { nodeId: "sip-1" } } };
|
|
11
|
+
return { ok: true };
|
|
12
|
+
},
|
|
13
|
+
) {
|
|
14
|
+
return {
|
|
15
|
+
baseUrl: "https://api.example.com",
|
|
16
|
+
json: vi.fn(json),
|
|
17
|
+
raw: vi.fn(),
|
|
18
|
+
multipart: vi.fn(async () => ({ id: "voice-row-1", voiceId: "abcd1234" })),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("KognitiveCloudVoiceClient phone APIs", () => {
|
|
23
|
+
it("manages custom xAI voices through cloud voice routes", async () => {
|
|
24
|
+
const voice = { id: "voice-row-1", voiceId: "abcd1234", name: "Support voice", provider: "xai-realtime" };
|
|
25
|
+
const transport = createTransport(async (path: string, init?: RequestInit) => {
|
|
26
|
+
if (path === "/api/cloud/voice/custom-voices") return { voices: [voice] };
|
|
27
|
+
if (path === "/api/cloud/voice/custom-voices/import" && init?.method === "POST") return voice;
|
|
28
|
+
if (path === "/api/cloud/voice/custom-voices/abcd1234" && init?.method === "PATCH") return { ...voice, name: "Updated" };
|
|
29
|
+
if (path === "/api/cloud/voice/custom-voices/abcd1234" && init?.method === "DELETE") return { success: true };
|
|
30
|
+
throw new Error(`Unexpected request: ${path}`);
|
|
31
|
+
});
|
|
32
|
+
const client = new KognitiveCloudVoiceClient(transport as any);
|
|
33
|
+
|
|
34
|
+
await expect(client.customVoices.list()).resolves.toEqual([voice]);
|
|
35
|
+
await expect(client.customVoices.import({
|
|
36
|
+
voiceId: "abcd1234",
|
|
37
|
+
name: "Support voice",
|
|
38
|
+
consent: { accepted: true },
|
|
39
|
+
})).resolves.toEqual(voice);
|
|
40
|
+
await expect(client.customVoices.update("abcd1234", { name: "Updated", useCase: "conversational" })).resolves.toMatchObject({ name: "Updated" });
|
|
41
|
+
await expect(client.customVoices.delete("abcd1234")).resolves.toEqual({ success: true });
|
|
42
|
+
|
|
43
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/custom-voices");
|
|
44
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/custom-voices/import", {
|
|
45
|
+
method: "POST",
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
voiceId: "abcd1234",
|
|
48
|
+
name: "Support voice",
|
|
49
|
+
consent: { accepted: true },
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/custom-voices/abcd1234", {
|
|
53
|
+
method: "PATCH",
|
|
54
|
+
body: JSON.stringify({ name: "Updated", useCase: "conversational", use_case: "conversational" }),
|
|
55
|
+
});
|
|
56
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/custom-voices/abcd1234", {
|
|
57
|
+
method: "DELETE",
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("sends custom voice clone uploads as multipart", async () => {
|
|
62
|
+
const transport = createTransport();
|
|
63
|
+
const client = new KognitiveCloudVoiceClient(transport as any);
|
|
64
|
+
const file = new Blob([new Uint8Array([1, 2, 3])], { type: "audio/wav" });
|
|
65
|
+
|
|
66
|
+
await expect(client.customVoices.clone({
|
|
67
|
+
file,
|
|
68
|
+
fileName: "sample.wav",
|
|
69
|
+
name: "Support voice",
|
|
70
|
+
useCase: "conversational",
|
|
71
|
+
consent: { accepted: true, country: "US", state: "CA" },
|
|
72
|
+
})).resolves.toMatchObject({ voiceId: "abcd1234" });
|
|
73
|
+
|
|
74
|
+
expect(transport.multipart).toHaveBeenCalledWith(
|
|
75
|
+
"/api/cloud/voice/custom-voices/clone",
|
|
76
|
+
expect.any(FormData),
|
|
77
|
+
{ method: "POST" },
|
|
78
|
+
);
|
|
79
|
+
const form = (transport.multipart as any).mock.calls[0][1] as FormData;
|
|
80
|
+
expect(form.get("name")).toBe("Support voice");
|
|
81
|
+
expect(form.get("use_case")).toBe("conversational");
|
|
82
|
+
expect(form.get("consent")).toBe(JSON.stringify({ accepted: true, country: "US", state: "CA" }));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("lists and creates phone connections through cloud voice routes", async () => {
|
|
86
|
+
const transport = createTransport();
|
|
87
|
+
const client = new KognitiveCloudVoiceClient(transport as any);
|
|
88
|
+
|
|
89
|
+
await expect(client.phone.connections.list()).resolves.toEqual([{ id: "conn_1" }]);
|
|
90
|
+
await client.phone.connections.create({
|
|
91
|
+
name: "Local Twilio",
|
|
92
|
+
accountSid: "AC123",
|
|
93
|
+
authToken: "secret",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/phone/connections");
|
|
97
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/phone/connections", {
|
|
98
|
+
method: "POST",
|
|
99
|
+
body: JSON.stringify({
|
|
100
|
+
name: "Local Twilio",
|
|
101
|
+
accountSid: "AC123",
|
|
102
|
+
authToken: "secret",
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("lists, creates, and updates phone numbers through cloud voice routes", async () => {
|
|
108
|
+
const transport = createTransport();
|
|
109
|
+
const client = new KognitiveCloudVoiceClient(transport as any);
|
|
110
|
+
|
|
111
|
+
await expect(client.phone.numbers.list()).resolves.toEqual([{ id: "num_1" }]);
|
|
112
|
+
await client.phone.numbers.create({
|
|
113
|
+
connectionId: "conn_1",
|
|
114
|
+
phoneNumber: "+15551234567",
|
|
115
|
+
inboundAgentSlug: "support",
|
|
116
|
+
outboundEnabled: true,
|
|
117
|
+
});
|
|
118
|
+
await client.phone.numbers.update({
|
|
119
|
+
id: "num_1",
|
|
120
|
+
label: "Support",
|
|
121
|
+
inboundAgentSlug: "support",
|
|
122
|
+
inboundEnabled: true,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/phone/numbers");
|
|
126
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/phone/numbers", {
|
|
127
|
+
method: "POST",
|
|
128
|
+
body: JSON.stringify({
|
|
129
|
+
connectionId: "conn_1",
|
|
130
|
+
phoneNumber: "+15551234567",
|
|
131
|
+
inboundAgentSlug: "support",
|
|
132
|
+
outboundEnabled: true,
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/phone/numbers", {
|
|
136
|
+
method: "PATCH",
|
|
137
|
+
body: JSON.stringify({
|
|
138
|
+
id: "num_1",
|
|
139
|
+
label: "Support",
|
|
140
|
+
inboundAgentSlug: "support",
|
|
141
|
+
inboundEnabled: true,
|
|
142
|
+
}),
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("checks phone bridge health through cloud voice routes", async () => {
|
|
147
|
+
const transport = createTransport();
|
|
148
|
+
const client = new KognitiveCloudVoiceClient(transport as any);
|
|
149
|
+
|
|
150
|
+
await expect(client.phone.bridge.health()).resolves.toEqual({ online: true, status: "online" });
|
|
151
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/phone/bridge");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("checks and reloads SIP diagnostics through cloud voice routes", async () => {
|
|
155
|
+
const transport = createTransport();
|
|
156
|
+
const client = new KognitiveCloudVoiceClient(transport as any);
|
|
157
|
+
|
|
158
|
+
await expect(client.phone.sip.diagnostics()).resolves.toEqual({ bridge: { online: true }, diagnostics: { nodeId: "sip-1" } });
|
|
159
|
+
await expect(client.phone.sip.reload()).resolves.toEqual({ bridge: { online: true }, diagnostics: { nodeId: "sip-1" } });
|
|
160
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/phone/sip");
|
|
161
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/phone/sip", {
|
|
162
|
+
method: "POST",
|
|
163
|
+
body: "{}",
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("lists live calls, loads details, and hangs up through call routes", async () => {
|
|
168
|
+
const liveCall = { id: "session_1", callLegId: "call_1", stateLabel: "Connected" };
|
|
169
|
+
const handoff = { id: "handoff_1", status: "active", mode: "browser" };
|
|
170
|
+
const token = { token: "listen.jwt", wsUrl: "wss://api.example.com/audio", scopes: ["listen"] };
|
|
171
|
+
const transport = createTransport(async (path: string, init?: RequestInit) => {
|
|
172
|
+
if (path === "/api/cloud/voice/calls/live?status=all&limit=25") return { calls: [liveCall] };
|
|
173
|
+
if (path === "/api/cloud/voice/calls/session_1" && !init) return liveCall;
|
|
174
|
+
if (path === "/api/cloud/voice/calls/session_1/hangup" && init?.method === "POST") return { call: liveCall, result: { ok: true } };
|
|
175
|
+
if (path === "/api/cloud/voice/calls/session_1/listen-token" && init?.method === "POST") return token;
|
|
176
|
+
if (path === "/api/cloud/voice/calls/session_1/handoff" && init?.method === "POST") return { handoff };
|
|
177
|
+
if (path === "/api/cloud/voice/calls/session_1/handoff/handoff_1/accept" && init?.method === "POST") return { handoff };
|
|
178
|
+
if (path === "/api/cloud/voice/calls/session_1/handoff/handoff_1/cancel" && init?.method === "POST") return { handoff: { ...handoff, status: "returned_to_ai" } };
|
|
179
|
+
return { ok: true };
|
|
180
|
+
});
|
|
181
|
+
const client = new KognitiveCloudVoiceClient(transport as any);
|
|
182
|
+
|
|
183
|
+
await expect(client.calls.live({ status: "all", limit: 25 })).resolves.toEqual([liveCall]);
|
|
184
|
+
await expect(client.calls.get("session_1")).resolves.toEqual(liveCall);
|
|
185
|
+
await expect(client.calls.hangup("session_1", "Done")).resolves.toEqual({ call: liveCall, result: { ok: true } });
|
|
186
|
+
await expect(client.calls.listenToken("session_1", { managerId: "mgr_1" })).resolves.toEqual(token);
|
|
187
|
+
await expect(client.calls.handoff.create("session_1", { mode: "browser", managerId: "mgr_1" })).resolves.toEqual({ handoff });
|
|
188
|
+
await expect(client.calls.handoff.accept("session_1", "handoff_1", { managerId: "mgr_1" })).resolves.toEqual({ handoff });
|
|
189
|
+
await expect(client.calls.handoff.cancel("session_1", "handoff_1", { returnToAi: true })).resolves.toEqual({ handoff: { ...handoff, status: "returned_to_ai" } });
|
|
190
|
+
|
|
191
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/calls/live?status=all&limit=25");
|
|
192
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/calls/session_1");
|
|
193
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/calls/session_1/hangup", {
|
|
194
|
+
method: "POST",
|
|
195
|
+
body: JSON.stringify({ reason: "Done" }),
|
|
196
|
+
});
|
|
197
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/calls/session_1/listen-token", {
|
|
198
|
+
method: "POST",
|
|
199
|
+
body: JSON.stringify({ managerId: "mgr_1" }),
|
|
200
|
+
});
|
|
201
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/calls/session_1/handoff", {
|
|
202
|
+
method: "POST",
|
|
203
|
+
body: JSON.stringify({ mode: "browser", managerId: "mgr_1" }),
|
|
204
|
+
});
|
|
205
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/calls/session_1/handoff/handoff_1/accept", {
|
|
206
|
+
method: "POST",
|
|
207
|
+
body: JSON.stringify({ managerId: "mgr_1" }),
|
|
208
|
+
});
|
|
209
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/calls/session_1/handoff/handoff_1/cancel", {
|
|
210
|
+
method: "POST",
|
|
211
|
+
body: JSON.stringify({ returnToAi: true }),
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("waits for a call to complete, emits status changes, and finalizes outcome", async () => {
|
|
216
|
+
const changes: string[] = [];
|
|
217
|
+
const events: string[] = [];
|
|
218
|
+
const queuedCall = {
|
|
219
|
+
id: "session_1",
|
|
220
|
+
status: "queued",
|
|
221
|
+
sessionStatus: "created",
|
|
222
|
+
stateLabel: "Dialing",
|
|
223
|
+
latestEventType: "voice.call.outbound.created",
|
|
224
|
+
outcomeStatus: null,
|
|
225
|
+
endedAt: null,
|
|
226
|
+
};
|
|
227
|
+
const completedCall = {
|
|
228
|
+
...queuedCall,
|
|
229
|
+
status: "completed",
|
|
230
|
+
sessionStatus: "completed",
|
|
231
|
+
stateLabel: "Ended",
|
|
232
|
+
latestEventType: "voice.phone.hangup.completed",
|
|
233
|
+
outcomeStatus: null,
|
|
234
|
+
endedAt: "2026-05-03T12:00:00.000Z",
|
|
235
|
+
};
|
|
236
|
+
const finalized = {
|
|
237
|
+
call: {
|
|
238
|
+
...completedCall,
|
|
239
|
+
outcomeStatus: "completed",
|
|
240
|
+
outcomeReason: "The call reached a normal terminal state.",
|
|
241
|
+
outcomeConfidence: 0.85,
|
|
242
|
+
},
|
|
243
|
+
status: "completed",
|
|
244
|
+
outcome: {
|
|
245
|
+
status: "completed",
|
|
246
|
+
reason: "The call reached a normal terminal state.",
|
|
247
|
+
confidence: 0.85,
|
|
248
|
+
},
|
|
249
|
+
outcomeSource: "derived",
|
|
250
|
+
};
|
|
251
|
+
let getCount = 0;
|
|
252
|
+
const transport = createTransport(async (path: string, init?: RequestInit) => {
|
|
253
|
+
if (path === "/api/cloud/voice/calls/session_1" && !init) {
|
|
254
|
+
getCount += 1;
|
|
255
|
+
return getCount <= 2 ? queuedCall : completedCall;
|
|
256
|
+
}
|
|
257
|
+
if (path === "/api/cloud/voice/calls/session_1/finalize" && init?.method === "POST") return finalized;
|
|
258
|
+
throw new Error(`Unexpected request: ${path}`);
|
|
259
|
+
});
|
|
260
|
+
const client = new KognitiveCloudVoiceClient(transport as any);
|
|
261
|
+
|
|
262
|
+
await expect(client.calls.wait("session_1", {
|
|
263
|
+
intervalMs: 10,
|
|
264
|
+
timeoutMs: 1_000,
|
|
265
|
+
onStatusChange: (event) => changes.push(`${event.status}:${event.sessionStatus}:${event.outcomeStatus ?? "none"}`),
|
|
266
|
+
onEvent: (event) => events.push(`${event.status}:${event.stateLabel}:${event.latestEventType ?? "none"}`),
|
|
267
|
+
})).resolves.toEqual(finalized);
|
|
268
|
+
|
|
269
|
+
expect(changes).toEqual([
|
|
270
|
+
"queued:created:none",
|
|
271
|
+
"completed:completed:none",
|
|
272
|
+
"completed:completed:completed",
|
|
273
|
+
]);
|
|
274
|
+
expect(events).toEqual([
|
|
275
|
+
"queued:Dialing:voice.call.outbound.created",
|
|
276
|
+
"completed:Ended:voice.phone.hangup.completed",
|
|
277
|
+
"completed:Ended:voice.phone.hangup.completed",
|
|
278
|
+
]);
|
|
279
|
+
expect(getCount).toBe(3);
|
|
280
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/calls/session_1/finalize", {
|
|
281
|
+
method: "POST",
|
|
282
|
+
body: JSON.stringify({}),
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("surfaces finalize errors after a terminal call is observed", async () => {
|
|
287
|
+
const completedCall = {
|
|
288
|
+
id: "session_1",
|
|
289
|
+
status: "completed",
|
|
290
|
+
sessionStatus: "completed",
|
|
291
|
+
stateLabel: "Ended",
|
|
292
|
+
latestEventType: "voice.phone.hangup.completed",
|
|
293
|
+
outcomeStatus: null,
|
|
294
|
+
endedAt: "2026-05-03T12:00:00.000Z",
|
|
295
|
+
};
|
|
296
|
+
const apiError = new KognitiveApiError("Finalize failed", 500, { error: "Finalize failed" });
|
|
297
|
+
const transport = createTransport(async (path: string, init?: RequestInit) => {
|
|
298
|
+
if (path === "/api/cloud/voice/calls/session_1" && !init) return completedCall;
|
|
299
|
+
if (path === "/api/cloud/voice/calls/session_1/finalize" && init?.method === "POST") throw apiError;
|
|
300
|
+
throw new Error(`Unexpected request: ${path}`);
|
|
301
|
+
});
|
|
302
|
+
const client = new KognitiveCloudVoiceClient(transport as any);
|
|
303
|
+
|
|
304
|
+
await expect(client.calls.wait("session_1", {
|
|
305
|
+
intervalMs: 10,
|
|
306
|
+
timeoutMs: 1_000,
|
|
307
|
+
})).rejects.toBe(apiError);
|
|
308
|
+
|
|
309
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/calls/session_1/finalize", {
|
|
310
|
+
method: "POST",
|
|
311
|
+
body: JSON.stringify({}),
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("finalizes when the session status is terminal even if the call leg is still active", async () => {
|
|
316
|
+
const sessionTerminalCall = {
|
|
317
|
+
id: "session_1",
|
|
318
|
+
status: "active",
|
|
319
|
+
sessionStatus: "completed",
|
|
320
|
+
stateLabel: "Ended",
|
|
321
|
+
latestEventType: "voice.session.completed",
|
|
322
|
+
outcomeStatus: null,
|
|
323
|
+
endedAt: null,
|
|
324
|
+
};
|
|
325
|
+
const finalized = {
|
|
326
|
+
call: {
|
|
327
|
+
...sessionTerminalCall,
|
|
328
|
+
outcomeStatus: "completed",
|
|
329
|
+
},
|
|
330
|
+
status: "completed",
|
|
331
|
+
outcome: {
|
|
332
|
+
status: "completed",
|
|
333
|
+
reason: "The call reached a normal terminal state.",
|
|
334
|
+
confidence: 0.85,
|
|
335
|
+
},
|
|
336
|
+
outcomeSource: "derived",
|
|
337
|
+
};
|
|
338
|
+
const transport = createTransport(async (path: string, init?: RequestInit) => {
|
|
339
|
+
if (path === "/api/cloud/voice/calls/session_1" && !init) return sessionTerminalCall;
|
|
340
|
+
if (path === "/api/cloud/voice/calls/session_1/finalize" && init?.method === "POST") return finalized;
|
|
341
|
+
throw new Error(`Unexpected request: ${path}`);
|
|
342
|
+
});
|
|
343
|
+
const client = new KognitiveCloudVoiceClient(transport as any);
|
|
344
|
+
|
|
345
|
+
await expect(client.calls.wait("session_1", {
|
|
346
|
+
intervalMs: 10,
|
|
347
|
+
timeoutMs: 1_000,
|
|
348
|
+
})).resolves.toEqual(finalized);
|
|
349
|
+
|
|
350
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/calls/session_1/finalize", {
|
|
351
|
+
method: "POST",
|
|
352
|
+
body: JSON.stringify({}),
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("times out while waiting for a non-terminal call", async () => {
|
|
357
|
+
const transport = createTransport(async (path: string, init?: RequestInit) => {
|
|
358
|
+
if (path === "/api/cloud/voice/calls/session_1" && !init) {
|
|
359
|
+
return {
|
|
360
|
+
id: "session_1",
|
|
361
|
+
status: "queued",
|
|
362
|
+
sessionStatus: "created",
|
|
363
|
+
stateLabel: "Dialing",
|
|
364
|
+
latestEventType: null,
|
|
365
|
+
outcomeStatus: null,
|
|
366
|
+
endedAt: null,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
throw new Error(`Unexpected request: ${path}`);
|
|
370
|
+
});
|
|
371
|
+
const client = new KognitiveCloudVoiceClient(transport as any);
|
|
372
|
+
|
|
373
|
+
await expect(client.calls.wait("session_1", {
|
|
374
|
+
intervalMs: 10,
|
|
375
|
+
timeoutMs: 20,
|
|
376
|
+
})).rejects.toThrow("did not complete within 20ms");
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe("KognitiveCloudVoiceClient agent management APIs", () => {
|
|
381
|
+
it("creates and optionally publishes an agent when use cannot find the slug", async () => {
|
|
382
|
+
const createdAgent = { id: "agent_1", slug: "support", status: "draft" };
|
|
383
|
+
const publishedAgent = { ...createdAgent, status: "published" };
|
|
384
|
+
const transport = createTransport(async (path: string, init?: RequestInit) => {
|
|
385
|
+
if (path === "/api/cloud/voice/agents/support" && !init) {
|
|
386
|
+
throw new KognitiveApiError("Agent not found", 404, { error: "Agent not found" });
|
|
387
|
+
}
|
|
388
|
+
if (path === "/api/cloud/voice/agents" && init?.method === "POST") return createdAgent;
|
|
389
|
+
if (path === "/api/cloud/voice/agents/support/publish" && init?.method === "POST") return publishedAgent;
|
|
390
|
+
throw new Error(`Unexpected request: ${path}`);
|
|
391
|
+
});
|
|
392
|
+
const client = new KognitiveCloudVoiceClient(transport as any);
|
|
393
|
+
|
|
394
|
+
await expect(client.agents.use("support", {
|
|
395
|
+
name: "Support",
|
|
396
|
+
publish: true,
|
|
397
|
+
config: {
|
|
398
|
+
instructions: "Help customers over voice.",
|
|
399
|
+
provider: "openai-realtime",
|
|
400
|
+
model: "gpt-realtime",
|
|
401
|
+
voice: "marin",
|
|
402
|
+
transport: "webrtc",
|
|
403
|
+
channels: { web: true, phone: true, outbound: true },
|
|
404
|
+
humanization: { openingMode: "auto", fillerStyle: "light" },
|
|
405
|
+
widget: { title: "Talk to support", launcher: "button" },
|
|
406
|
+
},
|
|
407
|
+
})).resolves.toEqual({ action: "created", agent: publishedAgent });
|
|
408
|
+
|
|
409
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/agents/support");
|
|
410
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/agents", {
|
|
411
|
+
method: "POST",
|
|
412
|
+
body: JSON.stringify({
|
|
413
|
+
name: "Support",
|
|
414
|
+
slug: "support",
|
|
415
|
+
description: undefined,
|
|
416
|
+
config: {
|
|
417
|
+
instructions: "Help customers over voice.",
|
|
418
|
+
provider: "openai-realtime",
|
|
419
|
+
model: "gpt-realtime",
|
|
420
|
+
voice: "marin",
|
|
421
|
+
transport: "webrtc",
|
|
422
|
+
channels: { web: true, phone: true, outbound: true },
|
|
423
|
+
humanization: { openingMode: "auto", fillerStyle: "light" },
|
|
424
|
+
widget: { title: "Talk to support", launcher: "button" },
|
|
425
|
+
},
|
|
426
|
+
}),
|
|
427
|
+
});
|
|
428
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/agents/support/publish", { method: "POST" });
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("updates an existing agent when createOrUse receives configuration", async () => {
|
|
432
|
+
const existingAgent = { id: "agent_1", slug: "support", status: "published" };
|
|
433
|
+
const updatedAgent = { ...existingAgent, status: "draft", draftVersion: 2 };
|
|
434
|
+
const transport = createTransport(async (path: string, init?: RequestInit) => {
|
|
435
|
+
if (path === "/api/cloud/voice/agents/support" && !init) return existingAgent;
|
|
436
|
+
if (path === "/api/cloud/voice/agents/support" && init?.method === "PUT") return updatedAgent;
|
|
437
|
+
throw new Error(`Unexpected request: ${path}`);
|
|
438
|
+
});
|
|
439
|
+
const client = new KognitiveCloudVoiceClient(transport as any);
|
|
440
|
+
|
|
441
|
+
await expect(client.agents.management.createOrUse("support", {
|
|
442
|
+
description: "Updated support agent",
|
|
443
|
+
config: {
|
|
444
|
+
instructions: "Answer with concise order help.",
|
|
445
|
+
tools: [{
|
|
446
|
+
id: "lookup_order",
|
|
447
|
+
type: "external_webhook",
|
|
448
|
+
name: "Lookup order",
|
|
449
|
+
config: { url: "https://example.com/tools/lookup-order" },
|
|
450
|
+
}],
|
|
451
|
+
},
|
|
452
|
+
})).resolves.toEqual({ action: "updated", agent: updatedAgent });
|
|
453
|
+
|
|
454
|
+
expect(transport.json).toHaveBeenCalledWith("/api/cloud/voice/agents/support", {
|
|
455
|
+
method: "PUT",
|
|
456
|
+
body: JSON.stringify({
|
|
457
|
+
description: "Updated support agent",
|
|
458
|
+
config: {
|
|
459
|
+
instructions: "Answer with concise order help.",
|
|
460
|
+
tools: [{
|
|
461
|
+
id: "lookup_order",
|
|
462
|
+
type: "external_webhook",
|
|
463
|
+
name: "Lookup order",
|
|
464
|
+
config: { url: "https://example.com/tools/lookup-order" },
|
|
465
|
+
}],
|
|
466
|
+
},
|
|
467
|
+
}),
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("returns the existing agent without updating when use is called without changes", async () => {
|
|
472
|
+
const existingAgent = { id: "agent_1", slug: "support", status: "published" };
|
|
473
|
+
const transport = createTransport(async (path: string, init?: RequestInit) => {
|
|
474
|
+
if (path === "/api/cloud/voice/agents/support" && !init) return existingAgent;
|
|
475
|
+
throw new Error(`Unexpected request: ${path}`);
|
|
476
|
+
});
|
|
477
|
+
const client = new KognitiveCloudVoiceClient(transport as any);
|
|
478
|
+
|
|
479
|
+
await expect(client.agents.management.use("support")).resolves.toEqual({ action: "used", agent: existingAgent });
|
|
480
|
+
expect(transport.json).toHaveBeenCalledTimes(1);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { createCloudVoiceTools } from "../server";
|
|
4
|
+
|
|
5
|
+
function signedRequest(body: unknown, secret: string) {
|
|
6
|
+
const timestamp = new Date().toISOString();
|
|
7
|
+
const raw = JSON.stringify(body);
|
|
8
|
+
const signature = `sha256=${createHmac("sha256", secret).update(`${timestamp}.${raw}`).digest("hex")}`;
|
|
9
|
+
return new Request("https://example.com/api/voice-tools", {
|
|
10
|
+
method: "POST",
|
|
11
|
+
headers: {
|
|
12
|
+
"content-type": "application/json",
|
|
13
|
+
"x-kognitive-timestamp": timestamp,
|
|
14
|
+
"x-kognitive-signature": signature,
|
|
15
|
+
},
|
|
16
|
+
body: raw,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("createCloudVoiceTools", () => {
|
|
21
|
+
it("routes a signed cloud voice tool request to the matching handler", async () => {
|
|
22
|
+
const handler = createCloudVoiceTools({
|
|
23
|
+
secret: "secret",
|
|
24
|
+
tools: {
|
|
25
|
+
lookup_order: async (input: { orderNumber: string }, ctx) => ({
|
|
26
|
+
orderNumber: input.orderNumber,
|
|
27
|
+
userId: ctx.session.userId,
|
|
28
|
+
}),
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const response = await handler(signedRequest({
|
|
33
|
+
type: "cloud_voice.tool.execute",
|
|
34
|
+
toolCallId: "tool_call_1",
|
|
35
|
+
toolId: "lookup_order",
|
|
36
|
+
input: { orderNumber: "A-1024" },
|
|
37
|
+
session: {
|
|
38
|
+
id: "session-db-id",
|
|
39
|
+
sessionId: "voice_session_1",
|
|
40
|
+
agentSlug: "support",
|
|
41
|
+
channel: "iframe",
|
|
42
|
+
userId: "user_1",
|
|
43
|
+
resourceId: { userId: "user_1" },
|
|
44
|
+
metadata: {},
|
|
45
|
+
},
|
|
46
|
+
}, "secret"));
|
|
47
|
+
|
|
48
|
+
await expect(response.json()).resolves.toEqual({
|
|
49
|
+
result: {
|
|
50
|
+
orderNumber: "A-1024",
|
|
51
|
+
userId: "user_1",
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("rejects invalid signatures", async () => {
|
|
57
|
+
const handler = createCloudVoiceTools({
|
|
58
|
+
secret: "secret",
|
|
59
|
+
tools: {
|
|
60
|
+
lookup_order: async () => ({ ok: true }),
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const response = await handler(signedRequest({
|
|
65
|
+
type: "cloud_voice.tool.execute",
|
|
66
|
+
toolCallId: "tool_call_1",
|
|
67
|
+
toolId: "lookup_order",
|
|
68
|
+
input: {},
|
|
69
|
+
session: {
|
|
70
|
+
id: "session-db-id",
|
|
71
|
+
sessionId: "voice_session_1",
|
|
72
|
+
agentSlug: "support",
|
|
73
|
+
channel: "iframe",
|
|
74
|
+
userId: "user_1",
|
|
75
|
+
resourceId: {},
|
|
76
|
+
metadata: {},
|
|
77
|
+
},
|
|
78
|
+
}, "wrong"));
|
|
79
|
+
|
|
80
|
+
expect(response.status).toBe(400);
|
|
81
|
+
await expect(response.json()).resolves.toEqual({ error: "Invalid Kognitive signature" });
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|