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/capture-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(), "capture-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", }), ); await writeFile(path.join(CHANNEL, "posts-archive"), "twitter 111\n"); const { POST } = await import("./route"); 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/capture-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("articles must be a boolean; the route names it among its keys", async () => { const bad = await post({ slug: SLUG, ids: ["111"], articles: "yes" }); assert.equal(bad.status, 400); assert.match(bad.error, /"articles" must be a boolean/); const unknown = await post({ slug: SLUG, ids: ["111"], article: true }); assert.equal(unknown.status, 400); assert.match(unknown.error, /unknown key\(s\): article — this route accepts .*articles/); }); test("both halves off is nothing to do unless the articles are asked for by name", async () => { for (const articles of [undefined, false]) { const res = await post({ slug: SLUG, ids: ["111"], shots: false, media: false, articles }); assert.equal(res.status, 400, String(articles)); assert.match(res.error, /both the screenshot and the media are turned off/); } // Asked for: past that check, to the next refusal — an id not archived. const stray = await post({ slug: SLUG, ids: ["999"], shots: false, media: false, articles: true }); assert.equal(stray.status, 400); assert.equal(stray.error, "1 id(s) not in demo-x's posts archive: 999"); // No job was ever written. assert.deepEqual(await readdir(path.join(ROOT, ".jobs")).catch(() => []), []); }); // Moved from e2e/ops-api.spec.ts (release 19, A2b): nothing below reaches X — // every case is refused before a job exists. 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("capture-posts refuses what it cannot capture, and an id not in the archive — before any job", async () => { await channel("test-filter", { handling: "youtube", name: "Test Title Filter", url: "https://www.youtube.com/@example/videos", }); 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", }); // An X channel with an empty posts archive. await channel("example-x", { handling: "transcribe", sourceKind: "social", platform: "twitter", postFetcher: "x-gallery-dl", socialHandle: "example_user", name: "Example (X)", url: "https://x.com/example_user", }); const video = await post({ slug: "test-filter", ids: ["1"] }); assert.equal(video.status, 400); assert.equal(video.error, "test-filter is not a social channel."); const bsky = await post({ slug: "example-bsky", ids: ["1"] }); assert.equal(bsky.status, 400); assert.match(bsky.error, /cannot capture posts/); const neither = await post({ slug: "example-x", ids: ["1"], shots: false, media: false }); assert.equal(neither.status, 400); assert.match(neither.error, /both the screenshot and the media are turned off/); // The channel's posts archive is empty: every id is a stray, named. const stray = await post({ slug: "example-x", ids: ["111", "222"] }); assert.equal(stray.status, 400); assert.equal(stray.error, "2 id(s) not in example-x's posts archive: 111, 222"); // The body's shape. const noIds = await post({ slug: "example-x" }); assert.equal(noIds.status, 400); assert.match(noIds.error, /"ids" is required/); const badFlag = await post({ slug: "example-x", ids: ["1"], shots: "yes" }); assert.equal(badFlag.status, 400); assert.match(badFlag.error, /"shots" must be a boolean/); const unknown = await post({ slug: "example-x", ids: ["1"], limit: 5 }); assert.equal(unknown.status, 400); assert.match(unknown.error, /unknown key\(s\): limit/); assert.deepEqual(await readdir(path.join(ROOT, ".jobs")).catch(() => []), []); });