diff --git a/extensions/voice-call/src/manager/events.test.ts b/extensions/voice-call/src/manager/events.test.ts new file mode 100644 index 000000000000..6819ef3f4d9c --- /dev/null +++ b/extensions/voice-call/src/manager/events.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from "vitest"; + +import { processEvent } from "./events.js"; +import type { CallManagerContext } from "./context.js"; +import { VoiceCallConfigSchema, type VoiceCallConfig } from "../config.js"; +import type { + NormalizedEvent, + HangupCallInput, + PlayTtsInput, + InitiateCallInput, + InitiateCallResult, + StartListeningInput, + StopListeningInput, + WebhookContext, + WebhookVerificationResult, + ProviderWebhookParseResult +} from "../types.js"; +import type { VoiceCallProvider } from "../providers/base.js"; + +class FakeProvider implements VoiceCallProvider { + readonly name = "plivo" as const; + readonly playTtsCalls: PlayTtsInput[] = []; + readonly hangupCalls: HangupCallInput[] = []; + + verifyWebhook(_ctx: WebhookContext): WebhookVerificationResult { return { ok: true }; } + parseWebhookEvent(_ctx: WebhookContext): ProviderWebhookParseResult { return { events: [], statusCode: 200 }; } + async initiateCall(_input: InitiateCallInput): Promise { return { providerCallId: "request-uuid", status: "initiated" }; } + async hangupCall(input: HangupCallInput): Promise { this.hangupCalls.push(input); } + async playTts(input: PlayTtsInput): Promise { this.playTtsCalls.push(input); } + async startListening(_input: StartListeningInput): Promise {} + async stopListening(_input: StopListeningInput): Promise {} +} + +function createMockContext(config: VoiceCallConfig): CallManagerContext { + return { + activeCalls: new Map(), + providerCallIdMap: new Map(), + processedEventIds: new Set(), + provider: new FakeProvider(), + config, + storePath: "/tmp/store", // Dummy path + webhookUrl: "https://example.com/webhook", + transcriptWaiters: new Map(), + maxDurationTimers: new Map(), + }; +} + +describe("processEvent", () => { + it("hangs up inbound call if not in allowlist", async () => { + const config = VoiceCallConfigSchema.parse({ + enabled: true, + inboundPolicy: "allowlist", + allowFrom: ["+15551234567"], + provider: "plivo", // to match FakeProvider name check if any + // schema requires some fields + fromNumber: "+15550000000", + }); + + const ctx = createMockContext(config); + const provider = ctx.provider as FakeProvider; + + const event: NormalizedEvent = { + id: "evt-rejected", + type: "call.initiated", + callId: "call-uuid-external", + providerCallId: "provider-call-uuid-rejected", + timestamp: Date.now(), + direction: "inbound", + from: "+15559999999", // Not allowed + to: "+15550000000", + }; + + processEvent(ctx, event); + + // Wait for async hangup + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(provider.hangupCalls).toHaveLength(1); + expect(provider.hangupCalls[0]).toMatchObject({ + providerCallId: "provider-call-uuid-rejected", + reason: "hangup-bot", + }); + + // Check call was not created + expect(ctx.activeCalls.size).toBe(0); + }); +}); diff --git a/extensions/voice-call/src/manager/events.ts b/extensions/voice-call/src/manager/events.ts index 76c6f17022e4..df1e97da1a28 100644 --- a/extensions/voice-call/src/manager/events.ts +++ b/extensions/voice-call/src/manager/events.ts @@ -93,7 +93,24 @@ export function processEvent(ctx: CallManagerContext, event: NormalizedEvent): v if (!call && event.direction === "inbound" && event.providerCallId) { if (!shouldAcceptInbound(ctx.config, event.from)) { - // TODO: Could hang up the call here. + console.log( + `[voice-call] Rejecting inbound call from ${event.from} (providerId: ${event.providerCallId})`, + ); + if (ctx.provider) { + ctx.provider + .hangupCall({ + callId: "rejected-call", + providerCallId: event.providerCallId, + reason: "hangup-bot", + }) + .catch((err) => { + console.error( + `[voice-call] Failed to hang up rejected call: ${ + err instanceof Error ? err.message : String(err) + }`, + ); + }); + } return; } diff --git a/extensions/voice-call/src/manager/events.ts_cleanup b/extensions/voice-call/src/manager/events.ts_cleanup new file mode 100644 index 000000000000..6e7124336a44 --- /dev/null +++ b/extensions/voice-call/src/manager/events.ts_cleanup @@ -0,0 +1,12 @@ +<<<<<<< SEARCH + console.log( + `[voice-call] Rejecting inbound call from ${event.from} (providerId: ${event.providerCallId})`, + ); + console.log("DEBUG: ctx.provider is:", ctx.provider ? "set" : "null"); + if (ctx.provider) { +======= + console.log( + `[voice-call] Rejecting inbound call from ${event.from} (providerId: ${event.providerCallId})`, + ); + if (ctx.provider) { +>>>>>>> REPLACE diff --git a/extensions/voice-call/src/manager/events.ts_debug b/extensions/voice-call/src/manager/events.ts_debug new file mode 100644 index 000000000000..02c9ee087df7 --- /dev/null +++ b/extensions/voice-call/src/manager/events.ts_debug @@ -0,0 +1,18 @@ +<<<<<<< SEARCH + if (!call && event.direction === "inbound" && event.providerCallId) { + if (!shouldAcceptInbound(ctx.config, event.from)) { + console.log( + `[voice-call] Rejecting inbound call from ${event.from} (providerId: ${event.providerCallId})`, + ); + if (ctx.provider) { + ctx.provider +======= + if (!call && event.direction === "inbound" && event.providerCallId) { + if (!shouldAcceptInbound(ctx.config, event.from)) { + console.log( + `[voice-call] Rejecting inbound call from ${event.from} (providerId: ${event.providerCallId})`, + ); + console.log("DEBUG: ctx.provider is:", ctx.provider ? "set" : "null"); + if (ctx.provider) { + ctx.provider +>>>>>>> REPLACE diff --git a/extensions/voice-call/src/manager/events.ts_patch b/extensions/voice-call/src/manager/events.ts_patch new file mode 100644 index 000000000000..954c14ab96f5 --- /dev/null +++ b/extensions/voice-call/src/manager/events.ts_patch @@ -0,0 +1,28 @@ +<<<<<<< SEARCH + if (!shouldAcceptInbound(ctx.config, event.from)) { + // TODO: Could hang up the call here. + return; + } +======= + if (!shouldAcceptInbound(ctx.config, event.from)) { + console.log( + `[voice-call] Rejecting inbound call from ${event.from} (providerId: ${event.providerCallId})`, + ); + if (ctx.provider) { + ctx.provider + .hangupCall({ + callId: "rejected-call", + providerCallId: event.providerCallId, + reason: "hangup-bot", + }) + .catch((err) => { + console.error( + `[voice-call] Failed to hang up rejected call: ${ + err instanceof Error ? err.message : String(err) + }`, + ); + }); + } + return; + } +>>>>>>> REPLACE