#!/usr/bin/env node // E2E fake gallery-dl for the X/Twitter post fetcher. // // The real binary walks an X timeline and, with `--dump-json`, prints one JSON // metadata object per tweet to stdout. This emits a small deterministic set in // exactly that shape so the posts pipeline (normalize → JSONL → archive) can be // driven end-to-end WITHOUT ever touching x.com — the suite must never depend // on X's live defenses, and no test run may risk a real account. // // Deterministic by design: the same tweets every run, so a re-run proves the // incremental (no-duplicate) guarantee. import { installFixtureWatchdog } from "./_watchdog.mjs"; // Never outlive the run that spawned us — see _watchdog.mjs. installFixtureWatchdog(); const args = process.argv.slice(2); // The account URL is the final positional argument. const url = args[args.length - 1] ?? ""; const handleMatch = /(?:x|twitter)\.com\/([^/?#]+)/i.exec(url); const handle = handleMatch ? handleMatch[1] : "faketester"; // `--post-range 1-N` caps how many posts the real binary emits; honour it so the // limit path is exercised. let limit = Infinity; const rangeIdx = args.indexOf("--post-range"); if (rangeIdx >= 0 && args[rangeIdx + 1]) { const m = /^1-(\d+)$/.exec(args[rangeIdx + 1]); if (m) limit = Number(m[1]); } // Mirror the real failure the Needs-cookies bucket models: when the fixture is // asked to simulate an expired session, exit non-zero with an auth-shaped // message on stderr. if (process.env.E2E_FAKE_GALLERY_DL_AUTH_FAIL === "1") { process.stderr.write( "[twitter][error] HTTP redirect to login page (401 Unauthorized)\n", ); process.exit(1); } const AUTHOR = { name: handle, nick: "Fake Tester" }; // Ids are deliberately > 2^53 so any accidental numeric parse would corrupt // them visibly rather than silently passing. const TWEETS = [ { tweet_id: "1899000000000000001", conversation_id: "1899000000000000001", date: "2026-06-01 12:00:00", content: "a fake tweet about zephyr archives", lang: "en", author: AUTHOR, user: AUTHOR, favorite_count: 5, retweet_count: 1, reply_count: 1, quote_count: 0, entities: { urls: [ { url: "https://t.co/fake", expanded_url: "https://example.com/fake" }, ], }, }, { tweet_id: "1899000000000000002", conversation_id: "1899000000000000001", date: "2026-06-01 13:00:00", content: "a fake reply in the same zephyr thread", lang: "en", author: AUTHOR, user: AUTHOR, in_reply_to_status_id_str: "1899000000000000001", in_reply_to_screen_name: handle, favorite_count: 2, }, { tweet_id: "1899000000000000003", conversation_id: "1899000000000000003", date: "2026-05-20 09:30:00", content: "an older standalone fake tweet", lang: "en", author: AUTHOR, user: AUTHOR, favorite_count: 0, }, ]; for (const tweet of TWEETS.slice(0, limit)) { process.stdout.write(JSON.stringify(tweet) + "\n"); }