Migration recovery backups no longer re-copy Folio-Backups (up to 21x bloat). bookChangedSince tracks visited dev:ino so a symlink loop in the book cannot wedge the main-process interval tick. Snapshot toggle check aligned to === false; drop dead home-shortening branch in Settings.
128 lines
4.7 KiB
JavaScript
128 lines
4.7 KiB
JavaScript
// Standalone test for continuous snapshot logic (src/main/backup.ts).
|
|
// Run with: node tests/run-snapshots-test.mjs
|
|
import fs from "fs";
|
|
import os from "os";
|
|
import path from "path";
|
|
import { bundleForTest } from "./helpers/bundle.mjs";
|
|
|
|
const { mod: project, cleanupBundle: cleanupProj } = await bundleForTest({
|
|
name: "folio-snap-project",
|
|
entryPoints: ["src/main/project.ts"],
|
|
});
|
|
const { mod: chapters, cleanupBundle: cleanupChap } = await bundleForTest({
|
|
name: "folio-snap-chapters",
|
|
entryPoints: ["src/main/chapters.ts"],
|
|
});
|
|
const { mod: backup, cleanupBundle: cleanupBackup } = await bundleForTest({
|
|
name: "folio-snap-backup",
|
|
entryPoints: ["src/main/backup.ts"],
|
|
});
|
|
|
|
const { createBook } = project;
|
|
const { createChapter, setChapterContent } = chapters;
|
|
const {
|
|
bindBackupSettings,
|
|
getSnapshotRoot,
|
|
listSnapshots,
|
|
createSnapshot,
|
|
pruneSnapshots,
|
|
bookChangedSince,
|
|
ensureRecoveryBackup,
|
|
getRecoveryRoot,
|
|
_resetSessionBackupGuard,
|
|
SNAPSHOT_DIR_NAME,
|
|
} = backup;
|
|
|
|
let failures = 0;
|
|
function ok(cond, msg) {
|
|
if (cond) console.log(" ok -", msg);
|
|
else { failures++; console.error("FAIL -", msg); }
|
|
}
|
|
|
|
// In-memory settings store (backup.ts stays Electron-free).
|
|
const settings = new Map();
|
|
bindBackupSettings({
|
|
get: (k, fb) => (settings.has(k) ? settings.get(k) : fb),
|
|
set: (k, v) => { settings.set(k, v); },
|
|
});
|
|
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "folio-snap-"));
|
|
createBook(dir, "Snap Book");
|
|
const c = createChapter(dir, "One");
|
|
setChapterContent(dir, c.id, "hello");
|
|
|
|
// default root = <book>/Folio-Backups
|
|
ok(getSnapshotRoot(dir) === path.join(dir, SNAPSHOT_DIR_NAME), "default snapshot root is per-book folder");
|
|
|
|
// snapshot creates a timestamped copy with content
|
|
const s1 = createSnapshot(dir, 20);
|
|
ok(typeof s1 === "string" && fs.existsSync(s1), "createSnapshot returns existing dir: " + s1);
|
|
ok(fs.existsSync(path.join(s1, "folio.json")), "snapshot contains folio.json");
|
|
ok(listSnapshots(dir).length === 1, "one snapshot listed");
|
|
|
|
// snapshots never nest: second snapshot must not contain the backup dir
|
|
await new Promise((r) => setTimeout(r, 5));
|
|
const s2 = createSnapshot(dir, 20);
|
|
ok(listSnapshots(dir).length === 2, "two snapshots listed");
|
|
ok(!fs.existsSync(path.join(s2, SNAPSHOT_DIR_NAME)), "snapshot does not contain nested backup dir");
|
|
|
|
// bookChangedSince: quiet -> false, touched -> true
|
|
ok(bookChangedSince(dir, Date.now()) === false, "no change since now");
|
|
setChapterContent(dir, c.id, "hello edited");
|
|
ok(bookChangedSince(dir, Date.now() - 1000) === true, "detects content change");
|
|
|
|
// pruning keeps newest N
|
|
await new Promise((r) => setTimeout(r, 5));
|
|
createSnapshot(dir, 20);
|
|
ok(listSnapshots(dir).length === 3, "three snapshots before prune");
|
|
pruneSnapshots(dir, 2);
|
|
const kept = listSnapshots(dir);
|
|
ok(kept.length === 2, "prune keeps newest 2");
|
|
ok(!fs.existsSync(s1), "oldest snapshot pruned");
|
|
|
|
// custom root honored
|
|
const custom = fs.mkdtempSync(path.join(os.tmpdir(), "folio-snaproot-"));
|
|
settings.set("snapshotLocation", custom);
|
|
ok(getSnapshotRoot(dir) === custom, "custom snapshot root honored");
|
|
const s3 = createSnapshot(dir, 20);
|
|
ok(s3.startsWith(custom), "snapshot lands in custom root");
|
|
settings.delete("snapshotLocation");
|
|
|
|
// missing dir -> null, no throw
|
|
ok(createSnapshot(path.join(dir, "nope"), 5) === null, "missing book returns null");
|
|
|
|
// symlink loop inside the book must not hang the change check
|
|
try {
|
|
fs.symlinkSync(dir, path.join(dir, "chapters", "loop"), "dir");
|
|
const t0 = Date.now();
|
|
const changed = bookChangedSince(dir, Date.now() + 60_000);
|
|
ok(changed === false && Date.now() - t0 < 5000, "symlink loop terminates without false positive");
|
|
} catch {
|
|
ok(true, "symlink loop test skipped (no symlink permission)");
|
|
}
|
|
|
|
// migration recovery backup excludes the snapshot dir (no 21x bloat)
|
|
const home = fs.mkdtempSync(path.join(os.tmpdir(), "folio-fakehome-"));
|
|
const origHome = process.env.HOME;
|
|
process.env.HOME = home;
|
|
try {
|
|
_resetSessionBackupGuard();
|
|
const dest = ensureRecoveryBackup(dir);
|
|
ok(typeof dest === "string" && fs.existsSync(dest), "recovery backup created");
|
|
ok(fs.existsSync(path.join(dest, "folio.json")), "recovery backup contains folio.json");
|
|
ok(!fs.existsSync(path.join(dest, SNAPSHOT_DIR_NAME)), "recovery backup excludes snapshots");
|
|
ok(getRecoveryRoot().startsWith(home), "recovery root honors HOME");
|
|
} finally {
|
|
process.env.HOME = origHome;
|
|
}
|
|
|
|
// cleanup
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
fs.rmSync(custom, { recursive: true, force: true });
|
|
fs.rmSync(home, { recursive: true, force: true });
|
|
cleanupProj();
|
|
cleanupChap();
|
|
cleanupBackup();
|
|
|
|
if (failures) { console.error(`${failures} snapshot check(s) FAILED`); process.exit(1); }
|
|
console.log("All snapshot checks passed.");
|