mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
fix(config-backup): retention survives an unreadable or reset settings.json, restore works on mode-less filesystems, compare checks snapshots too
This commit is contained in:
1 parent
c5f1e99036
commit
c982fa1ce5
6 files changed
+215
-22
No files matched your search
@@ -526,6 +526,30 @@ describe('restore trusts only what this module wrote', () => {
|
||||
expect(listConfigBackups(backups)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('skips the group/world bit test on a filesystem that does not keep modes, but never the symlink test', () => {
|
||||
put('settings.json', '{"a":1}');
|
||||
put('clis.json', '{"clis":{}}');
|
||||
const { id } = made();
|
||||
const file = join(backups, id, 'clis.json');
|
||||
chmodSync(file, 0o644); // what vfat, CIFS and WSL /mnt/d report for every file
|
||||
expect(() => restoreConfigBackup({ dataDir: data, backupDir: backups, id, keepsModes: false })).not.toThrow();
|
||||
|
||||
const real = join(root, 'elsewhere.json');
|
||||
writeFileSync(real, '{"clis":{}}');
|
||||
rmSync(file);
|
||||
symlinkSync(real, file);
|
||||
expect(() => restoreConfigBackup({ dataDir: data, backupDir: backups, id, keepsModes: false })).toThrow(/symlink/);
|
||||
});
|
||||
|
||||
it('compare applies the same check to a snapshot file', () => {
|
||||
put('settings.json', '{"a":1}');
|
||||
const { id } = made();
|
||||
chmodSync(join(backups, id, 'settings.json'), 0o644);
|
||||
expect(() => diffConfigBackups({ dataDir: data, backupDir: backups, from: id, to: 'current' })).toThrow(
|
||||
ConfigBackupCorruptError
|
||||
);
|
||||
});
|
||||
|
||||
it('refuses a snapshot folder that others can write to', () => {
|
||||
put('settings.json', '{"a":1}');
|
||||
const { id } = made();
|
||||
|
||||
@@ -9,10 +9,10 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { createRouteTestHarness } from './_route-test-utils.js';
|
||||
import { registerConfigBackupRoutes } from '../../src/web/routes/config-backup-routes.js';
|
||||
import { registerConfigBackupRoutes, runConfigBackup } from '../../src/web/routes/config-backup-routes.js';
|
||||
import { SETTINGS_PATH } from '../../src/web/route-helpers.js';
|
||||
import { getDataDir } from '../../src/config/instance.js';
|
||||
import { defaultConfigBackupDir } from '../../src/config-backup.js';
|
||||
import { createConfigBackup, defaultConfigBackupDir, listConfigBackups } from '../../src/config-backup.js';
|
||||
import { intentStore } from '../../src/intent-store.js';
|
||||
|
||||
let custom: string;
|
||||
@@ -183,3 +183,61 @@ describe('GET /api/config-backups/diff', () => {
|
||||
expect(missing.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('retention survives an unreadable or reset settings.json', () => {
|
||||
const DAY = 86_400_000;
|
||||
const BASE = Date.UTC(2025, 0, 1);
|
||||
const dir = () => defaultConfigBackupDir(getDataDir());
|
||||
/** 30 snapshots a day apart, made under a long-history retention (100 copies, no age limit). */
|
||||
function seed() {
|
||||
settings({ configBackupKeepCount: 100, configBackupKeepDays: 0 });
|
||||
for (let i = 0; i < 30; i++) {
|
||||
writeFileSync(
|
||||
SETTINGS_PATH,
|
||||
JSON.stringify({ displayName: `v${i}`, configBackupKeepCount: 100, configBackupKeepDays: 0 })
|
||||
);
|
||||
createConfigBackup({
|
||||
dataDir: getDataDir(),
|
||||
backupDir: dir(),
|
||||
reason: 'auto',
|
||||
now: BASE + i * DAY,
|
||||
keepCount: 100,
|
||||
keepDays: 0,
|
||||
});
|
||||
}
|
||||
expect(listConfigBackups(dir())).toHaveLength(30);
|
||||
}
|
||||
|
||||
it('a settings.json that does not parse still gets a snapshot but prunes nothing', async () => {
|
||||
seed();
|
||||
writeFileSync(SETTINGS_PATH, '{ "displayName": "half-written", }');
|
||||
const out = await runConfigBackup('auto');
|
||||
expect(out?.result.status).toBe('created');
|
||||
expect(out?.removed).toEqual([]);
|
||||
expect(listConfigBackups(dir())).toHaveLength(31);
|
||||
});
|
||||
|
||||
it('a reset settings.json prunes with the retention the newest snapshot recorded, not the defaults', async () => {
|
||||
seed();
|
||||
writeFileSync(SETTINGS_PATH, '{}');
|
||||
const out = await runConfigBackup('auto');
|
||||
expect(out?.result.status).toBe('created');
|
||||
// aged manifests (a year old) and keepDays 0: the defaults (20 copies, 30 days) would leave 20
|
||||
expect(listConfigBackups(dir())).toHaveLength(31);
|
||||
});
|
||||
|
||||
it('a settings.json that sets the retention still wins', async () => {
|
||||
seed();
|
||||
writeFileSync(SETTINGS_PATH, JSON.stringify({ configBackupKeepCount: 5, configBackupKeepDays: 0 }));
|
||||
await runConfigBackup('auto');
|
||||
expect(listConfigBackups(dir())).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('listing never creates config-backup-id', async () => {
|
||||
const { app } = await createRouteTestHarness(registerConfigBackupRoutes);
|
||||
const idFile = join(getDataDir(), 'config-backup-id');
|
||||
rmSync(idFile, { force: true });
|
||||
expect((await app.inject({ method: 'GET', url: '/api/config-backups' })).json().data.backups).toEqual([]);
|
||||
expect(existsSync(idFile)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user