fix(settings): clean up orphaned temp files and harden atomic writes

This commit is contained in:
Bohdan Triapitsyn
2026-08-22 11:55:38 +03:00
parent 735a9463ca
commit fcbe2c0414
8 changed files with 142 additions and 25 deletions
@@ -151,4 +151,71 @@ describe('settings runtime', () => {
await fsPromises.rm(tempRoot, { recursive: true, force: true });
}
});
it('cleans up orphaned settings.json.tmp files during startup migration', async () => {
const { runtime, settingsFilePath, tempRoot, cleanup } = await createRuntime();
try {
const settingsDir = path.dirname(settingsFilePath);
const orphan1 = path.join(settingsDir, 'settings.json.tmp-1234-11111-abc');
const orphan2 = path.join(settingsDir, 'settings.json.tmp-5678-22222-def');
const unrelated = path.join(settingsDir, 'other-file.json');
await fsPromises.writeFile(orphan1, '{"broken": true}', 'utf8');
await fsPromises.writeFile(orphan2, '{"broken": true}', 'utf8');
await fsPromises.writeFile(unrelated, '{"keep": true}', 'utf8');
await fsPromises.writeFile(settingsFilePath, '{"theme": "light"}', 'utf8');
await runtime.readSettingsFromDiskMigrated();
const files = await fsPromises.readdir(settingsDir);
expect(files).toContain('settings.json');
expect(files).toContain('other-file.json');
expect(files).not.toContain('settings.json.tmp-1234-11111-abc');
expect(files).not.toContain('settings.json.tmp-5678-22222-def');
} finally {
await cleanup();
}
});
it('removes temp file when writeSettingsToDisk encounters a write error', async () => {
const tempRoot = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'oc-settings-runtime-'));
const settingsFilePath = path.join(tempRoot, 'settings.json');
let capturedTmp = null;
const wrappedFs = {
...fsPromises,
rename: async (src, dst) => {
capturedTmp = src;
const error = new Error('unexpected disk failure');
error.code = 'EIO';
throw error;
},
};
const runtime = createSettingsRuntime({
fsPromises: wrappedFs,
path,
crypto,
SETTINGS_FILE_PATH: settingsFilePath,
sanitizeProjects: (projects) => Array.isArray(projects) ? projects : [],
sanitizeSettingsUpdate: (settings) => settings,
mergePersistedSettings: (_current, changes) => changes,
normalizeSettingsPaths: (settings) => ({ settings, changed: false }),
normalizeStringArray: (values) => Array.isArray(values) ? values.filter((value) => typeof value === 'string') : [],
formatSettingsResponse: (settings) => settings,
resolveDirectoryCandidate: (value) => value,
normalizeManagedRemoteTunnelHostname: (value) => value,
normalizeManagedRemoteTunnelPresets: (value) => value,
normalizeManagedRemoteTunnelPresetTokens: (value) => value,
syncManagedRemoteTunnelConfigWithPresets: async () => {},
upsertManagedRemoteTunnelToken: async () => {},
});
try {
await expect(runtime.writeSettingsToDisk({ theme: 'dark' })).rejects.toThrow('unexpected disk failure');
expect(capturedTmp).toBeTruthy();
const files = await fsPromises.readdir(tempRoot);
expect(files.some((f) => f.startsWith('settings.json.tmp-'))).toBe(false);
} finally {
await fsPromises.rm(tempRoot, { recursive: true, force: true });
}
});
});