import { test } from "node:test"; import assert from "node:assert/strict"; import { parseArgv } from "../bin/_parseFlags"; import type { Paths } from "../lib/paths"; import { builtStamp, indexStamp } from "./__fixtures__/stamps"; import type { BuiltStamp, DeployedFile, DeployRecord } from "./stamps"; import { STAGE_EXIT, stageCommand } from "./stageRun"; import { STAGES, STAGE_FLAGS, STAGE_KINDS, builtCheckedAt, deployKindOf, parseStageArgs, stageArgv, type Freshness, type NeedsInput, type StageRequest, type TargetState, } from "./stages"; // Run with: // pnpm --filter yt-dlp-transcript-common test // // The publish stages' needs() — one case (at least) per row of the plan's // "Stages" table (plans/release-18.md), plus `changedChannels`, `--force` and // "code newer is not stale" — and the child's argv, both ways. function target(over: Partial = {}): TargetState { return { built: null, deployed: null, changedChannels: [], configChangedAt: null, bundleProblem: null, ...over, }; } function input(over: Partial = {}): NeedsInput { return { index: { stamp: indexStamp(), lastIngestDoneAt: null, configChangedAt: null }, sites: { jer: target({ built: builtStamp() }) }, hub: target({ built: builtStamp({ target: "_hub", kind: "hub", inputSig: "hub-1" }) }), homepage: { ...target({ built: builtStamp({ target: "_homepage", kind: "homepage", inputSig: "s1", sourceCommit: "main-1" }), }), mainHead: "main-1", }, ...over, }; } const req = (kind: StageRequest["kind"], tgt: string, over: Partial = {}): StageRequest => ({ kind, target: tgt, runId: "run-1", ...over, }); const needs = (s: NeedsInput, r: StageRequest): Freshness => STAGES[r.kind].needs(s, r); const state = (f: Freshness) => f.state; const reason = (f: Freshness) => (f.state === "fresh" ? "" : f.reason); function deployed(over: Partial = {}, rec: Partial = {}): DeployedFile { const r: DeployRecord = { builtStampId: "b1", builtAt: 3_000, kind: "production", url: null, at: 4_000, liveCheck: null, ...rec, }; return { v: 1, target: "jer", production: r, previews: {}, ...over }; } test("the table: seven stages, each a publish- job on the publish queue", () => { assert.deepEqual(Object.keys(STAGES), [...STAGE_KINDS]); for (const k of STAGE_KINDS) { assert.equal(STAGES[k].kind, k); assert.equal(STAGES[k].jobKind, `publish-${k}`); assert.equal(STAGES[k].queueKey, "publish"); } }); // --- update-index ------------------------------------------------------------- test("update-index: fresh when a stamp exists and nothing is newer than its scan", () => { assert.equal(state(needs(input(), req("update-index", "_index"))), "fresh"); const none = input({ index: { stamp: null, lastIngestDoneAt: null, configChangedAt: null } }); assert.equal(reason(needs(none, req("update-index", "_index"))), "no index stamp yet"); const data = input({ index: { stamp: indexStamp(), lastIngestDoneAt: 1_001, configChangedAt: null } }); assert.equal(reason(needs(data, req("update-index", "_index"))), "new data since the last index"); const older = input({ index: { stamp: indexStamp(), lastIngestDoneAt: 999, configChangedAt: 1_000 } }); assert.equal(state(needs(older, req("update-index", "_index"))), "fresh", "not NEWER than scannedAt"); const cfg = input({ index: { stamp: indexStamp(), lastIngestDoneAt: null, configChangedAt: 5_000 } }); assert.match(reason(needs(cfg, req("update-index", "_index"))), /a config file changed/); assert.equal(reason(needs(input(), req("update-index", "_index", { force: true }))), "forced"); }); test("update-index: the settings it reads, by signature — not judged when the caller read none", () => { const signed = (stampSig: string | undefined, nowSig: string | undefined) => input({ index: { stamp: indexStamp(stampSig === undefined ? {} : { settingsSig: stampSig }), lastIngestDoneAt: null, configChangedAt: null, ...(nowSig === undefined ? {} : { settingsSig: nowSig }), }, }); assert.equal(state(needs(signed("a", "a"), req("update-index", "_index"))), "fresh"); assert.equal( reason(needs(signed("a", "b"), req("update-index", "_index"))), "the settings the index reads changed", ); assert.equal( reason(needs(signed(undefined, "b"), req("update-index", "_index"))), "the settings the index reads changed", "a stamp from before the signature reads as changed", ); assert.equal(state(needs(signed("a", undefined), req("update-index", "_index"))), "fresh"); }); // --- build-site ---------------------------------------------------------- test("build-site: blocked 'update the index first' with no stamp — even forced", () => { const none = input({ index: { stamp: null, lastIngestDoneAt: null, configChangedAt: null } }); assert.deepEqual(needs(none, req("build-site", "jer")), { state: "blocked", reason: "update the index first" }); assert.equal(state(needs(none, req("build-site", "jer", { force: true }))), "blocked"); }); test("build-site: fresh when the built inputSig is the stamp's, nothing changed and the bundle is sound", () => { assert.equal(state(needs(input(), req("build-site", "jer"))), "fresh"); }); test("build-site: changed channels make it stale before any index runs, named", () => { const s = input({ sites: { jer: target({ built: builtStamp(), changedChannels: ["a", "b", "c", "d", "e"] }) } }); assert.equal(reason(needs(s, req("build-site", "jer"))), "5 channels changed (a, b, c, d, …)"); const one = input({ sites: { jer: target({ built: builtStamp(), changedChannels: ["a"] }) } }); assert.equal(reason(needs(one, req("build-site", "jer"))), "1 channel changed (a)"); }); test("build-site: an inputSig mismatch is 'data changed'; a config change, a bad bundle, no build are stale", () => { const sig = input({ sites: { jer: target({ built: builtStamp({ inputSig: "older" }) }) } }); assert.equal(reason(needs(sig, req("build-site", "jer"))), "data changed"); const cfg = input({ sites: { jer: target({ built: builtStamp(), configChangedAt: 3_001 }) } }); assert.equal(reason(needs(cfg, req("build-site", "jer"))), "config changed"); const bad = input({ sites: { jer: target({ built: builtStamp(), bundleProblem: "holds a build of x" }) } }); assert.equal(reason(needs(bad, req("build-site", "jer"))), "holds a build of x"); const never = input({ sites: { jer: target() } }); assert.equal(reason(needs(never, req("build-site", "jer"))), "never built"); }); test("build-site: --force builds a fresh site; a code change alone is NOT stale", () => { assert.equal(reason(needs(input(), req("build-site", "jer", { force: true }))), "forced"); const code = input({ sites: { jer: target({ built: builtStamp({ commit: "an-older-commit", branch: "x" }) }) } }); assert.equal(state(needs(code, req("build-site", "jer"))), "fresh"); }); test("build-site: indexAfter waits for this run's index; an unknown site or one the index never saw is blocked", () => { assert.equal(state(needs(input(), req("build-site", "jer", { indexAfter: 2_000 }))), "fresh"); assert.match(reason(needs(input(), req("build-site", "jer", { indexAfter: 2_001 }))), /waiting for the index update/); assert.equal(reason(needs(input(), req("build-site", "nope"))), 'no site "nope"'); const unseen = input({ sites: { jer: target({ built: builtStamp() }), ani: target() } }); assert.match(reason(needs(unseen, req("build-site", "ani"))), /the index has not seen site "ani" — update the index first/); }); // --- build-site _all --runner docker ------------------------------------------ test("build-site _all: per site, as above", () => { const s = input({ index: { stamp: indexStamp({ sites: { jer: indexStamp().sites.jer, ani: { siteFp: null, statsFp: null, inputSig: "x" } } }), lastIngestDoneAt: null, configChangedAt: null, }, sites: { jer: target({ built: builtStamp() }), ani: target() }, }); assert.equal(reason(needs(s, req("build-site", "_all", { runner: "docker" }))), "1 of 2 sites to build (ani)"); const allFresh = input(); assert.equal(state(needs(allFresh, req("build-site", "_all", { runner: "docker" }))), "fresh"); const none = input({ index: { stamp: null, lastIngestDoneAt: null, configChangedAt: null } }); assert.equal(state(needs(none, req("build-site", "_all"))), "blocked"); }); // --- deploy-site -------------------------------------------------------------- test("deploy-site: fresh exactly when the record for that kind/branch names the built stamp", () => { const prod = input({ sites: { jer: target({ built: builtStamp(), deployed: deployed() }) } }); assert.equal(state(needs(prod, req("deploy-site", "jer"))), "fresh"); const newer = input({ sites: { jer: target({ built: builtStamp({ stampId: "b2" }), deployed: deployed() }) } }); assert.equal(reason(needs(newer, req("deploy-site", "jer"))), "a newer build is not deployed"); // The production record says nothing about a preview, or the local copy. assert.equal(reason(needs(prod, req("deploy-site", "jer", { preview: "r18" }))), 'never deployed to preview "r18"'); assert.equal(reason(needs(prod, req("deploy-site", "jer", { to: "local" }))), "never deployed (local)"); const pv = input({ sites: { jer: target({ built: builtStamp(), deployed: deployed({ previews: { r18: deployed().production! } }) }) }, }); assert.equal(state(needs(pv, req("deploy-site", "jer", { preview: "r18" }))), "fresh"); assert.equal(reason(needs(prod, req("deploy-site", "jer", { force: true }))), "forced"); }); test("deploy-site: refused with no build, a private site, no project (pages only), a bad bundle, builtAfter", () => { const never = input({ sites: { jer: target() } }); assert.equal(reason(needs(never, req("deploy-site", "jer"))), "no build of jer — archilyzer publish build jer"); const priv = input({ sites: { jer: target({ built: builtStamp(), deployProblem: "Site jer is private" }) } }); assert.equal(reason(needs(priv, req("deploy-site", "jer", { to: "local" }))), "Site jer is private"); const noProject = input({ sites: { jer: target({ built: builtStamp(), pagesProblem: "no project" }) } }); assert.equal(reason(needs(noProject, req("deploy-site", "jer"))), "no project"); assert.equal(state(needs(noProject, req("deploy-site", "jer", { to: "local" }))), "stale", "local needs no project"); const bad = input({ sites: { jer: target({ built: builtStamp(), bundleProblem: "torn" }) } }); assert.deepEqual(needs(bad, req("deploy-site", "jer")), { state: "blocked", reason: "torn" }); assert.match(reason(needs(input(), req("deploy-site", "jer", { builtAfter: 3_001 }))), /waiting for the build of jer/); assert.equal(state(needs(never, req("deploy-site", "jer", { force: true }))), "blocked", "force never deploys nothing"); }); test("deploy-site: production refuses a build made on another branch; a preview of it is fine", () => { const off = input({ sites: { jer: target({ built: builtStamp({ branch: "r18/stage-core" }) }) } }); assert.match(reason(needs(off, req("deploy-site", "jer"))), /built from branch "r18\/stage-core"; production ships only a build of main/); assert.equal(state(needs(off, req("deploy-site", "jer", { preview: "r18" }))), "stale"); const detached = input({ sites: { jer: target({ built: builtStamp({ branch: null }) }) } }); assert.match( reason(needs(detached, req("deploy-site", "jer"))), /built with no branch recorded \(a detached HEAD, or an image built without ARCHILYZER_BRANCH\); production ships only a build of main/, "no branch is refused like another branch", ); assert.equal(state(needs(detached, req("deploy-site", "jer", { preview: "r18" }))), "stale"); }); test("deploy-site under builtAfter: a run's NO-OP build (the bundle still matches the index this run updated) does not hold the deploy", () => { // The run started at 1_500; its index ran at 2_000 (stamp.builtAt); the // site's build was a no-op, so built.builtAt (500) predates the run. const old = builtStamp({ builtAt: 500 }); const s = input({ sites: { jer: target({ built: old }) } }); assert.equal(state(needs(s, req("deploy-site", "jer", { builtAfter: 1_500 }))), "stale", "same inputs, same bundle"); // …but not when the bundle does NOT match the current index (the build failed, say). const drifted = input({ sites: { jer: target({ built: builtStamp({ builtAt: 500, inputSig: "older" }) }) } }); assert.match(reason(needs(drifted, req("deploy-site", "jer", { builtAfter: 1_500 }))), /waiting for the build of jer/); // …nor when the index itself has not run since the run began. assert.match(reason(needs(s, req("deploy-site", "jer", { builtAfter: 2_500 }))), /waiting for the build of jer/); // A no-op build's checkedAt counts as well. const checked = input({ sites: { jer: target({ built: builtStamp({ builtAt: 500, checkedAt: 3_000, inputSig: "older" }) }) } }); assert.equal(state(needs(checked, req("deploy-site", "jer", { builtAfter: 2_500 }))), "stale"); // The hub and the homepage judge "current" by their own signatures. const hub = input({ hub: target({ built: builtStamp({ target: "_hub", kind: "hub", inputSig: "hub-1", builtAt: 500 }) }) }); assert.equal(state(needs(hub, req("deploy-hub", "_hub", { builtAfter: 1_500 }))), "stale"); const home = input(); home.homepage.built = { ...(home.homepage.built as BuiltStamp), builtAt: 500 }; assert.equal(state(needs(home, req("deploy-homepage", "_homepage", { builtAfter: 1_500 }))), "stale"); }); test("changedChannels and config changes are measured against max(builtAt, checkedAt)", () => { assert.equal(builtCheckedAt(builtStamp({ builtAt: 3_000 })), 3_000); assert.equal(builtCheckedAt(builtStamp({ builtAt: 3_000, checkedAt: 9_000 })), 9_000); const cfg = input({ sites: { jer: target({ built: builtStamp({ checkedAt: 9_000 }), configChangedAt: 5_000 }) } }); assert.equal(state(needs(cfg, req("build-site", "jer"))), "fresh", "a config change older than the last check"); }); // --- the hub ------------------------------------------------------------------ test("build-hub: fresh when built.inputSig is the stamp's hubSig", () => { assert.equal(state(needs(input(), req("build-hub", "_hub"))), "fresh"); const moved = input({ index: { stamp: indexStamp({ hubSig: "hub-2" }), lastIngestDoneAt: null, configChangedAt: null } }); assert.equal(reason(needs(moved, req("build-hub", "_hub"))), "the index or the pool it lists changed"); const ch = input({ hub: target({ built: builtStamp({ target: "_hub", kind: "hub", inputSig: "hub-1" }), changedChannels: ["x"] }) }); assert.equal(reason(needs(ch, req("build-hub", "_hub"))), "1 channel changed (x)"); const none = input({ index: { stamp: null, lastIngestDoneAt: null, configChangedAt: null } }); assert.equal(reason(needs(none, req("build-hub", "_hub"))), "update the index first"); }); test("deploy-hub: as deploy-site", () => { const s = input({ hub: target({ built: builtStamp({ target: "_hub", kind: "hub" }), deployed: deployed({ target: "_hub" }) }) }); assert.equal(state(needs(s, req("deploy-hub", "_hub"))), "fresh"); assert.equal(reason(needs(input({ hub: target() }), req("deploy-hub", "_hub"))), "no build of the hub — archilyzer publish hub"); const noProject = input({ hub: target({ built: builtStamp(), pagesProblem: "The hub has no Cloudflare Pages project" }) }); assert.match(reason(needs(noProject, req("deploy-hub", "_hub"))), /no Cloudflare Pages project/); }); // --- the homepage ------------------------------------------------------------- test("build-homepage: fresh when built from the current index stamp and main has not moved", () => { assert.equal(state(needs(input(), req("build-homepage", "_homepage"))), "fresh"); const idx = input({ index: { stamp: indexStamp({ stampId: "s2" }), lastIngestDoneAt: null, configChangedAt: null } }); assert.equal(reason(needs(idx, req("build-homepage", "_homepage"))), "the index was updated"); const moved = input(); moved.homepage.mainHead = "main-2"; assert.equal(reason(needs(moved, req("build-homepage", "_homepage"))), "main has moved since the source was published"); const noRepo = input(); noRepo.homepage.mainHead = null; noRepo.homepage.built = { ...(noRepo.homepage.built as BuiltStamp), sourceCommit: null }; assert.equal(state(needs(noRepo, req("build-homepage", "_homepage"))), "fresh", "no repository: the index decides"); }); test("deploy-homepage: as deploy-site", () => { const s = input(); s.homepage.deployed = deployed({ target: "_homepage" }); assert.equal(state(needs(s, req("deploy-homepage", "_homepage"))), "fresh"); s.homepage.built = null; assert.equal(reason(needs(s, req("deploy-homepage", "_homepage"))), "no build of the homepage — archilyzer publish homepage"); }); test("deployKindOf: --to local, --preview, else production", () => { assert.equal(deployKindOf({}), "production"); assert.equal(deployKindOf({ preview: "x" }), "preview"); assert.equal(deployKindOf({ to: "local" }), "local"); assert.equal(deployKindOf({ to: "pages" }), "production"); }); // --- argv ------------------------------------------------------------------- test("argv: the child's command line, pinned, and parsed back to the same request", () => { const r: StageRequest = { kind: "deploy-site", target: "jer", runId: "run-9", preview: "r18", force: true, builtAfter: 1_700_000_000_000, }; assert.deepEqual(STAGES["deploy-site"].argv(r), [ "stage", "deploy-site", "jer", "--run-id", "run-9", "--preview", "r18", "--force", "--built-after", "1700000000000", ]); const booleans = Object.entries(STAGE_FLAGS).filter(([, k]) => k === "boolean").map(([n]) => n); for (const sample of [ r, { kind: "update-index", target: "_index", runId: "a" }, { kind: "build-site", target: "_all", runId: "b", runner: "docker", skipArchives: true, indexAfter: 5 }, { kind: "build-site", target: "ani", runId: "c", allowMissingMedia: true }, { kind: "deploy-homepage", target: "_homepage", runId: "d", to: "local" }, ] as StageRequest[]) { const { positionals, flags } = parseArgv(stageArgv(sample), booleans); assert.equal(positionals[0], "stage"); assert.deepEqual(parseStageArgs(positionals.slice(1), flags), sample); } }); test("parseStageArgs refuses what the child must never guess", () => { const err = (pos: string[], flags: Record = { "run-id": "r" }) => { const out = parseStageArgs(pos, flags); return "error" in out ? out.error : ""; }; assert.match(err(["build"]), /which stage\?/); assert.match(err(["build-site"]), /which target\?/); assert.match(err(["build-site", "jer"], {}), /--run-id is required/); assert.match(err(["update-index", "jer"]), /the target is _index/); assert.match(err(["build-hub", "_index"]), /the target is _hub/); assert.match(err(["deploy-site", "_all"]), /the target is a site id/); assert.match(err(["deploy-site", "jer"], { "run-id": "r", to: "ftp" }), /--to is pages or local/); assert.match(err(["build-site", "_all"], { "run-id": "r", runner: "podman" }), /--runner is local or docker/); assert.match(err(["build-site", "jer"], { "run-id": "r", "index-after": "soon" }), /--index-after is a time/); assert.match(err(["deploy-site", "jer"], { "run-id": "r", preview: "x", to: "local" }), /two different deploys/); assert.match(err(["build-site", "../etc"]), /which target\?/); }); // --- the spawned command ------------------------------------------------------ test("stageCommand: common's tsx running bin/archilyzer.ts stage …, cwd common/, the heap cap for update-index only", () => { const paths = { monorepoRoot: "/repo" } as Paths; const idx = stageCommand(paths, { kind: "update-index", target: "_index", runId: "r1" }, { PATH: "/bin" }); assert.deepEqual(idx, { command: "/repo/common/node_modules/.bin/tsx", args: ["bin/archilyzer.ts", "stage", "update-index", "_index", "--run-id", "r1"], cwd: "/repo/common", env: { PATH: "/bin", NODE_OPTIONS: "--max-old-space-size=8192" }, }); const kept = stageCommand(paths, { kind: "update-index", target: "_index", runId: "r1" }, { NODE_OPTIONS: "--trace-warnings" }); assert.equal(kept.env.NODE_OPTIONS, "--trace-warnings --max-old-space-size=8192"); const build = stageCommand(paths, { kind: "build-site", target: "jer", runId: "r1" }, { PATH: "/bin" }); assert.deepEqual(build.env, { PATH: "/bin" }); assert.deepEqual(build.args, ["bin/archilyzer.ts", "stage", "build-site", "jer", "--run-id", "r1"]); }); test("the exit codes", () => { assert.deepEqual(STAGE_EXIT, { ok: 0, failed: 1, usage: 2, precondition: 3, cancelled: 130 }); });