import { test } from "node:test"; import assert from "node:assert/strict"; import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, symlinkSync, utimesSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { getPaths, type Paths } from "../lib/paths"; import { main } from "./compose-hub"; import { builtHubProblem } from "../lib/builtExport"; import { readGlobalAliases } from "../lib/aliasesStore"; // Run with: // pnpm --filter yt-dlp-transcript-common test // // hub-summary.json is OPTIONAL: a hub composed with no index to walk ships no // numbers — and must not ship the LAST build's numbers either. function fixturePaths(root: string): Paths { const sitesDir = path.join(root, "sites"); const homepageDir = path.join(sitesDir, "_homepage"); mkdirSync(homepageDir, { recursive: true }); const exportPublicDir = path.join(root, "public"); mkdirSync(exportPublicDir, { recursive: true }); return { ...getPaths(), transcriptsDir: root, channelsDir: path.join(root, "channels"), sitesDir, homepageDir, homepageConfigFile: path.join(homepageDir, "homepage.json"), homepageChartTemplatesFile: path.join(homepageDir, "chart-templates.json"), lmdbPath: path.join(root, "index.mdb"), // never created: no index exportPublicDir, exportIndexDir: path.join(root, ".export-index"), // The hub's tombstones read the shared posts tree (release 18): this // fixture's own, never the checkout's. exportSharedPostsDir: path.join(root, ".export-index", "shared", "posts"), }; } test("with no index, compose-hub writes the pool and no hub-summary.json, removing a stale one", async () => { const root = mkdtempSync(path.join(tmpdir(), "compose-hub-")); const log = console.log; try { const paths = fixturePaths(root); const stale = path.join(paths.exportPublicDir, "hub-summary.json"); writeFileSync(stale, '{"version":1,"sites":[]}'); console.log = () => {}; await main({ paths }); console.log = log; assert.ok(!existsSync(stale), "a stale summary must not survive"); assert.deepEqual( JSON.parse(readFileSync(path.join(paths.exportPublicDir, "hub-sites.json"), "utf8")), [], ); assert.ok(!existsSync(paths.lmdbPath), "composing must not create an index"); } finally { console.log = log; rmSync(root, { recursive: true, force: true }); } }); test("hub-sites.json publishes every accent as a hex: an id becomes its on-dark value", async () => { const root = mkdtempSync(path.join(tmpdir(), "compose-hub-")); const log = console.log; try { const paths = fixturePaths(root); const write = (id: string, site: Record) => { mkdirSync(path.join(paths.sitesDir, id), { recursive: true }); writeFileSync(path.join(paths.sitesDir, id, "site.json"), JSON.stringify(site)); }; write("named", { siteTitle: "Named", siteUrl: "https://named.example", accent: "violet" }); write("custom", { siteTitle: "Custom", siteUrl: "https://custom.example", accent: "#CC3366" }); write("plain", { siteTitle: "Plain", siteUrl: "https://plain.example" }); console.log = () => {}; await main({ paths }); console.log = log; const pool = JSON.parse( readFileSync(path.join(paths.exportPublicDir, "hub-sites.json"), "utf8"), ) as Array<{ siteId: string; accent?: string }>; const accentOf = (id: string) => pool.find((s) => s.siteId === id); assert.equal(accentOf("named")?.accent, "#b49cf2"); assert.equal(accentOf("custom")?.accent, "#cc3366"); assert.ok(accentOf("plain") && !("accent" in accentOf("plain")!)); } finally { console.log = log; rmSync(root, { recursive: true, force: true }); } }); test("in a worktree, compose-hub writes its own files and never through the links into the primary", async () => { const root = mkdtempSync(path.join(tmpdir(), "compose-hub-")); const log = console.log; try { const paths = fixturePaths(root); // The primary checkout's composed hub files, and the worktree's public/ // entries linking to them (as the worktree seed does). const primary = path.join(root, "primary-public"); mkdirSync(primary, { recursive: true }); const old = new Date("2026-09-25T20:47:37Z"); const files = ["hub-sites.json", "corpus.json", "llms.txt", "robots.txt", "_headers", "sw.js", "hub-summary.json"]; for (const f of files) { writeFileSync(path.join(primary, f), `live ${f}`); utimesSync(path.join(primary, f), old, old); symlinkSync(path.join(primary, f), path.join(paths.exportPublicDir, f)); } console.log = () => {}; await main({ paths }); console.log = log; for (const f of files) { const target = path.join(primary, f); assert.equal(readFileSync(target, "utf8"), `live ${f}`, `${f} in the primary`); assert.equal(statSync(target).mtimeMs, old.getTime(), `${f}'s mtime in the primary`); } // The worktree has its own: an empty pool, and no summary (no index). const at = (f: string) => path.join(paths.exportPublicDir, f); assert.ok(lstatSync(at("hub-sites.json")).isFile()); assert.equal(readFileSync(at("hub-sites.json"), "utf8"), "[]"); for (const f of ["corpus.json", "llms.txt", "robots.txt", "_headers"]) { assert.ok(lstatSync(at(f)).isFile(), `${f} is the worktree's own file`); } assert.ok(!existsSync(at("hub-summary.json"))); } finally { console.log = log; rmSync(root, { recursive: true, force: true }); } }); // Release 14 slice HS: an unlisted site builds and deploys, and the hub does // not list it — not a member, so not in federated search, corpus.json or // llms.txt. test("an unlisted site is in none of the hub's files; a listed one is in each", async () => { const root = mkdtempSync(path.join(tmpdir(), "compose-hub-")); const log = console.log; try { const paths = fixturePaths(root); const write = (id: string, site: Record) => { mkdirSync(path.join(paths.sitesDir, id), { recursive: true }); writeFileSync(path.join(paths.sitesDir, id, "site.json"), JSON.stringify(site)); }; write("fixture-shown", { siteTitle: "Shown Fixture", siteUrl: "https://fixture-shown.example" }); write("fixture-unlisted", { siteTitle: "Unlisted Fixture", siteUrl: "https://fixture-unlisted.example", listed: false, }); const lines: string[] = []; console.log = (...a: unknown[]) => void lines.push(a.join(" ")); await main({ paths }); console.log = log; const pool = JSON.parse( readFileSync(path.join(paths.exportPublicDir, "hub-sites.json"), "utf8"), ) as Array<{ siteId: string }>; assert.deepEqual(pool.map((s) => s.siteId), ["fixture-shown"]); for (const f of ["hub-sites.json", "corpus.json", "llms.txt"]) { const text = readFileSync(path.join(paths.exportPublicDir, f), "utf8"); assert.ok(text.includes("fixture-shown.example"), `${f} lists the listed site`); for (const needle of ["fixture-unlisted", "Unlisted Fixture"]) { assert.ok(!text.includes(needle), `${f} names ${needle}`); } } assert.match(lines.join("\n"), /compose-hub: 1 built-in pool site\(s\)/); } finally { console.log = log; rmSync(root, { recursive: true, force: true }); } }); // Report sites: a report-only site (`search: false`) publishes reports, not a // searchable archive — no hub member, whatever `listed` says. A full site with // reports stays one. test("a cited site is no hub member; a full site with reports is", async () => { const root = mkdtempSync(path.join(tmpdir(), "compose-hub-")); const log = console.log; try { const paths = fixturePaths(root); const write = (id: string, site: Record) => { mkdirSync(path.join(paths.sitesDir, id), { recursive: true }); writeFileSync(path.join(paths.sitesDir, id, "site.json"), JSON.stringify(site)); }; write("fixture-full", { siteTitle: "Full Fixture", siteUrl: "https://fixture-full.example", reports: ["demo-report"], }); write("fixture-cited", { siteTitle: "Cited Fixture", siteUrl: "https://fixture-cited.example", search: false, listed: true, }); console.log = () => {}; await main({ paths }); console.log = log; const pool = JSON.parse( readFileSync(path.join(paths.exportPublicDir, "hub-sites.json"), "utf8"), ) as Array<{ siteId: string }>; assert.deepEqual(pool.map((s) => s.siteId), ["fixture-full"]); for (const f of ["corpus.json", "llms.txt"]) { const text = readFileSync(path.join(paths.exportPublicDir, f), "utf8"); assert.ok(text.includes("fixture-full.example"), `${f} lists the full site`); assert.ok(!text.includes("fixture-cited"), `${f} names the cited site`); } } finally { console.log = log; rmSync(root, { recursive: true, force: true }); } }); // Release 17 slice XP (the review's HIGH 1): a site's compose leaves its data // in public/ — a private site's X posts included — and the hub builds from // public/ next. compose-hub removes every per-site entry, through a link only // the link, and ships the global alias dictionary as its own. test("compose-hub removes a site's data from public/, a linked entry by its link only", async () => { const root = mkdtempSync(path.join(tmpdir(), "compose-hub-")); const log = console.log; try { const paths = fixturePaths(root); const pub = paths.exportPublicDir; // What a private site's compose leaves. for (const tree of ["summaries", "transcripts", "subs", "digests", "stats", "archives"]) { mkdirSync(path.join(pub, tree, "x"), { recursive: true }); writeFileSync(path.join(pub, tree, "x", "page-0000.json"), "[]"); } for (const f of ["site.json", "tags.json", "duplicates.json", "chart-templates.json", "sitemap.xml"]) { writeFileSync(path.join(pub, f), "{}"); } writeFileSync(path.join(pub, "search-aliases.json"), JSON.stringify({ aliases: [{ site: 1 }] })); // posts/ as a worktree has it: a link into the primary checkout. const primaryPosts = path.join(root, "primary-public", "posts"); mkdirSync(path.join(primaryPosts, "jer-x"), { recursive: true }); writeFileSync(path.join(primaryPosts, "manifest.json"), '{"channels":[{"slug":"jer-x"}]}'); symlinkSync(primaryPosts, path.join(pub, "posts")); // A checked-in static asset stays. writeFileSync(path.join(pub, "globe.svg"), ""); // No global dictionary file: the seeded defaults are the global one. const hubPaths = { ...paths, globalAliasesFile: path.join(root, "no-aliases.json") }; console.log = () => {}; await main({ paths: hubPaths }); console.log = log; for (const gone of [ "summaries", "transcripts", "subs", "posts", "digests", "stats", "archives", "site.json", "tags.json", "duplicates.json", "chart-templates.json", "sitemap.xml", ]) { assert.ok(!existsSync(path.join(pub, gone)), `${gone} was removed`); } assert.ok(existsSync(path.join(primaryPosts, "manifest.json")), "the link's target is untouched"); assert.ok(existsSync(path.join(pub, "globe.svg"))); assert.ok(existsSync(path.join(pub, "hub-sites.json"))); const aliases = JSON.parse(readFileSync(path.join(pub, "search-aliases.json"), "utf8")); assert.deepEqual(aliases, { aliases: readGlobalAliases(hubPaths).aliases }); assert.ok(!JSON.stringify(aliases).includes('"site"'), "not the site's aliases"); } finally { console.log = log; rmSync(root, { recursive: true, force: true }); } }); // Release 18: Cloudflare's edge kept serving the hub's withdrawn X shard after // the deploy that removed it. While X posts are private the hub REPLACES those // paths — path for path, the rollout's requirement: posts/manifest.json (empty), // posts/thequartering-X/manifest.json (pageCount 0), posts/thequartering-X/ // page-0000.json ([]) — and serves posts/ no-store. A Bluesky channel is not X // and gets nothing; with X public the hub ships no posts/ at all. PRIVATE DATA // IS NEVER NAMED ON THE HUB: an X channel only a private site carries, and one // no site carries, get no tombstone — their slugs appear nowhere in public/. test("while X posts are private, the hub ships tombstones for every X channel, served no-store", async () => { const root = mkdtempSync(path.join(tmpdir(), "compose-hub-")); const log = console.log; try { const paths = fixturePaths(root); const pub = paths.exportPublicDir; const writeJson = (file: string, value: unknown) => { mkdirSync(path.dirname(file), { recursive: true }); writeFileSync(file, JSON.stringify(value)); }; const X = "thequartering-X"; const SKY = "jer-sky"; const PRIVATE_X = "only-private-x"; const NO_SITE_X = "no-site-x"; writeJson(path.join(paths.channelsDir, X, "config.json"), { handling: "youtube", url: "https://x.com/x", sourceKind: "social", platform: "twitter", }); writeJson(path.join(paths.channelsDir, SKY, "config.json"), { handling: "youtube", url: "https://bsky.app/profile/jer.example", sourceKind: "social", platform: "bluesky", }); for (const slug of [PRIVATE_X, NO_SITE_X]) { writeJson(path.join(paths.channelsDir, slug, "config.json"), { handling: "youtube", url: `https://x.com/${slug}`, sourceKind: "social", platform: "twitter", }); } const site = (siteId: string, slugs: string[], extra: Record = {}) => writeJson(path.join(paths.sitesDir, siteId, "site.json"), { siteId, siteTitle: siteId, channels: slugs.map((slug) => ({ slug, groupId: "default" })), ...extra, }); site("pub", [X, SKY]); site("mine", [PRIVATE_X, X], { audience: "private" }); for (const slug of [X, SKY, PRIVATE_X, NO_SITE_X]) { writeJson(path.join(paths.exportSharedPostsDir, slug, "manifest.json"), { version: 1, channelSlug: slug, pageCount: 1, maxPageBytes: 1000, generatedAt: "2026-10-01T00:00:00.000Z", slugToPage: { "1": 0 }, }); writeJson(path.join(paths.exportSharedPostsDir, slug, "page-0000.json"), [{ id: "1", text: "a post" }]); } console.log = () => {}; await main({ paths, settings: { social: { x: { visibility: "private" } } } }); console.log = log; const read = (rel: string) => JSON.parse(readFileSync(path.join(pub, rel), "utf8")); assert.deepEqual(read("posts/manifest.json").channels, []); assert.equal(read("posts/manifest.json").totalCount, 0); assert.equal(read(`posts/${X}/manifest.json`).pageCount, 0); assert.deepEqual(read(`posts/${X}/manifest.json`).slugToPage, {}); assert.deepEqual(read(`posts/${X}/page-0000.json`), []); assert.ok(!existsSync(path.join(pub, "posts", SKY)), "a Bluesky channel is not withdrawn"); // Private data is never named on the hub. assert.deepEqual(readdirSync(path.join(pub, "posts")).sort(), ["manifest.json", X]); for (const hidden of [PRIVATE_X, NO_SITE_X]) { const named = readdirSync(pub, { recursive: true, withFileTypes: true }) .filter((e) => e.isFile()) .filter((e) => readFileSync(path.join(e.parentPath, e.name), "utf8").includes(hidden)); assert.deepEqual(named.map((e) => e.name), [], `${hidden} is named in the hub's public/`); assert.ok(!existsSync(path.join(pub, "posts", hidden))); } const headers = readFileSync(path.join(pub, "_headers"), "utf8"); assert.ok( headers.endsWith( "# Withdrawn content (tombstones): never stored at the edge.\n" + "/posts/*\n Cache-Control: no-store\n Access-Control-Allow-Origin: *\n", ), headers, ); // corpus.json advertises no posts; and the hub bundle (public/ stands in // for out/) is still a hub: a posts/ of tombstones is its own. assert.ok(!readFileSync(path.join(pub, "corpus.json"), "utf8").includes("posts")); assert.equal(builtHubProblem(pub), null); // X public again: no posts/ at all, no no-store block. console.log = () => {}; await main({ paths, settings: {} }); console.log = log; assert.ok(!existsSync(path.join(pub, "posts"))); assert.ok(!readFileSync(path.join(pub, "_headers"), "utf8").includes("no-store")); } finally { console.log = log; rmSync(root, { recursive: true, force: true }); } });