mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor(voice-call): remove dead internal exports (#107130)
This commit is contained in:
committed by
GitHub
parent
6b0a835116
commit
1dc9d47130
@@ -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<typeof resolveVoiceCallSessionKey>[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");
|
||||
|
||||
@@ -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> = T extends SecretInput
|
||||
: T extends object
|
||||
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: T;
|
||||
export type VoiceCallConfigInput = DeepPartial<VoiceCallConfig>;
|
||||
type VoiceCallConfigInput = DeepPartial<VoiceCallConfig>;
|
||||
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<VoiceCallConfig, "numbers">,
|
||||
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<VoiceCallConfig, "agentId">;
|
||||
sessionKey: string;
|
||||
coreSession?: VoiceCallCoreSessionConfig;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<void> {
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
/** Restore nonterminal active calls and provider/event indexes from persisted records. */
|
||||
export function loadActiveCallsFromStore(storePath: string): {
|
||||
activeCalls: Map<CallId, CallRecord>;
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<VoiceCallRealtimeFastContextConfig> = {},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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<typeof startTunnel>) {
|
||||
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();
|
||||
|
||||
@@ -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<string> {
|
||||
/**
|
||||
* Start a Tailscale serve/funnel tunnel.
|
||||
*/
|
||||
export async function startTailscaleTunnel(config: {
|
||||
async function startTailscaleTunnel(config: {
|
||||
mode: "serve" | "funnel";
|
||||
port: number;
|
||||
path: string;
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ const OPENAI_TO_POLLY_MAP: Record<string, string> = {
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<typeof resolveVoiceCallConfig>[0];
|
||||
|
||||
const createConfig = (overrides: VoiceCallConfigInput = {}): VoiceCallConfig => {
|
||||
const base = VoiceCallConfigSchema.parse({});
|
||||
base.serve.port = 0;
|
||||
|
||||
@@ -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<typeof RealtimeAudioPacer>[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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
getTailscaleSelfInfo,
|
||||
setupTailscaleExposure,
|
||||
setupTailscaleExposureRoute,
|
||||
TAILSCALE_COMMAND_STDOUT_MAX_BYTES,
|
||||
} from "./tailscale.js";
|
||||
|
||||
function commandResult(overrides: Record<string, unknown> = {}) {
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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[],
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user