import { test } from "node:test"; import assert from "node:assert/strict"; import { clock, truncate, windowedTranscript, findHitsInCues, findHitsInText, findFirstMatchInRange, } from "./window"; import type { Cue } from "../vtt"; import { MCP_POLICY, VIEWER_POLICY } from "./policy"; const cue = (start: number, text: string): Cue => ({ start, end: start + 3, text }); test("clock renders 0 as 0:00 rather than the empty string", () => { assert.equal(clock(0), "0:00"); assert.equal(clock(-5), "0:00"); assert.equal(clock(65), "1:05"); assert.equal(clock(3725), "1:02:05"); }); test("truncate collapses whitespace and clips with the ellipsis inside the budget", () => { assert.equal(truncate(" a b\n c ", MCP_POLICY.snippetChars), "a b c"); assert.equal(truncate("abcdef", 4), "abc…"); assert.equal(truncate("abcd", 4), "abcd"); }); // The regression this guards: `max` used to default to MCP_POLICY.snippetChars, // so a viewer path that forgot to pass its width clipped at 240 with every test // green. The default is gone (tsc now requires the argument); this pins that the // two policies actually produce different widths, which is the observable the // silent default hid. test("the width is the CALLER's, and the two policies differ", () => { const long = "z".repeat(600); assert.equal(truncate(long, MCP_POLICY.snippetChars).length, 240); assert.equal(truncate(long, VIEWER_POLICY.snippetChars), long); }); test("windowedTranscript merges overlapping windows and stamps each line", () => { const cues = [ cue(0, "one"), cue(10, "needle here"), cue(20, "three"), cue(30, "needle again"), cue(40, "five"), ]; const r = windowedTranscript(cues, (t) => t.includes("needle"), { before: 15, after: 15, maxLines: MCP_POLICY.windowLineCap, }); assert.equal(r.matchCount, 2); // Both windows overlap, so every cue appears exactly once. assert.equal(r.lines.length, 5); assert.equal(r.lines[0], "[0:00] one"); assert.equal(r.lines[1], "[0:10] needle here"); }); test("windowedTranscript honours maxLines and the timestamps/stamp options", () => { const cues = Array.from({ length: 40 }, (_, i) => cue(i, `line ${i} needle`)); const capped = windowedTranscript(cues, () => true, { maxLines: 5 }); assert.equal(capped.lines.length, 5); const bare = windowedTranscript([cue(7, "hit")], () => true, { timestamps: false, maxLines: MCP_POLICY.windowLineCap, }); assert.deepEqual(bare.lines, ["hit"]); const linked = windowedTranscript([cue(7, "hit")], () => true, { stamp: (c, s) => `${c}|${s}`, maxLines: MCP_POLICY.windowLineCap, }); assert.deepEqual(linked.lines, ["[0:07|7] hit"]); }); test("findHitsInCues emits the matched cue, widening only across a cue boundary", () => { const cues = [ { start: 0, text: "the quick brown" }, { start: 3, text: "fox jumps over" }, { start: 6, text: "the lazy dog" }, ]; // Wholly inside one cue → that cue's text alone. const inside = findHitsInCues(cues, "jumps", false, null, 10); assert.deepEqual(inside, [{ start: 3, text: "fox jumps over" }]); // Straddling the first/second cue → emitted ONCE, on the cue the match // STARTS in, with the text widened to that cue's window. const across = findHitsInCues(cues, "brown fox", false, null, 10); assert.equal(across.length, 1); assert.equal(across[0].start, 0); assert.equal(across[0].text, "the quick brown fox jumps over"); }); test("findHitsInCues stops at the limit", () => { const cues = Array.from({ length: 10 }, (_, i) => ({ start: i, text: "needle" })); assert.equal(findHitsInCues(cues, "needle", false, null, 3).length, 3); }); test("findHitsInText emits one padded snippet with ellipses at the clipped ends", () => { const text = `${"a".repeat(200)} needle ${"b".repeat(200)}`; const hits = findHitsInText(text, "needle", false, null); assert.equal(hits.length, 1); assert.equal(hits[0].start, 0); assert.ok(hits[0].text.startsWith("…")); assert.ok(hits[0].text.endsWith("…")); assert.ok(hits[0].text.includes("needle")); assert.deepEqual(findHitsInText("", "needle", false, null), []); assert.deepEqual(findHitsInText("nothing", "needle", false, null), []); }); test("findFirstMatchInRange only accepts a match STARTING in the range", () => { const hay = "prev cur next"; // "cur" starts at 5, inside [5, 8). assert.deepEqual(findFirstMatchInRange(hay, 5, 8, "cur", false, null), { idx: 5, length: 3, }); // "next" starts at 9, outside the current cue's span. assert.equal(findFirstMatchInRange(hay, 5, 8, "next", false, null), null); // A zero-width regex advances rather than spinning forever: it settles on // the first offset inside the range. assert.deepEqual(findFirstMatchInRange(hay, 5, 8, "", true, /x*/), { idx: 5, length: 0, }); // Regex mode with no compiled regex is a miss, never a throw. assert.equal(findFirstMatchInRange(hay, 0, 13, "cur", true, null), null); });