import { cp, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; // A TEST CORPUS FOR THE OPS ROUTES' UNIT TESTS — imported ONLY by // `*.test.ts` files under app/api/ops. Never by a route. // // The refusals an ops route answers before any job are request/response and // nothing else, so they are tested here, in a tsx process, rather than in // e2e/ops-api.spec.ts against a running server (release 19, A2b): the same // e2e fixtures (`e2e/fixtures/test-transcripts/`), copied into a temp // dir; the same default test settings; and the same fake binaries, so a // refusal that regresses and starts a job runs a fake, never yt-dlp, next or // wrangler. // // The environment is set by `setupOpsCorpus`, which MUST run before the first // import of a route or of anything that calls getPaths() (it caches). // Each test file is its own process under `tsx --test`, so each gets its own. export const TOKEN = "test-token"; const EDITOR = path.resolve(/* turbopackIgnore: true */ import.meta.dirname, "..", "..", ".."); const FIXTURES = path.join(/* turbopackIgnore: true */ EDITOR, "e2e", "fixtures"); const BIN = path.join(/* turbopackIgnore: true */ FIXTURES, "bin"); export type OpsCorpus = { root: string; transcripts: string; settingsFile: string; // Settings as e2e's writeSettings writes them: the default test settings // with `patch` laid over, one level deep. writeSettings: (patch?: Record) => Promise; writeChannelConfig: (slug: string, config: Record) => Promise; writeSite: (siteId: string, site?: Record) => Promise; // The job sidecars on disk — "and nothing was queued". listJobIds: () => Promise; cleanup: () => Promise; }; export async function setupOpsCorpus(fixture: string | null): Promise { const root = await mkdtemp(path.join(os.tmpdir(), "ops-route-")); const transcripts = path.join(root, "test-transcripts"); const settingsFile = path.join(root, "test-settings.json"); await mkdir(transcripts, { recursive: true }); if (fixture) { await cp(path.join(/* turbopackIgnore: true */ FIXTURES, "test-transcripts", fixture), transcripts, { recursive: true, }); } Object.assign(process.env, { WORKER_TOKEN: TOKEN, TRANSCRIPTS_DIR: transcripts, EXPORT_PUBLIC_DIR: path.join(transcripts, ".export-public"), SETTINGS_FILE: settingsFile, YTDLP_BIN: path.join(/* turbopackIgnore: true */ BIN, "fake-ytdlp.mjs"), GALLERY_DL_BIN: path.join(/* turbopackIgnore: true */ BIN, "fake-gallery-dl.mjs"), FFMPEG_BIN: path.join(/* turbopackIgnore: true */ BIN, "fake-ffmpeg.mjs"), FFPROBE_BIN: path.join(/* turbopackIgnore: true */ BIN, "fake-ffprobe.mjs"), WHISPER_BIN: path.join(/* turbopackIgnore: true */ BIN, "fake-whisper.mjs"), WHISPER_MODEL: "/dev/null", CHOUGH_BIN: path.join(/* turbopackIgnore: true */ BIN, "fake-chough.mjs"), CHOUGH_MODEL: "/dev/null", PARAKEET_STITCH_BIN: path.join(/* turbopackIgnore: true */ BIN, "fake-parakeet-stitch.mjs"), PARAKEET_CLI: "/dev/null", PARAKEET_MODEL: "/dev/null", DIARIZE_BIN: path.join(/* turbopackIgnore: true */ BIN, "fake-diarize.mjs"), CLAUDE_BIN: path.join(/* turbopackIgnore: true */ BIN, "fake-claude.mjs"), FINDMNT_BIN: path.join(/* turbopackIgnore: true */ BIN, "fake-findmnt.mjs"), UDISKSCTL_BIN: path.join(/* turbopackIgnore: true */ BIN, "fake-udisksctl.mjs"), WRANGLER_BIN: path.join(/* turbopackIgnore: true */ BIN, "fake-wrangler.mjs"), EXPORT_NEXT_BIN: path.join(/* turbopackIgnore: true */ BIN, "fake-next.mjs"), ARCHILYZER_BRANCH: "main", CLOUDFLARE_API_TOKEN: "e2e-fake-token-never-sent", E2E_LIVE_CHECK: "skip", }); const writeSettings = async (patch: Record = {}) => { const base = JSON.parse( await readFile(path.join(/* turbopackIgnore: true */ FIXTURES, "test-settings.default.json"), "utf8"), ) as Record; const merged: Record = { ...base, ...patch }; for (const [k, v] of Object.entries(patch)) { const b = base[k]; if (isObject(v) && isObject(b)) merged[k] = { ...b, ...v }; } await writeFile(settingsFile, JSON.stringify(merged, null, 2)); }; await writeSettings({ minFreeDiskGB: 0 }); return { root, transcripts, settingsFile, writeSettings, async writeChannelConfig(slug, config) { const dir = path.join(transcripts, "channels", slug); await mkdir(dir, { recursive: true }); await writeFile( path.join(dir, "config.json"), JSON.stringify({ handling: "youtube", name: slug, ...config }, null, 2), ); }, async writeSite(siteId, site = {}) { const dir = path.join(transcripts, "sites", siteId); await mkdir(dir, { recursive: true }); await writeFile( path.join(dir, "site.json"), JSON.stringify( { siteId, siteTitle: siteId, siteDescription: "", headerTitle: siteId, homeTagline: "", groups: [{ id: "default", name: "All channels", selectedByDefault: true }], defaultGroupId: "default", channels: [], ...site, }, null, 2, ), ); }, async listJobIds() { return (await readdir(path.join(transcripts, ".jobs")).catch(() => [])) .filter((f) => f.endsWith(".meta.json")) .sort(); }, cleanup: () => rm(root, { recursive: true, force: true }), }; } function isObject(v: unknown): v is Record { return typeof v === "object" && v !== null && !Array.isArray(v); } type Handler = (request: Request, ctx?: never) => Promise; export type OpsRes = { status: number; body: Record & { error?: string } }; // POST a JSON body to a route handler, with the token unless told otherwise. export async function callPost( handler: Handler, body: unknown, headers: Record = { authorization: `Bearer ${TOKEN}` }, ): Promise { const res = await handler( new Request("http://localhost/api/ops/x", { method: "POST", headers: { ...headers, "content-type": "application/json" }, body: JSON.stringify(body), }), ); return { status: res.status, body: (await res.json()) as OpsRes["body"] }; } // GET a route handler. `params` for a dynamic segment ([slug], [id]). export async function callGet( handler: (request: Request, ctx: { params: Promise> }) => Promise, url = "http://localhost/api/ops/x", params: Record = {}, headers: Record = { authorization: `Bearer ${TOKEN}` }, ): Promise { const res = await handler(new Request(url, { headers }), { params: Promise.resolve(params), }); return { status: res.status, body: (await res.json()) as OpsRes["body"] }; } // HOLD THE PUBLISH QUEUE with a fabricated running job, as e2e's // holdPublishQueue does: a build or deploy stage a regressed refusal failed to // stop can then only QUEUE, never spawn. Writes no sidecar, so listJobIds does // not see it. export async function holdPublishQueue(): Promise { const { getRegistry, newJobId } = await import("yt-dlp-transcript-common/jobs/registry"); const record = { id: newJobId(), kind: "publish-hold", queueKey: "publish", status: "queued" as const, queuedAt: Date.now(), logPath: "/dev/null", }; getRegistry().register(record); getRegistry().enqueue(record, { start: () => {}, onCancel: () => {} }); }