fix(config-backup): reuse the newest backup as the undo copy when nothing changed

Merge-time fix for #661. The pre-restore copy was forced, so a restore made
while the live files equalled the newest backup spent a retention slot on a
duplicate. It now goes through the normal change check: when nothing changed
since the newest backup, no copy is made and safetySnapshot names that
backup. Two tests that relied on the forced copy now change a file before the
restore; a new one pins the reuse.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-22 19:18:59 +02:00
1 parent 8a216df12f
commit ebb50ef53a
5 files changed
+36 -12

No files matched your search

+1
View File
@@ -51,6 +51,7 @@ describe('configBackupOrigin', () => {
host.name = 'container-b'; // the container was recreated
const origin = configBackupOrigin(data);
expect(listConfigBackups(backups, origin)).toHaveLength(2);
writeFileSync(join(data, 'settings.json'), '{"a":2}'); // a change, so the restore takes a pre-restore copy
expect(() => restoreConfigBackup({ dataDir: data, backupDir: backups, id: made.id })).not.toThrow();
const removed = pruneConfigBackups(backups, { keepCount: 1, keepDays: 0, origin });
expect(removed).toHaveLength(2); // 3 now: the restore took a pre-restore copy
+16
View File
@@ -346,6 +346,19 @@ describe('restoreConfigBackup', () => {
}
});
it('reuses the newest backup as the undo copy when the live files already equal it', () => {
put('settings.json', '{"a":1}');
const first = createConfigBackup({ dataDir: data, backupDir: backups, reason: 'auto', now: T0 }) as { id: string };
put('settings.json', '{"a":2}');
const newest = createConfigBackup({ dataDir: data, backupDir: backups, reason: 'auto', now: T0 + DAY }) as {
id: string;
};
const r = restoreConfigBackup({ dataDir: data, backupDir: backups, id: first.id, now: T0 + 2 * DAY });
expect(r.safetySnapshot).toBe(newest.id);
expect(listConfigBackups(backups).map((b) => b.id)).toEqual([newest.id, first.id]); // no duplicate
expect(readFileSync(join(data, 'settings.json'), 'utf8')).toBe('{"a":1}');
});
it('a file it cannot write does not stop the others, and the error names both lists', () => {
put('settings.json', '{"good":true}');
put('clis.json', '{"clis":{}}');
@@ -644,9 +657,12 @@ describe('restore trusts only what this module wrote', () => {
const origin = configBackupOrigin(data);
expect(latestBackupRetention(backups, origin)).toEqual({ keepCount: 100, keepDays: 0 });
const id = listConfigBackups(backups, origin).at(-1)!.id;
// A change since the newest backup, so the restore makes a pre-restore copy rather than reusing it.
put('settings.json', '{"a":3}');
const r = restoreConfigBackup({ dataDir: data, backupDir: backups, id, now: T0 + 2000 });
expect(r.safetySnapshot).toBeTruthy();
const pre = listConfigBackups(backups, origin).find((b) => b.id === r.safetySnapshot)!;
expect(pre.reason).toBe('pre-restore');
expect(JSON.parse(readFileSync(join(backups, pre.id, 'manifest.json'), 'utf8'))).toMatchObject({
keepCount: 100,
keepDays: 0,