import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { test, after } from "node:test"; import assert from "node:assert/strict"; // Run with: pnpm -C editor exec tsx --test "app/**/*.test.ts" // // THE MERGE RULE of the editor's one settings writer: ONE LEVEL DEEP. An object // block merges over the stored block's keys; arrays and scalars replace; an // object nested INSIDE a block replaces. Same rule as editor/e2e/helpers.ts. // // SETTINGS SEAM as in common/lib/settingsWrite.test.ts: getPaths() memoizes, so // SETTINGS_FILE is set before the module under test is imported. const ROOT = mkdtempSync(path.join(os.tmpdir(), "save-settings-")); process.env.TRANSCRIPTS_DIR = ROOT; process.env.SETTINGS_FILE = path.join(ROOT, "settings.json"); const { mergeSettingsPatch, saveSettings } = await import("./saveSettings"); const { defaultSiteSettings, getSettings } = await import( "yt-dlp-transcript-common/lib/settings" ); after(() => rm(ROOT, { recursive: true, force: true })); test("an object block merges over the stored block, one level", () => { const base = defaultSiteSettings(); base.syncScheduler.maxConcurrentSyncs = 7; const out = mergeSettingsPatch(base, { syncScheduler: { enabled: true } as typeof base.syncScheduler, }); assert.equal(out.syncScheduler.enabled, true); assert.equal(out.syncScheduler.maxConcurrentSyncs, 7); // The input is not mutated. assert.equal(base.syncScheduler.enabled, false); }); test("arrays and scalars replace", () => { const base = defaultSiteSettings(); base.socialLinks = [{ label: "a", url: "/a", svg: "" }]; const out = mergeSettingsPatch(base, { socialLinks: [], adminTitle: "X" }); assert.deepEqual(out.socialLinks, []); assert.equal(out.adminTitle, "X"); assert.equal(out.minFreeDiskGB, base.minFreeDiskGB); }); test("an object nested inside a block replaces, it is not merged", () => { const base = defaultSiteSettings(); base.digest.apps = { a: { model: "m1", numCtx: 4096 } }; base.channelPriority.channels = { keep: { tier: "low" } }; const out = mergeSettingsPatch(base, { digest: { apps: { b: { model: "m2" } } } as unknown as typeof base.digest, channelPriority: { focus: { kind: "none" }, channels: { other: { tier: "paused" } }, }, }); assert.deepEqual(out.digest.apps, { b: { model: "m2" } }); assert.equal(out.digest.localAppId, base.digest.localAppId); assert.deepEqual(out.channelPriority.channels, { other: { tier: "paused" } }); }); // A LOCATION WRITE KEEPS THE DRIVE-HEALTH TIMINGS. The /storage location // actions patch `storage` with `{ locations, defaultLocationId }` only; the // one-level merge is what keeps `storage.health` (and `savedVideosLocationId`, // which an add once erased before slice 4a). test("a storage patch of the locations keeps storage.health", () => { const base = defaultSiteSettings(); base.storage = { ...base.storage, savedVideosLocationId: "cold", health: { budgetMs: 4_000, inFlightPerLocation: 2 }, }; const out = mergeSettingsPatch(base, { storage: { locations: [{ id: "cold", label: "Cold", root: "/mnt/cold", autoRepoint: false }], defaultLocationId: "cold", }, }); assert.deepEqual(out.storage.health, { budgetMs: 4_000, inFlightPerLocation: 2 }); assert.equal(out.storage.savedVideosLocationId, "cold"); assert.equal(out.storage.locations.length, 1); }); test("saveSettings writes the merged result and touches nothing else", async () => { writeFileSync( process.env.SETTINGS_FILE!, JSON.stringify({ adminTitle: "Kept", minFreeDiskGB: 9 }), ); await saveSettings({ minFreeDiskGB: 3, backfill: { concurrency: 4 } as never }); const s = getSettings(); assert.equal(s.adminTitle, "Kept"); assert.equal(s.minFreeDiskGB, 3); assert.equal(s.backfill.concurrency, 4); assert.equal(s.backfill.allowRedownload, false); const onDisk = JSON.parse(readFileSync(process.env.SETTINGS_FILE!, "utf8")); assert.equal(onDisk.adminTitle, "Kept"); }); // A STORED ICON THE CHECKER NOW REFUSES does not block an unrelated save: an // older build stored it, and a lane pause or a title must still save. The link // is written back byte-identical; only a NEW or EDITED icon is checked. test("an icon stored by an older build survives unrelated saves; editing it is checked", async () => { const old = ``; const good = ``; const stored = { label: "Old", url: "https://old.example", svg: old }; writeFileSync(process.env.SETTINGS_FILE!, JSON.stringify({ socialLinks: [stored] })); const linkOnDisk = () => (JSON.parse(readFileSync(process.env.SETTINGS_FILE!, "utf8")) as { socialLinks: unknown[] }).socialLinks; await saveSettings({ adminTitle: "Renamed" }); await saveSettings({ minFreeDiskGB: 2 }); const s = getSettings(); // A lane pause, the way the editor's pause control writes it. const lane = Object.keys(s.autoQueue)[0] as keyof typeof s.autoQueue; await saveSettings({ autoQueue: { ...s.autoQueue, [lane]: { ...s.autoQueue[lane], held: true } } }); await saveSettings({ channelPriority: { ...s.channelPriority, channels: { some: { tier: "paused" } } }, }); assert.deepEqual(linkOnDisk(), [stored], "byte-identical after four unrelated saves"); // Edited to something still refused: the save is refused, with the reason. await assert.rejects( saveSettings({ socialLinks: [{ ...stored, svg: `` }] }), /Social link "Old" has an invalid SVG: it has an element an icon has no use for \(style\)/, ); assert.deepEqual(linkOnDisk(), [stored]); // Edited to a good icon: saved, normalized. await saveSettings({ socialLinks: [{ ...stored, svg: good }] }); assert.deepEqual(linkOnDisk(), [ { ...stored, svg: `` }, ]); });