// The X post capture, against fakes only: a page that answers the capture's // evaluate calls from a recorded snapshot, a media downloader that writes // files, and a fake gallery-dl binary for the real download path. No browser, // no network. // // Run with: pnpm --filter yt-dlp-transcript-common exec tsx --test social/xPostCapture.test.ts import { test } from "node:test"; import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { chmod, mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import type { PageLike } from "./playwrightRuntime"; import { captureXPosts, classifyXPostSnapshot, shootXPost, type XCaptureDeps, type XPostSnapshot, } from "./xPostCapture"; import { readPostCapture, SHOT_FILENAME, writePostCapture } from "./postCapture"; import type { PostCaptureInput } from "./fetchers"; const sha = (b: string | Uint8Array) => createHash("sha256").update(b).digest("hex"); const PNG = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 9, 9]); const RECT = { x: 10, y: 120.4, width: 598.6, height: 300.2 }; const post = (sensitive = false): XPostSnapshot => ({ path: "/someone/status/1", text: "the post", article: { rect: RECT, sensitive }, }); const page = (text: string, p = "/i/status/1"): XPostSnapshot => ({ path: p, text, article: null }); // --- classification --------------------------------------------------------- test("classify: a rendered post is captured; its sensitive cover is noted", () => { assert.deepEqual(classifyXPostSnapshot(post()), { state: "captured" }); assert.deepEqual(classifyXPostSnapshot(post(true)), { state: "captured", sensitive: true }); }); test("classify: a login redirect or a logged-out page is a login wall that stops the run", () => { for (const s of [page("", "/i/flow/login"), page("", "/login"), page("Don't miss what's happening. Log in Sign up")]) { const v = classifyXPostSnapshot(s); assert.equal(v.state, "login-wall", JSON.stringify(s)); assert.ok(v.stop); } }); test("classify: deleted and walled posts are told apart, and neither stops the run", () => { for (const text of [ "This Post was deleted by the Post author. Learn more", "Hmm...this page doesn’t exist. Try searching for something else.", ]) { assert.deepEqual(classifyXPostSnapshot(page(text)), { state: "deleted" }, text); } for (const text of [ "This Post is from an account that no longer exists. Learn more", "This Post is from a suspended account. Learn more", "You’re unable to view this Post because this account owner limits who can view their Posts.", "This Post is unavailable.", ]) { assert.deepEqual(classifyXPostSnapshot(page(text)), { state: "unavailable" }, text); } }); test("classify: X refusing pages stops the run; an age wall and an empty page are errors that do not", () => { const refused = classifyXPostSnapshot(page("Something went wrong. Try reloading. Retry")); assert.equal(refused.state, "error"); assert.ok(refused.stop); const age = classifyXPostSnapshot(page("Age-restricted adult content.")); assert.equal(age.state, "error"); assert.equal(age.stop, undefined); const blank = classifyXPostSnapshot(page("")); assert.equal(blank.state, "error"); assert.equal(blank.stop, undefined); }); // --- the shot ---------------------------------------------------------------- type FakePage = PageLike & { calls: string[]; shots: unknown[] }; // A page whose snapshot evaluate returns `snaps` in turn (the last repeats); // the sensitive-cover click flips a sensitive article to an open one. function fakePage(snaps: XPostSnapshot[], opts: { gotoFails?: boolean } = {}): FakePage { let i = 0; const calls: string[] = []; const shots: unknown[] = []; return { calls, shots, goto: async (url: string) => { calls.push(`goto ${url}`); if (opts.gotoFails) throw new Error("net::ERR_TIMED_OUT\nat goto"); }, waitForSelector: async () => undefined, waitForTimeout: async () => undefined, on: () => {}, evaluate: async (script: string) => { if (script.includes("b.click()")) { calls.push("open-sensitive"); return 1; } if (script.includes("imgs.length")) return 0; calls.push("snapshot"); const s = snaps[Math.min(i, snaps.length - 1)]; i++; return s; }, screenshot: async (o?: unknown) => { shots.push(o); return PNG; }, }; } test("shot: the post's own box, clipped from a full-page PNG, hashed beside it", async () => { const dir = path.join(await mkdtemp(path.join(os.tmpdir(), "xshot-")), "1"); const p = fakePage([post()]); const res = await shootXPost(p, "1", dir); assert.equal(res.state, "captured"); assert.equal(p.calls[0], "goto https://x.com/i/status/1"); assert.deepEqual(p.shots, [ { type: "png", fullPage: true, clip: { x: 10, y: 120, width: 599, height: 301 } }, ]); assert.deepEqual(new Uint8Array(await readFile(path.join(dir, SHOT_FILENAME))), PNG); assert.deepEqual(res.shot, { name: SHOT_FILENAME, bytes: PNG.length, sha256: sha(PNG), url: "https://x.com/i/status/1", }); }); test("shot: a sensitive cover is opened before the shot", async () => { const dir = path.join(await mkdtemp(path.join(os.tmpdir(), "xshot-")), "1"); const p = fakePage([post(true), post(false)]); const res = await shootXPost(p, "1", dir); assert.equal(res.state, "captured"); assert.equal(res.sensitive, true); assert.ok(p.calls.includes("open-sensitive")); assert.equal(p.shots.length, 1); }); test("shot: a deleted post, a login wall or a failed load writes no shot", async () => { for (const [p, state] of [ [fakePage([page("This post was deleted by the post author.")]), "deleted"], [fakePage([page("", "/i/flow/login")]), "login-wall"], [fakePage([post()], { gotoFails: true }), "error"], ] as const) { const dir = path.join(await mkdtemp(path.join(os.tmpdir(), "xshot-")), "1"); const res = await shootXPost(p, "1", dir); assert.equal(res.state, state); assert.equal(res.shot, undefined); assert.equal(p.shots.length, 0); } }); // --- the run ----------------------------------------------------------------- type Harness = { deps: XCaptureDeps; pauses: number[]; opened: number; closed: number; downloads: string[]; }; // One snapshot per post id; the downloader writes `_1.jpg` unless the id // is in `noMedia`. function harness( snapsById: Record, opts: { noMedia?: string[]; mediaFails?: { id: string; needsCookies?: boolean } } = {}, ): Harness { const h: Harness = { deps: undefined as never, pauses: [], opened: 0, closed: 0, downloads: [] }; let current = ""; const pages = Object.fromEntries(Object.entries(snapsById).map(([id, s]) => [id, fakePage(s)])); const routed: PageLike = { goto: async (url: string, o?: unknown) => { current = url.split("/").pop()!; return pages[current].goto(url, o); }, waitForSelector: async () => undefined, waitForTimeout: async () => undefined, on: () => {}, evaluate: (s: string) => pages[current].evaluate(s), screenshot: (o) => pages[current].screenshot(o), }; h.deps = { openPage: async () => { h.opened++; return { page: routed, close: async () => void h.closed++ }; }, downloadMedia: async ({ id, dir }) => { h.downloads.push(id); if (opts.mediaFails?.id === id) { return { ok: false, error: "gallery-dl exited 4", needsCookies: opts.mediaFails.needsCookies }; } if (opts.noMedia?.includes(id)) return { ok: true, files: [] }; await writeFile(path.join(dir, `${id}_1.jpg`), `media of ${id}`); return { ok: true, files: [{ name: `${id}_1.jpg`, url: `https://pbs.example/${id}.jpg` }] }; }, pauseMs: () => 5_000, pause: async (ms) => void h.pauses.push(ms), now: () => new Date("2026-02-03T04:05:06.000Z"), }; return h; } async function input(ids: string[], over: Partial = {}): Promise { return { ids, handle: "example_user", outDir: await mkdtemp(path.join(os.tmpdir(), "posts-media-")), signal: new AbortController().signal, ...over, }; } test("run: shot then media per post, a paced gap before every contact after the first, a record per post", async () => { const h = harness({ "11": [post()], "22": [post()] }); const inp = await input(["11", "22"]); const res = await captureXPosts(inp, h.deps); assert.equal(res.stoppedEarly, undefined); assert.deepEqual(res.outcomes, [ { id: "11", state: "captured", availability: "available", files: 2 }, { id: "22", state: "captured", availability: "available", files: 2 }, ]); // Four contacts (two pages, two downloads): three gaps. assert.deepEqual(h.pauses, [5_000, 5_000, 5_000]); assert.equal(h.opened, 1); assert.equal(h.closed, 1); const rec = await readPostCapture(path.join(inp.outDir, "11")); assert.deepEqual(rec, { version: 1, id: "11", url: "https://x.com/i/status/11", capturedAt: "2026-02-03T04:05:06.000Z", state: "captured", shot: { name: SHOT_FILENAME, bytes: PNG.length, sha256: sha(PNG), url: "https://x.com/i/status/11" }, mediaState: "ok", media: [ { name: "11_1.jpg", bytes: "media of 11".length, sha256: sha("media of 11"), url: "https://pbs.example/11.jpg", }, ], }); }); test("run: posts already captured are skipped unless forced; a missing half is all that is redone", async () => { const h = harness({ "11": [post()], "22": [post()] }); const inp = await input(["11", "22"]); await captureXPosts(inp, h.deps); h.downloads.length = 0; const again = harness({ "11": [post()], "22": [post()] }); const res = await captureXPosts(inp, again.deps); assert.deepEqual(res.outcomes, []); assert.equal(again.opened, 0, "no browser for a run with nothing to shoot"); assert.deepEqual(again.pauses, []); // A post whose media failed earlier: only the media is fetched again. const rec = (await readPostCapture(path.join(inp.outDir, "22")))!; await writePostCapture(path.join(inp.outDir, "22"), { ...rec, mediaState: "error", media: [] }); const retry = harness({ "11": [post()], "22": [post()] }); const r2 = await captureXPosts(inp, retry.deps); assert.equal(retry.opened, 0); assert.deepEqual(retry.downloads, ["22"]); assert.equal(r2.outcomes.length, 1); assert.equal(r2.outcomes[0].availability, undefined, "a media-only pass says nothing about liveness"); const kept = (await readPostCapture(path.join(inp.outDir, "22")))!; assert.equal(kept.shot?.sha256, sha(PNG), "the shot on disk is kept"); const forced = harness({ "11": [post()], "22": [post()] }); const r3 = await captureXPosts({ ...inp, force: true }, forced.deps); assert.equal(r3.outcomes.length, 2); assert.deepEqual(forced.downloads, ["11", "22"]); }); test("run: a deleted or walled post is recorded, gets no download, and the run goes on", async () => { const h = harness({ "11": [page("This post was deleted by the post author.")], "22": [page("These posts are protected.")], "33": [post()], }, { noMedia: ["33"] }); const inp = await input(["11", "22", "33"]); const res = await captureXPosts(inp, h.deps); assert.deepEqual( res.outcomes.map((o) => [o.id, o.state, o.availability, o.files]), [ ["11", "deleted", "deleted", 0], ["22", "unavailable", "account_unavailable", 0], ["33", "captured", "available", 1], ], ); assert.deepEqual(h.downloads, ["33"]); const deleted = (await readPostCapture(path.join(inp.outDir, "11")))!; assert.equal(deleted.state, "deleted"); assert.equal(deleted.shot, undefined); assert.equal(deleted.mediaState, "skipped"); assert.equal((await readPostCapture(path.join(inp.outDir, "33")))!.mediaState, "none"); // A deleted post is settled: the next run does not ask X again. const next = harness({ "11": [post()], "22": [post()], "33": [post()] }); const r2 = await captureXPosts(inp, next.deps); assert.deepEqual(r2.outcomes.map((o) => o.id), ["22"]); }); test("run: a login wall stops the run at that post — recorded, no availability verdict, the rest untouched", async () => { const h = harness({ "11": [post()], "22": [page("", "/i/flow/login")], "33": [post()] }); const inp = await input(["11", "22", "33"]); const res = await captureXPosts(inp, h.deps); assert.equal(res.needsCookies, true); assert.match(res.stoppedEarly ?? "", /Stopped at 22/); assert.deepEqual(res.outcomes.map((o) => [o.id, o.state, o.availability]), [ ["11", "captured", "available"], ["22", "login-wall", undefined], ]); assert.equal((await readPostCapture(path.join(inp.outDir, "22")))!.state, "login-wall"); assert.equal(await readPostCapture(path.join(inp.outDir, "33")), null); assert.equal(h.closed, 1, "the browser is closed on the way out"); }); test("run: a media download refused for want of a login stops the run with needsCookies", async () => { const h = harness({ "11": [post()], "22": [post()] }, { mediaFails: { id: "11", needsCookies: true } }); const res = await captureXPosts(await input(["11", "22"]), h.deps); assert.equal(res.needsCookies, true); assert.deepEqual(res.outcomes.map((o) => o.id), ["11"]); assert.equal(res.outcomes[0].state, "captured", "the shot stands"); }); test("run: three failures in a row stop it rather than paging through the rest", async () => { const h = harness({ "1": [page("")], "2": [page("")], "3": [page("")], "4": [post()] }); const res = await captureXPosts(await input(["1", "2", "3", "4"]), h.deps); assert.deepEqual(res.outcomes.map((o) => o.state), ["error", "error", "error"]); assert.match(res.stoppedEarly ?? "", /3 posts in a row failed/); }); test("run: a cancel or a drain stops between posts", async () => { const ac = new AbortController(); const h = harness({ "11": [post()], "22": [post()] }); const inner = h.deps.downloadMedia; h.deps.downloadMedia = async (a) => { const r = await inner(a); ac.abort(); return r; }; const res = await captureXPosts(await input(["11", "22"], { signal: ac.signal }), h.deps); assert.deepEqual(res.outcomes.map((o) => o.id), ["11"]); assert.match(res.stoppedEarly ?? "", /Cancelled/); const drain = new AbortController(); drain.abort(); const d = harness({ "11": [post()] }); const r2 = await captureXPosts(await input(["11"], { drain: drain.signal }), d.deps); assert.deepEqual(r2.outcomes, []); assert.match(r2.stoppedEarly ?? "", /Drained/); assert.equal(d.opened, 0); }); test("run: media only, or shots only, does only that half", async () => { const h = harness({ "11": [post()] }); const inp = await input(["11"], { shots: false }); const res = await captureXPosts(inp, h.deps); assert.equal(h.opened, 0); assert.deepEqual(h.downloads, ["11"]); assert.deepEqual(res.outcomes, [{ id: "11", state: "captured", files: 1 }]); const s = harness({ "11": [post()] }); const r2 = await captureXPosts(await input(["11"], { media: false }), s.deps); assert.deepEqual(s.downloads, []); assert.deepEqual(r2.outcomes, [{ id: "11", state: "captured", availability: "available", files: 1 }]); }); // --- the real download path, over a fake gallery-dl ------------------------- test("media download: gallery-dl's tagged lines and the files it wrote, nothing from the network", async () => { const root = await mkdtemp(path.join(os.tmpdir(), "xmedia-")); const bin = path.join(root, "fake-gallery-dl.mjs"); const argsLog = path.join(root, "argv.json"); await writeFile( bin, `#!/usr/bin/env node import { writeFileSync } from "node:fs"; import path from "node:path"; const args = process.argv.slice(2); writeFileSync(process.env.FAKE_ARGS_LOG, JSON.stringify(args)); const dir = args[args.indexOf("-D") + 1]; const tag = "ARCHILYZER-CAPTURED"; if (process.env.FAKE_MODE === "auth") { process.stderr.write("[twitter][error] 401 Unauthorized\\n"); process.exit(4); } const file = path.join(dir, "77_1.jpg"); writeFileSync(file, "pixels"); process.stdout.write(file + "\\n"); process.stdout.write(tag + "\\thttps://pbs.example/77.jpg\\t" + file + "\\n"); `, ); await chmod(bin, 0o755); // Paths are read (and cached) on first use: point them at the temp root // before anything asks, so no real corpus is in reach. process.env.TRANSCRIPTS_DIR = path.join(root, "transcripts"); process.env.GALLERY_DL_BIN = bin; process.env.FAKE_ARGS_LOG = argsLog; const { downloadXPostMedia } = await import("./xGalleryDlFetcher"); const dir = path.join(root, "posts-media", "77"); await mkdir(dir, { recursive: true }); const ok = await downloadXPostMedia({ id: "77", dir, login: { cookieFile: "/jar.txt" }, signal: new AbortController().signal, }); assert.deepEqual(ok, { ok: true, files: [{ name: "77_1.jpg", url: "https://pbs.example/77.jpg" }] }); const argv = JSON.parse(await readFile(argsLog, "utf8")) as string[]; assert.ok(!argv.includes("--no-download")); assert.equal(argv[argv.indexOf("--cookies") + 1], "/jar.txt"); process.env.FAKE_MODE = "auth"; try { const refused = await downloadXPostMedia({ id: "77", dir, login: {}, signal: new AbortController().signal, }); assert.equal(refused.ok, false); assert.equal(!refused.ok && refused.needsCookies, true); } finally { delete process.env.FAKE_MODE; } });