From 1dc9d47130938c11f5ae397cdfce453d718477f9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 13 Jul 2026 21:42:11 -0700 Subject: [PATCH] refactor(voice-call): remove dead internal exports (#107130) --- extensions/voice-call/src/config.test.ts | 16 ++- extensions/voice-call/src/config.ts | 10 +- .../voice-call/src/manager.restore.test.ts | 3 +- .../voice-call/src/manager/events.test.ts | 2 - .../voice-call/src/manager/store.test.ts | 5 - extensions/voice-call/src/manager/store.ts | 5 - .../voice-call/src/media-stream.test.ts | 107 ++++++++++++++---- extensions/voice-call/src/media-stream.ts | 4 +- .../src/realtime-fast-context.test.ts | 4 +- .../voice-call/src/telephony-audio.test.ts | 44 +------ extensions/voice-call/src/telephony-audio.ts | 2 +- extensions/voice-call/src/tunnel.test.ts | 37 +++++- extensions/voice-call/src/tunnel.ts | 4 +- .../voice-call/src/voice-mapping.test.ts | 6 +- extensions/voice-call/src/voice-mapping.ts | 2 +- .../voice-call/src/webhook-exposure.test.ts | 9 +- extensions/voice-call/src/webhook-exposure.ts | 2 +- .../src/webhook.hangup-once.lifecycle.test.ts | 2 - extensions/voice-call/src/webhook.test.ts | 8 +- .../src/webhook/realtime-audio-pacer.test.ts | 11 +- .../src/webhook/realtime-audio-pacer.ts | 4 +- .../voice-call/src/webhook/tailscale.test.ts | 3 +- .../voice-call/src/webhook/tailscale.ts | 2 +- scripts/deadcode-exports.baseline.mjs | 15 --- 24 files changed, 162 insertions(+), 145 deletions(-) diff --git a/extensions/voice-call/src/config.test.ts b/extensions/voice-call/src/config.test.ts index 0c6e33466538..a534a0baf06d 100644 --- a/extensions/voice-call/src/config.test.ts +++ b/extensions/voice-call/src/config.test.ts @@ -2,10 +2,8 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { VoiceCallConfigSchema, - resolveVoiceCallAgentSessionKey, resolveTwilioAuthToken, resolveVoiceCallEffectiveConfig, - resolveVoiceCallNumberRouteKey, resolveVoiceCallNumberRouteKeyForCall, resolveVoiceCallSessionKey, validateProviderConfig, @@ -19,6 +17,19 @@ function createBaseConfig(provider: "telnyx" | "twilio" | "plivo" | "mock"): Voi return createVoiceCallBaseConfig({ provider }); } +function resolveVoiceCallAgentSessionKey(params: { + config: VoiceCallConfig; + sessionKey: string; + coreSession?: Parameters[0]["coreSession"]; +}): string { + return resolveVoiceCallSessionKey({ + config: params.config, + callId: "test-call", + explicitSessionKey: params.sessionKey, + coreSession: params.coreSession, + }); +} + function envRef(id: string) { return { source: "env" as const, provider: "default", id }; } @@ -570,7 +581,6 @@ describe("resolveVoiceCallConfig session routing", () => { }, }); - expect(resolveVoiceCallNumberRouteKey(config, "+1 (555) 000-1111")).toBe("+15550001111"); const effective = resolveVoiceCallEffectiveConfig(config, "+1 (555) 000-1111"); expect(effective.numberRouteKey).toBe("+15550001111"); diff --git a/extensions/voice-call/src/config.ts b/extensions/voice-call/src/config.ts index 12b1fc5ec6e6..ef33c0ad51ec 100644 --- a/extensions/voice-call/src/config.ts +++ b/extensions/voice-call/src/config.ts @@ -264,10 +264,6 @@ const VoiceCallRealtimeFastContextConfigSchema = z sources: ["memory", "sessions"], fallbackToConsult: false, }); -export type VoiceCallRealtimeFastContextConfig = z.infer< - typeof VoiceCallRealtimeFastContextConfigSchema ->; - const VoiceCallRealtimeAgentContextConfigSchema = z .object({ /** Inject a compact agent persona/context capsule into realtime voice instructions. */ @@ -518,7 +514,7 @@ type DeepPartial = T extends SecretInput : T extends object ? { [K in keyof T]?: DeepPartial } : T; -export type VoiceCallConfigInput = DeepPartial; +type VoiceCallConfigInput = DeepPartial; const TWILIO_AUTH_TOKEN_PATH = "plugins.entries.voice-call.config.twilio.authToken"; // ----------------------------------------------------------------------------- @@ -557,7 +553,7 @@ function normalizePhoneRouteKey(phone: string | undefined): string { return phone?.replace(/\D/g, "") ?? ""; } -export function resolveVoiceCallNumberRouteKey( +function resolveVoiceCallNumberRouteKey( config: Pick, phone: string | undefined, ): string | undefined { @@ -750,7 +746,7 @@ export function resolveVoiceCallSessionKey(params: { } /** Resolve persisted or integration-provided keys into the configured agent namespace. */ -export function resolveVoiceCallAgentSessionKey(params: { +function resolveVoiceCallAgentSessionKey(params: { config: Pick; sessionKey: string; coreSession?: VoiceCallCoreSessionConfig; diff --git a/extensions/voice-call/src/manager.restore.test.ts b/extensions/voice-call/src/manager.restore.test.ts index babf5913421d..c8753dcd3c81 100644 --- a/extensions/voice-call/src/manager.restore.test.ts +++ b/extensions/voice-call/src/manager.restore.test.ts @@ -13,7 +13,7 @@ import { makePersistedCall, writeCallsToStore, } from "./manager.test-harness.js"; -import { flushPendingCallRecordWritesForTest, loadActiveCallsFromStore } from "./manager/store.js"; +import { loadActiveCallsFromStore } from "./manager/store.js"; import { clearVoiceCallStateRuntime, setVoiceCallStateRuntime } from "./runtime-state.js"; function installStateRuntime(): void { @@ -182,7 +182,6 @@ describe("CallManager verification on restore", () => { const hangupCall = requireSingleHangupCall(provider); expect(hangupCall.reason).toBe("timeout"); - await flushPendingCallRecordWritesForTest(); expect(loadActiveCallsFromStore(storePath).activeCalls.size).toBe(0); }); diff --git a/extensions/voice-call/src/manager/events.test.ts b/extensions/voice-call/src/manager/events.test.ts index 5615a69d6ab3..6b5e4ba49b69 100644 --- a/extensions/voice-call/src/manager/events.test.ts +++ b/extensions/voice-call/src/manager/events.test.ts @@ -15,7 +15,6 @@ import type { AnswerCallInput, HangupCallInput, NormalizedEvent } from "../types import type { CallManagerContext } from "./context.js"; import { processEvent } from "./events.js"; import { speakInitialMessage } from "./outbound.js"; -import { flushPendingCallRecordWritesForTest } from "./store.js"; const logSpy = vi.hoisted(() => { const logEntries: string[] = []; @@ -78,7 +77,6 @@ afterEach(async () => { clearTimeout(waiter.timeout); } ctx.transcriptWaiters.clear(); - await flushPendingCallRecordWritesForTest(); fs.rmSync(ctx.storePath, { recursive: true, force: true }); } clearVoiceCallStateRuntime(); diff --git a/extensions/voice-call/src/manager/store.test.ts b/extensions/voice-call/src/manager/store.test.ts index dbc59a5699e1..1c886706eb75 100644 --- a/extensions/voice-call/src/manager/store.test.ts +++ b/extensions/voice-call/src/manager/store.test.ts @@ -16,7 +16,6 @@ import { clearVoiceCallStateRuntime, setVoiceCallStateRuntime } from "../runtime import { CallRecordSchema } from "../types.js"; import { findCallMatchesInStore, - flushPendingCallRecordWritesForTest, getCallHistoryFromStore, loadActiveCallsFromStore, persistCallRecord, @@ -73,7 +72,6 @@ describe("voice-call call record store", () => { ); persistCallRecord(storePath, call); - await flushPendingCallRecordWritesForTest(); expect(fs.existsSync(path.join(storePath, "calls.jsonl"))).toBe(false); const restored = loadActiveCallsFromStore(storePath); @@ -122,7 +120,6 @@ describe("voice-call call record store", () => { ); persistCallRecord(storePath, call); - await flushPendingCallRecordWritesForTest(); const restored = loadActiveCallsFromStore(storePath); const restoredCall = restored.activeCalls.get("call-large"); @@ -182,8 +179,6 @@ describe("voice-call call record store", () => { ), ); } - await flushPendingCallRecordWritesForTest(); - expect(await getCallHistoryFromStore(storePath, 100)).toHaveLength(100); const internalMatches = await findCallMatchesInStore(storePath, "call-target"); expect(internalMatches.byCallId).toMatchObject({ diff --git a/extensions/voice-call/src/manager/store.ts b/extensions/voice-call/src/manager/store.ts index e29de089fcf5..d4f958c85152 100644 --- a/extensions/voice-call/src/manager/store.ts +++ b/extensions/voice-call/src/manager/store.ts @@ -324,11 +324,6 @@ export function persistCallRecord(storePath: string, call: CallRecord): void { } } -/** Test hook for older async persistence call sites. */ -export async function flushPendingCallRecordWritesForTest(): Promise { - await Promise.resolve(); -} - /** Restore nonterminal active calls and provider/event indexes from persisted records. */ export function loadActiveCallsFromStore(storePath: string): { activeCalls: Map; diff --git a/extensions/voice-call/src/media-stream.test.ts b/extensions/voice-call/src/media-stream.test.ts index d9496f5f9aa0..a40566bc16e6 100644 --- a/extensions/voice-call/src/media-stream.test.ts +++ b/extensions/voice-call/src/media-stream.test.ts @@ -10,7 +10,7 @@ import type { import { createTalkSessionController, type TalkEvent } from "openclaw/plugin-sdk/realtime-voice"; import { describe, expect, it, vi } from "vitest"; import { WebSocket } from "ws"; -import { MediaStreamHandler, parseTwilioMediaMessage, sanitizeLogText } from "./media-stream.js"; +import { MediaStreamHandler } from "./media-stream.js"; import { connectWs, startUpgradeWsServer, @@ -185,18 +185,39 @@ describe("MediaStreamHandler TTS queue", () => { }); describe("MediaStreamHandler security hardening", () => { - it("wraps malformed Twilio media stream JSON with an owned parser error", () => { - let error: unknown; - try { - parseTwilioMediaMessage(Buffer.from("{not json")); - } catch (caught) { - error = caught; - } + it("wraps malformed Twilio media stream JSON with an owned parser error", async () => { + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const handler = new MediaStreamHandler({ + transcriptionProvider: createStubSttProvider(), + providerConfig: {}, + }); + const server = await startWsServer(handler); - expect(error).toBeInstanceOf(Error); - expect((error as Error).message).toBe("Twilio media stream message was malformed JSON"); - expect(error).not.toBeInstanceOf(SyntaxError); - expect((error as Error).cause).toBeInstanceOf(SyntaxError); + try { + const ws = await connectWs(server.url); + ws.send("{not json"); + + await vi.waitFor(() => { + expect(errorSpy).toHaveBeenCalledWith( + "[MediaStream] Error processing message:", + expect.objectContaining({ + message: "Twilio media stream message was malformed JSON", + }), + ); + }); + const error = errorSpy.mock.calls.find( + ([message]) => message === "[MediaStream] Error processing message:", + )?.[1]; + expect(error).toBeInstanceOf(Error); + expect(error).not.toBeInstanceOf(SyntaxError); + expect((error as Error).cause).toBeInstanceOf(SyntaxError); + + ws.close(); + await waitForClose(ws); + } finally { + errorSpy.mockRestore(); + await server.close(); + } }); it("rejects start frames when no stream acceptance validator is configured", async () => { @@ -436,19 +457,59 @@ describe("MediaStreamHandler security hardening", () => { expect(ws["close"]).toHaveBeenCalledWith(1013, "Backpressure: send buffer exceeded"); }); - it("sanitizes websocket close reason before logging", () => { - const reason = sanitizeLogText("forged\nline\r\tentry", 120); - expect(reason).not.toContain("\n"); - expect(reason).not.toContain("\r"); - expect(reason).not.toContain("\t"); - expect(reason).toContain("forged line entry"); + it("sanitizes websocket close reason before logging", async () => { + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const handler = new MediaStreamHandler({ + transcriptionProvider: createStubSttProvider(), + providerConfig: {}, + }); + const server = await startWsServer(handler); + + try { + const ws = await connectWs(server.url); + ws.close(1000, "forged\nline\r\tentry"); + await waitForClose(ws); + await vi.waitFor(() => { + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("reason: forged line entry")); + }); + const line = logSpy.mock.calls + .map(([message]) => String(message)) + .find((message) => message.includes("WebSocket closed")); + expect(line).not.toContain("\n"); + expect(line).not.toContain("\r"); + expect(line).not.toContain("\t"); + } finally { + logSpy.mockRestore(); + await server.close(); + } }); - it("truncates websocket close reason without splitting UTF-16 surrogate pairs", () => { - const reason = sanitizeLogText(`abc\uD83D\uDE80tail`, 4); - expect(reason).toBe("abc..."); - expect(reason).not.toContain("\uD83D"); - expect(reason).not.toContain("\uDE80"); + it("truncates websocket close reason without splitting UTF-16 surrogate pairs", async () => { + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const handler = new MediaStreamHandler({ + transcriptionProvider: createStubSttProvider(), + providerConfig: {}, + }); + const server = await startWsServer(handler); + + try { + const ws = await connectWs(server.url); + ws.close(1000, `${"a".repeat(119)}\uD83D\uDE80`); + await waitForClose(ws); + await vi.waitFor(() => { + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining(`reason: ${"a".repeat(119)}...`), + ); + }); + const line = logSpy.mock.calls + .map(([message]) => String(message)) + .find((message) => message.includes("WebSocket closed")); + expect(line).not.toContain("\uD83D"); + expect(line).not.toContain("\uDE80"); + } finally { + logSpy.mockRestore(); + await server.close(); + } }); it("closes idle pre-start connections after timeout", async () => { diff --git a/extensions/voice-call/src/media-stream.ts b/extensions/voice-call/src/media-stream.ts index cd9dfa49c19d..e6572ff1fc19 100644 --- a/extensions/voice-call/src/media-stream.ts +++ b/extensions/voice-call/src/media-stream.ts @@ -102,7 +102,7 @@ const MAX_INBOUND_MESSAGE_BYTES = 64 * 1024; const MAX_WS_BUFFERED_BYTES = 1024 * 1024; const CLOSE_REASON_LOG_MAX_CHARS = 120; -export function sanitizeLogText(value: string, maxChars: number): string { +function sanitizeLogText(value: string, maxChars: number): string { const sanitized = value .replace(/\p{Cc}/gu, " ") .replace(/\s+/g, " ") @@ -123,7 +123,7 @@ function normalizeWsMessageData(data: RawData): Buffer { return Buffer.from(data); } -export function parseTwilioMediaMessage(data: RawData): TwilioMediaMessage { +function parseTwilioMediaMessage(data: RawData): TwilioMediaMessage { const raw = normalizeWsMessageData(data); try { return JSON.parse(raw.toString("utf8")) as TwilioMediaMessage; diff --git a/extensions/voice-call/src/realtime-fast-context.test.ts b/extensions/voice-call/src/realtime-fast-context.test.ts index fb5a72d2dd1d..ead6cf870377 100644 --- a/extensions/voice-call/src/realtime-fast-context.test.ts +++ b/extensions/voice-call/src/realtime-fast-context.test.ts @@ -1,7 +1,6 @@ // Voice Call tests cover realtime fast context plugin behavior. import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import type { VoiceCallRealtimeFastContextConfig } from "./config.js"; const mocks = vi.hoisted(() => ({ resolveRealtimeVoiceFastContextConsult: vi.fn(), @@ -14,6 +13,9 @@ vi.mock("openclaw/plugin-sdk/realtime-voice", () => ({ import { resolveRealtimeFastContextConsult } from "./realtime-fast-context.js"; const cfg = {} as OpenClawConfig; +type VoiceCallRealtimeFastContextConfig = Parameters< + typeof resolveRealtimeFastContextConsult +>[0]["config"]; function createFastContextConfig( overrides: Partial = {}, diff --git a/extensions/voice-call/src/telephony-audio.test.ts b/extensions/voice-call/src/telephony-audio.test.ts index 8bd7208427d5..43ac03972912 100644 --- a/extensions/voice-call/src/telephony-audio.test.ts +++ b/extensions/voice-call/src/telephony-audio.test.ts @@ -1,6 +1,6 @@ // Voice Call tests cover telephony audio plugin behavior. import { describe, expect, it } from "vitest"; -import { convertPcmToMulaw8k, resamplePcmTo8k } from "./telephony-audio.js"; +import { convertPcmToMulaw8k } from "./telephony-audio.js"; function makeSinePcm( sampleRate: number, @@ -17,54 +17,12 @@ function makeSinePcm( return output; } -function rmsPcm(buffer: Buffer): number { - const samples = Math.floor(buffer.length / 2); - if (samples === 0) { - return 0; - } - let sum = 0; - for (let i = 0; i < samples; i++) { - const sample = buffer.readInt16LE(i * 2); - sum += sample * sample; - } - return Math.sqrt(sum / samples); -} - function unalignedCopy(buffer: Buffer): Buffer { const padded = Buffer.alloc(buffer.length + 1); buffer.copy(padded, 1); return padded.subarray(1); } -describe("telephony-audio resamplePcmTo8k", () => { - it("returns identical buffer for 8k input", () => { - const pcm8k = makeSinePcm(8_000, 1_000, 0.2); - const resampled = resamplePcmTo8k(pcm8k, 8_000); - expect(resampled).toBe(pcm8k); - }); - - it("preserves low-frequency speech-band energy when downsampling", () => { - const input = makeSinePcm(48_000, 1_000, 0.6); - const output = resamplePcmTo8k(input, 48_000); - expect(output.length).toBe(9_600); - expect(rmsPcm(output)).toBeGreaterThan(7_500); - }); - - it("attenuates out-of-band high frequencies before 8k telephony conversion", () => { - const lowTone = resamplePcmTo8k(makeSinePcm(48_000, 1_000, 0.6), 48_000); - const highTone = resamplePcmTo8k(makeSinePcm(48_000, 6_000, 0.6), 48_000); - const ratio = rmsPcm(highTone) / rmsPcm(lowTone); - expect(ratio).toBeLessThan(0.1); - }); - - it("matches the typed-array path for unaligned input buffers", () => { - const input = makeSinePcm(48_000, 1_000, 0.2); - const output = resamplePcmTo8k(input, 48_000); - const unalignedOutput = resamplePcmTo8k(unalignedCopy(input), 48_000); - expect(unalignedOutput.equals(output)).toBe(true); - }); -}); - describe("telephony-audio convertPcmToMulaw8k", () => { it("converts to 8k mu-law frame length", () => { const input = makeSinePcm(24_000, 1_000, 0.5); diff --git a/extensions/voice-call/src/telephony-audio.ts b/extensions/voice-call/src/telephony-audio.ts index d6101cedf3e8..149717f77100 100644 --- a/extensions/voice-call/src/telephony-audio.ts +++ b/extensions/voice-call/src/telephony-audio.ts @@ -1,5 +1,5 @@ // Voice Call plugin module implements telephony audio behavior. -export { convertPcmToMulaw8k, resamplePcmTo8k } from "openclaw/plugin-sdk/realtime-voice"; +export { convertPcmToMulaw8k } from "openclaw/plugin-sdk/realtime-voice"; /** * Chunk audio buffer into 20ms frames for streaming (8kHz mono mu-law). diff --git a/extensions/voice-call/src/tunnel.test.ts b/extensions/voice-call/src/tunnel.test.ts index d2a0660a71a3..f1ebd6a4c407 100644 --- a/extensions/voice-call/src/tunnel.test.ts +++ b/extensions/voice-call/src/tunnel.test.ts @@ -40,7 +40,42 @@ vi.mock("openclaw/plugin-sdk/process-runtime", () => ({ runCommandWithTimeout: mocks.runCommand, })); -import { startNgrokTunnel, startTailscaleTunnel, startTunnel } from "./tunnel.js"; +import { startTunnel } from "./tunnel.js"; + +async function requireTunnel(result: ReturnType) { + const tunnel = await result; + if (!tunnel) { + throw new Error("Expected tunnel to start"); + } + return tunnel; +} + +function startNgrokTunnel(config: { + port: number; + path: string; + authToken?: string; + domain?: string; +}) { + return requireTunnel( + startTunnel({ + provider: "ngrok", + port: config.port, + path: config.path, + ngrokAuthToken: config.authToken, + ngrokDomain: config.domain, + }), + ); +} + +function startTailscaleTunnel(config: { mode: "serve" | "funnel"; port: number; path: string }) { + return requireTunnel( + startTunnel({ + provider: config.mode === "serve" ? "tailscale-serve" : "tailscale-funnel", + port: config.port, + path: config.path, + }), + ); +} function nextProcess(): FakeChildProcess { const proc = new FakeChildProcess(); diff --git a/extensions/voice-call/src/tunnel.ts b/extensions/voice-call/src/tunnel.ts index 83a2c264dccc..52fd7fa400f3 100644 --- a/extensions/voice-call/src/tunnel.ts +++ b/extensions/voice-call/src/tunnel.ts @@ -60,7 +60,7 @@ export interface TunnelResult { * console.log('Public URL:', tunnel.publicUrl); * // Later: await tunnel.stop(); */ -export async function startNgrokTunnel(config: { +async function startNgrokTunnel(config: { port: number; path: string; authToken?: string; @@ -240,7 +240,7 @@ async function runNgrokCommand(args: string[]): Promise { /** * Start a Tailscale serve/funnel tunnel. */ -export async function startTailscaleTunnel(config: { +async function startTailscaleTunnel(config: { mode: "serve" | "funnel"; port: number; path: string; diff --git a/extensions/voice-call/src/voice-mapping.test.ts b/extensions/voice-call/src/voice-mapping.test.ts index fdcac44a2f58..7eab292f4bfa 100644 --- a/extensions/voice-call/src/voice-mapping.test.ts +++ b/extensions/voice-call/src/voice-mapping.test.ts @@ -1,6 +1,6 @@ // Voice Call tests cover voice mapping plugin behavior. import { describe, expect, it } from "vitest"; -import { DEFAULT_POLLY_VOICE, escapeXml, mapVoiceToPolly } from "./voice-mapping.js"; +import { escapeXml, mapVoiceToPolly } from "./voice-mapping.js"; describe("voice mapping", () => { it("escapes xml-special characters", () => { @@ -14,7 +14,7 @@ describe("voice mapping", () => { expect(mapVoiceToPolly("ECHO")).toBe("Polly.Matthew"); expect(mapVoiceToPolly("Polly.Brian")).toBe("Polly.Brian"); expect(mapVoiceToPolly("Google.en-US-Standard-C")).toBe("Google.en-US-Standard-C"); - expect(mapVoiceToPolly("unknown")).toBe(DEFAULT_POLLY_VOICE); - expect(mapVoiceToPolly(undefined)).toBe(DEFAULT_POLLY_VOICE); + expect(mapVoiceToPolly("unknown")).toBe("Polly.Joanna"); + expect(mapVoiceToPolly(undefined)).toBe("Polly.Joanna"); }); }); diff --git a/extensions/voice-call/src/voice-mapping.ts b/extensions/voice-call/src/voice-mapping.ts index c2d474f93745..435a11ff39f1 100644 --- a/extensions/voice-call/src/voice-mapping.ts +++ b/extensions/voice-call/src/voice-mapping.ts @@ -28,7 +28,7 @@ const OPENAI_TO_POLLY_MAP: Record = { /** * Default Polly voice when no mapping is found. */ -export const DEFAULT_POLLY_VOICE = "Polly.Joanna"; +const DEFAULT_POLLY_VOICE = "Polly.Joanna"; /** * Map OpenAI voice names to Twilio Polly equivalents. diff --git a/extensions/voice-call/src/webhook-exposure.test.ts b/extensions/voice-call/src/webhook-exposure.test.ts index 2ede2df3822c..e29e23918459 100644 --- a/extensions/voice-call/src/webhook-exposure.test.ts +++ b/extensions/voice-call/src/webhook-exposure.test.ts @@ -1,6 +1,6 @@ // Voice Call tests cover webhook exposure plugin behavior. import { describe, expect, it } from "vitest"; -import { isLocalOnlyWebhookHost, isProviderUnreachableWebhookUrl } from "./webhook-exposure.js"; +import { isProviderUnreachableWebhookUrl } from "./webhook-exposure.js"; describe("webhook exposure host classification", () => { it.each([ @@ -24,11 +24,4 @@ describe("webhook exposure host classification", () => { ])("does not reject public webhook URL %s", (url) => { expect(isProviderUnreachableWebhookUrl(url)).toBe(false); }); - - it.each(["[::1]", "[fc00::1]", "[fd00::1]", "::ffff:7f00:1", "::ffff:a00:1", "[fe80::1]"])( - "normalizes local/private URL hostnames like %s", - (host) => { - expect(isLocalOnlyWebhookHost(host)).toBe(true); - }, - ); }); diff --git a/extensions/voice-call/src/webhook-exposure.ts b/extensions/voice-call/src/webhook-exposure.ts index e60003f9b273..9ae2e92c6284 100644 --- a/extensions/voice-call/src/webhook-exposure.ts +++ b/extensions/voice-call/src/webhook-exposure.ts @@ -28,7 +28,7 @@ export function providerRequiresPublicWebhook(providerName: string | undefined): } /** Return true for localhost, private, or otherwise provider-unreachable hosts. */ -export function isLocalOnlyWebhookHost(hostname: string): boolean { +function isLocalOnlyWebhookHost(hostname: string): boolean { return isBlockedHostnameOrIp(hostname); } diff --git a/extensions/voice-call/src/webhook.hangup-once.lifecycle.test.ts b/extensions/voice-call/src/webhook.hangup-once.lifecycle.test.ts index 8af1c66484e8..65c458de7f0f 100644 --- a/extensions/voice-call/src/webhook.hangup-once.lifecycle.test.ts +++ b/extensions/voice-call/src/webhook.hangup-once.lifecycle.test.ts @@ -8,7 +8,6 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { VoiceCallConfigSchema, type VoiceCallConfig } from "./config.js"; import { CallManager } from "./manager.js"; import { createTestStorePath, FakeProvider } from "./manager.test-harness.js"; -import { flushPendingCallRecordWritesForTest } from "./manager/store.js"; import { clearVoiceCallStateRuntime, setVoiceCallStateRuntime } from "./runtime-state.js"; import type { WebhookContext, WebhookParseOptions } from "./types.js"; import { VoiceCallWebhookServer } from "./webhook.js"; @@ -184,7 +183,6 @@ describe("Voice-call webhook hangup-once lifecycle", () => { } finally { await firstServer.stop(); } - await flushPendingCallRecordWritesForTest(); expect(firstProvider.hangupCalls).toHaveLength(1); const secondProvider = new RejectInboundReplayProvider("plivo"); diff --git a/extensions/voice-call/src/webhook.test.ts b/extensions/voice-call/src/webhook.test.ts index f99d63e2c3db..d6d96ba8fe24 100644 --- a/extensions/voice-call/src/webhook.test.ts +++ b/extensions/voice-call/src/webhook.test.ts @@ -2,11 +2,7 @@ import { request, type IncomingMessage } from "node:http"; import type { RealtimeTranscriptionProviderPlugin } from "openclaw/plugin-sdk/realtime-transcription"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { - VoiceCallConfigSchema, - type VoiceCallConfig, - type VoiceCallConfigInput, -} from "./config.js"; +import { VoiceCallConfigSchema, resolveVoiceCallConfig, type VoiceCallConfig } from "./config.js"; import type { CallManager } from "./manager.js"; import type { VoiceCallProvider } from "./providers/base.js"; import { PlivoProvider } from "./providers/plivo.js"; @@ -77,6 +73,8 @@ type TwilioProviderTestDouble = VoiceCallProvider & | "clearTtsQueue" >; +type VoiceCallConfigInput = Parameters[0]; + const createConfig = (overrides: VoiceCallConfigInput = {}): VoiceCallConfig => { const base = VoiceCallConfigSchema.parse({}); base.serve.port = 0; diff --git a/extensions/voice-call/src/webhook/realtime-audio-pacer.test.ts b/extensions/voice-call/src/webhook/realtime-audio-pacer.test.ts index 95bad8a14038..71d7c60c57e2 100644 --- a/extensions/voice-call/src/webhook/realtime-audio-pacer.test.ts +++ b/extensions/voice-call/src/webhook/realtime-audio-pacer.test.ts @@ -1,11 +1,8 @@ // Voice Call tests cover realtime audio pacer plugin behavior. import { afterEach, describe, expect, it, vi } from "vitest"; -import { - RealtimeAudioPacer, - RealtimeMulawSpeechStartDetector, - calculateMulawRms, - type RealtimeAudioSerializer, -} from "./realtime-audio-pacer.js"; +import { RealtimeAudioPacer, RealtimeMulawSpeechStartDetector } from "./realtime-audio-pacer.js"; + +type RealtimeAudioSerializer = ConstructorParameters[0]["serializer"]; function createTwilioSerializer(streamSid: string): RealtimeAudioSerializer { return { @@ -134,8 +131,6 @@ describe("RealtimeMulawSpeechStartDetector", () => { const silence = Buffer.alloc(160, 0xff); const speech = Buffer.alloc(160, 0x00); - expect(calculateMulawRms(silence)).toBeLessThan(0.02); - expect(calculateMulawRms(speech)).toBeGreaterThan(0.02); expect(detector.accept(speech)).toBe(false); expect(detector.accept(speech)).toBe(true); expect(detector.accept(speech)).toBe(false); diff --git a/extensions/voice-call/src/webhook/realtime-audio-pacer.ts b/extensions/voice-call/src/webhook/realtime-audio-pacer.ts index 95d11eb71b0b..4e0a0250db50 100644 --- a/extensions/voice-call/src/webhook/realtime-audio-pacer.ts +++ b/extensions/voice-call/src/webhook/realtime-audio-pacer.ts @@ -30,7 +30,7 @@ type RealtimeAudioQueueItem = type RealtimeAudioSend = (message: string) => boolean; /** Provider-specific serializer for media, clear, and mark frames. */ -export interface RealtimeAudioSerializer { +interface RealtimeAudioSerializer { media(payloadBase64: string): string; clear(): string; mark(name: string): string; @@ -164,7 +164,7 @@ export class RealtimeAudioPacer { } /** Calculate normalized RMS from mulaw bytes. */ -export function calculateMulawRms(muLaw: Buffer): number { +function calculateMulawRms(muLaw: Buffer): number { if (muLaw.length === 0) { return 0; } diff --git a/extensions/voice-call/src/webhook/tailscale.test.ts b/extensions/voice-call/src/webhook/tailscale.test.ts index cb4d1d4286dc..a17130cb893c 100644 --- a/extensions/voice-call/src/webhook/tailscale.test.ts +++ b/extensions/voice-call/src/webhook/tailscale.test.ts @@ -14,7 +14,6 @@ import { getTailscaleSelfInfo, setupTailscaleExposure, setupTailscaleExposureRoute, - TAILSCALE_COMMAND_STDOUT_MAX_BYTES, } from "./tailscale.js"; function commandResult(overrides: Record = {}) { @@ -50,7 +49,7 @@ describe("voice-call tailscale helpers", () => { ["tailscale", "status", "--json", "--peers=false"], expect.objectContaining({ killProcessTree: true, - maxOutputBytes: { stdout: TAILSCALE_COMMAND_STDOUT_MAX_BYTES, stderr: 1 }, + maxOutputBytes: { stdout: 4 * 1024 * 1024, stderr: 1 }, terminateOnOutputLimit: { stdout: true }, timeoutMs: 2500, }), diff --git a/extensions/voice-call/src/webhook/tailscale.ts b/extensions/voice-call/src/webhook/tailscale.ts index b4b792b266ac..a1aabfac4c05 100644 --- a/extensions/voice-call/src/webhook/tailscale.ts +++ b/extensions/voice-call/src/webhook/tailscale.ts @@ -7,7 +7,7 @@ type TailscaleSelfInfo = { nodeId: string | null; }; -export const TAILSCALE_COMMAND_STDOUT_MAX_BYTES = 4 * 1024 * 1024; +const TAILSCALE_COMMAND_STDOUT_MAX_BYTES = 4 * 1024 * 1024; async function runTailscaleCommand( args: string[], diff --git a/scripts/deadcode-exports.baseline.mjs b/scripts/deadcode-exports.baseline.mjs index 3a560d3d5b2b..f5fd958db4af 100644 --- a/scripts/deadcode-exports.baseline.mjs +++ b/scripts/deadcode-exports.baseline.mjs @@ -391,22 +391,7 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [ "extensions/twitch/src/token.ts: TwitchTokenSource", "extensions/vault/src/cli.ts: testing", "extensions/voice-call/src/cli.ts: testing", - "extensions/voice-call/src/config.ts: resolveVoiceCallAgentSessionKey", - "extensions/voice-call/src/config.ts: resolveVoiceCallNumberRouteKey", - "extensions/voice-call/src/config.ts: VoiceCallConfigInput", - "extensions/voice-call/src/config.ts: VoiceCallRealtimeFastContextConfig", - "extensions/voice-call/src/manager/store.ts: flushPendingCallRecordWritesForTest", - "extensions/voice-call/src/media-stream.ts: parseTwilioMediaMessage", - "extensions/voice-call/src/media-stream.ts: sanitizeLogText", "extensions/voice-call/src/runtime-state.ts: clearVoiceCallStateRuntime", - "extensions/voice-call/src/telephony-audio.ts: resamplePcmTo8k", - "extensions/voice-call/src/tunnel.ts: startNgrokTunnel", - "extensions/voice-call/src/tunnel.ts: startTailscaleTunnel", - "extensions/voice-call/src/voice-mapping.ts: DEFAULT_POLLY_VOICE", - "extensions/voice-call/src/webhook-exposure.ts: isLocalOnlyWebhookHost", - "extensions/voice-call/src/webhook/realtime-audio-pacer.ts: calculateMulawRms", - "extensions/voice-call/src/webhook/realtime-audio-pacer.ts: RealtimeAudioSerializer", - "extensions/voice-call/src/webhook/tailscale.ts: TAILSCALE_COMMAND_STDOUT_MAX_BYTES", "extensions/whatsapp/src/agent-tools-call.ts: createWhatsAppCallTool", "extensions/whatsapp/src/agent-tools-call.ts: testing", "extensions/whatsapp/src/auto-reply/mentions.ts: isBotMentionedFromTargets",