import { test } from "node:test"; import assert from "node:assert/strict"; import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import type { Paths } from "../lib/paths"; import { emptyPostsManifest, tombstoneChannelManifest, tombstoneNoStoreForHub, tombstoneNoStoreForSite, tombstonePaths, withdrawnXChannels, writePostsTombstones, } from "./tombstones"; // Run with: pnpm --filter yt-dlp-transcript-common test const writeJson = (file: string, value: unknown) => { mkdirSync(path.dirname(file), { recursive: true }); writeFileSync(file, JSON.stringify(value)); }; const readJson = (file: string) => JSON.parse(readFileSync(file, "utf8")); function sharedTree(shared: string, slug: string, pages: number) { writeJson(path.join(shared, slug, "manifest.json"), { version: 1, channelSlug: slug, pageCount: pages, maxPageBytes: 4096, generatedAt: "2026-10-01T00:00:00.000Z", slugToPage: { a: 0 }, }); for (let i = 0; i < pages; i++) { writeJson(path.join(shared, slug, `page-${String(i).padStart(4, "0")}.json`), [{ id: `p${i}` }]); } } test("the tombstone shapes: a channel manifest with no pages and no size, a site manifest with no channels", () => { assert.deepEqual(tombstoneChannelManifest("x", "T"), { version: 1, channelSlug: "x", pageCount: 0, maxPageBytes: 0, generatedAt: "T", slugToPage: {}, }); assert.deepEqual(emptyPostsManifest("T", "pub"), { version: 1, channels: [], totalCount: 0, generatedAt: "T", siteId: "pub", }); assert.equal("siteId" in emptyPostsManifest("T"), false); }); test("writePostsTombstones: one empty page per shared page, replacing a real tree, never through a link", async () => { const root = mkdtempSync(path.join(tmpdir(), "tombstones-")); try { const shared = path.join(root, "shared", "posts"); sharedTree(shared, "big-x", 3); // A real tree an earlier public build shipped: replaced. const posts = path.join(root, "public", "posts"); writeJson(path.join(posts, "big-x", "page-0000.json"), [{ id: "p0", text: "a post" }]); writeJson(path.join(posts, "big-x", "page-0007.json"), [{ id: "p7" }]); // A linked tree (a worktree's public/ entries link into the primary's): // the link goes, its target is untouched. const primary = path.join(root, "primary", "posts", "linked-x"); writeJson(path.join(primary, "page-0000.json"), [{ id: "keep" }]); symlinkSync(primary, path.join(posts, "linked-x")); const written = await writePostsTombstones({ postsDir: posts, sharedPostsDir: shared, slugs: ["big-x", "linked-x"], generatedAt: "T", }); assert.deepEqual(written, [ { slug: "big-x", pages: 3 }, { slug: "linked-x", pages: 0 }, ]); assert.deepEqual(readdirSync(path.join(posts, "big-x")).sort(), [ "manifest.json", "page-0000.json", "page-0001.json", "page-0002.json", ]); for (const page of ["page-0000.json", "page-0001.json", "page-0002.json"]) { assert.deepEqual(readJson(path.join(posts, "big-x", page)), []); } // The shared tree's page cap (4096) is not carried: a tombstone has no size. assert.deepEqual(readJson(path.join(posts, "big-x", "manifest.json")), tombstoneChannelManifest("big-x", "T")); assert.deepEqual(readdirSync(path.join(posts, "linked-x")), ["manifest.json"]); assert.deepEqual(readJson(path.join(primary, "page-0000.json")), [{ id: "keep" }]); assert.deepEqual(tombstonePaths(written, { withManifest: true }), [ "posts/manifest.json", "posts/big-x/manifest.json", "posts/big-x/page-0000.json", "posts/big-x/page-0001.json", "posts/big-x/page-0002.json", "posts/linked-x/manifest.json", ]); assert.deepEqual(tombstonePaths([]), []); // Nothing to withdraw writes nothing — not even posts/. const none = path.join(root, "none", "posts"); assert.deepEqual(await writePostsTombstones({ postsDir: none, sharedPostsDir: shared, slugs: [] }), []); assert.equal(existsSync(none), false); } finally { rmSync(root, { recursive: true, force: true }); } }); test("the no-store paths: a site names its manifest and each tombstone; the hub its whole posts tree", () => { const t = [ { slug: "a-x", pages: 1 }, { slug: "b-x", pages: 0 }, ]; assert.deepEqual(tombstoneNoStoreForSite(t), ["/posts/manifest.json", "/posts/a-x/*", "/posts/b-x/*"]); assert.deepEqual(tombstoneNoStoreForHub(t), ["/posts/*"]); assert.deepEqual(tombstoneNoStoreForSite([]), []); assert.deepEqual(tombstoneNoStoreForHub([]), []); }); test("withdrawnXChannels: the X channels with a shared posts tree that a non-private site carries, only while X posts are private", async () => { const root = mkdtempSync(path.join(tmpdir(), "tombstones-")); try { const paths = { channelsDir: path.join(root, "channels"), exportSharedPostsDir: path.join(root, "shared", "posts"), } as Paths; const config = (slug: string, platform: string) => writeJson(path.join(paths.channelsDir, slug, "config.json"), { handling: "youtube", url: `https://example.test/${slug}`, sourceKind: "social", platform, }); config("z-x", "twitter"); config("a-x", "twitter"); config("sky", "bluesky"); config("no-tree-x", "twitter"); config("private-only-x", "twitter"); config("no-site-x", "twitter"); for (const slug of ["z-x", "a-x", "sky", "no-config", "private-only-x", "no-site-x"]) { sharedTree(paths.exportSharedPostsDir, slug, 1); } const members = (...slugs: string[]) => slugs.map((slug) => ({ slug, groupId: "default" })); const sites = [ { channels: members("a-x", "sky", "no-config", "no-tree-x") }, { audience: "public" as const, channels: members("z-x") }, // A private site's X channel that no public site carries: never named. { audience: "private" as const, channels: members("private-only-x", "a-x") }, ]; const priv = { social: { x: { visibility: "private" } } }; assert.deepEqual(await withdrawnXChannels(paths, priv, sites), ["a-x", "z-x"]); assert.deepEqual(await withdrawnXChannels(paths, {}, sites), []); assert.deepEqual(await withdrawnXChannels(paths, { social: { x: { visibility: "public" } } }, sites), []); assert.deepEqual(await withdrawnXChannels(paths, priv, []), [], "no site, no tombstone"); const empty = { ...paths, exportSharedPostsDir: path.join(root, "absent") } as Paths; assert.deepEqual(await withdrawnXChannels(empty, priv, sites), []); } finally { rmSync(root, { recursive: true, force: true }); } });