// Arg parsing for scripts/archilyzer-ops.mjs. No network: parseArgs is pure and // returns the request it WOULD make, which is the whole surface worth pinning — // the routes themselves are covered by editor/e2e/ops-api.spec.ts. // // Run with: pnpm test:scripts import assert from "node:assert/strict"; import test from "node:test"; import { REMOTE_LISTING_RESULT_MARKER, TRANSCRIBE_RESULT_MARKER, editorEnvFiles, followJob, loadEditorEnv, mainWorktreeOf, makeResultCapture, parseArgs, parseDotenv, previewUrlsIn, tokenHint, usage, } from "./archilyzer-ops.mjs"; test("no arguments prints usage", () => { assert.equal(parseArgs([]).help, true); assert.match(usage(), /pnpm ops /); }); test("an action becomes a POST to its route", () => { const p = parseArgs(["sync", "--json", '{"slug":"x","full":true}']); assert.equal(p.method, "POST"); assert.equal(p.path, "/api/ops/sync"); assert.deepEqual(p.body, { slug: "x", full: true }); assert.equal(p.wait, false); }); test("--wait and --json= are both accepted", () => { const p = parseArgs(["metadata-scan", '--json={"slug":"x"}', "--wait"]); assert.equal(p.wait, true); assert.deepEqual(p.body, { slug: "x" }); }); test("an action with no body posts an empty object", () => { const p = parseArgs(["build-index"]); assert.deepEqual(p.body, {}); }); test("an unknown action is refused by name, with the list", () => { const p = parseArgs(["sinc"]); assert.match(p.error, /unknown action "sinc"/); assert.match(p.error, /metadata-scan/); }); test("cut-release posts its body to /api/ops/cut-release and is a known action", () => { const p = parseArgs([ "cut-release", "--json", '{"workspace":"all","version":"next","commit":true}', ]); assert.equal(p.method, "POST"); assert.equal(p.path, "/api/ops/cut-release"); assert.deepEqual(p.body, { workspace: "all", version: "next", commit: true }); // It is on the list an unknown action is refused with, and usage says what // its body takes and where the route exists. assert.match(parseArgs(["cut-releases"]).error, /known: .*\bcut-release\b/); assert.match(usage(), /cut-release turns a changelog's \[Unreleased\]/); assert.match(usage(), /"all" cuts both with ONE version/); assert.match(usage(), /or neither: every check runs before either file is/); assert.match(usage(), /release 10 or later/); }); test("malformed --json is refused before any request", () => { assert.match(parseArgs(["sync", "--json", "{"]).error, /not valid JSON/); assert.match(parseArgs(["sync", "--json", "[1]"]).error, /must be a JSON object/); assert.match(parseArgs(["sync", "--json"]).error, /needs a JSON object/); }); test("positional arguments after an action are refused", () => { // `pnpm ops sync the-quartering` reads naturally and would otherwise be a // silent no-op body, so it is an error that names the fix. assert.match(parseArgs(["sync", "the-quartering"]).error, /--json/); }); test("get channel becomes a GET on the read route", () => { const p = parseArgs(["get", "channel", "the quartering"]); assert.equal(p.method, "GET"); assert.equal(p.path, "/api/ops/channel/the%20quartering"); }); test("get refuses an unknown noun and a missing argument", () => { assert.match(parseArgs(["get", "site", "x"]).error, /unknown noun/); assert.match(parseArgs(["get", "channel"]).error, /needs an argument/); }); test("an unknown flag is refused", () => { assert.match(parseArgs(["sync", "--force"]).error, /unknown flag/); }); test("get channel --counts asks for the live on-disk counts", () => { const p = parseArgs(["get", "channel", "x", "--counts"]); assert.equal(p.path, "/api/ops/channel/x?counts=1"); // Off by default: the counts walk every video directory. assert.equal(parseArgs(["get", "channel", "x"]).path, "/api/ops/channel/x"); }); // --- curated tags ----------------------------------------------------------- test("the tag actions are registered and post to their routes", () => { const defs = parseArgs(["tags", "--json", '{"op":"remove","tag":"x"}']); assert.equal(defs.path, "/api/ops/tags"); const videos = parseArgs([ "tag-videos", "--json", '{"tag":"x","op":"add","videos":[{"slug":"c","id":"v"}]}', ]); assert.equal(videos.path, "/api/ops/tag-videos"); assert.match(usage(), /tags/); }); test("keep-videos is registered and posts to its route", () => { const p = parseArgs([ "keep-videos", "--json", '{"slug":"c","match":"TheQuartering","dryRun":true}', ]); assert.equal(p.path, "/api/ops/keep-videos"); assert.deepEqual(p.body, { slug: "c", match: "TheQuartering", dryRun: true }); // Not a provenance-carrying write: the marker records a note, not a source. assert.equal(p.defaultSource, undefined); }); // A FOUR-THOUSAND-ID BODY IS WRITTEN BY A SCRIPT, NOT TYPED. parseArgs stays // pure — it reports the file it would read, and main() reads it — so this test // needs no filesystem. test("--file names a body file instead of an inline --json", () => { const p = parseArgs(["tag-videos", "--file", "ids.json"]); assert.equal(p.bodyFile, "ids.json"); assert.deepEqual(p.body, {}); assert.equal(parseArgs(["tag-videos", "--file=ids.json"]).bodyFile, "ids.json"); assert.match(parseArgs(["tag-videos", "--file"]).error, /needs a path/); assert.match( parseArgs(["tag-videos", "--file", "a.json", "--json", "{}"]).error, /mutually exclusive/, ); }); // WHO IS ASKING is defaulted for the one action that records provenance, and // for no other — a pin with no answer to "who said so" is what the field exists // to prevent. test("tag-videos carries an agent source; other actions carry none", () => { assert.match(parseArgs(["tag-videos"]).defaultSource, /^agent:/); assert.equal(parseArgs(["sync"]).defaultSource, undefined); assert.equal(parseArgs(["tags"]).defaultSource, undefined); }); test("get tags reads the whole vocabulary, or one tag's assignments", () => { assert.equal(parseArgs(["get", "tags"]).path, "/api/ops/tags"); assert.equal( parseArgs(["get", "tags", "eva-collab"]).path, "/api/ops/tags?tag=eva-collab", ); // The optional argument is per-noun: `get channel` still demands a slug. assert.match(parseArgs(["get", "channel"]).error, /needs an argument/); }); // ─── --wait, and the poll that fails ─── test("--wait-timeout is parsed, and refuses a non-number", () => { assert.equal(parseArgs(["build-index", "--wait", "--wait-timeout", "90"]).waitTimeout, 90); assert.equal(parseArgs(["build-index", "--wait-timeout=90"]).waitTimeout, 90); // Default is NONE: the queue may legitimately hold a job for hours. assert.equal(parseArgs(["build-index", "--wait"]).waitTimeout, null); assert.match(parseArgs(["build-index", "--wait-timeout"]).error, /needs a number/); assert.match(parseArgs(["build-index", "--wait-timeout", "soon"]).error, /positive number/); assert.match(parseArgs(["build-index", "--wait-timeout", "0"]).error, /positive number/); assert.match(usage(), /--wait-timeout/); }); // A fake editor. `log` is the sequence of log-poll outcomes — an Error is // thrown at the caller the way a starved server makes `fetch` reject. `job` is // the sequence of /api/ops/job/ answers: a status string, "gone" (the // editor does not know the id: a JSON 404) or an Error. function fakeEditor({ log = [], job = [] } = {}) { const calls = { log: 0, job: 0, jobHeaders: [] }; const reply = (body, status = 200) => ({ ok: status >= 200 && status < 300, status, json: async () => body, }); const doFetch = async (url, init) => { if (url.includes("/api/ops/job/")) { const next = job[Math.min(calls.job, job.length - 1)]; calls.job++; calls.jobHeaders.push(init?.headers ?? {}); if (next instanceof Error) throw next; if (next === "gone") return reply({ ok: false, error: "no job" }, 404); return reply({ ok: true, job: { id: "j1", status: next } }); } const next = log[Math.min(calls.log, log.length - 1)]; calls.log++; if (next instanceof Error) throw next; return reply(next); }; return { doFetch, calls }; } const follow = (jobId, editor, opts = {}) => followJob(jobId, true, { fetch: editor.doFetch, sleep: async () => {}, baseUrl: "http://editor", headers: { authorization: "Bearer t" }, ...opts, }); test("a poll that rejects does not end the follow", async () => { // THE BUG THIS FIXES: one `fetch failed` while an in-process build-index // starved the server used to abort --wait and report a job that was fine. const editor = fakeEditor({ log: [ { content: "a", nextOffset: 1, status: "running" }, new Error("fetch failed"), new Error("fetch failed"), { content: "b", nextOffset: 2, status: "done" }, ], }); assert.equal(await follow("j1", editor), "done"); // Two failures is below the probe threshold, so it never asked. assert.equal(editor.calls.job, 0); }); test("after three failures it asks the job route whether the job is still there", async () => { const boom = new Error("fetch failed"); // Still running ⇒ keep waiting, and the follow ends on the status it finally // reads rather than on a guess. const waiting = fakeEditor({ log: [boom, boom, boom, { content: "", nextOffset: 0, status: "done" }], job: ["running"], }); assert.equal(await follow("j1", waiting), "done"); assert.equal(waiting.calls.job, 1); // The job route is behind the token, and the probe carries it. assert.deepEqual(waiting.calls.jobHeaders[0], { authorization: "Bearer t" }); // Ended while the log was unreachable ⇒ the record's status is the answer, // even when the last log poll still fails. `failed` is reported, not hidden. const gone = fakeEditor({ log: [boom, boom, boom, boom], job: ["failed"], }); assert.equal(await follow("j1", gone), "failed"); }); test("a job the editor no longer knows, with an unreadable log, refuses to claim an outcome", async () => { const boom = new Error("fetch failed"); const editor = fakeEditor({ log: [boom], job: ["gone"] }); await assert.rejects(follow("j1", editor), /lost contact with job j1/); }); test("'archived' is not an outcome: the sidecar's status is", async () => { // The log route says "archived" for any id the registry forgot (evicted, or // the editor restarted); --wait used to print it and exit 1 for a job that // finished done. const done = fakeEditor({ log: [{ content: "", nextOffset: 0, status: "archived" }], job: ["done"], }); assert.equal(await follow("j1", done), "done"); // A sidecar that says nothing terminal leaves "archived" standing. const unknown = fakeEditor({ log: [{ content: "", nextOffset: 0, status: "archived" }], job: ["archived"], }); assert.equal(await follow("j1", unknown), "archived"); }); test("a waiting job's queue position is said once per change", async () => { const lines = []; const editor = fakeEditor({ log: [ { content: "", nextOffset: 0, status: "queued", queueKey: "youtube", queuePosition: 3 }, { content: "", nextOffset: 0, status: "queued", queueKey: "youtube", queuePosition: 3 }, { content: "", nextOffset: 0, status: "queued", queueKey: "youtube", queuePosition: 1 }, { content: "", nextOffset: 0, status: "running", queueKey: "youtube", queuePosition: 0 }, { content: "", nextOffset: 0, status: "done" }, ], }); const status = await followJob("j1", false, { fetch: editor.doFetch, sleep: async () => {}, baseUrl: "http://editor", say: (l) => lines.push(l), }); assert.equal(status, "done"); assert.deepEqual(lines, [ "[j1] queued — position 3 on youtube", "[j1] queued — position 1 on youtube", ]); }); test("--wait-timeout gives up on a job that never ends", async () => { const editor = fakeEditor({ log: [{ content: "", nextOffset: 0, status: "running" }], }); let clock = 0; await assert.rejects( follow("j1", editor, { timeoutSeconds: 5, now: () => (clock += 3000) }), /--wait-timeout: gave up after 5s/, ); }); // The two build routes used to disagree about the spelling of their one // argument, so the usage text is where a reader finds out they no longer do — // and deploy-site, added later, is in the same sentence rather than a footnote. test("usage says all three site routes take siteId or siteIds", () => { assert.match( usage(), /build-site, build-deploy and deploy-site all take "siteId"[\s\S]*"siteIds"/, ); }); // A preview is the feature an operator reaches for BEFORE a production deploy, // so the usage text has to say what it is, not merely that a key exists. test("usage explains preview deploys and names deploy-site", () => { const u = usage(); assert.match(u, /Actions:.*deploy-site/); assert.match(u, /"preview": ""/); assert.match(u, /https:\/\/\.\.pages\.dev/); assert.match(u, /"main" is refused/); }); test("deploy-site is a POST to its own route", () => { const parsed = parseArgs([ "deploy-site", "--json", '{"siteId":"anilyzer","preview":"tags-exclude"}', ]); assert.equal(parsed.method, "POST"); assert.equal(parsed.path, "/api/ops/deploy-site"); assert.deepEqual(parsed.body, { siteId: "anilyzer", preview: "tags-exclude" }); }); test("previewUrlsIn reads the alias off each job", () => { assert.deepEqual( previewUrlsIn({ ok: true, jobs: [ { siteId: "a", jobId: "j1", previewUrl: "https://p.a.pages.dev" }, { siteId: "b", jobId: "j2", previewUrl: "https://p.b.pages.dev" }, ], }), ["https://p.a.pages.dev", "https://p.b.pages.dev"], ); }); test("previewUrlsIn prints the single-job alias once, not twice", () => { // The route repeats jobs[0].previewUrl at the top level so a one-site caller // never indexes into `jobs`; printing both would read as two previews. assert.deepEqual( previewUrlsIn({ ok: true, jobs: [{ siteId: "a", jobId: "j1", previewUrl: "https://p.a.pages.dev" }], jobId: "j1", previewUrl: "https://p.a.pages.dev", }), ["https://p.a.pages.dev"], ); }); test("previewUrlsIn is empty for a production deploy", () => { assert.deepEqual(previewUrlsIn({ ok: true, jobs: [{ siteId: "a", jobId: "j1" }] }), []); assert.deepEqual(previewUrlsIn({ ok: true, jobId: "j1" }), []); assert.deepEqual(previewUrlsIn(null), []); }); test("a job route answering 'running' keeps the follow going", async () => { // THE OLD BUG, kept pinned: the absent-from-active branch returned whatever // the final poll said, so a stale list plus a recovered log endpoint printed // "running" as the job's result and exited 1 for a job that was fine. const boom = new Error("fetch failed"); const editor = fakeEditor({ log: [ boom, boom, boom, { content: "", nextOffset: 0, status: "running" }, { content: "", nextOffset: 0, status: "done" }, ], job: ["running"], }); assert.equal(await follow("j1", editor), "done"); }); test("an editor that answers nothing at all is given up on, not waited on", async () => { // Without --wait-timeout the null-probe path used to wait forever: the log // never answers AND the job route never answers, so nothing ever resets the // failure count and nothing ever concludes. const boom = new Error("fetch failed"); const editor = fakeEditor({ log: [boom], job: [boom] }); await assert.rejects(follow("j1", editor), /lost contact with the editor/); }); // THE TOKEN FROM THE EDITOR'S ENV FILES (A1). Every session used to write a // wrapper that sourced editor/.env before `pnpm ops`. test("parseDotenv reads KEY=value lines, quotes, export and comments", () => { assert.deepEqual( parseDotenv( [ "# a comment", "", "WORKER_TOKEN=abc123", "export ARCHILYZER_EDITOR_URL='http://localhost:3101'", 'QUOTED="has # hash"', "INLINE=value # trailing comment", "not a line", ].join("\n"), ), { WORKER_TOKEN: "abc123", ARCHILYZER_EDITOR_URL: "http://localhost:3101", QUOTED: "has # hash", INLINE: "value", }, ); }); test("loadEditorEnv fills only what is unset, first file wins, and takes only its two keys", () => { const files = { "/a/editor/.env.local": "WORKER_TOKEN=local\n", "/a/editor/.env": "WORKER_TOKEN=plain\nARCHILYZER_EDITOR_URL=http://x:1\nCLOUDFLARE_API_TOKEN=secret\n", }; const read = (p) => { if (!(p in files)) throw new Error("ENOENT"); return files[p]; }; const env = {}; const sources = loadEditorEnv(env, ["/missing/.env", ...Object.keys(files)], read); assert.deepEqual(env, { WORKER_TOKEN: "local", ARCHILYZER_EDITOR_URL: "http://x:1" }); assert.deepEqual(sources, { WORKER_TOKEN: "/a/editor/.env.local", ARCHILYZER_EDITOR_URL: "/a/editor/.env", }); // A variable already set — even to "" — is an explicit choice and wins. const set = { WORKER_TOKEN: "" }; const s2 = loadEditorEnv(set, Object.keys(files), read); assert.equal(set.WORKER_TOKEN, ""); assert.equal(s2.WORKER_TOKEN, "environment"); }); test("a linked worktree also reads the main worktree's env files, found off disk", () => { const disk = { "/repo/.claude/worktrees/w/.git": "gitdir: /repo/.git/worktrees/w\n", "/repo/.git/worktrees/w/commondir": "../..\n", }; const read = (p) => { if (!(p in disk)) throw new Error("ENOENT"); return disk[p]; }; assert.equal(mainWorktreeOf("/repo/.claude/worktrees/w", read), "/repo"); assert.deepEqual(editorEnvFiles("/repo/.claude/worktrees/w", read), [ "/repo/.claude/worktrees/w/editor/.env.local", "/repo/.claude/worktrees/w/editor/.env", "/repo/editor/.env.local", "/repo/editor/.env", ]); // The main checkout: `.git` is a directory, so reading it as a file fails. assert.equal(mainWorktreeOf("/repo", read), null); assert.deepEqual(editorEnvFiles("/repo", read), [ "/repo/editor/.env.local", "/repo/editor/.env", ]); }); test("the token hint names where the token came from, never its value", () => { assert.match(tokenHint(503, {}, "http://e"), /runs without WORKER_TOKEN/); assert.match(tokenHint(401, {}, "http://e"), /no WORKER_TOKEN in the environment or in /); const h = tokenHint(401, { WORKER_TOKEN: "/r/editor/.env" }, "http://e"); assert.match(h, /from \/r\/editor\/\.env is not the one the editor at http:\/\/e runs with/); }); test("--wait-timeout implies --wait", () => { // A timeout on a wait nobody asked for is a typo with no effect, not a // preference to honour silently. const p = parseArgs(["build-index", "--wait-timeout", "30"]); assert.equal(p.wait, true); assert.equal(p.waitTimeout, 30); }); // The hub's pair, added with its deploy path (release 7). A POST each, with // the body passed through untouched: the routes judge it. test("build-hub and deploy-hub are POSTs to their own routes, named in the usage", () => { const build = parseArgs(["build-hub", "--json", '{"deploy":true}', "--wait"]); assert.equal(build.method, "POST"); assert.equal(build.path, "/api/ops/build-hub"); assert.deepEqual(build.body, { deploy: true }); assert.equal(build.wait, true); const deploy = parseArgs(["deploy-hub"]); assert.equal(deploy.path, "/api/ops/deploy-hub"); assert.deepEqual(deploy.body, {}); assert.match(usage(), /Actions:.*build-hub, deploy-hub/); assert.match(usage(), /build-hub builds the hub into its bundle/); }); // Publishing as stages (release 18): one route, a verb in the body; the status // is a read. test("publish is a POST to its route, and get publish the status, named in the usage", () => { const p = parseArgs(["publish", "--json", '{"verb":"deploy","siteId":"jer","preview":"r18"}', "--wait"]); assert.equal(p.method, "POST"); assert.equal(p.path, "/api/ops/publish"); assert.deepEqual(p.body, { verb: "deploy", siteId: "jer", preview: "r18" }); assert.equal(p.wait, true); const g = parseArgs(["get", "publish"]); assert.equal(g.method, "GET"); assert.equal(g.path, "/api/ops/publish"); assert.match(usage(), /Actions:.*capture-posts, publish, build-index/); assert.match(usage(), /pnpm ops get publish/); assert.match(usage(), /"now" is Publish now/); assert.match(usage(), /publish's aliases, with their old/); }); // The homepage's pair (release 11 slice O4), the hub's twins: a POST each, the // body passed through untouched — the routes judge it. test("build-homepage and deploy-homepage are POSTs to their own routes, named in the usage", () => { const build = parseArgs(["build-homepage", "--json", '{"deploy":true}', "--wait"]); assert.equal(build.method, "POST"); assert.equal(build.path, "/api/ops/build-homepage"); assert.deepEqual(build.body, { deploy: true }); assert.equal(build.wait, true); const deploy = parseArgs(["deploy-homepage", "--json", '{"preview":"refresh"}']); assert.equal(deploy.method, "POST"); assert.equal(deploy.path, "/api/ops/deploy-homepage"); assert.deepEqual(deploy.body, { preview: "refresh" }); assert.match(usage(), /Actions:.*build-homepage, deploy-homepage/); assert.match(usage(), /build-homepage builds the homepage package into homepage\/out/); assert.match(usage(), /project archilyzer \(https:\/\/archilyzer\.pages\.dev\)/); }); // A social channel's post fetch: a POST to its route, the body passed through // untouched — the route and the action judge it ("full" with "older" included). test("fetch-posts is a POST to its route, named in the usage", () => { const p = parseArgs(["fetch-posts", "--json", '{"slug":"example-x","older":true}', "--wait"]); assert.equal(p.method, "POST"); assert.equal(p.path, "/api/ops/fetch-posts"); assert.deepEqual(p.body, { slug: "example-x", older: true }); assert.equal(p.wait, true); assert.match(usage(), /Actions:.*transcribe-bucket, fetch-posts/); assert.match(usage(), /"older": true walks back from the oldest/); assert.match(usage(), /read none\) is refused unless "force": true/); }); // A post capture: a POST to its route, the body passed through untouched — the // route and the action judge it (ids in the archive, both halves off). test("capture-posts is a POST to its route, named in the usage", () => { const p = parseArgs([ "capture-posts", "--json", '{"slug":"example-x","ids":["123"],"media":false}', ]); assert.equal(p.method, "POST"); assert.equal(p.path, "/api/ops/capture-posts"); assert.deepEqual(p.body, { slug: "example-x", ids: ["123"], media: false }); assert.match(usage(), /Actions:.*fetch-posts, capture-posts/); assert.match(usage(), /Every id must be in the channel's posts archive/); assert.match(usage(), /unless\s+"articles": false/); }); // A report site's evidence media: a POST to its route, the body passed through // untouched — the route judges the site id. test("reports-prepare is a POST to its route, named in the usage", () => { const p = parseArgs(["reports-prepare", "--json", '{"siteId":"demo-site"}']); assert.equal(p.method, "POST"); assert.equal(p.path, "/api/ops/reports-prepare"); assert.deepEqual(p.body, { siteId: "demo-site" }); assert.match(usage(), /reports-prepare cuts every clip/); }); test("reports-export is a POST to its route, named in the usage", () => { const p = parseArgs(["reports-export", "--json", '{"siteId":"demo-site","formats":["md"]}']); assert.equal(p.method, "POST"); assert.equal(p.path, "/api/ops/reports-export"); assert.deepEqual(p.body, { siteId: "demo-site", formats: ["md"] }); assert.match(usage(), /reports-export writes each published report/); }); test("persist-videos is a POST to its route, named in the usage", () => { const p = parseArgs([ "persist-videos", "--json", '{"items":[{"slug":"demo-channel","id":"abc123"}],"format":"video_720","dryRun":true}', ]); assert.equal(p.method, "POST"); assert.equal(p.path, "/api/ops/persist-videos"); assert.deepEqual(p.body, { items: [{ slug: "demo-channel", id: "abc123" }], format: "video_720", dryRun: true, }); assert.equal(p.defaultSource, undefined); // A long list arrives from a file. assert.equal(parseArgs(["persist-videos", "--file", "list.json"]).bodyFile, "list.json"); assert.match(usage(), /persist-videos/); }); test("fetch-windows is a POST to its route, named in the usage", () => { const p = parseArgs(["fetch-windows", "--json", '{"siteId":"demo-site","dryRun":true}']); assert.equal(p.method, "POST"); assert.equal(p.path, "/api/ops/fetch-windows"); assert.deepEqual(p.body, { siteId: "demo-site", dryRun: true }); assert.equal(parseArgs(["fetch-windows", "--file", "windows.json"]).bodyFile, "windows.json"); assert.match(usage(), /fetch-windows fetches clip windows/); }); test("feed-metadata posts {slug, dryRun} to /api/ops/feed-metadata", () => { const p = parseArgs(["feed-metadata", "--json", '{"slug":"demo-channel","dryRun":true}']); assert.equal(p.method, "POST"); assert.equal(p.path, "/api/ops/feed-metadata"); assert.deepEqual(p.body, { slug: "demo-channel", dryRun: true }); }); // One video's metadata re-read (the video page's "Refresh metadata"): a POST // with the body passed through untouched — the route judges it. test("refresh-metadata is a POST to its route, named in the usage", () => { const p = parseArgs([ "refresh-metadata", "--json", '{"slug":"c","id":"H64QQZuw-aA"}', "--wait", ]); assert.equal(p.method, "POST"); assert.equal(p.path, "/api/ops/refresh-metadata"); assert.deepEqual(p.body, { slug: "c", id: "H64QQZuw-aA" }); assert.equal(p.wait, true); assert.match(usage(), /Actions:.*metadata-scan, refresh-metadata/); assert.match(usage(), /refresh-metadata re-reads ONE video/); }); // --- transcribe: a job whose log carries a RESULT --------------------------- test("transcribe posts its body and carries the result marker", () => { const p = parseArgs([ "transcribe", "--json", '{"path":"/abs/clip.mp4","start":120,"end":150}', "--wait", ]); assert.equal(p.method, "POST"); assert.equal(p.path, "/api/ops/transcribe"); assert.deepEqual(p.body, { path: "/abs/clip.mp4", start: 120, end: 150 }); assert.equal(p.wait, true); assert.equal(p.resultMarker, TRANSCRIBE_RESULT_MARKER); // No other action carries one. assert.equal(parseArgs(["sync", "--json", '{"slug":"x"}']).resultMarker, undefined); assert.match(usage(), /transcribe runs ONE local file through a local transcription worker/); assert.match(usage(), /it is printed on stdout/); }); test("the result line is taken out of the log, across chunk boundaries", () => { const result = { cues: [{ start: 120.5, end: 121, text: "hi" }], text: "hi" }; const line = `${TRANSCRIBE_RESULT_MARKER}${JSON.stringify(result)}`; const cap = makeResultCapture(TRANSCRIBE_RESULT_MARKER); let echoed = ""; // The log arrives in polls that split lines anywhere — the result line too. const log = `Transcribe /abs/clip.mp4\nprogress 50%\n${line}\nafter\n`; for (const chunk of [log.slice(0, 7), log.slice(7, 40), log.slice(40, 60), log.slice(60)]) { echoed += cap.feed(chunk); } const { echo, result: got } = cap.finish(); echoed += echo; assert.deepEqual(got, result); assert.equal(echoed, "Transcribe /abs/clip.mp4\nprogress 50%\nafter\n"); }); test("a log with no result line captures null and echoes everything", () => { const cap = makeResultCapture(TRANSCRIBE_RESULT_MARKER); const echoed = cap.feed("one\ntwo"); const { echo, result } = cap.finish(); assert.equal(result, null); assert.equal(echoed + echo, "one\ntwo"); }); test("followJob hands each chunk to onContent", async () => { const seen = []; const editor = fakeEditor({ log: [ { content: "a\n", nextOffset: 2, status: "running" }, { content: "b\n", nextOffset: 4, status: "done" }, ], }); const status = await follow("j1", editor, { onContent: (c) => (seen.push(c), ""), }); assert.equal(status, "done"); assert.deepEqual(seen, ["a\n", "b\n"]); }); // JOBS OVER OPS (A2): the /jobs list and its row buttons, without a browser. test("get job reads one job, --tail adds its log's last lines", () => { assert.equal(parseArgs(["get", "job", "01ABC"]).path, "/api/ops/job/01ABC"); assert.equal(parseArgs(["get", "job", "01ABC", "--tail"]).path, "/api/ops/job/01ABC?tail=40"); assert.equal(parseArgs(["get", "job", "01ABC", "--tail", "5"]).path, "/api/ops/job/01ABC?tail=5"); assert.equal(parseArgs(["get", "job", "--tail=7", "01ABC"]).path, "/api/ops/job/01ABC?tail=7"); assert.match(parseArgs(["get", "job"]).error, /needs an argument/); assert.match(parseArgs(["get", "job", "x", "--tail=0"]).error, /--tail takes/); }); test("get jobs carries its filters as a query, and refuses one it does not take", () => { assert.equal(parseArgs(["get", "jobs"]).path, "/api/ops/jobs"); assert.equal( parseArgs(["get", "jobs", "--failed", "--kind", "sync", "--slug=demo", "--limit", "5"]).path, "/api/ops/jobs?failed=1&kind=sync&slug=demo&limit=5", ); assert.equal(parseArgs(["get", "jobs", "--active"]).path, "/api/ops/jobs?active=1"); assert.match(parseArgs(["get", "jobs", "--tail"]).error, /get jobs does not take --tail/); assert.match(parseArgs(["get", "channel", "x", "--active"]).error, /does not take --active/); assert.match(parseArgs(["get", "jobs", "--limit", "0"]).error, /--limit needs a whole number/); assert.match(parseArgs(["get", "jobs", "--kind"]).error, /--kind needs a value/); // A read's flag on an action is a mistake, not a silent no-op. assert.match(parseArgs(["sync", "--failed"]).error, /belongs to a read/); }); test("job posts the verb and the ids to /api/ops/job", () => { const p = parseArgs(["job", "cancel", "A1", "B2", "--wait"]); assert.equal(p.method, "POST"); assert.equal(p.path, "/api/ops/job"); assert.deepEqual(p.body, { verb: "cancel", ids: ["A1", "B2"] }); assert.equal(p.wait, true); assert.deepEqual(parseArgs(["job", "retry-failed"]).body, { verb: "retry-failed" }); for (const verb of ["drain", "promote", "force-release", "retry"]) { assert.deepEqual(parseArgs(["job", verb, "A1"]).body, { verb, ids: ["A1"] }); } assert.match(parseArgs(["job", "kill", "A1"]).error, /unknown verb "kill"/); assert.match(parseArgs(["job", "cancel"]).error, /needs one or more job ids/); assert.match(parseArgs(["job", "retry-failed", "A1"]).error, /takes no ids/); assert.match(parseArgs(["job", "cancel", "A1", "--json", "{}"]).error, /not a body/); }); test("job wait sends nothing and follows the ids", () => { const p = parseArgs(["job", "wait", "A1", "B2", "--wait-timeout", "60"]); assert.deepEqual(p.waitFor, ["A1", "B2"]); assert.equal(p.method, undefined); assert.equal(p.waitTimeout, 60); }); test("usage names the job reads and verbs", () => { assert.match(usage(), /pnpm ops get job \[--tail/); assert.match(usage(), /pnpm ops get jobs \[--active \| --failed\]/); assert.match(usage(), /job \.\.\. is a \/jobs row's button/); }); // THE READ SIDE (A3). test("the read-side nouns are GETs on their routes, named in the usage", () => { assert.equal(parseArgs(["get", "settings"]).path, "/api/ops/settings"); assert.equal(parseArgs(["get", "settings", "autoQueue"]).path, "/api/ops/settings?key=autoQueue"); for (const noun of ["storage", "sites", "workers", "auto-queue", "scheduler"]) { const p = parseArgs(["get", noun]); assert.equal(p.method, "GET", noun); assert.equal(p.path, `/api/ops/${noun}`, noun); } assert.equal(parseArgs(["get", "cleanup", "the-quartering"]).path, "/api/ops/cleanup/the-quartering"); assert.match(parseArgs(["get", "cleanup"]).error, /needs an argument/); assert.match(usage(), /pnpm ops get settings \[\]/); assert.match(usage(), /get cleanup is one channel's \/cleanup row/); }); // ARCHIVAL WRITES (A4). test("the archival writes are POSTs to their routes, each named in the usage", () => { for (const action of [ "settings", "clear-platform-hold", "workers", "transcribe-one", "delete-file", "do-not-clean", "cleanup", ]) { const p = parseArgs([action, "--json", "{}"]); assert.equal(p.method, "POST", action); assert.equal(p.path, `/api/ops/${action}`, action); assert.match(usage(), new RegExp(`^${action} `, "m"), action); } // `get settings` and `settings` are two different requests on one route. assert.equal(parseArgs(["get", "settings"]).method, "GET"); assert.match(usage(), /"dryRun": true answers each channel's preview/); assert.match(usage(), /"publish", "enabled"\?, "held"\?, "action"\?/); }); test("get remote-listing posts the job, waits, and carries the result marker", () => { const r = parseArgs(["get", "remote-listing", "demo-odysee"]); assert.equal(r.method, "POST"); assert.equal(r.path, "/api/ops/remote-listing"); assert.deepEqual(r.body, { slug: "demo-odysee" }); assert.equal(r.wait, true); assert.equal(r.resultMarker, REMOTE_LISTING_RESULT_MARKER); assert.equal(REMOTE_LISTING_RESULT_MARKER, "@@remote-listing "); assert.match(parseArgs(["get", "remote-listing"]).error, /needs an argument/); assert.match(parseArgs(["get", "remote-listing", "x", "--counts"]).error, /does not take --counts/); assert.equal(parseArgs(["get", "remote-listing", "x", "--wait-timeout", "60"]).waitTimeout, 60); // The action form, waited on, prints the result too. const a = parseArgs(["remote-listing", "--json", '{"slug":"x"}', "--wait"]); assert.equal(a.path, "/api/ops/remote-listing"); assert.equal(a.resultMarker, REMOTE_LISTING_RESULT_MARKER); }); test("usage documents import-archive-org's three shapes and the remote listing", () => { const u = usage(); assert.match(u, /import-archive-org imports archive\.org media/); assert.match(u, /"query": ""/); assert.match(u, /RESTRICTED/); assert.match(u, /remote-listing lists an Odysee or BitChute channel/); }); test("build-cues is an action; get transcript reads one video's cues, --slug naming the channel", () => { const b = parseArgs(["build-cues", "--json", '{"slug":"x","ids":["a"]}']); assert.equal(b.path, "/api/ops/build-cues"); assert.deepEqual(b.body, { slug: "x", ids: ["a"] }); const t = parseArgs(["get", "transcript", "vid1", "--slug", "demo"]); assert.equal(t.method, "GET"); assert.equal(t.path, "/api/ops/transcript?id=vid1&slug=demo"); assert.equal(parseArgs(["get", "transcript", "vid1"]).path, "/api/ops/transcript?id=vid1"); assert.match(parseArgs(["get", "transcript"]).error, /needs an argument/); assert.match(parseArgs(["get", "transcript", "v", "--counts"]).error, /does not take --counts/); assert.match(usage(), /build-cues writes each video's transcript\.cues\.json/); }); test("get coverage reads one channel's coverage", () => { const r = parseArgs(["get", "coverage", "demo"]); assert.equal(r.method, "GET"); assert.equal(r.path, "/api/ops/coverage?slug=demo"); assert.match(parseArgs(["get", "coverage"]).error, /needs an argument/); assert.match(usage(), /get coverage is what a channel holds by date/); assert.match(usage(), /recordedDateTitlePattern/); });