evOo-local/packages/core/src/speech-text.test.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

196 lines
7.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
isUnspeakableSpeechText,
narrateTool,
speakable,
speechFromBlocks,
spokenDecision,
toUtterances,
} from "./speech-text.js";
describe("speakable", () => {
it("names a code block instead of reading it", () => {
const out = speakable(
"Here's the fix:\n\n```ts\nconst x: number = 1;\nif (x) throw new Error('no');\n```\n\nThat's it.",
);
expect(out).toContain("TypeScript code block");
expect(out).not.toContain("const x");
expect(out).not.toContain("throw");
expect(out).toContain("That's it.");
});
it("closes an unterminated fence rather than swallowing the rest", () => {
const out = speakable("Working on it:\n\n```sh\nnpm test");
expect(out).toContain("Working on it");
expect(out).toContain("code block");
expect(out).not.toContain("npm test");
});
it("keeps a link's words and drops its URL", () => {
expect(speakable("See [the README](https://example.com/a/b?c=d) for more")).toBe(
"See the README for more",
);
});
it("turns a bare URL into a noun", () => {
expect(speakable("Deployed to https://status.example.com/health now")).toBe(
"Deployed to a link now",
);
});
it("says the file, not the path to it", () => {
expect(speakable("I changed server/drivers/acp/core.ts today")).toBe("I changed core.ts today");
});
it("keeps short inline code but not long snippets", () => {
expect(speakable("Run `pnpm test` first")).toBe("Run pnpm test first");
const long = speakable(`Use \`${"x".repeat(60)}\` here`);
expect(long).toBe("Use that snippet here");
});
it("strips list scaffolding but keeps the pause between items", () => {
const out = speakable("- first thing\n- second thing\n- third thing");
expect(out).toBe("first thing. second thing. third thing");
});
it("reads a table as rows, not pipes", () => {
const out = speakable("| Name | State |\n| --- | --- |\n| Scout | idle |");
expect(out).not.toContain("|");
expect(out).not.toContain("---");
expect(out).toContain("Scout, idle");
});
it("drops emphasis markers, emoji and checkboxes", () => {
expect(speakable("**Done** ✅ — [x] shipped the _thing_")).toBe("Done — shipped the thing");
});
it("gives a heading a full stop so the voice breathes", () => {
expect(speakable("## Results\nAll green")).toBe("Results. All green");
});
it("collapses the punctuation its own substitutions create", () => {
expect(speakable("Done.\n\n\n- one\n\n- two")).toBe("Done. one. two");
});
it("is empty for empty input", () => {
expect(speakable("")).toBe("");
expect(speakable(" \n\n ")).toBe("");
});
it("never speaks pasted handoff tool JSON", () => {
const raw =
'Since the handoff is given:\n{"name": "handoff_to_bot", "parameters": {"message": "same task", "confirm_name": "Boss"}}';
expect(speakable(raw)).toBe("");
expect(speakable("assistant: " + raw)).toBe("");
});
it("joins message blocks into something a voice can read", () => {
expect(
speechFromBlocks([
{ kind: "text", text: "Done." },
{ kind: "ask", text: "Ship it?", status: "pending" },
]),
).toContain("Ship it?");
});
it("drops tool-call JSON and tool-activity blocks from voice text", () => {
const toolJson = JSON.stringify({
name: "handoff_to_bot",
arguments: { targetBotId: "bot-b", message: "Take over" },
});
expect(isUnspeakableSpeechText(toolJson)).toBe(true);
expect(
speechFromBlocks([
{ kind: "text", text: toolJson },
{ kind: "progress", text: "Using browser", activity: true },
{ kind: "text", text: "I'll hand this off." },
]),
).toBe("I'll hand this off.");
});
it("drops XML tool_call payloads", () => {
expect(isUnspeakableSpeechText('<tool_call>{"name":"bash","arguments":{}}</tool_call>')).toBe(
true,
);
});
});
describe("toUtterances", () => {
it("splits on sentences", () => {
const out = toUtterances("The tests pass now. I changed two files. Want me to push it?");
expect(out).toHaveLength(3);
expect(out[0]).toBe("The tests pass now.");
expect(out[2]).toBe("Want me to push it?");
});
it("does not split inside a decimal or an abbreviation", () => {
const out = toUtterances(
"It dropped to 11.7 seconds per step, i.e. about half of what it was before.",
);
expect(out).toHaveLength(1);
});
it("glues a fragment onto its neighbour", () => {
const out = toUtterances("Yes. The whole suite is green and nothing else changed.");
expect(out).toHaveLength(1);
expect(out[0]).toContain("Yes.");
});
it("breaks a runaway sentence at a clause, never mid-word", () => {
const long = `I looked at ${Array.from({ length: 40 }, (_, i) => `item ${i}`).join(", ")} and finished.`;
const out = toUtterances(long, { maxChars: 120 });
expect(out.length).toBeGreaterThan(1);
for (const piece of out) expect(piece.length).toBeLessThanOrEqual(140);
expect(out.join(" ")).toContain("item 39");
});
it("runs its input through the spoken register first", () => {
const out = toUtterances("Fixed it.\n\n```js\nconsole.log(1)\n```\n\nShipped.");
expect(out.join(" ")).not.toContain("console.log");
});
it("is empty for text that speaks to nothing", () => {
expect(toUtterances("```\ncode only\n```")).not.toContain("code only");
expect(toUtterances("")).toEqual([]);
});
});
describe("narrateTool", () => {
it("turns tool names into something worth hearing", () => {
expect(narrateTool("Bash")).toBe("running a command");
expect(narrateTool("Read")).toBe("reading a file");
expect(narrateTool("Edit")).toBe("editing a file");
expect(narrateTool("WebSearch")).toBe("searching the web");
expect(narrateTool("screenshot")).toBe("looking at the screen");
expect(narrateTool("computer_observe")).toBe("looking at the screen");
expect(narrateTool("browser_snapshot")).toBe("using the page");
expect(narrateTool("write_file")).toBe("writing a file");
expect(narrateTool("run_subagent")).toBe("starting a subagent");
});
it("sees through an MCP tool prefix", () => {
expect(narrateTool("mcp__computer__click")).toBe("using the computer");
expect(narrateTool("mcp__agents__ask_bot")).toBe("asking a teammate");
});
it("stays quiet for chips the user is already told about another way", () => {
expect(narrateTool("auto-approved Bash:git (always allowed)")).toBeNull();
expect(narrateTool("error: claude exited 1")).toBeNull();
expect(narrateTool("")).toBeNull();
});
it("falls back to naming an unknown tool, without reading its argv", () => {
expect(narrateTool("deploy_thing")).toBe("running deploy_thing");
expect(narrateTool('curl -X POST "https://x/y" --data @{}')).toBeNull();
});
});
describe("spokenDecision", () => {
it("accepts a clear yes or no and refuses a sentence that merely contains one", () => {
expect(spokenDecision("yes")).toBe("yes");
expect(spokenDecision("No thanks")).toBe("no");
expect(spokenDecision("I'm sure that's fine, go look at the logs")).toBeNull();
expect(spokenDecision("")).toBeNull();
});
});