Files
immich-folio/lib/__tests__/backup-prune.test.ts
Ralksta a3a6319c29 release: v0.16.0
See CHANGELOG.md for the full v0.16.0 entry.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Ralf <rohlenbo79@gmail.com>
Co-authored-by: Paul Thies <110185070+ImScheinox@users.noreply.github.com>
Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com>
2026-09-22 14:12:13 +02:00

112 lines
4.4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import os from 'os';
import path from 'path';
import { mkdtemp, mkdir, readdir, rm, writeFile } from 'fs/promises';
/**
* Pre-restore snapshots were exempt from pruning while still being listed, so
* every restore added a row that was never removed. After a dozen restores the
* backup modal showed nothing but pre-restore entries and the ordinary saves
* had dropped off the end.
*
* They are still counted separately from saves — ten saves must not push out
* the one snapshot that undoes a restore — but each kind is now capped.
*
* The service resolves content/ from process.cwd() at import time, so each test
* points cwd at a fresh temp directory and re-imports it.
*/
let root: string;
let contentDir: string;
let backupDir: string;
let service: typeof import('../admin/yaml-service');
const stamp = (n: number) => `2026-05-${String(n).padStart(2, '0')}T12-00-00-000Z`;
beforeEach(async () => {
root = await mkdtemp(path.join(os.tmpdir(), 'folio-prune-'));
contentDir = path.join(root, 'content');
backupDir = path.join(contentDir, '.backups');
await mkdir(backupDir, { recursive: true });
vi.spyOn(process, 'cwd').mockReturnValue(root);
vi.spyOn(console, 'log').mockImplementation(() => {});
vi.resetModules();
service = await import('../admin/yaml-service');
});
afterEach(async () => {
vi.restoreAllMocks();
await rm(root, { recursive: true, force: true });
});
const backups = async (kind?: 'pre-restore') => {
const files = (await readdir(backupDir)).filter((f) => f.startsWith('settings.yaml'));
return kind
? files.filter((f) => f.includes(kind))
: files.filter((f) => !f.includes('pre-restore'));
};
describe('backup pruning', () => {
it('caps pre-restore snapshots instead of letting them accumulate', async () => {
await writeFile(path.join(contentDir, 'settings.yaml'), 'title: Folio\n');
for (let i = 1; i <= 14; i++) {
await writeFile(path.join(backupDir, `settings.yaml.${stamp(i)}.pre-restore.bak`), 'old');
}
await service.writeSettingsYaml({ title: 'Folio' });
expect(await backups('pre-restore')).toHaveLength(10);
});
it('keeps the newest pre-restore snapshots, not the oldest', async () => {
await writeFile(path.join(contentDir, 'settings.yaml'), 'title: Folio\n');
for (let i = 1; i <= 14; i++) {
await writeFile(path.join(backupDir, `settings.yaml.${stamp(i)}.pre-restore.bak`), 'old');
}
await service.writeSettingsYaml({ title: 'Folio' });
const kept = await backups('pre-restore');
expect(kept).toContain(`settings.yaml.${stamp(14)}.pre-restore.bak`);
expect(kept).not.toContain(`settings.yaml.${stamp(1)}.pre-restore.bak`);
});
it('does not let saves push out the snapshot that undoes a restore', async () => {
await writeFile(path.join(contentDir, 'settings.yaml'), 'title: Folio\n');
await writeFile(path.join(backupDir, `settings.yaml.${stamp(1)}.pre-restore.bak`), 'the undo');
for (let i = 2; i <= 13; i++) {
await writeFile(path.join(backupDir, `settings.yaml.${stamp(i)}.bak`), 'a save');
}
await service.writeSettingsYaml({ title: 'Folio' });
// Saves are capped, but the lone pre-restore snapshot survives them.
expect(await backups()).toHaveLength(10);
expect(await backups('pre-restore')).toEqual([`settings.yaml.${stamp(1)}.pre-restore.bak`]);
});
it('prunes on restore, without waiting for the next save', async () => {
await writeFile(path.join(contentDir, 'settings.yaml'), 'title: current\n');
await writeFile(path.join(backupDir, `settings.yaml.${stamp(1)}.bak`), 'title: old\n');
for (let i = 2; i <= 13; i++) {
await writeFile(path.join(backupDir, `settings.yaml.${stamp(i)}.pre-restore.bak`), 'old');
}
await service.restoreBackup(`settings.yaml.${stamp(1)}.bak`);
// The restore adds one of its own, and the cap holds all the same.
expect(await backups('pre-restore')).toHaveLength(10);
});
it('leaves the other file backups alone', async () => {
await writeFile(path.join(contentDir, 'settings.yaml'), 'title: Folio\n');
for (let i = 1; i <= 14; i++) {
await writeFile(path.join(backupDir, `gallery.yaml.${stamp(i)}.pre-restore.bak`), 'old');
}
await service.writeSettingsYaml({ title: 'Folio' });
const gallery = (await readdir(backupDir)).filter((f) => f.startsWith('gallery.yaml'));
expect(gallery).toHaveLength(14);
});
});