fix(persistence): preserve authoritative state on write failures

Keep SQLite authoritative after completed migration even when legacy JSON is later invalid, reject non-object config documents before any migration state is written, and guard async secret masking by input generation. Persist renderer-facing config and queue mutations before updating memory so SQLite errors reject IPC calls and retain the last durable queue snapshot.
This commit is contained in:
Sucukdeluxe
2026-08-12 00:21:55 +02:00
parent 47be523b41
commit 6f02e9aa3a
8 changed files with 275 additions and 138 deletions
+36
View File
@@ -181,6 +181,42 @@ describe('migrateJsonToSqlite', () => {
expect(JSON.parse(language!.value)).toBe('de');
});
test('keeps SQLite config queue and secrets authoritative after a corrupted legacy config restart', () => {
const configPath = writeJson('config.json', { language: 'de', client_secret: 'persisted-secret' });
writeJson('download_queue.json', [{ id: 'q1', status: 'pending', title: 'Persisted queue item' }]);
let secrets = createSecretStore(db, new MemorySecureStorage());
migrateJsonToSqlite({ db, appDataDir, secrets });
db.close();
db = openDatabase(path.join(tmpDir, 'app.db'));
secrets = createSecretStore(db, new MemorySecureStorage());
fs.writeFileSync(configPath, '{ no longer valid JSON', 'utf-8');
const restart = migrateJsonToSqlite({ db, appDataDir, secrets });
expect(restart).toMatchObject({ alreadyApplied: true, errors: [] });
expect(JSON.parse(db.get<{ value: string }>('SELECT value FROM config_kv WHERE key = ?', ['language'])!.value)).toBe('de');
expect(db.all<{ id: string }>('SELECT id FROM queue_items ORDER BY queue_position')).toEqual([{ id: 'q1' }]);
expect(secrets.get('twitch_client_secret')).toBe('persisted-secret');
});
test.each([
['null', null],
['false', false],
['zero', 0],
['empty string', ''],
])('does not mark or partially migrate a syntactically valid but invalid %s config', (_, invalidConfig) => {
writeJson('config.json', invalidConfig);
writeJson('download_queue.json', [{ id: 'q1', status: 'pending' }]);
const result = migrateJsonToSqlite({ db, appDataDir });
expect(result.errors).toEqual([{ source: 'config.json', message: 'Config JSON must be an object' }]);
expect(db.all('SELECT * FROM config_kv')).toEqual([]);
expect(db.all('SELECT * FROM queue_items')).toEqual([]);
expect(db.get('SELECT name FROM migrations_applied WHERE name = ?', ['authoritative-state-v1'])).toBeUndefined();
});
test('does not let the former shadow-migration marker skip authoritative secret import', () => {
const configPath = writeJson('config.json', { language: 'de', client_secret: 'legacy-secret' });
db.run('INSERT INTO migrations_applied(name, payload) VALUES (?, ?)', ['v4-to-v5-jsons', '{}']);