test(voice-call): cover Twilio provider state cleanup

This commit is contained in:
Vincent Koc
2026-07-31 18:04:47 +08:00
parent 86ea6ec5f2
commit b7fd16b329
@@ -28,6 +28,50 @@ function createProvider(): TwilioProvider {
);
}
type TwilioPrivateCallState = {
callWebhookUrls: Map<string, string>;
callStreamMap: Map<string, string>;
streamAuthTokens: Map<string, string>;
twimlStorage: Map<string, string>;
notifyCalls: Set<string>;
activeStreamCalls: Set<string>;
};
function getTwilioPrivateCallState(provider: TwilioProvider): TwilioPrivateCallState {
return provider as unknown as TwilioPrivateCallState;
}
function seedTwilioPrivateCallState(params: {
provider: TwilioProvider;
callId: string;
providerCallId: string;
}): void {
const state = getTwilioPrivateCallState(params.provider);
state.callWebhookUrls.set(
params.providerCallId,
`https://example.ngrok.app/voice/twilio?callId=${params.callId}`,
);
state.callStreamMap.set(params.providerCallId, "MZ-private-state");
state.streamAuthTokens.set(params.providerCallId, "stream-token");
state.twimlStorage.set(params.callId, "<Response><Say>Hello</Say></Response>");
state.notifyCalls.add(params.callId);
state.activeStreamCalls.add(params.providerCallId);
}
function expectTwilioPrivateCallStateReleased(params: {
provider: TwilioProvider;
callId: string;
providerCallId: string;
}): void {
const state = getTwilioPrivateCallState(params.provider);
expect(state.callWebhookUrls.has(params.providerCallId)).toBe(false);
expect(state.callStreamMap.has(params.providerCallId)).toBe(false);
expect(state.streamAuthTokens.has(params.providerCallId)).toBe(false);
expect(state.twimlStorage.has(params.callId)).toBe(false);
expect(state.notifyCalls.has(params.callId)).toBe(false);
expect(state.activeStreamCalls.has(params.providerCallId)).toBe(false);
}
function createContext(rawBody: string, query?: WebhookContext["query"]): WebhookContext {
return {
headers: {},
@@ -366,6 +410,55 @@ describe("TwilioProvider", () => {
expect(nextBody).not.toContain("hold-queue");
});
it("releases all provider call state on terminal callbacks and late replays", () => {
const provider = createProvider();
const callId = "call-terminal";
const providerCallId = "CA-terminal";
seedTwilioPrivateCallState({ provider, callId, providerCallId });
const terminal = createContext(
`CallStatus=completed&Direction=outbound-api&CallSid=${providerCallId}`,
{ callId, type: "status" },
);
const first = provider.parseWebhookEvent(terminal).events[0];
expect(first).toMatchObject({
type: "call.ended",
callId,
providerCallId,
reason: "completed",
});
expectTwilioPrivateCallStateReleased({ provider, callId, providerCallId });
const lateReplay = provider.parseWebhookEvent(terminal).events[0];
expect(lateReplay).toMatchObject({
type: "call.ended",
callId,
providerCallId,
reason: "completed",
});
expectTwilioPrivateCallStateReleased({ provider, callId, providerCallId });
});
it("releases all provider call state before repeated explicit hangups", async () => {
const provider = createProvider();
const callId = "call-hangup";
const providerCallId = "CA-hangup";
seedTwilioPrivateCallState({ provider, callId, providerCallId });
const apiRequest = createApiRequestMock();
(
provider as unknown as {
apiRequest: TwilioApiRequest;
}
).apiRequest = apiRequest;
const input = { callId, providerCallId, reason: "hangup-bot" as const };
await provider.hangupCall(input);
expectTwilioPrivateCallStateReleased({ provider, callId, providerCallId });
await provider.hangupCall(input);
expectTwilioPrivateCallStateReleased({ provider, callId, providerCallId });
expect(apiRequest).toHaveBeenCalledTimes(2);
});
it("QUEUE_TWIML references /voice/hold-music waitUrl", () => {
const provider = createProvider();
const firstInbound = createContext("CallStatus=ringing&Direction=inbound&CallSid=CA611");