// The publish state READ (release 18): readPublishInputs over a scratch corpus // — the sites and their policies, the config mtimes, the ingest job metas and // the report regenerations, the live and the ended publish stages. // // Run with: pnpm --filter yt-dlp-transcript-common exec tsx --test publish/publishState.test.ts // // SETTINGS SEAM, as settingsSchema.test.ts: `getPaths()` memoizes at module // scope, so every root is set before anything imports lib/paths. Never a real // corpus. import { mkdirSync, mkdtempSync, utimesSync, writeFileSync } from "node:fs"; import { rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { test, after } from "node:test"; import assert from "node:assert/strict"; const ROOT = mkdtempSync(path.join(os.tmpdir(), "publish-state-")); process.env.TRANSCRIPTS_DIR = path.join(ROOT, "transcripts"); process.env.SITES_DIR = path.join(ROOT, "transcripts", "sites"); process.env.EXPORT_INDEX_DIR = path.join(ROOT, "export-index"); process.env.EXPORT_BUILDS_DIR = path.join(ROOT, "builds"); process.env.SETTINGS_FILE = path.join(ROOT, "settings.json"); process.env.CHARTS_CONFIG_FILE = path.join(ROOT, "charts.json"); process.env.SEARCH_ALIASES_FILE = path.join(ROOT, "transcripts", "search-aliases.json"); process.env.CURATED_TAGS_FILE = path.join(ROOT, "transcripts", "tags.json"); const { getPaths } = await import("../lib/paths"); const { readPublishInputs, readPublishStatus, resetPublishStateCache } = await import("./publishState"); const { stageSpec } = await import("./publishStages"); const { indexSettingsSig } = await import("./inputSig"); const { writeIndexStamp } = await import("./stamps"); const { indexStamp } = await import("./__fixtures__/stamps"); const { getSettings } = await import("../lib/settings"); type JobRecord = import("../jobs/registry").JobRecord; after(() => rm(ROOT, { recursive: true, force: true })); const paths = getPaths(); const T = 1_900_000_000_000; const sec = (ms: number) => ms / 1000; function writeJson(file: string, value: unknown, mtime?: number): void { mkdirSync(path.dirname(file), { recursive: true }); writeFileSync(file, JSON.stringify(value)); if (mtime !== undefined) utimesSync(file, sec(mtime), sec(mtime)); } function meta(id: string, m: Record): void { writeJson(path.join(paths.jobsDir, `${id}.meta.json`), { id, queueKey: "q", queuedAt: T - 100_000, ...m }); } function setup(): void { writeJson(paths.settingsFile, { publish: { enabled: true, hub: "build", previewBranch: "smoke" } }, T - 500_000); writeJson( path.join(paths.sitesDir, "alpha", "site.json"), { siteId: "alpha", siteTitle: "Alpha", channels: [{ slug: "a-one" }, { slug: "shared" }], cloudflareProject: "alpha", publish: { auto: "preview" }, }, T - 400_000, ); writeJson( path.join(paths.sitesDir, "secret", "site.json"), { siteId: "secret", siteTitle: "Secret", channels: [{ slug: "shared" }], audience: "private", publish: { auto: "production" } }, T - 300_000, ); writeJson(path.join(paths.sitesDir, "alpha", "tags.json"), {}, T - 50_000); // Ingest metas: a member's sync ended done; a running download (not yet an // ingest); a non-ingest kind; a done ingest with no channel (the index only). meta("01J00000000000000000000001", { kind: "sync", status: "done", channelSlug: "a-one", endedAt: T - 20_000 }); meta("01J00000000000000000000002", { kind: "download-missing", status: "running", channelSlug: "shared" }); meta("01J00000000000000000000003", { kind: "scan-media", status: "done", channelSlug: "shared", endedAt: T - 1_000 }); meta("01J00000000000000000000004", { kind: "normalize-transcripts", status: "done", endedAt: T - 10_000 }); // An ended publish stage: a deploy refused for want of its build. meta("01J00000000000000000000005", { kind: "publish-deploy-site", queueKey: "publish", status: "failed", exitCode: 3, endedAt: T - 5_000, spec: stageSpec({ kind: "deploy-site", target: "alpha", runId: "run-x", preview: "smoke", builtAfter: T - 9_000 }), }); // A lane unit's only trace: the channel's report regenerated. writeJson(path.join(paths.channelsDir, "shared", "snapshot.json"), {}, T - 15_000); mkdirSync(path.join(paths.channelsDir, "a-one"), { recursive: true }); } test("readPublishInputs: sites, policies, mtimes, ingest signals, ended and live stages", async () => { setup(); resetPublishStateCache(); const live: JobRecord[] = [ { id: "01JLIVE0000000000000000001", kind: "publish-update-index", queueKey: "publish", status: "running", queuedAt: T - 2_000, startedAt: T - 1_000, logPath: "/dev/null", spec: stageSpec({ kind: "update-index", target: "_index", runId: "run-live" }), }, // A finished one in the registry wins over its (absent) meta. { id: "01JLIVE0000000000000000002", kind: "fetch-posts", queueKey: "platform:x.com", status: "done", channelSlug: "shared", queuedAt: T - 9_000, endedAt: T - 3_000, logPath: "/dev/null", }, ]; const i = await readPublishInputs(paths, { now: T, live, laneKnown: false }); assert.deepEqual(i.sites.map((s) => [s.siteId, s.policy, s.private, s.members.join(",")]), [ ["alpha", "preview", false, "a-one,shared"], ["secret", "build", true, "shared"], ]); const alpha = i.sites[0]; assert.equal(alpha.configChangedAt, T - 50_000, "its tags.json is the newest of its config"); assert.equal(alpha.cloudflareProject, "alpha"); assert.equal(i.sites[1].deployProblem !== null, true, "a private site is never deployed"); assert.equal(i.settings.previewBranch, "smoke"); assert.equal(i.hub.policy, "build"); assert.equal(i.homepage.policy, "off"); // Ingest: the member's sync, the registry's fetch-posts, the report. assert.equal(i.ingestByChannel["a-one"], T - 20_000); assert.equal(i.ingestByChannel.shared, T - 3_000, "fetch-posts (registry) beats the snapshot (T-15s)"); assert.equal(i.index.lastIngestDoneAt, T - 3_000); // The index's config: the settings file is the newest of the index inputs // here — and tags.json of a site is not one of them. assert.equal(i.index.configChangedAt, T - 300_000); assert.deepEqual( i.jobs.map((j) => [j.kind, j.target, j.status, j.runId]), [["update-index", "_index", "running", "run-live"]], ); assert.deepEqual( i.ended.map((e) => [e.kind, e.target, e.exitCode, e.builtAfter, e.preview]), [["deploy-site", "alpha", 3, T - 9_000, "smoke"]], ); assert.equal(i.lane.known, false); assert.equal(i.index.stamp, null); }); test("readPublishStatus: the chips a fresh scratch corpus shows", async () => { setup(); resetPublishStateCache(); const s = await readPublishStatus(paths, { now: T, live: [], laneKnown: false }); assert.equal(s.index.chip.text, "no index yet"); assert.deepEqual(s.plan.steps.map((x) => `${x.kind} ${x.target}`), [ "update-index _index", "build-site alpha", "deploy-site alpha", "build-site secret", "build-hub _hub", ]); const alpha = s.sites.find((t) => t.target === "alpha")!; assert.deepEqual(alpha.chips.deployed, { tone: "blocked", text: "waiting for its build" }); assert.equal(alpha.previewUrl, "https://smoke.alpha.pages.dev"); assert.equal(s.lane.enabled, true); assert.equal(s.lane.due, true, "no stamp: a pass is due"); }); test("the index is stale on the settings it reads, not on a settings-file write", async () => { setup(); resetPublishStateCache(); // A stamp newer than every input, signed over the settings as they are. await writeIndexStamp(paths, indexStamp({ scannedAt: T + 1, builtAt: T + 1, settingsSig: indexSettingsSig(getSettings()) })); const fresh = async () => (await readPublishStatus(paths, { now: T + 2, live: [], laneKnown: false })).index; assert.equal((await fresh()).fresh, true, "signed over these settings"); // A priority change (a pause click, a drive auto-pause) writes the file NOW. writeJson(paths.settingsFile, { publish: { enabled: true, hub: "build", previewBranch: "smoke" }, channelPriority: { channels: { "a-one": { tier: "high" } } }, }); resetPublishStateCache(); assert.equal((await fresh()).fresh, true, "the file is newer than the stamp; what the index reads is not"); // A social link is rendered by every build: the index is stale. writeJson(paths.settingsFile, { publish: { enabled: true, hub: "build", previewBranch: "smoke" }, socialLinks: [{ label: "Site", url: "https://example.com", svg: '' }], }); resetPublishStateCache(); const after = await fresh(); assert.equal(after.fresh, false); assert.deepEqual(after.freshness, { state: "stale", reason: "the settings the index reads changed" }); });