feat(config): persist config.patch updates atomically

This commit is contained in:
William Valentin
2026-02-15 22:03:21 -08:00
parent c314e0f067
commit 0220ec10dd
13 changed files with 205 additions and 11 deletions
+1
View File
@@ -1,2 +1,3 @@
export { loadConfig, deepMerge } from './loader.js';
export { persistConfig } from './persistence.js';
export { configSchema, MODEL_PROVIDERS, type ModelProvider, type Config, type TelegramConfig, type ModelConfig, type CronJobConfig, type AgentsConfig, type CompactionConfig, type ToolProfile, type ToolOverrideConfig, type ToolsConfig, type SandboxConfig, type AgentConfigEntry, type RoutingConfig, type ServerConfig } from './schema.js';
+52
View File
@@ -0,0 +1,52 @@
import { describe, it, expect, afterEach } from 'vitest';
import { mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { persistConfig } from './persistence.js';
import { configSchema } from './schema.js';
const testRoots: string[] = [];
afterEach(() => {
while (testRoots.length > 0) {
const dir = testRoots.pop();
if (!dir) {continue;}
try {
rmSync(dir, { recursive: true, force: true });
} catch {}
}
});
function makeConfig() {
return configSchema.parse({
telegram: { bot_token: 'test-token', allowed_chat_ids: [1] },
models: { default: { provider: 'anthropic', model: 'claude-3' } },
hooks: { confirm: ['shell.exec'], log: [], silent: [] },
});
}
describe('persistConfig', () => {
it('writes config to target path', () => {
const dir = mkdtempSync(join(tmpdir(), 'flynn-config-persist-'));
testRoots.push(dir);
const configPath = join(dir, 'config.yaml');
persistConfig(configPath, makeConfig());
const written = readFileSync(configPath, 'utf-8');
expect(written).toContain('telegram:');
expect(written).toContain('models:');
});
it('creates .bak when overwriting existing config', () => {
const dir = mkdtempSync(join(tmpdir(), 'flynn-config-persist-'));
testRoots.push(dir);
const configPath = join(dir, 'config.yaml');
writeFileSync(configPath, 'legacy: true\n', 'utf-8');
persistConfig(configPath, makeConfig());
const backupPath = `${configPath}.bak`;
expect(existsSync(backupPath)).toBe(true);
expect(readFileSync(backupPath, 'utf-8')).toContain('legacy: true');
});
});
+26
View File
@@ -0,0 +1,26 @@
import { copyFileSync, existsSync, mkdirSync, renameSync, writeFileSync } from 'fs';
import { dirname } from 'path';
import { stringify } from 'yaml';
import type { Config } from './schema.js';
/**
* Persist config atomically:
* 1) Backup existing file to <path>.bak
* 2) Write new YAML to temp file
* 3) Rename temp file into place
*/
export function persistConfig(configPath: string, config: Config): void {
const dir = dirname(configPath);
mkdirSync(dir, { recursive: true });
const yaml = stringify(config);
const tmpPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
const backupPath = `${configPath}.bak`;
if (existsSync(configPath)) {
copyFileSync(configPath, backupPath);
}
writeFileSync(tmpPath, yaml, 'utf-8');
renameSync(tmpPath, configPath);
}