




















@@ -1,4 +1,4 @@
1-import { request } from "node:http";
1+import { request, type IncomingMessage } from "node:http";
22import type { RealtimeTranscriptionProviderPlugin } from "openclaw/plugin-sdk/realtime-transcription";
33import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
44import { VoiceCallConfigSchema, type VoiceCallConfig } from "./config.js";
@@ -941,7 +941,9 @@ describe("VoiceCallWebhookServer pre-auth webhook guards", () => {
941941if (enteredReads === 8) {
942942releaseReads();
943943}
944-await unblockReads;
944+if (enteredReads <= 8) {
945+await unblockReads;
946+}
945947return "CallSid=CA123&SpeechResult=hello";
946948});
947949@@ -967,6 +969,80 @@ describe("VoiceCallWebhookServer pre-auth webhook guards", () => {
967969await server.stop();
968970}
969971});
972+973+it("limits missing remote addresses with a shared fallback bucket", async () => {
974+const twilioProvider: VoiceCallProvider = {
975+ ...provider,
976+name: "twilio",
977+verifyWebhook: () => ({ ok: true, verifiedRequestKey: "twilio:req:test" }),
978+};
979+const { manager } = createManager([]);
980+const config = createConfig({ provider: "twilio" });
981+const server = new VoiceCallWebhookServer(config, manager, twilioProvider);
982+const runWebhookPipeline = (
983+server as unknown as {
984+runWebhookPipeline: (
985+req: IncomingMessage,
986+webhookPath: string,
987+) => Promise<{ statusCode: number; body: string }>;
988+}
989+).runWebhookPipeline.bind(server);
990+991+let enteredReads = 0;
992+let releaseReads!: () => void;
993+let unblockReadBodies!: () => void;
994+const enteredEightReads = new Promise<void>((resolve) => {
995+releaseReads = resolve;
996+});
997+const unblockReads = new Promise<void>((resolve) => {
998+unblockReadBodies = resolve;
999+});
1000+const readBodySpy = vi.spyOn(
1001+server as unknown as {
1002+readBody: (req: unknown, maxBytes: number, timeoutMs?: number) => Promise<string>;
1003+},
1004+"readBody",
1005+);
1006+readBodySpy.mockImplementation(async () => {
1007+enteredReads += 1;
1008+if (enteredReads === 8) {
1009+releaseReads();
1010+}
1011+await unblockReads;
1012+return "CallSid=CA123&SpeechResult=hello";
1013+});
1014+1015+const makeRequestWithoutRemoteAddress = () =>
1016+({
1017+method: "POST",
1018+url: "/voice/webhook",
1019+headers: { "x-twilio-signature": "sig" },
1020+socket: { remoteAddress: undefined },
1021+}) as unknown as IncomingMessage;
1022+1023+try {
1024+const inFlightRequests = Array.from({ length: 8 }, () =>
1025+runWebhookPipeline(makeRequestWithoutRemoteAddress(), "/voice/webhook"),
1026+);
1027+await enteredEightReads;
1028+1029+const rejected = await runWebhookPipeline(
1030+makeRequestWithoutRemoteAddress(),
1031+"/voice/webhook",
1032+);
1033+expect(rejected.statusCode).toBe(429);
1034+expect(rejected.body).toBe("Too Many Requests");
1035+expect(readBodySpy).toHaveBeenCalledTimes(8);
1036+1037+unblockReadBodies();
1038+1039+const settled = await Promise.all(inFlightRequests);
1040+expect(settled.every((response) => response.statusCode === 200)).toBe(true);
1041+} finally {
1042+unblockReadBodies();
1043+readBodySpy.mockRestore();
1044+}
1045+});
9701046});
97110479721048describe("VoiceCallWebhookServer response normalization", () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。