import test from "node:test"; import assert from "node:assert/strict"; import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; // Run with: // pnpm -C editor exec tsx --test "app/api/ops/fetch-posts/route.test.ts" // // The body's shape and the refusals that come before any job: every case here // is answered from the disk alone, so a temp corpus with one X channel is the // whole world — no job is queued and nothing reaches X. const ROOT = await mkdtemp(path.join(os.tmpdir(), "fetch-posts-route-")); const SLUG = "demo-x"; const CHANNEL = path.join(ROOT, "channels", SLUG); // Set before the route (and getPaths, which caches) is first imported. process.env.WORKER_TOKEN = "test-token"; process.env.TRANSCRIPTS_DIR = ROOT; process.env.SETTINGS_FILE = path.join(ROOT, "settings.json"); await mkdir(CHANNEL, { recursive: true }); await writeFile( path.join(CHANNEL, "config.json"), JSON.stringify({ handling: "transcribe", sourceKind: "social", platform: "twitter", postFetcher: "x-gallery-dl", socialHandle: "example_user", name: "Example (X)", url: "https://x.com/example_user", }), ); // An account that shows no posts: nothing archived, and the last timeline // fetch finished having read none. await writeFile( path.join(CHANNEL, "posts-state.json"), JSON.stringify({ lastFetchedAt: "2026-01-01T00:00:00.000Z", lastFetchedCount: 0 }), ); const { POST } = await import("./route"); const { EMPTY_ACCOUNT_OLDER_REFUSAL } = await import( "yt-dlp-transcript-common/controller/fetchPosts" ); test.after(() => rm(ROOT, { recursive: true, force: true })); async function post(body: Record): Promise<{ status: number; error: string }> { const res = await POST( new Request("http://localhost/api/ops/fetch-posts", { method: "POST", headers: { authorization: "Bearer test-token", "content-type": "application/json", }, body: JSON.stringify(body), }), ); return { status: res.status, error: ((await res.json()) as { error?: string }).error ?? "" }; } test("force must be a boolean, and applies only to an older walk", async () => { const bad = await post({ slug: SLUG, older: true, force: "yes" }); assert.equal(bad.status, 400); assert.match(bad.error, /"force" must be a boolean/); const alone = await post({ slug: SLUG, force: true }); assert.equal(alone.status, 400); assert.equal(alone.error, '"force" applies only to an older-posts fetch.'); }); test("an older walk over an account that shows no posts is refused before any job", async () => { const res = await post({ slug: SLUG, older: true }); assert.equal(res.status, 400); assert.equal(res.error, EMPTY_ACCOUNT_OLDER_REFUSAL); // No job was ever written. assert.deepEqual(await readdir(path.join(ROOT, ".jobs")).catch(() => []), []); }); test("pages must be a positive whole number", async () => { const res = await post({ slug: SLUG, pages: 0 }); assert.equal(res.status, 400); assert.match(res.error, /pages/); }); // Moved from e2e/ops-api.spec.ts (release 19, A2b): every case is refused // before a job exists, so it needs no server. async function channel(slug: string, config: Record): Promise { await mkdir(path.join(ROOT, "channels", slug), { recursive: true }); await writeFile(path.join(ROOT, "channels", slug, "config.json"), JSON.stringify(config)); } test("fetch-posts refuses a channel that is not social, full with older, and an older fetch its fetcher cannot do — before any job", async () => { await channel("test-filter", { handling: "youtube", name: "Test Title Filter", url: "https://www.youtube.com/@example/videos", }); // A social channel whose fetcher has no older-posts walk. await channel("example-bsky", { handling: "transcribe", sourceKind: "social", platform: "bluesky", postFetcher: "bluesky-atproto", socialHandle: "example.bsky.social", name: "Example (Bluesky)", url: "https://bsky.app/profile/example.bsky.social", }); // The sentence the action gives for a video channel. const video = await post({ slug: "test-filter" }); assert.equal(video.status, 400); assert.equal(video.error, "test-filter is not a social channel."); const videoOlder = await post({ slug: "test-filter", older: true }); assert.equal(videoOlder.status, 400); assert.equal(videoOlder.error, "test-filter is not a social channel."); // Two walks at once, on any channel. const both = await post({ slug: "example-bsky", full: true, older: true }); assert.equal(both.status, 400); assert.match(both.error, /different walks — run one at a time/); const bothVideo = await post({ slug: "test-filter", full: true, older: true }); assert.equal(bothVideo.status, 400); assert.match(bothVideo.error, /different walks/); // A fetcher with no older walk, a floor that is not a date, a floor // without older, and a limit that is not a count. const bsky = await post({ slug: "example-bsky", older: true }); assert.equal(bsky.status, 400); assert.match(bsky.error, /cannot fetch older posts/); const floor = await post({ slug: "example-bsky", older: true, floor: "2020-13-01" }); assert.equal(floor.status, 400); assert.match(floor.error, /is not a date/); const floorAlone = await post({ slug: "example-bsky", floor: "2020-01-01" }); assert.equal(floorAlone.status, 400); assert.match(floorAlone.error, /only to an older-posts fetch/); const limit = await post({ slug: "example-bsky", limit: 0 }); assert.equal(limit.status, 400); assert.match(limit.error, /"limit" must be a whole number above zero/); assert.deepEqual(await readdir(path.join(ROOT, ".jobs")).catch(() => []), []); });