Files
oh-my-pi-telegram/tests/voice.test.ts
Evandro Fonseca Junior d1d04001d0
Release / release (push) Failing after 5s
Validate / validate (push) Failing after 5m21s
chore: fork @llblab/[email protected] as @evandrodevbr/oh-my-pi-telegram
Fork of upstream llblab/[email protected].0 (SHA afe09c5) repackaged as a
drop-in extension for can1357/oh-my-pi (omp >= 16.5.2).

Changes vs upstream:
- Rename package: @llblab/pi-telegram -> @evandrodevbr/oh-my-pi-telegram
- Bump version: 0.22.0 -> 0.22.0-evandro.1
- Update repository/homepage/bugs URLs to evandrodevbr
- Add omp field in package.json (displayName + compatibleWith)
- README: prepend 'Oh My Pi (omp) Compatibility' section with install
  commands, verified-components table, upstream lineage, and rationale
- CHANGELOG: add '0.22.0-evandro.1' entry under Unreleased
- docs/: rename @llblab/pi-telegram imports to @evandrodevbr/oh-my-pi-telegram
- tests/public-api.test.ts: rename package import paths

Gates re-run on the fork:
- npm run typecheck  -> tsc --noEmit (success)
- npm test           -> 1229 pass / 0 fail / 1 platform-only skip
- npm run audit      -> found 0 vulnerabilities
- npm run pack:check -> evandrodevbr-oh-my-pi-telegram-0.22.0-evandro.1.tgz

Verified:
- omp install --link ./pi-telegram -> Linked OK
- omp plugin list -> @evandrodevbr/[email protected].0-evandro.1

Upstream lineage: badlogic/pi-telegram -> llblab/pi-telegram (0.22.0) -> evandrodevbr/oh-my-pi-telegram (this fork)
2026-07-15 14:18:16 -03:00

401 lines
13 KiB
TypeScript

/**
* Tests for the voice domain.
* Covers policy resolution, turn tagging, suppression helpers, the provider registry,
* and voice-specific markup parsing.
*/
import assert from "node:assert/strict";
import { afterEach, beforeEach, test } from "node:test";
import {
clearTelegramVoiceTranscriptionProviders,
computeVoiceTurnFlags,
getTelegramVoiceReplyMode,
getTelegramVoiceTranscriptionProviders,
isVoiceTurn,
registerTelegramVoiceTranscriptionProvider,
shouldSuppressPreviewForVoice,
} from "../lib/voice.ts";
import {
clearTelegramVoiceSynthesisProviders,
getTelegramVoiceSynthesisProviders,
hasTelegramVoiceSynthesisProvider,
planTelegramVoiceReply,
registerTelegramVoiceSynthesisProvider,
stripTelegramCommentMarkupForPreview,
} from "../lib/outbound.ts";
// --- Test Setup ---
beforeEach(() => {
clearTelegramVoiceSynthesisProviders();
clearTelegramVoiceTranscriptionProviders();
});
afterEach(() => {
clearTelegramVoiceSynthesisProviders();
clearTelegramVoiceTranscriptionProviders();
});
// --- Policy Resolution ---
test("getTelegramVoiceReplyMode returns 'manual' by default", () => {
assert.equal(getTelegramVoiceReplyMode(), "manual");
assert.equal(getTelegramVoiceReplyMode(undefined), "manual");
assert.equal(getTelegramVoiceReplyMode({}), "manual");
assert.equal(getTelegramVoiceReplyMode({ voice: {} }), "manual");
});
test("getTelegramVoiceReplyMode reads valid mode from config", () => {
assert.equal(
getTelegramVoiceReplyMode({ voice: { replyMode: "mirror" } }),
"mirror",
);
assert.equal(
getTelegramVoiceReplyMode({ voice: { replyMode: "always" } }),
"always",
);
assert.equal(
getTelegramVoiceReplyMode({ voice: { replyMode: "manual" } }),
"manual",
);
});
test("getTelegramVoiceReplyMode ignores invalid config values", () => {
assert.equal(
getTelegramVoiceReplyMode({ voice: { replyMode: "invalid" as any } }),
"manual",
);
assert.equal(
getTelegramVoiceReplyMode({ voice: { replyMode: "foo" as any } }),
"manual",
);
});
test("getTelegramVoiceReplyMode ignores provider policy without config", () => {
registerTelegramVoiceSynthesisProvider(
{
getVoicePolicy: () => ({ replyMode: "always" }),
} as any,
{ id: "test-provider-1" },
);
assert.equal(getTelegramVoiceReplyMode({}), "manual");
});
test("getTelegramVoiceReplyMode reads config even when provider returns invalid policy", () => {
registerTelegramVoiceSynthesisProvider(
{
getVoicePolicy: () => ({ replyMode: "invalid" as any }),
} as any,
{ id: "bad-provider" },
);
const result = getTelegramVoiceReplyMode({ voice: { replyMode: "mirror" } });
assert.equal(result, "mirror");
});
test("getTelegramVoiceReplyMode defaults to manual despite provider policies", () => {
registerTelegramVoiceSynthesisProvider(
{
getVoicePolicy: () => ({ replyMode: "mirror" }),
} as any,
{ id: "mirror-provider" },
);
registerTelegramVoiceSynthesisProvider(
{
getVoicePolicy: () => ({ replyMode: "always" }),
} as any,
{ id: "always-provider" },
);
assert.equal(getTelegramVoiceReplyMode(), "manual");
});
// --- Turn Tagging Helpers ---
test("computeVoiceTurnFlags works for all modes", () => {
assert.deepEqual(computeVoiceTurnFlags("mirror", true), {
voiceReplyPreferred: true,
voiceReplyRequired: false,
});
assert.deepEqual(computeVoiceTurnFlags("mirror", false), {
voiceReplyPreferred: false,
voiceReplyRequired: false,
});
assert.deepEqual(computeVoiceTurnFlags("always", false), {
voiceReplyPreferred: false,
voiceReplyRequired: true,
});
assert.deepEqual(computeVoiceTurnFlags("manual", true), {
voiceReplyPreferred: false,
voiceReplyRequired: false,
});
});
test("isVoiceTurn detects voice-tagged turns correctly", () => {
assert.equal(isVoiceTurn({ voiceReplyPreferred: true }), true);
assert.equal(isVoiceTurn({ voiceReplyRequired: true }), true);
assert.equal(
isVoiceTurn({ voiceReplyPreferred: true, voiceReplyRequired: true }),
true,
);
assert.equal(
isVoiceTurn({ voiceReplyPreferred: false, voiceReplyRequired: false }),
false,
);
assert.equal(isVoiceTurn(null), false);
assert.equal(isVoiceTurn(undefined), false);
assert.equal(isVoiceTurn({}), false);
});
// --- Preview Suppression ---
test("shouldSuppressPreviewForVoice works correctly", () => {
assert.equal(
shouldSuppressPreviewForVoice({ voiceReplyPreferred: true }),
true,
);
assert.equal(
shouldSuppressPreviewForVoice({ voiceReplyRequired: true }),
true,
);
assert.equal(
shouldSuppressPreviewForVoice({
voiceReplyPreferred: false,
voiceReplyRequired: false,
}),
false,
);
assert.equal(shouldSuppressPreviewForVoice(null), false);
assert.equal(shouldSuppressPreviewForVoice(undefined), false);
});
// --- Voice Markup Parsing ---
test("planTelegramVoiceReply extracts simple voice text", () => {
const result = planTelegramVoiceReply(
"Hello\n\n<!-- telegram_voice: World -->",
);
assert.equal(result.voiceText, "World");
assert.ok(result.voiceReplies?.length === 1);
});
test("planTelegramVoiceReply extracts lang and rate attributes", () => {
const result = planTelegramVoiceReply(
'Say\n\n<!-- telegram_voice lang="de" rate="1.2": Hallo -->',
);
assert.equal(result.lang, "de");
assert.equal(result.rate, "1.2");
assert.equal(result.voiceText, "Hallo");
});
test("planTelegramVoiceReply handles colon shorthand form", () => {
const result = planTelegramVoiceReply(
"Text\n\n<!-- telegram_voice: This is the voice text -->",
);
assert.equal(result.voiceText, "This is the voice text");
assert.ok(result.voiceReplies?.length === 1);
});
test("planTelegramVoiceReply handles multiple voice blocks", () => {
const result = planTelegramVoiceReply(
"First\n\n<!-- telegram_voice: One -->\n\nand second\n\n<!-- telegram_voice: Two -->",
);
assert.equal(result.voiceReplies?.length, 2);
assert.equal(result.voiceText, "One\n\nTwo");
assert.ok(result.markdown.includes("First"));
assert.ok(result.markdown.includes("and second"));
assert.ok(!result.markdown.includes("telegram_voice"));
});
test("planTelegramVoiceReply returns cleaned markdown", () => {
const result = planTelegramVoiceReply(
"Normal\n\n<!-- telegram_voice: Voice only -->\n\ntext",
);
assert.ok(result.markdown.includes("Normal"));
assert.ok(result.markdown.includes("text"));
assert.equal(result.voiceText, "Voice only");
assert.ok(!result.markdown.includes("telegram_voice"));
});
// --- Voice Provider Registry ---
test("Voice synthesis provider registry - basic register / get / has / clear", () => {
assert.equal(hasTelegramVoiceSynthesisProvider(), false);
assert.equal(getTelegramVoiceSynthesisProviders().length, 0);
const dispose1 = registerTelegramVoiceSynthesisProvider(
() => Promise.resolve("audio.mp3"),
{ id: "p1" },
);
assert.equal(hasTelegramVoiceSynthesisProvider(), true);
assert.equal(getTelegramVoiceSynthesisProviders().length, 1);
const dispose2 = registerTelegramVoiceSynthesisProvider(
{
getVoicePolicy: () => ({ replyMode: "always" }),
getVoicePromptContribution: () => "Be concise.",
} as any,
{ id: "p2" },
);
assert.equal(getTelegramVoiceSynthesisProviders().length, 2);
dispose1();
assert.equal(getTelegramVoiceSynthesisProviders().length, 1);
dispose2();
assert.equal(hasTelegramVoiceSynthesisProvider(), false);
});
test("Voice synthesis provider registry accepts both function and object form", () => {
// Function form (backward compat)
registerTelegramVoiceSynthesisProvider(() => Promise.resolve("audio1"), {
id: "fn",
});
// Object form
registerTelegramVoiceSynthesisProvider(
{
getVoicePolicy: () => ({ replyMode: "mirror" }),
} as any,
{ id: "obj" },
);
const providers = getTelegramVoiceSynthesisProviders();
assert.equal(providers.length, 2);
assert.equal(typeof providers[0], "function");
assert.equal(typeof providers[1], "object");
});
test("Voice synthesis generated ids skip existing registry entries", () => {
registerTelegramVoiceSynthesisProvider(() => Promise.resolve("stable.ogg"), {
id: "voice-synthesis-provider-0",
});
registerTelegramVoiceSynthesisProvider(() =>
Promise.resolve("generated.ogg"),
);
assert.equal(getTelegramVoiceSynthesisProviders().length, 2);
});
test("Voice synthesis stable id stale disposer does not delete replacement", () => {
const disposeFirst = registerTelegramVoiceSynthesisProvider(
() => Promise.resolve("first.ogg"),
{ id: "stable" },
);
registerTelegramVoiceSynthesisProvider(() => Promise.resolve("second.ogg"), {
id: "stable",
});
disposeFirst();
assert.equal(getTelegramVoiceSynthesisProviders().length, 1);
});
test("Voice synthesis fixture re-registers cleanly across lifecycle boundaries", () => {
const registerFixture = (generation: string) =>
registerTelegramVoiceSynthesisProvider(
() => Promise.resolve(`${generation}.ogg`),
{ id: "fixture-tts" },
);
const disposeSessionStart = registerFixture("session-start");
const disposeResume = registerFixture("resume");
const disposeReload = registerFixture("reload");
assert.equal(getTelegramVoiceSynthesisProviders().length, 1);
disposeSessionStart();
disposeResume();
assert.equal(getTelegramVoiceSynthesisProviders().length, 1);
disposeReload();
assert.equal(getTelegramVoiceSynthesisProviders().length, 0);
});
test("Voice synthesis generated ids do not collide after disposal", () => {
const disposeFirst = registerTelegramVoiceSynthesisProvider(() =>
Promise.resolve("first.ogg"),
);
registerTelegramVoiceSynthesisProvider(() => Promise.resolve("second.ogg"));
disposeFirst();
registerTelegramVoiceSynthesisProvider(() => Promise.resolve("third.ogg"));
assert.equal(getTelegramVoiceSynthesisProviders().length, 2);
});
test("Voice transcription generated ids skip existing registry entries", () => {
registerTelegramVoiceTranscriptionProvider(() => Promise.resolve("stable"), {
id: "voice-transcription-provider-0",
});
registerTelegramVoiceTranscriptionProvider(() =>
Promise.resolve("generated"),
);
assert.equal(getTelegramVoiceTranscriptionProviders().length, 2);
});
test("Voice transcription stable id stale disposer does not delete replacement", () => {
const disposeFirst = registerTelegramVoiceTranscriptionProvider(
() => Promise.resolve("first"),
{ id: "stable" },
);
registerTelegramVoiceTranscriptionProvider(() => Promise.resolve("second"), {
id: "stable",
});
disposeFirst();
assert.equal(getTelegramVoiceTranscriptionProviders().length, 1);
});
test("Voice transcription fixture re-registers cleanly across lifecycle boundaries", () => {
const registerFixture = (generation: string) =>
registerTelegramVoiceTranscriptionProvider(
() => Promise.resolve(generation),
{ id: "fixture-stt" },
);
const disposeSessionStart = registerFixture("session-start");
const disposeResume = registerFixture("resume");
const disposeReload = registerFixture("reload");
assert.equal(getTelegramVoiceTranscriptionProviders().length, 1);
disposeSessionStart();
disposeResume();
assert.equal(getTelegramVoiceTranscriptionProviders().length, 1);
disposeReload();
assert.equal(getTelegramVoiceTranscriptionProviders().length, 0);
});
test("Voice transcription generated ids do not collide after disposal", () => {
const disposeFirst = registerTelegramVoiceTranscriptionProvider(() =>
Promise.resolve("first"),
);
registerTelegramVoiceTranscriptionProvider(() => Promise.resolve("second"));
disposeFirst();
registerTelegramVoiceTranscriptionProvider(() => Promise.resolve("third"));
assert.equal(getTelegramVoiceTranscriptionProviders().length, 2);
});
test("Voice synthesis provider registry clear works reliably for tests", () => {
registerTelegramVoiceSynthesisProvider(() => Promise.resolve("x"), {
id: "tmp",
});
assert.equal(hasTelegramVoiceSynthesisProvider(), true);
clearTelegramVoiceSynthesisProviders();
assert.equal(hasTelegramVoiceSynthesisProvider(), false);
});
// --- Stripping And Generic Parser Interaction ---
test("stripTelegramCommentMarkupForPreview removes voice blocks and normalizes whitespace", () => {
const input = "Hello\n\n<!-- telegram_voice: World -->\n\nWorld";
const result = stripTelegramCommentMarkupForPreview(input);
assert.ok(!result.includes("telegram_voice"));
assert.ok(!result.includes("\n\n\n"));
});
test("planTelegramVoiceReply works with the original generic parsers (fence + comment)", () => {
const input = "Text\n```\ncode\n```\n<!-- telegram_voice: Spoken -->";
const result = planTelegramVoiceReply(input);
assert.equal(result.voiceText, "Spoken");
assert.ok(result.markdown.includes("Text"));
assert.ok(result.markdown.includes("code"));
});