import { test } from "node:test"; import assert from "node:assert/strict"; import { buildSearchHandoff, type HandoffGroup } from "./aiHandoff"; import type { LayerHit } from "./search/leafPipeline"; function hit(start: number, text: string): LayerHit { return { leafId: "l1", scope: "transcripts", start, text }; } function group(slug: string, hits: LayerHit[]): HandoffGroup { return { slug, title: `Title ${slug}`, channel: "Chan", uploadDate: "20200101", hits }; } const refs: Record = { "chan/aaa": { id: "aaa", webpageUrl: "https://x/aaa" }, "chan/bbb": { id: "bbb", webpageUrl: "https://x/bbb" }, }; const lookup = (slug: string) => refs[slug]; test("buildSearchHandoff maps groups to videos with ids, urls, and clocks", () => { const h = buildSearchHandoff( [group("chan/aaa", [hit(65, "hello world"), hit(0, "meta")])], lookup, ["graham platner"], ); assert.equal(h.label, "graham platner"); assert.equal(h.totalVideos, 1); assert.equal(h.truncated, false); assert.equal(h.videos.length, 1); const v = h.videos[0]; assert.equal(v.key, "chan/aaa"); assert.equal(v.videoId, "aaa"); assert.equal(v.url, "https://x/aaa"); assert.deepEqual(v.snippets[0], { clock: "1:05", seconds: 65, text: "hello world" }); // start 0 → "0:00" clock, whitespace collapsed. assert.equal(v.snippets[1].clock, "0:00"); }); test("buildSearchHandoff joins multiple queries with AND, falls back when empty", () => { assert.equal( buildSearchHandoff([group("chan/aaa", [hit(1, "a")])], lookup, ["one", " two "]).label, "one AND two", ); assert.equal( buildSearchHandoff([group("chan/aaa", [hit(1, "a")])], lookup, []).label, "your search", ); }); test("buildSearchHandoff caps videos and hits, flagging truncation", () => { const groups = Array.from({ length: 25 }, (_, i) => group(`chan/${i}`, Array.from({ length: 10 }, (_, j) => hit(j, `h${j}`))), ); const h = buildSearchHandoff(groups, lookup, ["q"], { maxVideos: 5, maxHitsPerVideo: 3, }); assert.equal(h.videos.length, 5); assert.equal(h.totalVideos, 25); assert.equal(h.videos[0].snippets.length, 3); assert.equal(h.truncated, true); // both video cap and per-video hit overflow }); test("buildSearchHandoff passes the whole set and tiers snippets (top full, tail indexed)", () => { const groups = Array.from({ length: 20 }, (_, i) => group(`chan/${i}`, Array.from({ length: 5 }, (_, j) => hit(j, `h${j}`))), ); const h = buildSearchHandoff(groups, () => undefined, ["q"], { fullExcerptVideos: 3, maxHitsPerVideo: 5, }); // Default maxVideos no longer caps at 20 — the whole set is passed. assert.equal(h.videos.length, 20); // Top tier keeps full excerpts; the tail is indexed with a single snippet. assert.equal(h.videos[0].snippets.length, 5); assert.equal(h.videos[2].snippets.length, 5); assert.equal(h.videos[3].snippets.length, 1); assert.equal(h.videos[19].snippets.length, 1); // The intentional tail trim is not flagged as truncation. assert.equal(h.truncated, false); }); test("buildSearchHandoff marks truncated when the search itself was capped", () => { const h = buildSearchHandoff([group("chan/aaa", [hit(1, "a")])], lookup, ["q"], { searchCapped: true, }); assert.equal(h.truncated, true); }); test("buildSearchHandoff falls back to slug when no summary ref is found", () => { const h = buildSearchHandoff([group("chan/unknown", [hit(1, "a")])], lookup, ["q"]); assert.equal(h.videos[0].videoId, "chan/unknown"); assert.equal(h.videos[0].url, undefined); }); test("buildSearchHandoff with no groups yields an empty, untruncated handoff", () => { const h = buildSearchHandoff([], lookup, ["q"]); assert.equal(h.videos.length, 0); assert.equal(h.totalVideos, 0); assert.equal(h.truncated, false); });