import { test } from "node:test"; import assert from "node:assert/strict"; import { linkifyCitations, parseClock, resolveCitationSeconds, } from "./citations"; import type { RetrievedVideo } from "../lib/askRetrieval"; const CID = "r0"; function video(over: Partial = {}): RetrievedVideo { return { key: "ch/v1", videoId: "v1", title: "A video", channel: "Chan", uploadDate: "20200101", snippets: [ { clock: "0:05", seconds: 5, text: "" }, { clock: "1:00", seconds: 60, text: "" }, { clock: "2:30", seconds: 150, text: "" }, ], ...over, }; } test("parseClock: mm:ss and h:mm:ss, rejects malformed", () => { assert.equal(parseClock("0:05"), 5); assert.equal(parseClock("12:34"), 12 * 60 + 34); assert.equal(parseClock("1:02:03"), 3723); assert.equal(parseClock("5"), null); // no colon assert.equal(parseClock("1:2:3:4"), null); // too many parts assert.equal(parseClock("1:xx"), null); // non-numeric }); test("linkifyCitations: bare [n] links with a plain anchor", () => { const out = linkifyCitations("see point [1] here", 3, CID); assert.equal(out, "see point [[1]](#cite-r0-1) here"); }); test("linkifyCitations: [n @ mm:ss] encodes the parsed second into the anchor", () => { const out = linkifyCitations("claim [2 @ 12:34]", 3, CID); // Anchor tail is --; display text keeps the full marker. assert.equal(out, "claim [[2 @ 12:34]](#cite-r0-2-754)"); }); test("linkifyCitations: tolerates h:mm:ss and stray spaces", () => { assert.equal( linkifyCitations("[3 @ 1:02:03]", 3, CID), "[[3 @ 1:02:03]](#cite-r0-3-3723)", ); assert.equal( linkifyCitations("[ 1 @ 0:05 ]", 3, CID), "[[ 1 @ 0:05 ]](#cite-r0-1-5)", ); }); test("linkifyCitations: out-of-range n stays plain text", () => { assert.equal(linkifyCitations("[5] and [2]", 3, CID), "[5] and [[2]](#cite-r0-2)"); assert.equal(linkifyCitations("[9 @ 1:00]", 3, CID), "[9 @ 1:00]"); }); test("linkifyCitations: maxN 0 is a no-op", () => { assert.equal(linkifyCitations("[1] [2]", 0, CID), "[1] [2]"); }); test("linkifyCitations: skips citations inside code spans", () => { assert.equal( linkifyCitations("real [1] but `code [2]` and ```\n[3]\n``` fenced", 3, CID), "real [[1]](#cite-r0-1) but `code [2]` and ```\n[3]\n``` fenced", ); }); test("resolveCitationSeconds: no parsed time → first snippet", () => { assert.equal(resolveCitationSeconds(video(), NaN), 5); }); test("resolveCitationSeconds: snaps a parsed time to the nearest known snippet", () => { // 58s is closest to the 60s snippet; 140s closest to 150s. assert.equal(resolveCitationSeconds(video(), 58), 60); assert.equal(resolveCitationSeconds(video(), 140), 150); // An exact hit stays put. assert.equal(resolveCitationSeconds(video(), 5), 5); }); test("resolveCitationSeconds: passes a parsed time through when the source has no snippets", () => { assert.equal(resolveCitationSeconds(video({ snippets: [] }), 42), 42); }); test("resolveCitationSeconds: undefined for a missing source", () => { assert.equal(resolveCitationSeconds(undefined, 5), undefined); assert.equal(resolveCitationSeconds(undefined, NaN), undefined); });