Files
Keysat cb961cd2d9 Accept YouTube /live/ and /shorts/ URLs in extractVideoId
The video-id regex only matched /watch?v=, youtu.be, /embed/, and /v/
forms, so youtube.com/live/<id> and youtube.com/shorts/<id> links were
rejected with "Invalid YouTube URL". Add both forms to the server and
frontend extractors (kept in sync) and cover them with tests.

Ship as 0.2.159.
2026-06-15 23:29:57 -05:00

246 lines
7.3 KiB
JavaScript

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");
});
});