sync: update from private repo (d8074a7)
CI / build-and-test (push) Has been cancelled

This commit is contained in:
oss-sync
2026-06-05 06:05:30 +00:00
parent a44f6b41e2
commit e00ea9fb0c
91 changed files with 530 additions and 257 deletions
+31 -1
View File
@@ -1,6 +1,6 @@
// src/config-manager.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { mkdtempSync, writeFileSync, readFileSync } from 'fs';
import { mkdtempSync, writeFileSync, readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { ConfigManager } from './config-manager.js';
@@ -85,6 +85,36 @@ describe('ConfigManager', () => {
expect(cm.getConfig().provider.workers[0]?.model).toBe('new-model');
});
it('creates config.yaml on first save when the file does not exist (fresh install on defaults)', () => {
// Fresh OSS deploy: the server boots on defaults with no config.yaml on
// disk. The first Settings-UI save must CREATE the file, not crash with
// ENOENT trying to back up a non-existent file.
const freshPath = join(tempDir, 'fresh-config.yaml');
expect(existsSync(freshPath)).toBe(false);
const cm = new ConfigManager(freshPath);
const result = cm.updateConfig({
llm: {
workers: [{
id: 'w1',
connectionType: 'direct',
endpoint: 'http://host:11434/v1',
model: 'qwen3:8b',
roles: ['auto', 'fast'],
maxConcurrency: 1,
enabled: true,
}],
},
});
expect(result.ok).toBe(true);
expect(existsSync(freshPath)).toBe(true);
const raw = readFileSync(freshPath, 'utf-8');
expect(raw).toContain('config_version: 2');
expect(raw).toContain('http://host:11434/v1');
expect(cm.getConfig().provider.workers[0]?.model).toBe('qwen3:8b');
});
it('rejects invalid config (unparseable YAML file)', () => {
const cm = new ConfigManager(configPath);
// Corrupt the file, then try to reload — loadConfig will fall back to defaults
+15 -5
View File
@@ -1,6 +1,6 @@
// src/config-manager.ts
import { EventEmitter } from 'events';
import { readFileSync, writeFileSync, statSync } from 'fs';
import { readFileSync, writeFileSync, statSync, existsSync, unlinkSync } from 'fs';
import { stringify } from 'yaml';
import { loadConfig, toSnakeKeys, type AppConfig } from './config.js';
import { createHash } from 'crypto';
@@ -155,15 +155,25 @@ export class ConfigManager {
const snakeConfig = toSnakeKeys(merged) as Record<string, unknown>;
const yamlStr = stringify(snakeConfig, { lineWidth: 120 });
// Validate BEFORE writing: backup, write, validate, rollback on failure
const backupContent = readFileSync(this.configPath, 'utf-8');
// Validate BEFORE writing: backup, write, validate, rollback on failure.
// config.yaml may not exist yet — a fresh install boots on defaults with no
// file on disk, and the first save must CREATE it rather than ENOENT trying
// to back up a missing file. With no prior content, a failed save is rolled
// back by removing the file we just created.
const backupContent = existsSync(this.configPath)
? readFileSync(this.configPath, 'utf-8')
: null;
try {
writeFileSync(this.configPath, yamlStr, 'utf-8');
logger.info(`[config-manager] config written to ${this.configPath}`);
this.currentConfig = loadConfig(this.configPath);
} catch (e) {
// Restore original file on validation failure
writeFileSync(this.configPath, backupContent, 'utf-8');
// Restore original on validation failure (or drop the file we created).
if (backupContent !== null) {
writeFileSync(this.configPath, backupContent, 'utf-8');
} else {
try { unlinkSync(this.configPath); } catch { /* nothing to revert */ }
}
logger.warn(`[config-manager] config update failed, reverted: ${e}`);
return { ok: false, errors: e, message: 'Invalid config — changes reverted' };
}