



























@@ -679,6 +679,71 @@ describe("VoiceCallWebhookServer replay handling", () => {
679679},
680680);
681681682+it("serves initial provider TwiML before the realtime shortcut", async () => {
683+const parseWebhookEvent = vi.fn(() => ({ events: [], statusCode: 200 }));
684+const consumeInitialTwiML = vi.fn(
685+() =>
686+'<Response><Play digits="ww123456#" /><Redirect method="POST">https://example.test</Redirect></Response>',
687+);
688+const buildTwiMLPayload = vi.fn(() => ({
689+statusCode: 200,
690+headers: { "Content-Type": "text/xml" },
691+body: '<Response><Connect><Stream url="wss://example.test/voice/stream/realtime/token" /></Connect></Response>',
692+}));
693+const twilioProvider: VoiceCallProvider = {
694+ ...provider,
695+name: "twilio",
696+verifyWebhook: () => ({ ok: true, verifiedRequestKey: "twilio:req:rt-stored" }),
697+ parseWebhookEvent,
698+ consumeInitialTwiML,
699+};
700+const { manager, processEvent } = createManager([]);
701+const config = createConfig({
702+provider: "twilio",
703+inboundPolicy: "disabled",
704+realtime: {
705+enabled: true,
706+streamPath: "/voice/stream/realtime",
707+instructions: "Be helpful.",
708+toolPolicy: "safe-read-only",
709+tools: [],
710+providers: {},
711+},
712+});
713+const server = new VoiceCallWebhookServer(config, manager, twilioProvider);
714+server.setRealtimeHandler({
715+ buildTwiMLPayload,
716+getStreamPathPattern: () => "/voice/stream/realtime",
717+handleWebSocketUpgrade: () => {},
718+registerToolHandler: () => {},
719+setPublicUrl: () => {},
720+} as unknown as RealtimeCallHandler);
721+722+try {
723+const baseUrl = await server.start();
724+const requestUrl = requireBoundRequestUrl(server, baseUrl);
725+requestUrl.searchParams.set("callId", "call-1");
726+const response = await fetch(requestUrl.toString(), {
727+method: "POST",
728+headers: {
729+"content-type": "application/x-www-form-urlencoded",
730+"x-twilio-signature": "sig",
731+},
732+body: "CallSid=CA123&Direction=outbound-api&CallStatus=in-progress&From=%2B15550001111&To=%2B15550002222",
733+});
734+735+expect(response.status).toBe(200);
736+const body = await response.text();
737+expect(body).toContain('<Play digits="ww123456#"');
738+expect(consumeInitialTwiML).toHaveBeenCalledTimes(1);
739+expect(buildTwiMLPayload).not.toHaveBeenCalled();
740+expect(parseWebhookEvent).not.toHaveBeenCalled();
741+expect(processEvent).not.toHaveBeenCalled();
742+} finally {
743+await server.stop();
744+}
745+});
746+682747it("rejects non-allowlisted inbound realtime calls before creating a stream token", async () => {
683748const buildTwiMLPayload = vi.fn(() => ({
684749statusCode: 200,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。