feat(config): persist config.patch updates atomically
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user