@kodelyth/voice-call 2026.5.42 → 2026.6.1
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/package.json +16 -4
- package/api.ts +0 -16
- package/cli-metadata.ts +0 -10
- package/config-api.ts +0 -12
- package/index.test.ts +0 -1075
- package/index.ts +0 -863
- package/runtime-api.ts +0 -20
- package/runtime-entry.ts +0 -1
- package/setup-api.ts +0 -47
- package/src/allowlist.test.ts +0 -18
- package/src/allowlist.ts +0 -19
- package/src/cli.test.ts +0 -12
- package/src/cli.ts +0 -866
- package/src/config-compat.test.ts +0 -130
- package/src/config-compat.ts +0 -227
- package/src/config.test.ts +0 -542
- package/src/config.ts +0 -883
- package/src/core-bridge.ts +0 -14
- package/src/deep-merge.test.ts +0 -40
- package/src/deep-merge.ts +0 -23
- package/src/gateway-continue-operation.ts +0 -200
- package/src/http-headers.test.ts +0 -16
- package/src/http-headers.ts +0 -15
- package/src/manager/context.ts +0 -50
- package/src/manager/events.test.ts +0 -578
- package/src/manager/events.ts +0 -332
- package/src/manager/lifecycle.ts +0 -53
- package/src/manager/lookup.test.ts +0 -52
- package/src/manager/lookup.ts +0 -35
- package/src/manager/outbound.test.ts +0 -629
- package/src/manager/outbound.ts +0 -508
- package/src/manager/state.ts +0 -48
- package/src/manager/store.ts +0 -107
- package/src/manager/timers.test.ts +0 -127
- package/src/manager/timers.ts +0 -113
- package/src/manager/twiml.test.ts +0 -13
- package/src/manager/twiml.ts +0 -17
- package/src/manager.closed-loop.test.ts +0 -259
- package/src/manager.inbound-allowlist.test.ts +0 -183
- package/src/manager.notify.test.ts +0 -390
- package/src/manager.restore.test.ts +0 -310
- package/src/manager.test-harness.ts +0 -127
- package/src/manager.ts +0 -441
- package/src/media-stream.test.ts +0 -953
- package/src/media-stream.ts +0 -876
- package/src/providers/base.ts +0 -99
- package/src/providers/mock.test.ts +0 -86
- package/src/providers/mock.ts +0 -185
- package/src/providers/plivo.test.ts +0 -93
- package/src/providers/plivo.ts +0 -601
- package/src/providers/shared/call-status.test.ts +0 -24
- package/src/providers/shared/call-status.ts +0 -24
- package/src/providers/shared/guarded-json-api.test.ts +0 -127
- package/src/providers/shared/guarded-json-api.ts +0 -49
- package/src/providers/telnyx.test.ts +0 -489
- package/src/providers/telnyx.ts +0 -419
- package/src/providers/twilio/api.test.ts +0 -184
- package/src/providers/twilio/api.ts +0 -100
- package/src/providers/twilio/twiml-policy.test.ts +0 -84
- package/src/providers/twilio/twiml-policy.ts +0 -87
- package/src/providers/twilio/webhook.ts +0 -34
- package/src/providers/twilio.test.ts +0 -607
- package/src/providers/twilio.ts +0 -861
- package/src/providers/twilio.types.ts +0 -17
- package/src/realtime-agent-context.test.ts +0 -101
- package/src/realtime-agent-context.ts +0 -149
- package/src/realtime-defaults.ts +0 -3
- package/src/realtime-fast-context.test.ts +0 -74
- package/src/realtime-fast-context.ts +0 -27
- package/src/realtime-transcription.runtime.ts +0 -4
- package/src/realtime-voice.runtime.ts +0 -5
- package/src/response-generator.test.ts +0 -385
- package/src/response-generator.ts +0 -348
- package/src/response-model.test.ts +0 -71
- package/src/response-model.ts +0 -23
- package/src/runtime.test.ts +0 -625
- package/src/runtime.ts +0 -528
- package/src/telephony-audio.test.ts +0 -61
- package/src/telephony-audio.ts +0 -12
- package/src/telephony-tts.test.ts +0 -196
- package/src/telephony-tts.ts +0 -235
- package/src/test-fixtures.ts +0 -82
- package/src/tts-provider-voice.test.ts +0 -34
- package/src/tts-provider-voice.ts +0 -21
- package/src/tunnel.test.ts +0 -173
- package/src/tunnel.ts +0 -314
- package/src/types.ts +0 -311
- package/src/utils.test.ts +0 -17
- package/src/utils.ts +0 -14
- package/src/voice-mapping.test.ts +0 -32
- package/src/voice-mapping.ts +0 -65
- package/src/webhook/realtime-audio-pacer.test.ts +0 -146
- package/src/webhook/realtime-audio-pacer.ts +0 -204
- package/src/webhook/realtime-handler.test.ts +0 -1450
- package/src/webhook/realtime-handler.ts +0 -1382
- package/src/webhook/stale-call-reaper.test.ts +0 -89
- package/src/webhook/stale-call-reaper.ts +0 -38
- package/src/webhook/stream-frame-adapter.test.ts +0 -187
- package/src/webhook/stream-frame-adapter.ts +0 -219
- package/src/webhook/tailscale.test.ts +0 -216
- package/src/webhook/tailscale.ts +0 -129
- package/src/webhook-exposure.test.ts +0 -33
- package/src/webhook-exposure.ts +0 -84
- package/src/webhook-security.test.ts +0 -813
- package/src/webhook-security.ts +0 -982
- package/src/webhook.hangup-once.lifecycle.test.ts +0 -179
- package/src/webhook.test.ts +0 -1615
- package/src/webhook.ts +0 -933
- package/src/webhook.types.ts +0 -5
- package/src/websocket-test-support.ts +0 -72
- package/tsconfig.json +0 -16
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { VoiceCallConfigSchema } from "./config.js";
|
|
3
|
-
import { CallManager } from "./manager.js";
|
|
4
|
-
import {
|
|
5
|
-
createTestStorePath,
|
|
6
|
-
FakeProvider,
|
|
7
|
-
makePersistedCall,
|
|
8
|
-
writeCallsToStore,
|
|
9
|
-
} from "./manager.test-harness.js";
|
|
10
|
-
import { flushPendingCallRecordWritesForTest, loadActiveCallsFromStore } from "./manager/store.js";
|
|
11
|
-
|
|
12
|
-
function requireSingleActiveCall(manager: CallManager) {
|
|
13
|
-
const activeCalls = manager.getActiveCalls();
|
|
14
|
-
expect(activeCalls).toHaveLength(1);
|
|
15
|
-
const activeCall = activeCalls[0];
|
|
16
|
-
if (!activeCall) {
|
|
17
|
-
throw new Error("expected restored active call");
|
|
18
|
-
}
|
|
19
|
-
return activeCall;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
|
23
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
24
|
-
throw new Error(`expected ${label} to be a record`);
|
|
25
|
-
}
|
|
26
|
-
return value as Record<string, unknown>;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function requireSingleHangupCall(provider: FakeProvider) {
|
|
30
|
-
expect(provider.hangupCalls).toHaveLength(1);
|
|
31
|
-
return requireRecord(provider.hangupCalls[0], "hangup call");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
describe("CallManager verification on restore", () => {
|
|
35
|
-
afterEach(() => {
|
|
36
|
-
vi.useRealTimers();
|
|
37
|
-
vi.restoreAllMocks();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
async function initializeManager(params?: {
|
|
41
|
-
callOverrides?: Parameters<typeof makePersistedCall>[0];
|
|
42
|
-
providerResult?: FakeProvider["getCallStatusResult"];
|
|
43
|
-
configureProvider?: (provider: FakeProvider) => void;
|
|
44
|
-
configOverrides?: Partial<{ maxDurationSeconds: number }>;
|
|
45
|
-
}) {
|
|
46
|
-
const storePath = createTestStorePath();
|
|
47
|
-
const call = makePersistedCall(params?.callOverrides);
|
|
48
|
-
writeCallsToStore(storePath, [call]);
|
|
49
|
-
|
|
50
|
-
const provider = new FakeProvider();
|
|
51
|
-
if (params?.providerResult) {
|
|
52
|
-
provider.getCallStatusResult = params.providerResult;
|
|
53
|
-
}
|
|
54
|
-
params?.configureProvider?.(provider);
|
|
55
|
-
|
|
56
|
-
const config = VoiceCallConfigSchema.parse({
|
|
57
|
-
enabled: true,
|
|
58
|
-
provider: "plivo",
|
|
59
|
-
fromNumber: "+15550000000",
|
|
60
|
-
...params?.configOverrides,
|
|
61
|
-
});
|
|
62
|
-
const manager = new CallManager(config, storePath);
|
|
63
|
-
await manager.initialize(provider, "https://example.com/voice/webhook");
|
|
64
|
-
|
|
65
|
-
return { call, manager, provider, storePath };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
it("skips stale calls reported terminal by provider", async () => {
|
|
69
|
-
const { manager } = await initializeManager({
|
|
70
|
-
providerResult: { status: "completed", isTerminal: true },
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
expect(manager.getActiveCalls()).toHaveLength(0);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("keeps calls reported active by provider", async () => {
|
|
77
|
-
const { call, manager } = await initializeManager({
|
|
78
|
-
providerResult: { status: "in-progress", isTerminal: false },
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const activeCall = requireSingleActiveCall(manager);
|
|
82
|
-
expect(activeCall.callId).toBe(call.callId);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("keeps calls when provider returns unknown (transient error)", async () => {
|
|
86
|
-
const { call, manager } = await initializeManager({
|
|
87
|
-
providerResult: { status: "error", isTerminal: false, isUnknown: true },
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
const activeCall = requireSingleActiveCall(manager);
|
|
91
|
-
expect(activeCall.callId).toBe(call.callId);
|
|
92
|
-
expect(activeCall.state).toBe(call.state);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("skips calls older than maxDurationSeconds", async () => {
|
|
96
|
-
const { manager, provider, storePath } = await initializeManager({
|
|
97
|
-
callOverrides: {
|
|
98
|
-
startedAt: Date.now() - 600_000,
|
|
99
|
-
answeredAt: Date.now() - 590_000,
|
|
100
|
-
},
|
|
101
|
-
configOverrides: { maxDurationSeconds: 300 },
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
expect(manager.getActiveCalls()).toHaveLength(0);
|
|
105
|
-
const hangupCall = requireSingleHangupCall(provider);
|
|
106
|
-
expect(hangupCall.reason).toBe("timeout");
|
|
107
|
-
|
|
108
|
-
await flushPendingCallRecordWritesForTest();
|
|
109
|
-
expect(loadActiveCallsFromStore(storePath).activeCalls.size).toBe(0);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("skips calls without providerCallId", async () => {
|
|
113
|
-
const { manager } = await initializeManager({
|
|
114
|
-
callOverrides: { providerCallId: undefined, state: "initiated" },
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
expect(manager.getActiveCalls()).toHaveLength(0);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("keeps call when getCallStatus throws (verification failure)", async () => {
|
|
121
|
-
const { call, manager } = await initializeManager({
|
|
122
|
-
configureProvider: (provider) => {
|
|
123
|
-
provider.getCallStatus = async () => {
|
|
124
|
-
throw new Error("network failure");
|
|
125
|
-
};
|
|
126
|
-
},
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
const activeCall = requireSingleActiveCall(manager);
|
|
130
|
-
expect(activeCall.callId).toBe(call.callId);
|
|
131
|
-
expect(activeCall.state).toBe(call.state);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("summarizes repeated restored-call verification outcomes", async () => {
|
|
135
|
-
const now = Date.now();
|
|
136
|
-
const storePath = createTestStorePath();
|
|
137
|
-
const calls = [
|
|
138
|
-
makePersistedCall({
|
|
139
|
-
callId: "missing-provider-a",
|
|
140
|
-
providerCallId: undefined,
|
|
141
|
-
state: "initiated",
|
|
142
|
-
startedAt: now - 10_000,
|
|
143
|
-
answeredAt: undefined,
|
|
144
|
-
}),
|
|
145
|
-
makePersistedCall({
|
|
146
|
-
callId: "missing-provider-b",
|
|
147
|
-
providerCallId: undefined,
|
|
148
|
-
state: "initiated",
|
|
149
|
-
startedAt: now - 10_000,
|
|
150
|
-
answeredAt: undefined,
|
|
151
|
-
}),
|
|
152
|
-
makePersistedCall({
|
|
153
|
-
callId: "expired-a",
|
|
154
|
-
providerCallId: "expired-provider-a",
|
|
155
|
-
state: "initiated",
|
|
156
|
-
startedAt: now - 600_000,
|
|
157
|
-
answeredAt: undefined,
|
|
158
|
-
}),
|
|
159
|
-
makePersistedCall({
|
|
160
|
-
callId: "terminal-a",
|
|
161
|
-
providerCallId: "terminal-provider-a",
|
|
162
|
-
state: "initiated",
|
|
163
|
-
startedAt: now - 20_000,
|
|
164
|
-
answeredAt: undefined,
|
|
165
|
-
}),
|
|
166
|
-
makePersistedCall({
|
|
167
|
-
callId: "terminal-b",
|
|
168
|
-
providerCallId: "terminal-provider-b",
|
|
169
|
-
state: "initiated",
|
|
170
|
-
startedAt: now - 20_000,
|
|
171
|
-
answeredAt: undefined,
|
|
172
|
-
}),
|
|
173
|
-
makePersistedCall({
|
|
174
|
-
callId: "unknown-a",
|
|
175
|
-
providerCallId: "unknown-provider-a",
|
|
176
|
-
state: "initiated",
|
|
177
|
-
startedAt: now - 20_000,
|
|
178
|
-
answeredAt: undefined,
|
|
179
|
-
}),
|
|
180
|
-
makePersistedCall({
|
|
181
|
-
callId: "active-a",
|
|
182
|
-
providerCallId: "active-provider-a",
|
|
183
|
-
state: "initiated",
|
|
184
|
-
startedAt: now - 20_000,
|
|
185
|
-
answeredAt: undefined,
|
|
186
|
-
}),
|
|
187
|
-
makePersistedCall({
|
|
188
|
-
callId: "failure-a",
|
|
189
|
-
providerCallId: "failure-provider-a",
|
|
190
|
-
state: "initiated",
|
|
191
|
-
startedAt: now - 20_000,
|
|
192
|
-
answeredAt: undefined,
|
|
193
|
-
}),
|
|
194
|
-
];
|
|
195
|
-
writeCallsToStore(storePath, calls);
|
|
196
|
-
|
|
197
|
-
const provider = new FakeProvider();
|
|
198
|
-
provider.getCallStatus = async ({ providerCallId }) => {
|
|
199
|
-
if (providerCallId.startsWith("terminal-provider")) {
|
|
200
|
-
return { status: "completed", isTerminal: true };
|
|
201
|
-
}
|
|
202
|
-
if (providerCallId.startsWith("unknown-provider")) {
|
|
203
|
-
return { status: "unknown", isTerminal: false, isUnknown: true };
|
|
204
|
-
}
|
|
205
|
-
if (providerCallId.startsWith("active-provider")) {
|
|
206
|
-
return { status: "in-progress", isTerminal: false };
|
|
207
|
-
}
|
|
208
|
-
throw new Error("network failure");
|
|
209
|
-
};
|
|
210
|
-
const config = VoiceCallConfigSchema.parse({
|
|
211
|
-
enabled: true,
|
|
212
|
-
provider: "plivo",
|
|
213
|
-
fromNumber: "+15550000000",
|
|
214
|
-
maxDurationSeconds: 300,
|
|
215
|
-
});
|
|
216
|
-
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
217
|
-
const manager = new CallManager(config, storePath);
|
|
218
|
-
|
|
219
|
-
await manager.initialize(provider, "https://example.com/voice/webhook");
|
|
220
|
-
|
|
221
|
-
expect(
|
|
222
|
-
manager
|
|
223
|
-
.getActiveCalls()
|
|
224
|
-
.map((call) => call.callId)
|
|
225
|
-
.toSorted(),
|
|
226
|
-
).toEqual(["active-a", "failure-a", "unknown-a"]);
|
|
227
|
-
const hangupCall = requireSingleHangupCall(provider);
|
|
228
|
-
expect(hangupCall.callId).toBe("expired-a");
|
|
229
|
-
expect(hangupCall.providerCallId).toBe("expired-provider-a");
|
|
230
|
-
expect(hangupCall.reason).toBe("timeout");
|
|
231
|
-
expect(logSpy).toHaveBeenCalledWith(
|
|
232
|
-
"[voice-call] Skipped 2 restored call(s) with no providerCallId",
|
|
233
|
-
);
|
|
234
|
-
expect(logSpy).toHaveBeenCalledWith(
|
|
235
|
-
"[voice-call] Skipped 1 restored call(s) older than maxDurationSeconds",
|
|
236
|
-
);
|
|
237
|
-
expect(logSpy).toHaveBeenCalledWith(
|
|
238
|
-
"[voice-call] Skipped 2 restored call(s) with provider status: completed",
|
|
239
|
-
);
|
|
240
|
-
expect(logSpy).toHaveBeenCalledWith(
|
|
241
|
-
"[voice-call] Kept 1 restored call(s) confirmed active by provider",
|
|
242
|
-
);
|
|
243
|
-
expect(logSpy).toHaveBeenCalledWith(
|
|
244
|
-
"[voice-call] Kept 1 restored call(s) with unknown provider status (relying on timer)",
|
|
245
|
-
);
|
|
246
|
-
expect(logSpy).toHaveBeenCalledWith(
|
|
247
|
-
"[voice-call] Kept 1 restored call(s) after verification failure (relying on timer)",
|
|
248
|
-
);
|
|
249
|
-
expect(logSpy.mock.calls.map((call) => String(call[0])).join("\n")).not.toContain("terminal-a");
|
|
250
|
-
|
|
251
|
-
logSpy.mockRestore();
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it("uses only remaining max duration for restored answered calls", async () => {
|
|
255
|
-
vi.useFakeTimers();
|
|
256
|
-
const now = new Date("2026-03-17T03:07:00Z");
|
|
257
|
-
vi.setSystemTime(now);
|
|
258
|
-
const { manager, provider } = await initializeManager({
|
|
259
|
-
callOverrides: {
|
|
260
|
-
startedAt: now.getTime() - 290_000,
|
|
261
|
-
answeredAt: now.getTime() - 290_000,
|
|
262
|
-
state: "answered",
|
|
263
|
-
},
|
|
264
|
-
configOverrides: { maxDurationSeconds: 300 },
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
expect(manager.getActiveCalls()).toHaveLength(1);
|
|
268
|
-
await vi.advanceTimersByTimeAsync(9_000);
|
|
269
|
-
expect(manager.getActiveCalls()).toHaveLength(1);
|
|
270
|
-
expect(provider.hangupCalls).toHaveLength(0);
|
|
271
|
-
|
|
272
|
-
await vi.advanceTimersByTimeAsync(1_100);
|
|
273
|
-
expect(manager.getActiveCalls()).toHaveLength(0);
|
|
274
|
-
const hangupCall = requireSingleHangupCall(provider);
|
|
275
|
-
expect(hangupCall.reason).toBe("timeout");
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it("restores dedupe keys from terminal persisted calls so replayed webhooks stay ignored", async () => {
|
|
279
|
-
const storePath = createTestStorePath();
|
|
280
|
-
const persisted = makePersistedCall({
|
|
281
|
-
state: "completed",
|
|
282
|
-
endedAt: Date.now() - 5_000,
|
|
283
|
-
endReason: "completed",
|
|
284
|
-
processedEventIds: ["evt-terminal-init"],
|
|
285
|
-
});
|
|
286
|
-
writeCallsToStore(storePath, [persisted]);
|
|
287
|
-
|
|
288
|
-
const provider = new FakeProvider();
|
|
289
|
-
const config = VoiceCallConfigSchema.parse({
|
|
290
|
-
enabled: true,
|
|
291
|
-
provider: "plivo",
|
|
292
|
-
fromNumber: "+15550000000",
|
|
293
|
-
});
|
|
294
|
-
const manager = new CallManager(config, storePath);
|
|
295
|
-
await manager.initialize(provider, "https://example.com/voice/webhook");
|
|
296
|
-
|
|
297
|
-
manager.processEvent({
|
|
298
|
-
id: "evt-terminal-init",
|
|
299
|
-
type: "call.initiated",
|
|
300
|
-
callId: String(persisted.providerCallId),
|
|
301
|
-
providerCallId: String(persisted.providerCallId),
|
|
302
|
-
timestamp: Date.now(),
|
|
303
|
-
direction: "outbound",
|
|
304
|
-
from: "+15550000000",
|
|
305
|
-
to: "+15550000001",
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
expect(manager.getActiveCalls()).toHaveLength(0);
|
|
309
|
-
});
|
|
310
|
-
});
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { VoiceCallConfigSchema } from "./config.js";
|
|
5
|
-
import { CallManager } from "./manager.js";
|
|
6
|
-
import type { VoiceCallProvider } from "./providers/base.js";
|
|
7
|
-
import type {
|
|
8
|
-
GetCallStatusInput,
|
|
9
|
-
GetCallStatusResult,
|
|
10
|
-
HangupCallInput,
|
|
11
|
-
InitiateCallInput,
|
|
12
|
-
InitiateCallResult,
|
|
13
|
-
PlayTtsInput,
|
|
14
|
-
ProviderWebhookParseResult,
|
|
15
|
-
StartListeningInput,
|
|
16
|
-
StopListeningInput,
|
|
17
|
-
WebhookContext,
|
|
18
|
-
WebhookVerificationResult,
|
|
19
|
-
} from "./types.js";
|
|
20
|
-
|
|
21
|
-
export class FakeProvider implements VoiceCallProvider {
|
|
22
|
-
readonly name: "plivo" | "twilio" | "telnyx";
|
|
23
|
-
twilioStreamConnectEnabled = true;
|
|
24
|
-
readonly playTtsCalls: PlayTtsInput[] = [];
|
|
25
|
-
readonly hangupCalls: HangupCallInput[] = [];
|
|
26
|
-
readonly startListeningCalls: StartListeningInput[] = [];
|
|
27
|
-
readonly stopListeningCalls: StopListeningInput[] = [];
|
|
28
|
-
getCallStatusResult: GetCallStatusResult = { status: "in-progress", isTerminal: false };
|
|
29
|
-
|
|
30
|
-
constructor(name: "plivo" | "twilio" | "telnyx" = "plivo") {
|
|
31
|
-
this.name = name;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
verifyWebhook(_ctx: WebhookContext): WebhookVerificationResult {
|
|
35
|
-
return { ok: true };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
parseWebhookEvent(_ctx: WebhookContext): ProviderWebhookParseResult {
|
|
39
|
-
return { events: [], statusCode: 200 };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async initiateCall(_input: InitiateCallInput): Promise<InitiateCallResult> {
|
|
43
|
-
return { providerCallId: "request-uuid", status: "initiated" };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async hangupCall(input: HangupCallInput): Promise<void> {
|
|
47
|
-
this.hangupCalls.push(input);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async playTts(input: PlayTtsInput): Promise<void> {
|
|
51
|
-
this.playTtsCalls.push(input);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async startListening(input: StartListeningInput): Promise<void> {
|
|
55
|
-
this.startListeningCalls.push(input);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async stopListening(input: StopListeningInput): Promise<void> {
|
|
59
|
-
this.stopListeningCalls.push(input);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async getCallStatus(_input: GetCallStatusInput): Promise<GetCallStatusResult> {
|
|
63
|
-
return this.getCallStatusResult;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
isConversationStreamConnectEnabled(): boolean {
|
|
67
|
-
return this.name === "twilio" && this.twilioStreamConnectEnabled;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function createTestStorePath(): string {
|
|
72
|
-
return fs.mkdtempSync(path.join(os.tmpdir(), "klaw-voice-call-test-"));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export async function createManagerHarness(
|
|
76
|
-
configOverrides: Record<string, unknown> = {},
|
|
77
|
-
provider = new FakeProvider(),
|
|
78
|
-
): Promise<{
|
|
79
|
-
manager: CallManager;
|
|
80
|
-
provider: FakeProvider;
|
|
81
|
-
}> {
|
|
82
|
-
const config = VoiceCallConfigSchema.parse({
|
|
83
|
-
enabled: true,
|
|
84
|
-
provider: "plivo",
|
|
85
|
-
fromNumber: "+15550000000",
|
|
86
|
-
...configOverrides,
|
|
87
|
-
});
|
|
88
|
-
const manager = new CallManager(config, createTestStorePath());
|
|
89
|
-
await manager.initialize(provider, "https://example.com/voice/webhook");
|
|
90
|
-
return { manager, provider };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function markCallAnswered(manager: CallManager, callId: string, eventId: string): void {
|
|
94
|
-
manager.processEvent({
|
|
95
|
-
id: eventId,
|
|
96
|
-
type: "call.answered",
|
|
97
|
-
callId,
|
|
98
|
-
providerCallId: "request-uuid",
|
|
99
|
-
timestamp: Date.now(),
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function writeCallsToStore(storePath: string, calls: Record<string, unknown>[]): void {
|
|
104
|
-
fs.mkdirSync(storePath, { recursive: true });
|
|
105
|
-
const logPath = path.join(storePath, "calls.jsonl");
|
|
106
|
-
const lines = calls.map((c) => JSON.stringify(c)).join("\n") + "\n";
|
|
107
|
-
fs.writeFileSync(logPath, lines);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function makePersistedCall(
|
|
111
|
-
overrides: Record<string, unknown> = {},
|
|
112
|
-
): Record<string, unknown> {
|
|
113
|
-
return {
|
|
114
|
-
callId: `call-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
115
|
-
providerCallId: `prov-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
116
|
-
provider: "plivo",
|
|
117
|
-
direction: "outbound",
|
|
118
|
-
state: "answered",
|
|
119
|
-
from: "+15550000000",
|
|
120
|
-
to: "+15550000001",
|
|
121
|
-
startedAt: Date.now() - 30_000,
|
|
122
|
-
answeredAt: Date.now() - 25_000,
|
|
123
|
-
transcript: [],
|
|
124
|
-
processedEventIds: [],
|
|
125
|
-
...overrides,
|
|
126
|
-
};
|
|
127
|
-
}
|