From bbd4b39afb09726cd813f8212c247c9ac100df82 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 22 Mar 2026 19:44:47 -0700 Subject: [PATCH] test(voice-call): cover helper utilities --- extensions/voice-call/src/allowlist.test.ts | 22 ++++++++++ extensions/voice-call/src/deep-merge.test.ts | 40 +++++++++++++++++++ .../voice-call/src/manager/twiml.test.ts | 12 ++++++ .../voice-call/src/voice-mapping.test.ts | 34 ++++++++++++++++ 4 files changed, 108 insertions(+) create mode 100644 extensions/voice-call/src/allowlist.test.ts create mode 100644 extensions/voice-call/src/deep-merge.test.ts create mode 100644 extensions/voice-call/src/manager/twiml.test.ts create mode 100644 extensions/voice-call/src/voice-mapping.test.ts diff --git a/extensions/voice-call/src/allowlist.test.ts b/extensions/voice-call/src/allowlist.test.ts new file mode 100644 index 00000000000..bfa2c36a228 --- /dev/null +++ b/extensions/voice-call/src/allowlist.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import { isAllowlistedCaller, normalizePhoneNumber } from "./allowlist.js"; + +describe("voice-call allowlist", () => { + it("normalizes phone numbers by stripping non-digits", () => { + expect(normalizePhoneNumber("+1 (415) 555-0123")).toBe("14155550123"); + expect(normalizePhoneNumber(" 020-7946-0958 ")).toBe("02079460958"); + expect(normalizePhoneNumber("")).toBe(""); + expect(normalizePhoneNumber()).toBe(""); + }); + + it("matches normalized allowlist entries and rejects blank callers", () => { + expect( + isAllowlistedCaller("14155550123", ["+1 (415) 555-0123", " 020-7946-0958 "]), + ).toBe(true); + expect(isAllowlistedCaller("02079460958", ["+1 (415) 555-0123", " 020-7946-0958 "])).toBe( + true, + ); + expect(isAllowlistedCaller("", ["+1 (415) 555-0123"])).toBe(false); + expect(isAllowlistedCaller("14155550123", ["", "abc"])).toBe(false); + }); +}); diff --git a/extensions/voice-call/src/deep-merge.test.ts b/extensions/voice-call/src/deep-merge.test.ts new file mode 100644 index 00000000000..f22656c311e --- /dev/null +++ b/extensions/voice-call/src/deep-merge.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; +import { deepMergeDefined } from "./deep-merge.js"; + +describe("deepMergeDefined", () => { + it("deep merges nested plain objects and preserves base values for undefined overrides", () => { + expect( + deepMergeDefined( + { + provider: { voice: "alloy", language: "en" }, + enabled: true, + }, + { + provider: { voice: "echo", language: undefined }, + enabled: undefined, + }, + ), + ).toEqual({ + provider: { voice: "echo", language: "en" }, + enabled: true, + }); + }); + + it("replaces non-objects directly and blocks dangerous prototype keys", () => { + expect(deepMergeDefined(["a"], ["b"])).toEqual(["b"]); + expect(deepMergeDefined("base", undefined)).toBe("base"); + expect( + deepMergeDefined( + { safe: { keep: true } }, + { + safe: { next: true }, + __proto__: { polluted: true }, + constructor: { polluted: true }, + prototype: { polluted: true }, + }, + ), + ).toEqual({ + safe: { keep: true, next: true }, + }); + }); +}); diff --git a/extensions/voice-call/src/manager/twiml.test.ts b/extensions/voice-call/src/manager/twiml.test.ts new file mode 100644 index 00000000000..e4a0e2f4dfe --- /dev/null +++ b/extensions/voice-call/src/manager/twiml.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from "vitest"; +import { generateNotifyTwiml } from "./twiml.js"; + +describe("generateNotifyTwiml", () => { + it("renders escaped xml with the requested voice", () => { + expect(generateNotifyTwiml(`Call & "logged"`, "Polly.Joanna")).toBe(` + + Call <ended> & "logged" + +`); + }); +}); diff --git a/extensions/voice-call/src/voice-mapping.test.ts b/extensions/voice-call/src/voice-mapping.test.ts new file mode 100644 index 00000000000..4ce63a324c2 --- /dev/null +++ b/extensions/voice-call/src/voice-mapping.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; +import { + DEFAULT_POLLY_VOICE, + escapeXml, + getOpenAiVoiceNames, + isOpenAiVoice, + mapVoiceToPolly, +} from "./voice-mapping.js"; + +describe("voice mapping", () => { + it("escapes xml-special characters", () => { + expect(escapeXml(`5 < 6 & "quote" 'apostrophe' > 4`)).toBe( + "5 < 6 & "quote" 'apostrophe' > 4", + ); + }); + + it("maps openai voices, passes through provider voices, and falls back to default", () => { + expect(mapVoiceToPolly("alloy")).toBe("Polly.Joanna"); + 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); + }); + + it("detects known openai voices and lists them", () => { + expect(isOpenAiVoice("nova")).toBe(true); + expect(isOpenAiVoice("NOVA")).toBe(true); + expect(isOpenAiVoice("Polly.Joanna")).toBe(false); + expect(getOpenAiVoiceNames()).toEqual( + expect.arrayContaining(["alloy", "echo", "fable", "nova", "onyx", "shimmer"]), + ); + }); +});