import { test } from "node:test"; import assert from "node:assert/strict"; import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { MANIFEST_ONLY_SIGNATURE, reconcileChannelTree } from "./compose-site"; // Run with: // pnpm --filter yt-dlp-transcript-common test // // reconcileChannelTree materializes a site's member subset of a shared // per-channel tree (transcripts, subs, posts, digests) into public/. Importing // it runs nothing: compose-site's main() only runs as the entry point. // // The case that motivated this file: a social channel's transcripts tree is a // manifest.json with `pageCount: 0` and nothing else (it never enters the video // scan), and the signature ignores manifest.json — so it signed as "" and was // DELETED, while corpus.json advertised it. jeralyzer's thequartering-X 404. function fixture(): { src: string; dest: string; cleanup: () => void } { const root = mkdtempSync(path.join(tmpdir(), "compose-site-")); const src = path.join(root, "shared"); const dest = path.join(root, "public"); mkdirSync(src, { recursive: true }); return { src, dest, cleanup: () => rmSync(root, { recursive: true, force: true }) }; } const MANIFEST = JSON.stringify({ pageCount: 0, count: 0 }); const quiet = () => {}; test("a manifest-only member is copied, not removed", async () => { const { src, dest, cleanup } = fixture(); try { mkdirSync(path.join(src, "posts-only")); writeFileSync(path.join(src, "posts-only", "manifest.json"), MANIFEST); const sigs = await reconcileChannelTree("transcripts", src, dest, ["posts-only"], {}, quiet); assert.deepEqual(sigs, { "posts-only": MANIFEST_ONLY_SIGNATURE }); assert.equal( readFileSync(path.join(dest, "posts-only", "manifest.json"), "utf8"), MANIFEST, ); } finally { cleanup(); } }); test("an unchanged member is skipped, a changed one re-copied", async () => { const { src, dest, cleanup } = fixture(); try { mkdirSync(path.join(src, "chan")); writeFileSync(path.join(src, "chan", "manifest.json"), MANIFEST); writeFileSync(path.join(src, "chan", "page-0000.json"), "[1]"); const logs: string[] = []; const log = (m: string) => logs.push(m); const first = await reconcileChannelTree("transcripts", src, dest, ["chan"], {}, log); assert.ok(first.chan && first.chan !== MANIFEST_ONLY_SIGNATURE); // Unchanged source: the dest is left alone — a marker written into it // survives, which a re-copy would have wiped. writeFileSync(path.join(dest, "chan", "marker"), "x"); const second = await reconcileChannelTree("transcripts", src, dest, ["chan"], first, log); assert.deepEqual(second, first); assert.ok(existsSync(path.join(dest, "chan", "marker"))); assert.match(logs[1], /0 copied, 1 unchanged/); // The same holds for a manifest-only member on its second compose. mkdirSync(path.join(src, "social")); writeFileSync(path.join(src, "social", "manifest.json"), MANIFEST); const third = await reconcileChannelTree("transcripts", src, dest, ["chan", "social"], second, log); writeFileSync(path.join(dest, "social", "marker"), "x"); await reconcileChannelTree("transcripts", src, dest, ["chan", "social"], third, log); assert.ok(existsSync(path.join(dest, "social", "marker"))); // Pages arriving change the signature, so the tree is re-copied. writeFileSync(path.join(src, "social", "page-0000.json"), "[2]"); const fourth = await reconcileChannelTree("transcripts", src, dest, ["chan", "social"], third, log); assert.notEqual(fourth.social, MANIFEST_ONLY_SIGNATURE); assert.ok(existsSync(path.join(dest, "social", "page-0000.json"))); assert.ok(!existsSync(path.join(dest, "social", "marker"))); } finally { cleanup(); } }); test("a member with no source (not even a manifest) is removed, and a non-member pruned", async () => { const { src, dest, cleanup } = fixture(); try { mkdirSync(path.join(dest, "gone"), { recursive: true }); writeFileSync(path.join(dest, "gone", "manifest.json"), MANIFEST); mkdirSync(path.join(dest, "former"), { recursive: true }); mkdirSync(path.join(src, "empty-dir")); mkdirSync(path.join(dest, "empty-dir"), { recursive: true }); const sigs = await reconcileChannelTree( "transcripts", src, dest, ["gone", "empty-dir"], { gone: "old" }, quiet, ); assert.deepEqual(sigs, {}); assert.ok(!existsSync(path.join(dest, "gone"))); assert.ok(!existsSync(path.join(dest, "empty-dir"))); assert.ok(!existsSync(path.join(dest, "former"))); } finally { cleanup(); } }); test("a linked tree (a worktree's public/) becomes this checkout's own; the primary's is untouched", async () => { const { src, dest, cleanup } = fixture(); try { // The primary's composed tree, and the worktree's public/transcripts // linking to it. const primaryTree = path.join(path.dirname(dest), "primary-transcripts"); mkdirSync(path.join(primaryTree, "live-chan"), { recursive: true }); writeFileSync(path.join(primaryTree, "live-chan", "page-0000.json"), "live"); symlinkSync(primaryTree, dest); mkdirSync(path.join(src, "chan")); writeFileSync(path.join(src, "chan", "manifest.json"), MANIFEST); writeFileSync(path.join(src, "chan", "page-0000.json"), "[]"); await reconcileChannelTree("transcripts", src, dest, ["chan"], {}, quiet); assert.ok(lstatSync(dest).isDirectory() && !lstatSync(dest).isSymbolicLink()); assert.deepEqual(readdirSync(dest), ["chan"]); // Nothing was copied into, or pruned from, the primary's tree. assert.deepEqual(readdirSync(primaryTree), ["live-chan"]); assert.equal(readFileSync(path.join(primaryTree, "live-chan", "page-0000.json"), "utf8"), "live"); } finally { cleanup(); } });