mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 10:00:17 +05:30
fix(migration): handle already-v5 settings and ensure 'failed' branch writes v5 marker
This commit is contained in:
@@ -25,6 +25,10 @@ class MigrationRunner {
|
|||||||
return 'migrated';
|
return 'migrated';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[migration-runner] transform failed:', err.message);
|
console.error('[migration-runner] transform failed:', err.message);
|
||||||
|
// Back up the original and write v5 marker so future launches skip migration.
|
||||||
|
// Without this, every launch would fail again and the user stays on defaults.
|
||||||
|
fs.copyFileSync(this.file, this.backup);
|
||||||
|
fs.writeFileSync(this.file, JSON.stringify({ ...raw, 'migration.version': 5 }, null, 2));
|
||||||
return 'failed';
|
return 'failed';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,15 @@ const v4SettingsSchema = z.object({
|
|||||||
snippets: z.array(z.unknown()).default([]),
|
snippets: z.array(z.unknown()).default([]),
|
||||||
}).passthrough();
|
}).passthrough();
|
||||||
|
|
||||||
|
const v5OnlyFields = ['updateChannel', 'autoCheckUpdates', 'firstRun'];
|
||||||
|
|
||||||
|
function isAlreadyV5(data) {
|
||||||
|
if (!data || typeof data !== 'object') return false;
|
||||||
|
if (data['migration.version'] === 5) return true;
|
||||||
|
// Check for v5-only fields — a v4 file would never have these
|
||||||
|
return v5OnlyFields.some((f) => f in data);
|
||||||
|
}
|
||||||
|
|
||||||
const v5SettingsShape = {
|
const v5SettingsShape = {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
tabSize: 4,
|
tabSize: 4,
|
||||||
@@ -39,6 +48,12 @@ const v5SettingsShape = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function migrateV4ToV5(v4) {
|
function migrateV4ToV5(v4) {
|
||||||
|
// If data already looks like v5 (has migration.version=5 or v5-only fields),
|
||||||
|
// return it as-is so the runner can mark it done without re-migrating.
|
||||||
|
// This handles the case where a buggy v5 run wrote v5 fields without the marker.
|
||||||
|
if (isAlreadyV5(v4)) {
|
||||||
|
return v4;
|
||||||
|
}
|
||||||
const parsed = v4SettingsSchema.parse(v4 || {});
|
const parsed = v4SettingsSchema.parse(v4 || {});
|
||||||
return {
|
return {
|
||||||
...v5SettingsShape,
|
...v5SettingsShape,
|
||||||
|
|||||||
@@ -10,7 +10,16 @@ export const v4SettingsSchema = z.object({
|
|||||||
snippets: z.array(z.unknown()).default([]),
|
snippets: z.array(z.unknown()).default([]),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const v5OnlyFields = ['updateChannel', 'autoCheckUpdates', 'firstRun'];
|
||||||
|
|
||||||
|
function isAlreadyV5(data: unknown): boolean {
|
||||||
|
if (!data || typeof data !== 'object') return false;
|
||||||
|
if ((data as Record<string, unknown>)['migration.version'] === 5) return true;
|
||||||
|
return v5OnlyFields.some((f) => f in (data as Record<string, unknown>));
|
||||||
|
}
|
||||||
|
|
||||||
export function migrateV4ToV5(v4: unknown): z.infer<typeof settingsSchema> {
|
export function migrateV4ToV5(v4: unknown): z.infer<typeof settingsSchema> {
|
||||||
|
if (isAlreadyV5(v4)) return v4 as z.infer<typeof settingsSchema>;
|
||||||
const parsed = v4SettingsSchema.parse(v4);
|
const parsed = v4SettingsSchema.parse(v4);
|
||||||
const defaults = settingsSchema.parse({});
|
const defaults = settingsSchema.parse({});
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -34,12 +34,20 @@ describe('MigrationRunner', () => {
|
|||||||
fs.rmSync(dir, { recursive: true });
|
fs.rmSync(dir, { recursive: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns failed and preserves v4 on transform throw', () => {
|
test('returns failed and writes v5 marker so subsequent runs skip migration', () => {
|
||||||
const dir = tmpDir();
|
const dir = tmpDir();
|
||||||
fs.writeFileSync(path.join(dir, 'settings.json'), JSON.stringify({ theme: 'light' }));
|
fs.writeFileSync(path.join(dir, 'settings.json'), JSON.stringify({ theme: 'light' }));
|
||||||
const r = new MigrationRunner({ dir, transform: () => { throw new Error('boom'); } });
|
const r = new MigrationRunner({ dir, transform: () => { throw new Error('boom'); } });
|
||||||
expect(r.run()).toBe('failed');
|
expect(r.run()).toBe('failed');
|
||||||
|
// v5 marker is written so next launch skips migration
|
||||||
|
expect(JSON.parse(fs.readFileSync(path.join(dir, 'settings.json'), 'utf-8'))['migration.version']).toBe(5);
|
||||||
|
// Original data is preserved (theme: light is still there)
|
||||||
expect(fs.readFileSync(path.join(dir, 'settings.json'), 'utf-8')).toContain('light');
|
expect(fs.readFileSync(path.join(dir, 'settings.json'), 'utf-8')).toContain('light');
|
||||||
|
// Backup exists
|
||||||
|
expect(fs.existsSync(path.join(dir, 'settings.v4.bak.json'))).toBe(true);
|
||||||
|
// Subsequent run skips
|
||||||
|
const r2 = new MigrationRunner({ dir, transform: () => { throw new Error('should not be called'); } });
|
||||||
|
expect(r2.run()).toBe('skipped');
|
||||||
fs.rmSync(dir, { recursive: true });
|
fs.rmSync(dir, { recursive: true });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user