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: `` },
]);
});