// EdgeTTS (Microsoft Edge "Read Aloud") audio-tts provider (#6668). // // EdgeTTS is a reverse-engineered WebSocket endpoint with no API key, so // there is no live upstream we can validate against in CI (Hard Rule #18 // TDD path). This suite covers everything that is a pure function: the // Sec-MS-GEC token/HMAC construction, WS message framing, binary-chunk // demuxing, SSML building/escaping, registry lookup, and the error path // (mocked WebSocket failure -> sanitized error response, no stack leak). import test from "node:test"; import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { computeSecMsGec, buildConnectionId, buildSpeechConfigMessage, buildSsmlMessage, buildSsml, buildEdgeTtsWsUrl, escapeSsmlText, normalizeEdgeVoice, isTurnEndMessage, demuxAudioChunk, synthesizeEdgeTts, handleEdgeTtsSpeech, type MinimalWebSocket, } from "../../open-sse/executors/edgeTts.ts"; import { getSpeechProvider, parseSpeechModel } from "../../open-sse/config/audioRegistry.ts"; import { resolvePublicCred } from "../../open-sse/utils/publicCreds.ts"; // ─── Sec-MS-GEC token (HMAC-ish SHA-256 construction) ────────────────────── test("computeSecMsGec is deterministic for the same 5-minute window", () => { const base = Date.UTC(2026, 6, 17, 12, 0, 0); // 2026-07-17T12:00:00Z const a = computeSecMsGec(base); const b = computeSecMsGec(base + 60_000); // +1 minute, same 5-minute bucket assert.equal(a, b, "same rounded-down 5-minute window must hash identically"); assert.match(a, /^[0-9A-F]{64}$/, "output must be a 64-char uppercase hex SHA-256 digest"); }); test("computeSecMsGec changes across a 5-minute window boundary", () => { const base = Date.UTC(2026, 6, 17, 12, 0, 0); const before = computeSecMsGec(base - 1); // just before the 5-min bucket const after = computeSecMsGec(base); assert.notEqual(before, after); }); test("computeSecMsGec matches the reference rany2/edge-tts algorithm shape", () => { // Cross-check against a hand-computed reference vector for a fixed instant, // using the same constants/algorithm documented in drm.py: // ticks = floor((nowMs/1000 + 11644473600) - (... % 300)) * 1e7 // sha256(`${ticks}${TRUSTED_CLIENT_TOKEN}`).hexdigest().upper() const nowMs = Date.UTC(2026, 0, 1, 0, 0, 0); const winEpoch = 11644473600; let ticks = nowMs / 1000 + winEpoch; ticks -= ticks % 300; ticks *= 1e7; const token = resolvePublicCred("edgetts_token"); const expected = createHash("sha256") .update(`${Math.floor(ticks)}${token}`, "ascii") .digest("hex") .toUpperCase(); assert.equal(computeSecMsGec(nowMs), expected); }); // ─── publicCreds shape assertion (Hard Rule #11) ─────────────────────────── test("edgetts_token is embedded via resolvePublicCred, not a string literal", () => { const token = resolvePublicCred("edgetts_token"); assert.equal(typeof token, "string"); assert.ok(token.length > 0, "embedded default must decode to a non-empty token"); // The well-known public trusted-client-token format used by every Edge // build and every open-source edge-tts port: 32 uppercase hex chars. assert.match(token, /^[0-9A-F]{32}$/); }); test("resolvePublicCred('edgetts_token') is stable across repeated calls", () => { // No envName is passed for this key (there's no legacy .env var to migrate // from — it's a brand-new provider), so it must always resolve to the same // embedded default rather than reading from process.env. assert.equal(resolvePublicCred("edgetts_token"), resolvePublicCred("edgetts_token")); }); // ─── Connection id / message framing ─────────────────────────────────────── test("buildConnectionId returns a 32-char lowercase hex id with no dashes", () => { const id = buildConnectionId(); assert.match(id, /^[0-9a-f]{32}$/); }); test("buildConnectionId is unique per call", () => { const ids = new Set(Array.from({ length: 20 }, () => buildConnectionId())); assert.equal(ids.size, 20); }); test("buildSpeechConfigMessage frames a valid speech.config WS text message", () => { const msg = buildSpeechConfigMessage("Tue, 01 Jan 2026 00:00:00 GMT"); assert.match(msg, /^X-Timestamp:Tue, 01 Jan 2026 00:00:00 GMT\r\n/); assert.match(msg, /Content-Type:application\/json; charset=utf-8\r\n/); assert.match(msg, /Path:speech\.config\r\n\r\n/); const jsonPart = msg.slice(msg.indexOf("\r\n\r\n") + 4); const parsed = JSON.parse(jsonPart); assert.equal( parsed.context.synthesis.audio.outputFormat, "audio-24khz-48kbitrate-mono-mp3" ); }); test("buildSsmlMessage frames a valid ssml WS text message carrying the SSML body", () => { const ssml = buildSsml({ text: "hello" }); const msg = buildSsmlMessage("req-123", ssml, "Tue, 01 Jan 2026 00:00:00 GMT"); assert.match(msg, /^X-RequestId:req-123\r\n/); assert.match(msg, /Content-Type:application\/ssml\+xml\r\n/); assert.match(msg, /Path:ssml\r\n\r\n/); assert.ok(msg.endsWith(ssml), "message must end with the exact SSML payload"); }); test("buildEdgeTtsWsUrl includes TrustedClientToken, Sec-MS-GEC, and ConnectionId", () => { const url = new URL(buildEdgeTtsWsUrl(Date.UTC(2026, 6, 17))); assert.equal(url.protocol, "wss:"); assert.equal(url.hostname, "speech.platform.bing.com"); assert.ok(url.searchParams.get("TrustedClientToken")); assert.match(url.searchParams.get("Sec-MS-GEC") || "", /^[0-9A-F]{64}$/); assert.match(url.searchParams.get("ConnectionId") || "", /^[0-9a-f]{32}$/); }); // ─── SSML building / escaping (untrusted-input safety) ───────────────────── test("escapeSsmlText escapes all five XML special characters", () => { assert.equal( escapeSsmlText(` & "quoted" 'single'`), "<tag> & "quoted" 'single'" ); }); test("buildSsml embeds escaped text and rejects SSML injection via prosody attrs", () => { const ssml = buildSsml({ text: "pwned", rate: "'; ", }); assert.ok(!ssml.includes("