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("handles nested code fences (4+ backticks) without leaking inner code syntax or rogue backticks", () => { const out = speakable( "Here is an example with nested code fences:\n````markdown\n```typescript\nconst x = 10;\n```\n````\nAnd that concludes our example.\n", ); expect(out).toContain("Markdown code block"); expect(out).not.toContain("const x"); expect(out).not.toContain("`"); expect(out).toContain("concludes our example"); }); it("handles tilde-delimited variable length fences cleanly", () => { const out = speakable("Here is bash:\n\n~~~~sh\n~~~sh\necho hi\n~~~\n~~~~\n\nDone."); expect(out).toContain("shell code block"); expect(out).not.toContain("echo hi"); expect(out).not.toContain("~"); expect(out).toContain("Done"); }); it("handles unclosed variable-length code fences cleanly", () => { const out = speakable("Working:\n\n````markdown\n```ts\nconst x = 1;\n```"); expect(out).toContain("Markdown code block"); expect(out).not.toContain("const x"); expect(out).not.toContain("`"); expect(out).toContain("Working"); }); it("names expanded languages in code blocks", () => { expect(speakable("```python\nprint(1)\n```")).toContain("Python code block"); expect(speakable("```rust\nfn main() {}\n```")).toContain("Rust code block"); expect(speakable("```html\n
\n```")).toContain("HTML code block"); expect(speakable("```css\nbody {}\n```")).toContain("CSS code block"); expect(speakable("```md\n# Title\n```")).toContain("Markdown code block"); }); 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('