import { test } from "node:test"; import assert from "node:assert/strict"; import http from "node:http"; import type { AddressInfo } from "node:net"; import { fetchClip, parseSeconds, planWindow, renderFetchClip, validateFetchClipArgs, type FetchClipDeps, type FetchClipRequest, type HttpInit, } from "./fetchClip"; // ─── A scripted editor: no network, no timers ─── // // `respond` answers each request in turn; `sleep` records and advances the // fake clock by exactly what it was asked to wait, so a 90 s wait costs no real // time and the poll count is exact. type Answer = { status: number; body: unknown } | Error; type Call = { url: string; method: string; headers: Record; body?: unknown; signal?: AbortSignal; }; function editor( respond: (call: Call, n: number) => Answer, env: Record = { WORKER_TOKEN: "tok", ARCHILYZER_EDITOR_URL: "http://editor.test/", }, ) { const calls: Call[] = []; const sleeps: number[] = []; let clock = 1_000_000; const deps: FetchClipDeps = { env, fetch: async (url: string, init?: HttpInit) => { const call: Call = { url, method: init?.method ?? "GET", headers: init?.headers ?? {}, body: init?.body !== undefined ? JSON.parse(init.body) : undefined, signal: init?.signal, }; calls.push(call); const a = respond(call, calls.length - 1); if (a instanceof Error) throw a; return { status: a.status, json: async () => a.body }; }, sleep: async (ms: number) => { sleeps.push(ms); clock += ms; }, now: () => clock, }; return { deps, calls, sleeps }; } // Answers in order; running off the end is a test bug, said loudly. function seq(...answers: Answer[]) { return (call: Call, n: number): Answer => { const a = answers[n]; if (!a) throw new Error(`unscripted request #${n}: ${call.method} ${call.url}`); return a; }; } const CLIP_FILE = "/corpus/channels/chan/data/vid/clips/7.00-23.00.mp4"; function windowRequest(extra: Partial> = {}): FetchClipRequest { const v = validateFetchClipArgs({ channel: "chan", video: "vid", start: 10, end: 20, reason: "the quote in the report", report: "rep-1", ...extra, }); assert.ok(v.ok, v.ok ? "" : v.error); return v.request; } function fullRequest(extra: Partial> = {}): FetchClipRequest { const v = validateFetchClipArgs({ channel: "chan", video: "vid", full: true, reason: "summarise the whole stream", report: "rep-1", ...extra, }); assert.ok(v.ok, v.ok ? "" : v.error); return v.request; } const CTX = { channel: "chan", video: "vid" }; // ─── Times and windows ─── test("parseSeconds reads seconds, mm:ss and h:mm:ss", () => { assert.equal(parseSeconds("1:02:03"), 3723); assert.equal(parseSeconds("1:02"), 62); assert.equal(parseSeconds("75:30"), 4530); assert.equal(parseSeconds("0:07.5"), 7.5); assert.equal(parseSeconds("12"), 12); assert.equal(parseSeconds(" 12.25 "), 12.25); assert.equal(parseSeconds(12.5), 12.5); assert.equal(parseSeconds(0), 0); for (const bad of ["abc", "1:60", "1:2:3:4", "1:60:00", "-3", "", -3, Number.NaN, null, undefined, {}]) { assert.equal(parseSeconds(bad), null, `${String(bad)} is not a time`); } }); test("planWindow pads, rounds to the editor's two decimals, and clamps at 0", () => { assert.deepEqual(planWindow({ video: "vid", start: 10, end: 20, pad: 3 }), { from: 7, to: 23 }); // 7.004 → "7.00", 23.006 → "23.01": the name IS the window. assert.deepEqual( planWindow({ video: "vid", start: 10.004, end: 20.006, pad: 3 }), { from: 7, to: 23.01 }, ); assert.deepEqual(planWindow({ video: "vid", start: 1, end: 5, pad: 3 }), { from: 0, to: 8 }); assert.deepEqual(planWindow({ video: "vid", start: 1, end: 5, pad: 0 }), { from: 1, to: 5 }); }); test("planWindow caps the window at 900 s AFTER padding", () => { // 3 → 897 padded is exactly 0 → 900: allowed. assert.deepEqual(planWindow({ video: "v", start: 3, end: 897, pad: 3 }), { from: 0, to: 900 }); // 895 s of citation is 901 s once padded: refused, with the padded numbers. assert.deepEqual(planWindow({ video: "v", start: 10, end: 905, pad: 3 }), { error: "fetch_clip: the window 7.00–908.00 is 901s; the editor fetches at most " + "900s per window — cite a narrower span", }); }); test("planWindow refuses a bad id and an inverted span, in the plan's words", () => { for (const video of ["..", ".", "a/b", "a b", ""]) { assert.deepEqual(planWindow({ video, start: 1, end: 2, pad: 3 }), { error: `fetch_clip: video "${video}" must match /^[\\w.-]+$/`, }); } assert.deepEqual(planWindow({ video: "v", start: 20, end: 10, pad: 3 }), { error: "fetch_clip: start (20) must be less than end (10)", }); assert.deepEqual(planWindow({ video: "v", start: 10, end: 10, pad: 3 }), { error: "fetch_clip: start (10) must be less than end (10)", }); }); // ─── Arguments ─── test("argument errors say what to fix, before any HTTP", () => { const err = (args: Record) => { const v = validateFetchClipArgs(args); assert.equal(v.ok, false); return v.ok ? "" : v.error; }; const base = { channel: "chan", video: "vid", start: 10, end: 20, reason: "why" }; assert.equal( err({ ...base, channel: undefined }), "fetch_clip: channel is required (the channel slug)", ); assert.equal( err({ ...base, video: " " }), "fetch_clip: video is required (the archive's video id)", ); assert.equal(err({ ...base, video: "../x" }), 'fetch_clip: video "../x" must match /^[\\w.-]+$/'); assert.equal( err({ ...base, start: "soon" }), 'fetch_clip: start "soon" is not a time (use seconds, mm:ss or h:mm:ss)', ); assert.equal( err({ ...base, end: "1:99" }), 'fetch_clip: end "1:99" is not a time (use seconds, mm:ss or h:mm:ss)', ); assert.equal(err({ ...base, start: "0:20", end: "0:10" }), "fetch_clip: start (20) must be less than end (10)"); assert.equal( err({ ...base, reason: " " }), "fetch_clip: reason is required — one line saying why these seconds are " + "needed (it is stored beside the file)", ); assert.match(err({ ...base, pad: -1 }), /^fetch_clip: pad "-1" must be a finite number/); assert.match(err({ ...base, pad: "3" }), /^fetch_clip: pad "3" must be a finite number/); assert.match(err({ ...base, start: undefined }), /^fetch_clip: start is required/); }); test("wait_seconds is clamped to [0, 300] and defaults to 90", () => { const wait = (w: unknown) => { const v = validateFetchClipArgs({ job: "j1", wait_seconds: w }); assert.ok(v.ok); return v.request.waitSeconds; }; assert.equal(wait(undefined), 90); assert.equal(wait(500), 300); assert.equal(wait(-5), 0); assert.equal(wait(12), 12); assert.equal(wait("30"), 90); }); test("a job alone is a whole request; everything else is then ignored", () => { const v = validateFetchClipArgs({ job: " j7 ", video: "../bad", start: "nope" }); assert.deepEqual(v, { ok: true, request: { job: "j7", waitSeconds: 90 } }); }); test("full mode ignores start/end/pad entirely — not required, not validated", () => { const v = validateFetchClipArgs({ channel: "chan", video: "vid", full: true, start: "not a time", end: -4, pad: -1, reason: "why", }); assert.ok(v.ok, v.ok ? "" : v.error); assert.deepEqual(v.request, { target: { kind: "full", channel: "chan", video: "vid" }, reason: "why", report: undefined, waitSeconds: 90, }); // …but a reason is still required. const noReason = validateFetchClipArgs({ channel: "chan", video: "vid", full: true }); assert.equal(noReason.ok, false); }); // ─── The POST ─── test("the POST carries requestedBy mcp, the report as manifest, and a reason cut to 400", async () => { const { deps, calls } = editor( seq({ status: 200, body: { cached: true, file: CLIP_FILE, from: 7, to: 23, bytes: 10, provenance: null } }), ); const long = "x".repeat(500); const req = windowRequest({ reason: long }); if ("target" in req && req.target.kind === "window") { req.target.webpageUrl = "https://www.youtube.com/watch?v=vid"; } await fetchClip(req, deps); assert.equal(calls.length, 1); assert.equal(calls[0].method, "POST"); // The trailing slash on ARCHILYZER_EDITOR_URL does not double up. assert.equal(calls[0].url, "http://editor.test/api/media/fetch-window"); assert.equal(calls[0].headers.authorization, "Bearer tok"); assert.deepEqual(calls[0].body, { channelSlug: "chan", videoId: "vid", webpageUrl: "https://www.youtube.com/watch?v=vid", from: 7, to: 23, pad: 3, requestedBy: "mcp", manifest: "rep-1", reason: "x".repeat(400), }); }); test("no editor configured is an outcome, and nothing is sent", async () => { for (const env of [{}, { ARCHILYZER_EDITOR_URL: "http://localhost:3001" }, { WORKER_TOKEN: " " }]) { const { deps, calls } = editor(seq(), env); const outcome = await fetchClip(windowRequest(), deps); assert.deepEqual(outcome, { kind: "no_editor" }); assert.equal(calls.length, 0); const r = renderFetchClip(outcome); assert.equal(r.isError, true); assert.match(r.text, /^fetch_clip: no editor configured\./); assert.match(r.text, /ARCHILYZER_EDITOR_URL/); assert.match(r.text, /WORKER_TOKEN/); assert.match(r.text, /no-editor fallback/); } }); test("the editor URL defaults to localhost:3001", async () => { const { deps, calls } = editor( seq({ status: 200, body: { cached: true, file: CLIP_FILE, from: 7, to: 23, bytes: 1 } }), { WORKER_TOKEN: "tok" }, ); await fetchClip(windowRequest(), deps); assert.equal(calls[0].url, "http://localhost:3001/api/media/fetch-window"); }); // ─── Answers ─── test("200 cached: a WIDER window is named as such, with its own span", async () => { const wide = "/corpus/channels/chan/data/vid/clips/0.00-60.00.mp4"; const { deps } = editor( seq({ status: 200, body: { cached: true, file: wide, from: 0, to: 60, bytes: 123456, provenance: { requestedBy: "umtool" } }, }), ); const outcome = await fetchClip(windowRequest(), deps); const r = renderFetchClip(outcome, CTX); assert.equal(r.isError, false); assert.equal( r.text, [ "Already on disk — a WIDER cached window that contains 7.00–23.00: 0.00–60.00.", "", `file: ${wide}`, "window: 0.00–60.00 (60s)", "bytes: 123456", "requested by umtool", "This path is a read-only corpus artifact: play or copy it, never move, " + "edit or delete it. The editor prunes clips by age (evict-clips); " + "provenance sits beside it as 0.00-60.00.json.", ].join("\n"), ); }); test("200 cached: the exact window (within 0.02 s) says so", async () => { const { deps } = editor( seq({ status: 200, body: { cached: true, file: CLIP_FILE, from: 7.01, to: 23, bytes: 5, provenance: null } }), ); const r = renderFetchClip(await fetchClip(windowRequest(), deps), CTX); assert.match(r.text, /^Already on disk — the exact window: 7\.01–23\.00\./); assert.ok(!/requested by/.test(r.text)); }); test("202 → running → done: the poll's file, span and bytes", async () => { const { deps, calls, sleeps } = editor( seq( { status: 202, body: { cached: false, jobId: "j1", file: CLIP_FILE, from: 7, to: 23 } }, { status: 200, body: { status: "running", jobId: "j1" } }, { status: 200, body: { status: "done", jobId: "j1", file: CLIP_FILE, from: 7, to: 23, bytes: 4096 } }, ), ); const outcome = await fetchClip(windowRequest(), deps); assert.equal(outcome.kind, "fetched"); assert.deepEqual(calls.map((c) => c.method), ["POST", "GET", "GET"]); assert.equal(calls[1].url, "http://editor.test/api/media/fetch-window/j1"); assert.equal(calls[1].headers.authorization, "Bearer tok"); assert.deepEqual(sleeps, [1000, 1000]); const r = renderFetchClip(outcome, CTX); assert.equal(r.isError, false); assert.match(r.text, /^Fetched 7\.00–23\.00 of chan\/vid \(job j1, 2s waited\)\.\n\n/); assert.match(r.text, new RegExp(`\\nfile: ${CLIP_FILE.replace(/\./g, "\\.")}\\n`)); assert.match(r.text, /\nwindow: 7\.00–23\.00 \(16s\)\n/); assert.match(r.text, /\nbytes: 4096\n/); assert.match(r.text, /provenance sits beside it as 7\.00-23\.00\.json\.$/); }); test("202 → failed: the job's log tail comes back", async () => { const { deps } = editor( seq( { status: 202, body: { cached: false, jobId: "j1", file: CLIP_FILE, from: 7, to: 23 } }, { status: 200, body: { status: "failed", jobId: "j1", error: "ERROR: [youtube] vid: Video unavailable" } }, ), ); const r = renderFetchClip(await fetchClip(windowRequest(), deps), CTX); assert.equal(r.isError, true); assert.equal(r.text, "Editor job j1 failed.\n\nlog tail:\nERROR: [youtube] vid: Video unavailable"); }); test("the wait runs out → queued with the job id, without real timers", async () => { const { deps, calls, sleeps } = editor((call, n) => n === 0 ? { status: 202, body: { cached: false, jobId: "j1", file: CLIP_FILE, from: 7, to: 23 } } : { status: 200, body: { status: "running", jobId: "j1" } }, ); const outcome = await fetchClip(windowRequest({ wait_seconds: 3 }), deps); assert.deepEqual(outcome, { kind: "queued", jobId: "j1", status: "running", waited: 3 }); assert.deepEqual(sleeps, [1000, 1000, 1000]); assert.equal(calls.length, 4); const r = renderFetchClip(outcome, CTX); assert.equal(r.isError, false, "a wait that runs out is not a failure"); assert.equal( r.text, 'Still running on the editor (job j1, waited 3s). Call fetch_clip again with job: "j1" ' + "to keep waiting — never repeat the original request while it runs (that would queue " + "a second fetch). Nothing is lost: the fetch continues on the editor, and once it has " + "finished the same request finds it cached.", ); }); test("wait_seconds 0 returns the job at once, still queued", async () => { const { deps, calls, sleeps } = editor( seq({ status: 202, body: { cached: false, jobId: "j1", file: CLIP_FILE, from: 7, to: 23 } }), ); const outcome = await fetchClip(windowRequest({ wait_seconds: 0 }), deps); assert.deepEqual(outcome, { kind: "queued", jobId: "j1", status: "queued", waited: 0 }); assert.equal(calls.length, 1); assert.deepEqual(sleeps, []); }); test("resume by job issues no POST, and polls before it sleeps", async () => { const { deps, calls, sleeps } = editor( seq({ status: 200, body: { status: "done", jobId: "j9", file: CLIP_FILE, from: 7, to: 23, bytes: 77 } }), ); const outcome = await fetchClip({ job: "j9", waitSeconds: 90 }, deps); assert.deepEqual(calls.map((c) => `${c.method} ${c.url}`), [ "GET http://editor.test/api/media/fetch-window/j9", ]); assert.deepEqual(sleeps, []); // A resume knows no channel/video; the window comes from the poll. const r = renderFetchClip(outcome); assert.match(r.text, /^Fetched 7\.00–23\.00 \(job j9, 0s waited\)\.\n\n/); assert.match(r.text, /\nwindow: 7\.00–23\.00 \(16s\)\n/); }); test("409: the platform and the seconds left", async () => { const { deps } = editor( seq({ status: 409, body: { error: "youtube is in a rate-limit cooldown (13s remaining).", cooldownMs: 12_500, platform: "youtube" }, }), ); const r = renderFetchClip(await fetchClip(windowRequest(), deps), CTX); assert.equal(r.isError, true); assert.equal( r.text, "The editor is in a youtube rate-limit cooldown — 13s remaining. Wait, then call " + "fetch_clip again. (youtube is in a rate-limit cooldown (13s remaining).)", ); }); test("401 and a disabled 503 are token problems; 503 disabled says the endpoint is off", async () => { const bad = editor(seq({ status: 401, body: { error: "invalid worker token" } })); assert.deepEqual(renderFetchClip(await fetchClip(windowRequest(), bad.deps), CTX), { text: "The editor refused the token (HTTP 401): invalid worker token. WORKER_TOKEN must " + "equal the value the editor runs with (editor/.env).", isError: true, }); const off = editor( seq({ status: 503, body: { error: "worker endpoint disabled (set WORKER_TOKEN to enable)" } }), ); assert.deepEqual(renderFetchClip(await fetchClip(windowRequest(), off.deps), CTX), { text: "The editor refused the token (HTTP 503): worker endpoint disabled (set WORKER_TOKEN " + "to enable). WORKER_TOKEN must equal the value the editor runs with (editor/.env). " + "The editor has no WORKER_TOKEN set, so its fetch endpoint is off.", isError: true, }); }); test("a 503 that is not the token (an unmounted drive) passes through verbatim", async () => { const drive = "Channel media for chan is unreachable: /mnt/platter is not mounted"; const { deps } = editor(seq({ status: 503, body: { error: drive } })); assert.deepEqual(renderFetchClip(await fetchClip(windowRequest(), deps), CTX), { text: `The editor refused (HTTP 503): ${drive}`, isError: true, }); }); test("any other refusal is passed through verbatim", async () => { const { deps } = editor(seq({ status: 507, body: { error: "Low disk space: 2.1 GB free" } })); assert.deepEqual(renderFetchClip(await fetchClip(windowRequest(), deps), CTX), { text: "The editor refused (HTTP 507): Low disk space: 2.1 GB free", isError: true, }); }); test("an unreachable editor is named, with its URL", async () => { const { deps } = editor(seq(new Error("connect ECONNREFUSED 127.0.0.1:3001"))); assert.deepEqual(renderFetchClip(await fetchClip(windowRequest(), deps), CTX), { text: "Could not reach the editor at http://editor.test: connect ECONNREFUSED 127.0.0.1:3001. Is it running?", isError: true, }); }); test("a poll the editor does not know says it may have restarted", async () => { const { deps } = editor(seq({ status: 404, body: { error: "no such job" } })); assert.deepEqual(renderFetchClip(await fetchClip({ job: "j1", waitSeconds: 5 }, deps)), { text: "Polling editor job j1 failed (HTTP 404): no such job; the job is unknown to this " + "editor (restarted? wrong ARCHILYZER_EDITOR_URL?)", isError: true, }); }); // ─── full: true — the whole recording ─── test("full: the POST is exactly {channelSlug, videoId, full, requestedBy, manifest, reason}", async () => { // start/end/pad given anyway: they are ignored, not sent. for (const req of [fullRequest(), fullRequest({ start: 10, end: 20, pad: 5 })]) { const { deps, calls } = editor( seq({ status: 202, body: { cached: false, jobId: "j2", file: null, from: 0, to: 0 } }), ); await fetchClip({ ...req, waitSeconds: 0 } as FetchClipRequest, deps); assert.deepEqual(calls[0].body, { channelSlug: "chan", videoId: "vid", full: true, requestedBy: "mcp", manifest: "rep-1", reason: "summarise the whole stream", }); } }); test("full: 200 cached names the saved-video store, with no window line", async () => { const saved = "/corpus/saved-videos/chan/vid/source.mkv"; const { deps } = editor( seq({ status: 200, body: { cached: true, file: saved, from: 0, to: 0, bytes: 9_000_000, provenance: { requestedBy: "umtool" } }, }), ); const r = renderFetchClip(await fetchClip(fullRequest(), deps), CTX); assert.equal(r.isError, false); assert.equal( r.text, [ "Already on disk — the whole recording of chan/vid.", "", `file: ${saved}`, "bytes: 9000000", "requested by umtool", "This path is a read-only corpus artifact: play or copy it, never move, edit " + "or delete it. It lives in the editor's saved-video store, not in clips/; " + "the editor's keep-videos rule decides how long it stays.", ].join("\n"), ); }); test("full: 202 → done carries the file and bytes, and no window line", async () => { const saved = "/corpus/saved-videos/chan/vid/source.mkv"; const { deps } = editor( seq( { status: 202, body: { cached: false, jobId: "j2", file: null, from: 0, to: 0 } }, { status: 200, body: { status: "done", jobId: "j2", file: saved, bytes: 42 } }, ), ); const r = renderFetchClip(await fetchClip(fullRequest(), deps), CTX); assert.equal(r.isError, false); assert.match(r.text, /^Fetched the whole recording of chan\/vid \(job j2, 1s waited\)\.\n\n/); assert.match(r.text, /\nbytes: 42\n/); assert.ok(!/window:/.test(r.text), r.text); assert.match(r.text, /saved-video store, not in clips\//); }); test("full: a done job that names no file is an error", async () => { const { deps } = editor( seq( { status: 202, body: { cached: false, jobId: "j2", file: null, from: 0, to: 0 } }, { status: 200, body: { status: "done", jobId: "j2" } }, ), ); assert.deepEqual(renderFetchClip(await fetchClip(fullRequest(), deps), CTX), { text: "Editor job j2 finished but named no file; check the video's page in the editor.", isError: true, }); }); test("full: a 404 says a whole recording needs a video the editor knows; a window's does not", async () => { const unknown = "Could not determine the video URL: no metadata.info.json and the playlist does " + "not contain a matching entry."; const full = editor(seq({ status: 404, body: { error: unknown } })); assert.deepEqual(renderFetchClip(await fetchClip(fullRequest(), full.deps), CTX), { text: `The editor refused (HTTP 404): ${unknown} A whole-recording fetch needs a video ` + "the editor already knows (a metadata.info.json or a playlist entry); fetch a " + "window instead, or add the video to the channel first.", isError: true, }); const win = editor(seq({ status: 404, body: { error: "Channel not found" } })); assert.deepEqual(renderFetchClip(await fetchClip(windowRequest(), win.deps), CTX), { text: "The editor refused (HTTP 404): Channel not found", isError: true, }); }); test("a resumed whole-recording job renders as one (no from/to in its answer)", async () => { const saved = "/corpus/saved-videos/chan/vid/source.mkv"; const { deps } = editor( seq({ status: 200, body: { status: "done", jobId: "j2", file: saved, bytes: 42 } }), ); const r = renderFetchClip(await fetchClip({ job: "j2", waitSeconds: 90 }, deps)); assert.match(r.text, /^Fetched the whole recording \(job j2, 0s waited\)\./); assert.ok(!/window:/.test(r.text)); }); test("the editor dropping out WHILE POLLING hands back the job, and says not to repeat the request", async () => { // Full mode is where it matters: a repeated full: true would queue a second // whole-recording download, because the saved-video pointer is only checked // when a request arrives. const { deps, calls } = editor( seq( { status: 202, body: { cached: false, jobId: "j3", file: null, from: 0, to: 0 } }, new Error("socket hang up"), ), ); const outcome = await fetchClip(fullRequest(), deps); assert.deepEqual(calls.map((c) => c.method), ["POST", "GET"]); const r = renderFetchClip(outcome, CTX); assert.deepEqual(r, { text: "Could not reach the editor at http://editor.test while polling job j3: socket hang " + 'up. Is it running? Call fetch_clip again with job: "j3" — do not repeat the original ' + "request, the fetch may still be running.", isError: true, }); // …and the call it names is a resume: one poll, no second POST. const again = editor( seq({ status: 200, body: { status: "done", jobId: "j3", file: "/corpus/saved-videos/x.mkv", bytes: 9 } }), ); const v = validateFetchClipArgs({ job: "j3" }); assert.ok(v.ok); const resumed = renderFetchClip(await fetchClip(v.request, again.deps)); assert.deepEqual(again.calls.map((c) => c.method), ["GET"]); assert.match(resumed.text, /^Fetched the whole recording \(job j3, 0s waited\)\./); }); // ─── Every request is bounded, and a failure says why ─── test("every request — the POST and each poll — carries its own timeout signal", async () => { const { deps, calls } = editor( seq( { status: 202, body: { cached: false, jobId: "j1", file: CLIP_FILE, from: 7, to: 23 } }, { status: 200, body: { status: "running", jobId: "j1" } }, { status: 200, body: { status: "done", jobId: "j1", file: CLIP_FILE, from: 7, to: 23, bytes: 1 } }, ), ); await fetchClip(windowRequest(), deps); assert.equal(calls.length, 3); for (const c of calls) assert.ok(c.signal instanceof AbortSignal, `${c.method} has a signal`); // One signal per request, not one shared deadline for the whole call. assert.equal(new Set(calls.map((c) => c.signal)).size, 3); }); test("Node's bare 'fetch failed' gets its cause appended", async () => { const { deps } = editor( seq(new TypeError("fetch failed", { cause: new Error("connect ECONNREFUSED 127.0.0.1:3001") })), ); assert.equal( renderFetchClip(await fetchClip(windowRequest(), deps), CTX).text, "Could not reach the editor at http://editor.test: fetch failed (connect ECONNREFUSED " + "127.0.0.1:3001). Is it running?", ); }); test("a timed-out POST says so, and that the fetch may have been queued anyway", async () => { const { deps } = editor( seq(new DOMException("The operation was aborted due to timeout", "TimeoutError")), ); assert.deepEqual(renderFetchClip(await fetchClip(fullRequest(), deps), CTX), { text: "Could not reach the editor at http://editor.test: no answer within 15 s (request " + "timed out). Is it running? It may have queued the fetch anyway: check the editor's " + "/jobs page before asking again.", isError: true, }); }); // A real socket, a real fetch: an editor that accepts the connection and never // answers — a POST stuck on a hung mount — must not hold the call. async function silentEditor(answerPost?: unknown) { const server = http.createServer((req, res) => { if (answerPost && req.method === "POST") { res.writeHead(202, { "content-type": "application/json" }); res.end(JSON.stringify(answerPost)); } // Anything else: never answer. }); await new Promise((r) => server.listen(0, "127.0.0.1", r)); const { port } = server.address() as AddressInfo; const close = () => { server.closeAllConnections(); return new Promise((r) => server.close(() => r())); }; return { url: `http://127.0.0.1:${port}`, port, close }; } function realDeps(url: string, requestTimeoutMs: number): FetchClipDeps { let clock = 0; return { env: { WORKER_TOKEN: "tok", ARCHILYZER_EDITOR_URL: url }, fetch: (u, init) => globalThis.fetch(u, init), sleep: async (ms) => { clock += ms; }, now: () => clock, requestTimeoutMs, }; } test("a real editor that never answers the POST is given up on at the request timeout", { timeout: 10_000 }, async () => { const ed = await silentEditor(); try { const t0 = Date.now(); const r = renderFetchClip(await fetchClip(windowRequest(), realDeps(ed.url, 150)), CTX); const took = Date.now() - t0; assert.ok(took < 5000, `gave up after ${took} ms`); assert.equal(r.isError, true); assert.equal( r.text, `Could not reach the editor at ${ed.url}: no answer within 0.15 s (request timed out). ` + "Is it running? It may have queued the fetch anyway: check the editor's /jobs page " + "before asking again.", ); } finally { await ed.close(); } }); test("a real editor that stops answering a poll hands back the job", { timeout: 10_000 }, async () => { const ed = await silentEditor({ cached: false, jobId: "j4", file: null, from: 0, to: 0 }); try { const r = renderFetchClip(await fetchClip(fullRequest(), realDeps(ed.url, 150)), CTX); assert.equal( r.text, `Could not reach the editor at ${ed.url} while polling job j4: no answer within 0.15 s ` + '(request timed out). Is it running? Call fetch_clip again with job: "j4" — do not ' + "repeat the original request, the fetch may still be running.", ); } finally { await ed.close(); } }); test("a real refused connection names its cause", { timeout: 10_000 }, async () => { const ed = await silentEditor(); const url = ed.url; await ed.close(); // nothing listens there now const r = renderFetchClip(await fetchClip(windowRequest(), realDeps(url, 5000)), CTX); assert.match(r.text, /: fetch failed \(connect ECONNREFUSED 127\.0\.0\.1:\d+\)\. Is it running\?$/); }); test("onPoll is told after every poll that finds the job still waiting, and cannot break the fetch", async () => { const { deps } = editor( seq( { status: 202, body: { cached: false, jobId: "j1", file: CLIP_FILE, from: 7, to: 23 } }, { status: 200, body: { status: "queued", jobId: "j1" } }, { status: 200, body: { status: "running", jobId: "j1" } }, { status: 200, body: { status: "done", jobId: "j1", file: CLIP_FILE, from: 7, to: 23, bytes: 1 } }, ), ); const seen: string[] = []; const outcome = await fetchClip(windowRequest(), deps, { onPoll: (p) => { seen.push(`${p.polls} ${p.jobId} ${p.status} ${p.waited}s`); throw new Error("the client went away"); }, }); assert.deepEqual(seen, ["1 j1 queued 1s", "2 j1 running 2s"]); assert.equal(outcome.kind, "fetched"); }); // ─── maxHeight ─── test("maxHeight: a whole number from 144 to 2160 rides on the target; anything else is refused", () => { const base = { channel: "chan", video: "vid", start: 10, end: 20, reason: "why" }; for (const bad of [143, 2161, 720.5, "720", 0]) { const v = validateFetchClipArgs({ ...base, maxHeight: bad }); assert.equal(v.ok, false); assert.equal( v.ok ? "" : v.error, `fetch_clip: maxHeight "${String(bad)}" must be a whole number of pixels from 144 to 2160`, ); } const win = validateFetchClipArgs({ ...base, maxHeight: 480 }); assert.ok(win.ok && "target" in win.request); assert.equal(win.ok && "target" in win.request ? win.request.target.maxHeight : null, 480); const full = validateFetchClipArgs({ ...base, full: true, maxHeight: 1080 }); assert.ok(full.ok && "target" in full.request); assert.deepEqual(full.ok && "target" in full.request ? full.request.target : null, { kind: "full", channel: "chan", video: "vid", maxHeight: 1080, }); // null is "not given", as JSON clients often send it. const none = validateFetchClipArgs({ ...base, maxHeight: null }); assert.ok(none.ok && "target" in none.request); assert.equal(none.ok && "target" in none.request ? "maxHeight" in none.request.target : true, false); }); test("maxHeight is sent in the POST for a window and for a whole recording", async () => { for (const req of [windowRequest({ maxHeight: 480 }), fullRequest({ maxHeight: 720 })]) { const { deps, calls } = editor( seq({ status: 202, body: { cached: false, jobId: "j3", file: null, from: 0, to: 0 } }), ); await fetchClip({ ...req, waitSeconds: 0 } as FetchClipRequest, deps); const body = calls[0].body as Record; assert.equal(body.maxHeight, "target" in req && req.target.kind === "full" ? 720 : 480); } }); test("a cached window's height is shown, and one taller than maxHeight is said to be", async () => { const cached = (height: number) => editor( seq({ status: 200, body: { cached: true, file: CLIP_FILE, from: 7, to: 23, bytes: 10, provenance: null, height }, }), ).deps; const fits = renderFetchClip(await fetchClip(windowRequest({ maxHeight: 720 }), cached(720)), CTX); assert.match(fits.text, /\nheight: 720p\n/); const tall = renderFetchClip(await fetchClip(windowRequest({ maxHeight: 480 }), cached(1080)), CTX); assert.equal(tall.isError, false); assert.match( tall.text, /\nheight: 1080p — taller than the maxHeight 480 asked for; this file was fetched earlier and is served as it is\n/, ); // No cap asked: the height is still given, with no comparison. const plain = renderFetchClip(await fetchClip(windowRequest(), cached(1080)), CTX); assert.match(plain.text, /\nheight: 1080p\n/); // An editor that reports no height: no line at all. const { deps } = editor( seq({ status: 200, body: { cached: true, file: CLIP_FILE, from: 7, to: 23, bytes: 10, provenance: null } }), ); assert.ok(!/height:/.test(renderFetchClip(await fetchClip(windowRequest(), deps), CTX).text)); }); test("full: a finished recording taller than maxHeight says why", async () => { const saved = "/corpus/saved-videos/chan/vid/source.mp4"; const { deps } = editor( seq( { status: 202, body: { cached: false, jobId: "j4", file: null, from: 0, to: 0 } }, { status: 200, body: { status: "done", jobId: "j4", file: saved, bytes: 42, height: 1080 } }, ), ); const r = renderFetchClip(await fetchClip(fullRequest({ maxHeight: 720 }), deps), CTX); assert.equal(r.isError, false); assert.match(r.text, /\nheight: 1080p — taller than the maxHeight 720 asked for; a whole recording is saved at 720p/); });