import { test } from "node:test"; import assert from "node:assert/strict"; import { existsSync, readdirSync, readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { parseArgv } from "./_parseFlags"; import { argumentProblem, booleanFlags, passthroughCommand, resolveCommand, runCli, usage, type Command, } from "./_cli"; import { COMMANDS } from "./archilyzer"; import { cutMain, formatAllLine, formatCutLines, formatShowLine, parseCutArgs, parseShowArgs, showMain, } from "./release"; // Run with: // pnpm --filter yt-dlp-transcript-common test // // The archilyzer CLI's parser and lookup — the rules every row obeys. The // command table itself (archilyzer.ts) is imported only at the end, for the // `release` rows, which the release runbook's step 0 calls by name. function cmd(path: string[], extra: Partial = {}): Command { return { path, usage: `the ${path.join(" ")} command`, run: async () => 0, ...extra, }; } test("parseArgv keeps positionals in order and reads both flag spellings", () => { assert.deepEqual( parseArgv(["deploy", "site", "anilyzer", "--preview", "tags", "--x=1"]), { positionals: ["deploy", "site", "anilyzer"], flags: { preview: "tags", x: "1" } }, ); }); test("a boolean flag never swallows the positional after it", () => { assert.deepEqual(parseArgv(["build", "site", "--nodata", "jeralyzer"], ["nodata"]), { positionals: ["build", "site", "jeralyzer"], flags: { nodata: true }, }); // Without the declaration it would — which is why runCli declares them. assert.deepEqual(parseArgv(["--nodata", "jeralyzer"]).flags, { nodata: "jeralyzer" }); }); test("a lone -- is skipped, so `pnpm run build -- --nodata` still reads the flag", () => { assert.deepEqual(parseArgv(["build", "site", "--", "--nodata"], ["nodata"]), { positionals: ["build", "site"], flags: { nodata: true }, }); }); test("a bare trailing flag and -h are true", () => { assert.deepEqual(parseArgv(["deploy", "--preview"]).flags, { preview: true }); assert.deepEqual(parseArgv(["-h"]).flags, { help: true }); }); test("resolveCommand picks the LONGEST matching path and returns the rest", () => { const table = [cmd(["build"]), cmd(["build", "site"]), cmd(["build", "hub"])]; const hit = resolveCommand(table, ["build", "site", "anilyzer"]); assert.deepEqual(hit?.command.path, ["build", "site"]); assert.deepEqual(hit?.rest, ["anilyzer"]); assert.deepEqual(resolveCommand(table, ["build", "all"])?.command.path, ["build"]); assert.equal(resolveCommand(table, ["deploy"]), null); // Table order does not matter. assert.deepEqual( resolveCommand([...table].reverse(), ["build", "hub"])?.command.path, ["build", "hub"], ); }); test("argumentProblem refuses an unknown flag, a valued boolean, a bare string flag, an extra positional", () => { const c = cmd(["deploy", "site"], { flags: { preview: "string", nodata: "boolean" }, maxPositionals: 1, }); assert.equal(argumentProblem(c, { preview: "x" }, ["a"]), null); assert.equal(argumentProblem(c, { help: true }, []), null); assert.match(argumentProblem(c, { preveiw: "x" }, [])!, /unknown flag --preveiw \(accepts --preview, --nodata\)/); assert.match(argumentProblem(c, { nodata: "yes" }, [])!, /--nodata takes no value/); assert.match(argumentProblem(c, { preview: true }, [])!, /--preview needs a value/); assert.match(argumentProblem(c, {}, ["a", "b"])!, /unexpected argument "b"/); assert.match(argumentProblem(cmd(["index"]), { x: true }, [])!, /it takes none/); }); test("booleanFlags is the union of every boolean flag, plus help", () => { const table = [ cmd(["a"], { flags: { nodata: "boolean", preview: "string" } }), cmd(["b"], { flags: { check: "boolean" } }), ]; assert.deepEqual(booleanFlags(table).sort(), ["check", "help", "nodata"]); }); test("usage lists every command on its own line", () => { const u = usage([cmd(["index"]), cmd(["build", "site"])]); assert.match(u, /^Usage:/); assert.match(u, /archilyzer index\s+the index command/); assert.match(u, /archilyzer build site\s+the build site command/); }); test("runCli runs the command with the rest, and refuses with 2 before running", async () => { const seen: unknown[] = []; const table = [ cmd(["build", "site"], { flags: { nodata: "boolean" }, maxPositionals: 1, run: async (ctx) => { seen.push(ctx.positionals, ctx.flags); return 7; }, }), ]; const quiet = { log: () => {}, error: () => {} }; assert.equal(await runCli(table, ["build", "site", "x", "--nodata"], {}, quiet), 7); assert.deepEqual(seen, [["x"], { nodata: true }]); assert.equal(await runCli(table, ["build", "site", "--bogus"], {}, quiet), 2); assert.equal(await runCli(table, ["nope"], {}, quiet), 2); assert.equal(await runCli(table, [], {}, quiet), 2); assert.equal(await runCli(table, ["--help"], {}, quiet), 0); assert.equal(await runCli(table, ["build", "site", "--help"], {}, quiet), 0); assert.equal(seen.length, 2, "no refused call reached run"); }); // --- archilyzer release (release 10 slice P) -------------------------------- test("release cut and release show are table rows; --commit never swallows a positional", () => { const cut = resolveCommand(COMMANDS, ["release", "cut", "all", "next"]); assert.deepEqual(cut?.command.path, ["release", "cut"]); assert.deepEqual(cut?.rest, ["all", "next"]); assert.deepEqual(resolveCommand(COMMANDS, ["release", "show"])?.command.path, [ "release", "show", ]); const parsed = parseArgv( ["release", "cut", "--commit", "all", "next", "--date", "2026-09-26"], booleanFlags(COMMANDS), ); assert.deepEqual(parsed, { positionals: ["release", "cut", "all", "next"], flags: { commit: true, date: "2026-09-26" }, }); const command = cut!.command; assert.equal(argumentProblem(command, parsed.flags, ["all", "next"]), null); assert.match(argumentProblem(command, { date: true }, [])!, /--date needs a value/); assert.match(argumentProblem(command, { tag: true }, [])!, /unknown flag --tag \(accepts --commit, --date\)/); assert.match(argumentProblem(command, {}, ["all", "next", "x"])!, /unexpected argument "x"/); }); test("usage lists release show and release cut with their arguments", () => { const u = usage(COMMANDS); assert.match(u, /archilyzer release show\s+\[editor\|export\]/); assert.match( u, /archilyzer release cut\s+ \[--commit\] \[--date YYYY-MM-DD\]/, ); }); test("parseCutArgs checks the changelog, the version and the date before anything is read", () => { assert.deepEqual(parseCutArgs(["all", "next"], { commit: true }), { target: "all", version: "next", commit: true, }); assert.deepEqual(parseCutArgs(["editor", "0.10.0"], { date: "2026-09-25" }), { target: "editor", version: "0.10.0", commit: false, date: "2026-09-25", }); const error = (p: string[], f = {}) => { const r = parseCutArgs(p, f); return "error" in r ? r.error : ""; }; assert.match(error([]), /which changelog\? editor, export or all/); assert.match(error(["site", "next"]), /"site" is not editor, export or all/); assert.match(error(["editor"]), /which version\? X\.Y\.Z, next or next-minor/); assert.match(error(["editor", "v1"]), /"v1" is not a valid semver/); assert.match(error(["editor", "next"], { date: "yesterday" }), /"yesterday" is not in YYYY-MM-DD form/); assert.match(error(["editor", "next"], { date: "2026-13-45" }), /"2026-13-45" is not a calendar date/); assert.deepEqual(parseShowArgs([]), ["editor", "export"]); assert.deepEqual(parseShowArgs(["all"]), ["editor", "export"]); assert.deepEqual(parseShowArgs(["export"]), ["export"]); assert.match((parseShowArgs(["hub"]) as { error: string }).error, /"hub" is not editor or export/); }); test("a refused release command exits 2 and prints why, touching nothing", async () => { const errors: string[] = []; const out = { log: () => {}, error: (s: string) => errors.push(s) }; const ctx = (positionals: string[]) => ({ positionals, flags: {}, env: {}, argv: [] }); assert.equal(await cutMain(ctx(["site", "next"]), out), 2); assert.equal(await showMain(ctx(["hub"]), out), 2); assert.equal(errors.length, 2); }); test("release cut prints one line per changelog: the heading and the commit, the refusal, what a failure stopped", () => { assert.deepEqual( formatCutLines({ ok: false, version: "0.10.0", results: [ { ok: true, workspace: "editor", version: "0.10.0", heading: "## [0.10.0] - 2026-09-26", committed: true, commitSha: "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b", }, { ok: false, workspace: "export", error: "Nothing pending to release under [Unreleased]." }, ], notAttempted: [], }).map((l) => l.line), [ "editor: ## [0.10.0] - 2026-09-26 (committed 1a2b3c4d)", "export: failed — Nothing pending to release under [Unreleased].", ], ); assert.deepEqual( formatCutLines({ ok: false, version: "0.9.1", results: [{ ok: false, workspace: "editor", error: "Could not write x: EACCES" }], notAttempted: ["export"], }), [ { line: "editor: failed — Could not write x: EACCES", failed: true }, { line: "export: not cut — stopped at the failure above", failed: true }, ], ); // `all` refused by its preflight: it says nothing was written. assert.deepEqual( formatCutLines({ ok: false, version: "0.9.1", results: [ { ok: false, workspace: "export", error: "Nothing pending to release under [Unreleased]." }, ], notAttempted: ["editor"], untouched: true, }).map((l) => l.line), [ "export: failed — Nothing pending to release under [Unreleased].", "editor: not cut — all cuts both or neither, and nothing was written", ], ); assert.deepEqual( formatCutLines({ ok: true, version: "0.9.1", results: [ { ok: true, workspace: "export", version: "0.9.1", heading: "## [0.9.1] - 2026-09-26", committed: false, }, ], notAttempted: [], }), [{ line: "export: ## [0.9.1] - 2026-09-26 (not committed)", failed: false }], ); }); test("release show says the latest release, its date and what is pending, and what all would cut", () => { const editor = { ok: true as const, workspace: "editor" as const, file: "/r/editor/CHANGELOG.md", latest: { version: "0.9.0", date: "2026-09-26" }, hasUnreleased: true, pending: 8, next: "0.9.1", nextMinor: "0.10.0", }; const exp = { ...editor, workspace: "export" as const, latest: { version: "0.9.2", date: null }, pending: 1, next: "0.9.3", nextMinor: "0.10.0", }; assert.equal( formatShowLine(editor), "editor: latest 0.9.0 (2026-09-26); 8 bullets pending under [Unreleased]; next 0.9.1, next-minor 0.10.0", ); assert.equal( formatShowLine(exp), "export: latest 0.9.2 (undated); 1 bullet pending under [Unreleased]; next 0.9.3, next-minor 0.10.0", ); assert.match(formatShowLine({ ...editor, pending: 0 }), /; nothing pending under \[Unreleased\];/); assert.match(formatShowLine({ ...editor, hasUnreleased: false, pending: 0 }), /; no \[Unreleased\] heading;/); assert.match(formatShowLine({ ...editor, latest: null }), /^editor: no release yet;/); assert.equal( formatShowLine({ ok: false, workspace: "export", file: "/x", error: "Could not read /x: ENOENT" }), "export: Could not read /x: ENOENT", ); // `all` resolves against the HIGHER latest, as cutReleases does. assert.equal(formatAllLine([editor, exp]), "all: next 0.9.3, next-minor 0.10.0"); assert.equal(formatAllLine([editor]), null); }); // --- archilyzer source (release 12 slice R) -------------------------------- test("usage lists source publish, source audit and build homepage's --no-source", () => { const u = usage(COMMANDS); assert.match(u, /archilyzer source publish\s+\[--force\] \[--check\] \[--keep-scratch\] the scrubbed git mirror/); assert.match(u, /archilyzer source audit\s+\[\] the denied-literal gate/); assert.match(u, /archilyzer build homepage\s+\[--no-source\] compose \+ source publish \+ next build/); }); test("source publish takes its three booleans and nothing else; none swallows a word", () => { const hit = resolveCommand(COMMANDS, ["source", "publish"]); assert.deepEqual(hit?.command.path, ["source", "publish"]); const parsed = parseArgv(["source", "publish", "--check", "--force", "--keep-scratch"], booleanFlags(COMMANDS)); assert.deepEqual(parsed, { positionals: ["source", "publish"], flags: { check: true, force: true, "keep-scratch": true }, }); assert.equal(argumentProblem(hit!.command, parsed.flags, []), null); assert.match(argumentProblem(hit!.command, { preview: "x" }, [])!, /unknown flag --preview \(accepts --force, --check, --keep-scratch\)/); assert.match(argumentProblem(hit!.command, {}, ["extra"])!, /unexpected argument "extra"/); }); test("source audit takes one git dir; build homepage takes --no-source only", () => { const audit = resolveCommand(COMMANDS, ["source", "audit", "/tmp/clone/.git"]); assert.deepEqual(audit?.rest, ["/tmp/clone/.git"]); assert.equal(argumentProblem(audit!.command, {}, audit!.rest), null); assert.match(argumentProblem(audit!.command, {}, ["a", "b"])!, /unexpected argument "b"/); const home = resolveCommand(COMMANDS, ["build", "homepage"]); const parsed = parseArgv(["build", "homepage", "--no-source"], booleanFlags(COMMANDS)); assert.deepEqual(parsed, { positionals: ["build", "homepage"], flags: { "no-source": true } }); assert.equal(argumentProblem(home!.command, parsed.flags, []), null); assert.match(argumentProblem(home!.command, { force: true }, [])!, /unknown flag --force \(accepts --no-source\)/); }); // ── passthrough commands (one-core Phase 4 slice 3) ───────────────────────── test("a passthrough command gets every word after its path, verbatim and unchecked", async () => { let seen: { argv: string[]; positionals: string[] } | null = null; const table = [ cmd(["posts", "fetch"], { passthrough: true, run: async (ctx) => { seen = { argv: ctx.argv, positionals: ctx.positionals }; return 7; }, }), cmd(["posts"], { maxPositionals: 1 }), ]; const quiet = { log: () => {}, error: () => {} }; assert.equal( await runCli(table, ["posts", "fetch", "--slug", "x", "--full", "--", "--weird=1"], {}, quiet), 7, ); assert.deepEqual(seen, { argv: ["--slug", "x", "--full", "--", "--weird=1"], positionals: [] }); // `--help` as the first word after the path is the usage line, not the bin's. const logged: string[] = []; assert.equal( await runCli(table, ["posts", "fetch", "--help"], {}, { log: (s) => logged.push(s), error: () => {} }), 0, ); assert.match(logged.join("\n"), /archilyzer posts fetch/); }); test("a passthrough path matches only the LEADING words, never a flag's value", () => { const table = [cmd(["mcp"], { passthrough: true }), cmd(["run"], { maxPositionals: 9 })]; assert.equal(passthroughCommand(table, ["run", "digest", "--lane", "mcp"]), null); assert.deepEqual(passthroughCommand(table, ["mcp", "--local", "x"])?.path, ["mcp"]); assert.equal(passthroughCommand(table, ["--local", "mcp"]), null); }); test("every bin in common/bin is reachable as a subcommand", () => { const here = path.dirname(fileURLToPath(import.meta.url)); const table = readFileSync(path.join(here, "archilyzer.ts"), "utf8"); const reached = new Set([ ...[...table.matchAll(/import\("\.\/([\w-]+)"\)/g)].map((m) => m[1]), ...[...table.matchAll(/script\(\[[^\]]*\],\s*"([\w-]+)\.ts"/g)].map((m) => m[1]), ]); const bins = readdirSync(here) .filter((n) => n.endsWith(".ts") && !n.endsWith(".test.ts") && !n.startsWith("_")) .map((n) => n.slice(0, -3)) .filter((n) => n !== "archilyzer"); assert.deepEqual(bins.filter((b) => !reached.has(b)), []); // And every script row names a file that exists. for (const m of table.matchAll(/script\(\[[^\]]*\],\s*"([\w-]+\.ts)"/g)) { assert.ok(existsSync(path.join(here, m[1])), m[1]); } // No two rows share a path. const paths = COMMANDS.map((c) => c.path.join(" ")); assert.equal(new Set(paths).size, paths.length); }); // ── publishing as stages (release 18 slice S1) ────────────────────────────── test("the stage row: kind and target, the flags the child's argv carries, nothing else", async () => { const { STAGE_FLAGS, stageArgv } = await import("../publish/stages"); const hit = resolveCommand(COMMANDS, ["stage", "deploy-site", "jer"]); assert.deepEqual(hit?.command.path, ["stage"]); assert.deepEqual(hit?.rest, ["deploy-site", "jer"]); assert.deepEqual(hit!.command.flags, { ...STAGE_FLAGS }); // The argv the editor spawns parses through runCli's own parser, cleanly. const argv = stageArgv({ kind: "build-site", target: "_all", runId: "run-1", runner: "docker", force: true, skipArchives: true, indexAfter: 17, }); assert.deepEqual(argv, [ "stage", "build-site", "_all", "--run-id", "run-1", "--runner", "docker", "--force", "--skip-archives", "--index-after", "17", ]); const parsed = parseArgv(argv, booleanFlags(COMMANDS)); assert.deepEqual(parsed.positionals, ["stage", "build-site", "_all"]); assert.equal(argumentProblem(hit!.command, parsed.flags, ["build-site", "_all"]), null); assert.match(argumentProblem(hit!.command, {}, ["a", "b", "c"])!, /unexpected argument "c"/); assert.match(argumentProblem(hit!.command, { nodata: true }, [])!, /unknown flag --nodata/); }); test("the publish rows and their flags (status and now are S3's)", () => { const row = (...p: string[]) => resolveCommand(COMMANDS, p)?.command; assert.deepEqual(row("publish", "index")?.flags ?? {}, {}); assert.deepEqual(row("publish", "build", "jer")?.flags, { runner: "string", force: "boolean", "skip-archives": "boolean", // release 19 A8 "allow-missing-media": "boolean", out: "string", }); assert.equal(row("publish", "build", "all")?.maxPositionals, 1); assert.deepEqual(row("publish", "deploy", "all")?.flags, { preview: "string", to: "string", force: "boolean" }); assert.deepEqual(row("publish", "hub")?.flags, { deploy: "boolean", "deploy-only": "boolean", preview: "string", force: "boolean", }); assert.deepEqual(row("publish", "homepage")?.flags, { deploy: "boolean", "deploy-only": "boolean", preview: "string", to: "string", force: "boolean", }); const parsed = parseArgv(["publish", "hub", "--deploy", "--preview", "r18"], booleanFlags(COMMANDS)); assert.deepEqual(parsed, { positionals: ["publish", "hub"], flags: { deploy: true, preview: "r18" } }); const deploy = row("publish", "deploy")!; assert.match(argumentProblem(deploy, { preveiw: "x" }, ["jer"])!, /unknown flag --preveiw \(accepts --preview, --to, --force\)/); assert.match(argumentProblem(deploy, { to: true }, ["jer"])!, /--to needs a value/); const u = usage(COMMANDS); assert.match(u, /archilyzer publish index\s+update the index/); assert.match(u, /archilyzer publish build\s+ \[--runner local\|docker\|auto\] \[--force\] \[--skip-archives\]/); assert.match(u, /archilyzer publish deploy\s+ \[--preview \] \[--to local\] \[--force\]/); assert.match(u, /archilyzer stage\s+ --run-id /); }); test("build site, build all and deploy site are printed aliases of the publish rows", async () => { const { ALIASES } = await import("./publish"); assert.equal(ALIASES.buildSite("jer", false), "archilyzer publish index && archilyzer publish build jer --force"); assert.equal(ALIASES.buildSite("jer", true), "archilyzer publish build jer --force", "--nodata skips the index"); assert.equal(ALIASES.buildAll, "archilyzer publish index && archilyzer publish build all --runner auto"); assert.equal(ALIASES.deploySite("jer"), "archilyzer publish deploy jer"); assert.equal(ALIASES.deployHub(), "archilyzer publish hub --deploy-only"); assert.equal(ALIASES.deployHomepage("r18"), "archilyzer publish homepage --deploy-only --preview r18"); assert.equal(ALIASES.deploySite("jer", "r18"), "archilyzer publish deploy jer --preview r18"); const row = (...p: string[]) => resolveCommand(COMMANDS, p)!.command; // Same flags as ever: a script that called them still parses. assert.deepEqual(row("build", "site").flags, { nodata: "boolean", "skip-archives": "boolean", "allow-missing-media": "boolean" }); assert.deepEqual(row("build", "all").flags, { "skip-archives": "boolean" }); assert.deepEqual(row("deploy", "site").flags, { preview: "string" }); for (const r of [row("build", "site"), row("build", "all"), row("deploy", "site")]) { assert.match(r.usage, /alias: /); } }); test("publish hub|homepage refuse --deploy with --deploy-only (usage, nothing run)", async () => { const { publishHub, publishHomepage } = await import("./publish"); const lines: string[] = []; const out = { log: (l: string) => lines.push(l), error: (l: string) => lines.push(l) }; assert.equal(await publishHub({ deploy: true, deployOnly: true }, out), 2); assert.equal(await publishHomepage({ deploy: true, deployOnly: true }, out), 2); assert.match(lines.join("\n"), /--deploy-only ships the bundle as built — one of them/); }); test("publish hub / homepage refuse a deploy's flag without --deploy (usage, nothing run)", async () => { const { publishHub, publishHomepage } = await import("./publish"); const errors: string[] = []; const out = { log: () => {}, error: (s: string) => errors.push(s) }; assert.equal(await publishHub({ preview: "r18" }, out), 2); assert.equal(await publishHomepage({ to: "local" }, out), 2); assert.match(errors.join("\n"), /--preview is a deploy — add --deploy/); assert.match(errors.join("\n"), /--preview and --to are a deploy's — add --deploy/); });