From c2eb8c987df0b7699418a1a666720e4f3fcdffc2 Mon Sep 17 00:00:00 2001 From: mgabrielramos <83740829+mgabrielramos@users.noreply.github.com> Date: Sat, 14 Feb 2026 00:06:02 +0000 Subject: [PATCH] feat(voice-call): explicitly hang up rejected inbound calls Modified `extensions/voice-call/src/manager.ts` to call `provider.hangupCall` when an inbound call is rejected by policy. Added regression test in `extensions/voice-call/src/manager.test.ts`. --- extensions/voice-call/src/manager.test.ts | 44 ++++++++++++++++++++++- extensions/voice-call/src/manager.ts | 18 +++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/extensions/voice-call/src/manager.test.ts b/extensions/voice-call/src/manager.test.ts index 6f409667d4d1..9ef2bbf59f26 100644 --- a/extensions/voice-call/src/manager.test.ts +++ b/extensions/voice-call/src/manager.test.ts @@ -31,7 +31,10 @@ class FakeProvider implements VoiceCallProvider { async initiateCall(_input: InitiateCallInput): Promise { return { providerCallId: "request-uuid", status: "initiated" }; } - async hangupCall(_input: HangupCallInput): Promise {} + readonly hangupCalls: HangupCallInput[] = []; + async hangupCall(input: HangupCallInput): Promise { + this.hangupCalls.push(input); + } async playTts(input: PlayTtsInput): Promise { this.playTtsCalls.push(input); } @@ -104,4 +107,43 @@ describe("CallManager", () => { expect(provider.playTtsCalls).toHaveLength(1); expect(provider.playTtsCalls[0]?.text).toBe("Hello there"); }); + + it("explicitly hangs up when inbound call is rejected", async () => { + // Configure manager to only allow specific numbers + const config = VoiceCallConfigSchema.parse({ + enabled: true, + provider: "plivo", + fromNumber: "+15550000000", + inboundPolicy: "allowlist", + allowFrom: ["+15559999999"], // Only allow this number + }); + + const storePath = path.join(os.tmpdir(), `openclaw-hangup-test-${Date.now()}`); + const provider = new FakeProvider(); + const manager = new CallManager(config, storePath); + manager.initialize(provider, "https://example.com/voice/webhook"); + + // Simulate inbound call from a blocked number + const blockedNumber = "+15550000001"; + manager.processEvent({ + id: "evt-rej-1", + type: "call.ringing", + callId: "call-rej-1", + providerCallId: "provider-rej-1", + timestamp: Date.now(), + direction: "inbound", + from: blockedNumber, + to: "+15550000000", + }); + + // Wait briefly for any async operations + await new Promise((resolve) => setTimeout(resolve, 50)); + + // Verify hangupCall was called + expect(provider.hangupCalls).toHaveLength(1); + expect(provider.hangupCalls[0]).toMatchObject({ + providerCallId: "provider-rej-1", + reason: "busy", + }); + }); }); diff --git a/extensions/voice-call/src/manager.ts b/extensions/voice-call/src/manager.ts index 8ffbf855f606..7e94749e38f5 100644 --- a/extensions/voice-call/src/manager.ts +++ b/extensions/voice-call/src/manager.ts @@ -552,7 +552,23 @@ export class CallManager { if (!call && event.direction === "inbound" && event.providerCallId) { // Check if we should accept this inbound call if (!this.shouldAcceptInbound(event.from)) { - // TODO: Could hang up the call here + // Explicitly hang up rejected calls + if (this.provider) { + // Use a temporary call ID for the rejection record since we aren't creating a full call record + const rejectedCallId = `rejected-${event.providerCallId}`; + this.provider + .hangupCall({ + callId: rejectedCallId, + providerCallId: event.providerCallId, + reason: "busy", + }) + .catch((err) => { + console.warn( + `[voice-call] Failed to hang up rejected call ${event.providerCallId}:`, + err, + ); + }); + } return; }