import test from "node:test"; import assert from "node:assert/strict"; import { mkdtemp, readFile, rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { callPost, setupOpsCorpus } from "./_testCorpus"; import { coercedLeaves, settingsPatchProblem } from "./settings/patch"; // Run with: // pnpm -C editor exec tsx --test "app/api/ops/_writes.test.ts" // // The archival writes (release 19, A4), against a temp corpus: the settings // patch end to end (its write needs no revalidation), and every other route's // refusals — the ones answered before an action revalidates a page, which only // a running server can do. Nothing here starts a job. const corpus = await setupOpsCorpus("one-youtube-channel-with-data"); type Post = Parameters[0]; const route = async (name: string) => (await import(`./${name}/route`)).POST as Post; const SLUG = "test-youtube"; const VIDEO = "20240101_test1234567"; const elsewhere = await mkdtemp(path.join(os.tmpdir(), "ops-relocate-root-")); test.after(async () => { await corpus.cleanup(); await rm(elsewhere, { recursive: true, force: true }); }); const readSettingsFile = async () => JSON.parse(await readFile(corpus.settingsFile, "utf8")) as Record; test("settings: an unknown key, an owned key and a coerced value are refused, and nothing is written", async () => { const settings = await route("settings"); const before = await readSettingsFile(); const unknown = await callPost(settings, { patch: { minFreeDiskGb: 5 } }); assert.equal(unknown.status, 400); assert.match(String(unknown.body.error), /unknown settings key\(s\): minFreeDiskGb/); const owned = await callPost(settings, { patch: { channelPriority: {} } }); assert.equal(owned.status, 400); assert.match(String(owned.body.error), /"channelPriority" is not patched here: .*pnpm ops channel-priority/); // The schema clamps rather than throwing: a value it would not keep as sent // is named with what it would have saved. const clamped = await callPost(settings, { patch: { minFreeDiskGB: -5 } }); assert.equal(clamped.status, 400); assert.match(String(clamped.body.error), /minFreeDiskGB: sent -5, would be saved as \d+/); const empty = await callPost(settings, { patch: {} }); assert.equal(empty.status, 400); const notObject = await callPost(settings, { patch: [] }); assert.equal(notObject.status, 400); assert.deepEqual(await readSettingsFile(), before); }); test("settings: a valid patch is written through saveSettings and answered with the saved value", async () => { const settings = await route("settings"); const res = await callPost(settings, { patch: { minFreeDiskGB: 7, syncScheduler: { fullSweepIntervalMinutes: 0 } }, }); assert.equal(res.status, 200, JSON.stringify(res.body)); assert.deepEqual(res.body.changed, ["minFreeDiskGB", "syncScheduler"]); const value = res.body.value as Record; assert.equal(value.minFreeDiskGB, 7); const file = await readSettingsFile(); assert.equal(file.minFreeDiskGB, 7); // One level merged: the block's other keys are kept, not dropped. assert.equal((file.syncScheduler as Record).fullSweepIntervalMinutes, 0); await corpus.writeSettings({ minFreeDiskGB: 0 }); }); test("the patch rule: leaves compared, arrays element by element, absent keys not compared", () => { assert.deepEqual(coercedLeaves({ a: 1, b: { c: "x" } }, { a: 1, b: { c: "x", d: 2 } }, "k"), []); assert.deepEqual(coercedLeaves({ b: { c: "x" } }, { b: { c: "y" } }, "k"), [ 'k.b.c: sent "x", would be saved as "y"', ]); assert.deepEqual(coercedLeaves([1, 2], [1], "k"), ["k: sent [1,2], would be saved as [1]"]); assert.equal(settingsPatchProblem({ a: 1 }, ["a"], { a: 1 }), null); }); test("lane: the publish lane is a lane; an unknown one is refused by the full list", async () => { const lane = await route("lane"); const bogus = await callPost(lane, { lane: "transcode", held: true }); assert.equal(bogus.status, 400); assert.match(String(bogus.body.error), /transcription, download, digest, backfill, publish/); // Switched off: the publish page's Start refuses, and so does this. const start = await callPost(lane, { lane: "publish", action: "start" }); assert.equal(start.status, 400, JSON.stringify(start.body)); assert.ok(String(start.body.error).length > 0); }); test("clear-platform-hold refuses a name that is not a platform", async () => { const res = await callPost(await route("clear-platform-hold"), { platform: "you tube/../x" }); assert.equal(res.status, 400); assert.match(String(res.body.error), /^Not a platform: /); const none = await callPost(await route("clear-platform-hold"), {}); assert.equal(none.status, 400); }); test("workers: the op and the ids are checked at the door", async () => { const workers = await route("workers"); const op = await callPost(workers, { op: "restart", ids: ["a"] }); assert.equal(op.status, 400); assert.match(String(op.body.error), /"op" must be one of enable, disable, drain/); const ids = await callPost(workers, { op: "enable", ids: [] }); assert.equal(ids.status, 400); }); test("transcribe-one, delete-file and do-not-clean refuse before anything is touched", async () => { const one = await route("transcribe-one"); const traversing = await callPost(one, { slug: SLUG, id: VIDEO, file: "../audio.mp3" }); assert.equal(traversing.status, 400); assert.match(String(traversing.body.error), /is not a file name in the video's directory/); const noChannel = await callPost(one, { slug: "no-such", id: VIDEO }); assert.equal(noChannel.status, 400); assert.match(String(noChannel.body.error), /no-such/); const del = await route("delete-file"); const suspicious = await callPost(del, { slug: SLUG, id: VIDEO, file: "../config.json" }); assert.equal(suspicious.status, 400); assert.match(String(suspicious.body.error), /Refusing to delete suspicious filename/); const noFile = await callPost(del, { slug: SLUG, id: VIDEO }); assert.equal(noFile.status, 400); assert.match(String(noFile.body.error), /"file" is required/); const keep = await route("do-not-clean"); const missing = await callPost(keep, { slug: SLUG, id: "not-a-video" }); assert.equal(missing.status, 400); assert.equal(missing.body.error, "Video directory not found: not-a-video"); const badKeep = await callPost(keep, { slug: SLUG, id: VIDEO, keep: "yes" }); assert.equal(badKeep.status, 400); assert.match(String(badKeep.body.error), /"keep" must be a boolean/); assert.deepEqual(await corpus.listJobIds(), []); }); test("cleanup refuses an unknown sweep and a traversing slug, before any job", async () => { const cleanup = await route("cleanup"); const sweep = await callPost(cleanup, { slug: SLUG, sweep: "everything" }); assert.equal(sweep.status, 400); assert.match( String(sweep.body.error), /"sweep" must be one of transcribed, extra-formats, wrong-format, failed-transcriptions/, ); const slug = await callPost(cleanup, { slug: "../x", sweep: "transcribed" }); assert.equal(slug.status, 400); assert.deepEqual(await corpus.listJobIds(), []); }); test("relocate dryRun answers each channel's preview and moves nothing", async () => { const relocate = await route("relocate"); const res = await callPost(relocate, { slugs: [SLUG, "no-such"], root: elsewhere, dryRun: true }); assert.equal(res.status, 200, JSON.stringify(res.body)); assert.equal(res.body.dryRun, true); const [ok, missing] = res.body.previews as { slug: string; preview?: unknown; error?: string }[]; assert.equal(ok.slug, SLUG); assert.ok(ok.preview, JSON.stringify(ok)); assert.equal(missing.slug, "no-such"); assert.ok(missing.error); // No move: the config names no media dir, and no job exists. const config = JSON.parse( await readFile(path.join(corpus.transcripts, "channels", SLUG, "config.json"), "utf8"), ) as { mediaDir?: string }; assert.equal(config.mediaDir, undefined); assert.deepEqual(await corpus.listJobIds(), []); const both = await callPost(relocate, { slugs: [SLUG], root: elsewhere, locationId: "x", dryRun: true }); assert.equal(both.status, 400); });