diff --git a/extensions/voice-call/src/manager.inbound-allowlist.test.ts b/extensions/voice-call/src/manager.inbound-allowlist.test.ts index 3325f48ea50..e3891553e97 100644 --- a/extensions/voice-call/src/manager.inbound-allowlist.test.ts +++ b/extensions/voice-call/src/manager.inbound-allowlist.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { createManagerHarness } from "./manager.test-harness.js"; +import { FakeProvider, createManagerHarness } from "./manager.test-harness.js"; describe("CallManager inbound allowlist", () => { it("rejects inbound calls with missing caller ID when allowlist enabled", async () => { @@ -103,6 +103,57 @@ describe("CallManager inbound allowlist", () => { ]); }); + it("retries rejected inbound hangup after a transient provider failure", async () => { + class FlakyHangupProvider extends FakeProvider { + hangupFailuresRemaining = 1; + + override async hangupCall(input: Parameters[0]): Promise { + this.hangupCalls.push(input); + if (this.hangupFailuresRemaining > 0) { + this.hangupFailuresRemaining -= 1; + throw new Error("provider down"); + } + } + } + + const provider = new FlakyHangupProvider(); + const { manager } = await createManagerHarness( + { + inboundPolicy: "disabled", + }, + provider, + ); + + manager.processEvent({ + id: "evt-reject-fail-init", + type: "call.initiated", + callId: "provider-flaky", + providerCallId: "provider-flaky", + timestamp: Date.now(), + direction: "inbound", + from: "+15553333333", + to: "+15550000000", + }); + await Promise.resolve(); + + manager.processEvent({ + id: "evt-reject-fail-ring", + type: "call.ringing", + callId: "provider-flaky", + providerCallId: "provider-flaky", + timestamp: Date.now(), + direction: "inbound", + from: "+15553333333", + to: "+15550000000", + }); + + expect(manager.getCallByProviderCallId("provider-flaky")).toBeUndefined(); + expect(provider.hangupCalls).toEqual([ + expect.objectContaining({ providerCallId: "provider-flaky" }), + expect.objectContaining({ providerCallId: "provider-flaky" }), + ]); + }); + it("accepts inbound calls that exactly match the allowlist", async () => { const { manager } = await createManagerHarness({ inboundPolicy: "allowlist", diff --git a/extensions/voice-call/src/manager/events.ts b/extensions/voice-call/src/manager/events.ts index adad2687897..ad9c7c37ff4 100644 --- a/extensions/voice-call/src/manager/events.ts +++ b/extensions/voice-call/src/manager/events.ts @@ -138,6 +138,7 @@ export function processEvent(ctx: EventContext, event: NormalizedEvent): void { reason: "hangup-bot", }) .catch((err) => { + ctx.rejectedProviderCallIds.delete(pid); const message = formatErrorMessage(err); console.warn(`[voice-call] Failed to reject inbound call ${pid}:`, message); });