src/voice/sentenceAccumulator.test.ts

import { test } from "bun:test"; import assert from "node:assert/strict"; import { SentenceAccumulator } from "./sentenceAccumulator.ts";

test("SentenceAccumulator emits the first chunk eagerly after two sentences once threshold is met", () => { const emitted: string[] = []; const accumulator = new SentenceAccumulator({ eagerFirstChunk: true, eagerMinChars: 20, maxBufferChars: 120, onSentence(text) { emitted.push(text); } });

accumulator.push("Let me check that out for you. "); assert.deepEqual(emitted, []);

accumulator.push("I want one more sentence."); assert.equal(emitted[0], "Let me check that out for you. I want one more sentence."); });

test("SentenceAccumulator keeps the first chunk intact until sentence punctuation arrives", () => { const emitted: string[] = []; const accumulator = new SentenceAccumulator({ eagerFirstChunk: true, eagerMinChars: 30, maxBufferChars: 120, minSentencesPerChunk: 1, onSentence(text) { emitted.push(text); } });

accumulator.push("Yo donky, what's good my guy, we back in the "); assert.deepEqual(emitted, []);

accumulator.push("vc!"); assert.deepEqual(emitted, ["Yo donky, what's good my guy, we back in the vc!"]); });

test("SentenceAccumulator waits for a sentence break after the first emitted chunk", () => { const emitted: string[] = []; const accumulator = new SentenceAccumulator({ eagerFirstChunk: true, eagerMinChars: 10, maxBufferChars: 120, minSentencesPerChunk: 1, onSentence(text) { emitted.push(text); } });

accumulator.push("First sentence. Second"); accumulator.push(" sentence is still coming"); assert.deepEqual(emitted, ["First sentence."]);

accumulator.push(" through now."); assert.deepEqual(emitted, ["First sentence.", "Second sentence is still coming through now."]); });

test("SentenceAccumulator waits for two sentence breaks after the first emitted chunk by default", () => { const emitted: string[] = []; const accumulator = new SentenceAccumulator({ eagerFirstChunk: true, eagerMinChars: 10, maxBufferChars: 120, onSentence(text) { emitted.push(text); } });

accumulator.push("First sentence. Second sentence. "); assert.deepEqual(emitted, ["First sentence. Second sentence."]);

accumulator.push("Third sentence. "); assert.deepEqual(emitted, ["First sentence. Second sentence."]);

accumulator.push("Fourth sentence."); assert.deepEqual(emitted, [ "First sentence. Second sentence.", "Third sentence. Fourth sentence." ]); });

test("SentenceAccumulator merges tiny post-first chunks into the next chunk", () => { const emitted: string[] = []; const accumulator = new SentenceAccumulator({ eagerFirstChunk: true, eagerMinChars: 10, maxBufferChars: 120, minSentencesPerChunk: 1, onSentence(text) { emitted.push(text); } });

accumulator.push("First sentence. "); accumulator.push("How you doin'? "); assert.deepEqual(emitted, ["First sentence."]);

accumulator.push("Glad you made it through."); assert.deepEqual(emitted, [ "First sentence.", "How you doin'? Glad you made it through." ]); });

test("SentenceAccumulator does not split the first chunk on an internal word boundary without a clean ending", () => { const emitted: string[] = []; const accumulator = new SentenceAccumulator({ eagerFirstChunk: true, eagerMinChars: 16, maxBufferChars: 120, minSentencesPerChunk: 1, onSentence(text) { emitted.push(text); } });

accumulator.push("yo vuhlp, what's good"); assert.deepEqual(emitted, []);

accumulator.flush(); assert.deepEqual(emitted, ["yo vuhlp, what's good"]); });

test("SentenceAccumulator flush emits trailing text without punctuation", () => { const emitted: string[] = []; const accumulator = new SentenceAccumulator({ minSentencesPerChunk: 1, onSentence(text) { emitted.push(text); } });

accumulator.push("still thinking about"); accumulator.push(" the ending"); accumulator.flush();

assert.deepEqual(emitted, ["still thinking about the ending"]); });

test("SentenceAccumulator flush preserves a deferred tiny post-first chunk", () => { const emitted: string[] = []; const accumulator = new SentenceAccumulator({ eagerFirstChunk: true, eagerMinChars: 10, maxBufferChars: 120, minSentencesPerChunk: 1, onSentence(text) { emitted.push(text); } });

accumulator.push("First sentence. "); accumulator.push("That's rare."); assert.deepEqual(emitted, ["First sentence."]);

accumulator.flush(); assert.deepEqual(emitted, ["First sentence.", "That's rare."]); });

test("SentenceAccumulator forces a chunk when the buffer grows too large", () => { const emitted: string[] = []; const accumulator = new SentenceAccumulator({ eagerFirstChunk: false, maxBufferChars: 30, minSentencesPerChunk: 1, onSentence(text) { emitted.push(text); } });

accumulator.push("this sentence has no punctuation but should still break eventually because it is long");

assert.equal(emitted.length > 0, true); assert.equal(emitted[0].length <= 30, true); assert.equal(emitted.join(" ").includes("this sentence has no punctuation"), true); });

test("SentenceAccumulator does not split on the colon inside an inline soundboard directive", () => { const emitted: string[] = []; const accumulator = new SentenceAccumulator({ eagerFirstChunk: true, eagerMinChars: 10, maxBufferChars: 120, minSentencesPerChunk: 1, onSentence(text) { emitted.push(text); } });

accumulator.push("First sentence. "); assert.deepEqual(emitted, ["First sentence."]);

accumulator.push("[[SOUNDBOARD:airhorn@123]] hold that thought."); assert.deepEqual(emitted, ["First sentence.", "[[SOUNDBOARD:airhorn@123]] hold that thought."]); });

test("SentenceAccumulator does not force-break inside an inline soundboard directive", () => { const emitted: string[] = []; const accumulator = new SentenceAccumulator({ eagerFirstChunk: false, maxBufferChars: 24, minSentencesPerChunk: 1, onSentence(text) { emitted.push(text); } });

accumulator.push("lead [[SOUNDBOARD:airhorn@123]] tail"); accumulator.flush();

assert.equal( emitted.some((chunk) => chunk.includes("[[SOUNDBOARD:") && !chunk.includes("]]")), false ); });