import { test, describe } from "node:test"; import { strict as assert } from "node:assert"; import { sendEvent, extractVideoId, formatTime, parseTimestampedTranscript, safeText, retryGemini, } from "../util.js"; describe("extractVideoId", () => { test("extracts from standard watch URL", () => { assert.equal(extractVideoId("https://www.youtube.com/watch?v=dQw4w9WgXcQ"), "dQw4w9WgXcQ"); }); test("extracts from youtu.be short URL", () => { assert.equal(extractVideoId("https://youtu.be/dQw4w9WgXcQ"), "dQw4w9WgXcQ"); }); test("extracts from /embed/ URL", () => { assert.equal(extractVideoId("https://www.youtube.com/embed/dQw4w9WgXcQ"), "dQw4w9WgXcQ"); }); test("extracts from /live/ URL (with ?si tracking param)", () => { assert.equal( extractVideoId("https://www.youtube.com/live/QEq1Fa-Br0U?si=CqlsUBpyTs_ksqi3"), "QEq1Fa-Br0U" ); }); test("extracts from /shorts/ URL", () => { assert.equal(extractVideoId("https://www.youtube.com/shorts/dQw4w9WgXcQ"), "dQw4w9WgXcQ"); }); test("accepts a bare 11-character id", () => { assert.equal(extractVideoId("dQw4w9WgXcQ"), "dQw4w9WgXcQ"); }); test("returns null for non-YouTube URL", () => { assert.equal(extractVideoId("https://example.com/video"), null); }); test("returns null for empty / null input", () => { assert.equal(extractVideoId(""), null); assert.equal(extractVideoId(null), null); assert.equal(extractVideoId(undefined), null); }); test("returns null for too-short id", () => { assert.equal(extractVideoId("dQw4w9W"), null); }); }); describe("formatTime", () => { test("formats sub-minute as M:SS", () => { assert.equal(formatTime(0), "0:00"); assert.equal(formatTime(5), "0:05"); assert.equal(formatTime(59), "0:59"); }); test("formats minutes as M:SS", () => { assert.equal(formatTime(60), "1:00"); assert.equal(formatTime(125), "2:05"); assert.equal(formatTime(3599), "59:59"); }); test("formats hours+ as H:MM:SS", () => { assert.equal(formatTime(3600), "1:00:00"); assert.equal(formatTime(3661), "1:01:01"); assert.equal(formatTime(7325), "2:02:05"); }); test("floors fractional seconds", () => { assert.equal(formatTime(60.9), "1:00"); }); }); describe("parseTimestampedTranscript", () => { test("parses [M:SS] format", () => { const entries = parseTimestampedTranscript( "[0:00] Welcome\n[0:05] Today we're talking about\n[0:30] Topic two" ); assert.equal(entries.length, 3); assert.deepEqual(entries[0], { text: "Welcome", offset: 0, duration: 5 }); assert.equal(entries[1].offset, 5); assert.equal(entries[1].duration, 25); // Last entry gets default 15s duration assert.equal(entries[2].offset, 30); assert.equal(entries[2].duration, 15); }); test("parses H:MM:SS format", () => { const entries = parseTimestampedTranscript("[1:02:30] Hello"); assert.equal(entries[0].offset, 1 * 3600 + 2 * 60 + 30); }); test("parses (M:SS) format", () => { const entries = parseTimestampedTranscript("(0:00) Welcome"); assert.equal(entries.length, 1); assert.equal(entries[0].text, "Welcome"); }); test("strips markdown bold markers", () => { const entries = parseTimestampedTranscript("**[0:00]** Hello world"); assert.equal(entries[0].text, "Hello world"); }); test("ignores blank lines", () => { const entries = parseTimestampedTranscript("\n[0:00] one\n\n[0:05] two\n"); assert.equal(entries.length, 2); }); test("returns empty array for unparseable input", () => { assert.deepEqual(parseTimestampedTranscript(""), []); assert.deepEqual(parseTimestampedTranscript("no timestamps here"), []); }); }); describe("safeText", () => { test("returns .text when accessible", () => { assert.equal(safeText({ text: "hello" }), "hello"); }); test("falls back to candidates[0].content.parts when .text throws", () => { const fake = { get text() { throw new Error("nope"); }, candidates: [{ content: { parts: [{ text: "from " }, { text: "parts" }] } }], }; assert.equal(safeText(fake), "from parts"); }); test("returns empty string when neither path yields text", () => { assert.equal(safeText({}), ""); assert.equal(safeText({ candidates: [] }), ""); }); test("does not throw on null / weird shapes", () => { assert.equal(safeText({ candidates: [{ content: null }] }), ""); }); }); describe("retryGemini", () => { test("returns successful result on first try", async () => { const result = await retryGemini(async () => "ok", { retries: 3, delayMs: 1 }); assert.equal(result, "ok"); }); test("retries on 503 and eventually succeeds", async () => { let attempts = 0; const result = await retryGemini( async () => { attempts++; if (attempts < 3) { const err = new Error("temporary"); err.status = 503; throw err; } return "ok"; }, { retries: 5, delayMs: 1 } ); assert.equal(result, "ok"); assert.equal(attempts, 3); }); test("retries on network error patterns", async () => { let attempts = 0; const result = await retryGemini( async () => { attempts++; if (attempts < 2) throw new Error("fetch failed"); return "ok"; }, { retries: 3, delayMs: 1 } ); assert.equal(result, "ok"); assert.equal(attempts, 2); }); test("does NOT retry on non-retryable errors", async () => { let attempts = 0; await assert.rejects( retryGemini( async () => { attempts++; throw new Error("bad request: invalid argument"); }, { retries: 5, delayMs: 1 } ) ); assert.equal(attempts, 1); }); test("throws after exhausting retries", async () => { let attempts = 0; await assert.rejects( retryGemini( async () => { attempts++; const err = new Error("still 503"); err.status = 503; throw err; }, { retries: 3, delayMs: 1 } ) ); assert.equal(attempts, 3); }); test("calls log function on retries", async () => { const logs = []; await retryGemini( async () => { const err = new Error("503"); err.status = 503; throw err; }, { retries: 2, delayMs: 1, label: "test", log: (msg) => logs.push(msg) } ).catch(() => {}); // retries: 2 → the loop logs twice: the "failed, retrying in …s" notice // before attempt 2's wait, then "Retrying… (attempt 2/2)" at the top of // attempt 2. (The test previously expected 1, written before the // top-of-attempt retry line existed.) assert.equal(logs.length, 2); assert.match(logs[0], /test/); assert.match(logs[0], /retrying/); }); }); describe("sendEvent", () => { test("writes a properly formatted SSE frame", () => { let written = ""; const fakeRes = { write: (chunk) => { written += chunk; } }; sendEvent(fakeRes, "result", { foo: "bar" }); assert.equal(written, 'event: result\ndata: {"foo":"bar"}\n\n'); }); test("handles primitive data", () => { let written = ""; const fakeRes = { write: (chunk) => { written += chunk; } }; sendEvent(fakeRes, "ping", null); assert.equal(written, "event: ping\ndata: null\n\n"); }); });