





















@@ -559,6 +559,145 @@ async function postWebhookFormWithHeadersResult(
559559});
560560}
561561562+async function requestWebSocketUpgrade(
563+server: VoiceCallWebhookServer,
564+baseUrl: string,
565+pathname: string,
566+): Promise<
567+| { kind: "response"; statusCode: number; body: string }
568+| { kind: "upgrade"; statusCode: number }
569+| { kind: "error"; code: string | undefined }
570+> {
571+const requestUrl = requireBoundRequestUrl(server, baseUrl);
572+requestUrl.pathname = pathname;
573+requestUrl.search = "";
574+return await new Promise((resolve) => {
575+let settled = false;
576+const finish = (
577+result:
578+| { kind: "response"; statusCode: number; body: string }
579+| { kind: "upgrade"; statusCode: number }
580+| { kind: "error"; code: string | undefined },
581+) => {
582+if (settled) {
583+return;
584+}
585+settled = true;
586+clearTimeout(timer);
587+resolve(result);
588+};
589+const timer = setTimeout(() => {
590+req.destroy();
591+finish({ kind: "error", code: "timeout" });
592+}, 2_000);
593+const req = request(
594+{
595+hostname: requestUrl.hostname,
596+port: requestUrl.port,
597+path: requestUrl.pathname,
598+method: "GET",
599+headers: {
600+connection: "Upgrade",
601+upgrade: "websocket",
602+},
603+},
604+(res) => {
605+res.setEncoding("utf8");
606+let responseBody = "";
607+res.on("data", (chunk) => {
608+responseBody += chunk;
609+});
610+res.on("end", () => {
611+finish({
612+kind: "response",
613+statusCode: res.statusCode ?? 0,
614+body: responseBody,
615+});
616+});
617+},
618+);
619+req.on("upgrade", (res, socket) => {
620+socket.destroy();
621+finish({ kind: "upgrade", statusCode: res.statusCode ?? 0 });
622+});
623+req.on("error", (error: NodeJS.ErrnoException) => {
624+finish({ kind: "error", code: error.code });
625+});
626+req.end();
627+});
628+}
629+630+describe("VoiceCallWebhookServer realtime WebSocket routing", () => {
631+function createRealtimeRoutingServer(streamPathPattern: string): {
632+server: VoiceCallWebhookServer;
633+handleWebSocketUpgrade: ReturnType<typeof vi.fn<RealtimeCallHandler["handleWebSocketUpgrade"]>>;
634+} {
635+const { manager } = createManager([]);
636+const server = new VoiceCallWebhookServer(
637+createConfig({
638+realtime: {
639+enabled: true,
640+streamPath: streamPathPattern,
641+instructions: "Be helpful.",
642+toolPolicy: "safe-read-only",
643+tools: [],
644+providers: {},
645+},
646+}),
647+manager,
648+provider,
649+);
650+const handleWebSocketUpgrade = vi.fn<RealtimeCallHandler["handleWebSocketUpgrade"]>(
651+(_req, socket) => {
652+socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
653+socket.destroy();
654+},
655+);
656+server.setRealtimeHandler({
657+buildTwiMLPayload: () => ({
658+statusCode: 200,
659+headers: { "Content-Type": "text/xml" },
660+body: "<Response />",
661+}),
662+getStreamPathPattern: () => streamPathPattern,
663+ handleWebSocketUpgrade,
664+registerToolHandler: () => {},
665+setPublicUrl: () => {},
666+} as unknown as RealtimeCallHandler);
667+return { server, handleWebSocketUpgrade };
668+}
669+670+it("does not route sibling paths through the realtime stream handler", async () => {
671+const { server, handleWebSocketUpgrade } =
672+createRealtimeRoutingServer("/voice/stream/realtime");
673+674+try {
675+const baseUrl = await server.start();
676+const valid = await requestWebSocketUpgrade(server, baseUrl, "/voice/stream/realtime/token");
677+expect(valid).toMatchObject({ kind: "response", statusCode: 401 });
678+expect(handleWebSocketUpgrade).toHaveBeenCalledTimes(1);
679+680+await requestWebSocketUpgrade(server, baseUrl, "/voice/stream/realtime-extra/token");
681+expect(handleWebSocketUpgrade).toHaveBeenCalledTimes(1);
682+} finally {
683+await server.stop();
684+}
685+});
686+687+it("routes root stream child paths through the realtime stream handler", async () => {
688+const { server, handleWebSocketUpgrade } = createRealtimeRoutingServer("/");
689+690+try {
691+const baseUrl = await server.start();
692+const valid = await requestWebSocketUpgrade(server, baseUrl, "/token");
693+expect(valid).toMatchObject({ kind: "response", statusCode: 401 });
694+expect(handleWebSocketUpgrade).toHaveBeenCalledTimes(1);
695+} finally {
696+await server.stop();
697+}
698+});
699+});
700+562701describe("VoiceCallWebhookServer stale call reaper", () => {
563702beforeEach(() => {
564703vi.useFakeTimers();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。