git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
276 lines
7.6 KiB
JavaScript
276 lines
7.6 KiB
JavaScript
/**
|
|
* Unit tests for Config
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import { Config } from '../../../src/config/config.js';
|
|
import { writeFileSync, unlinkSync, existsSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { tmpdir } from 'os';
|
|
|
|
describe('Config', () => {
|
|
let testConfigPath;
|
|
let originalEnv;
|
|
|
|
beforeEach(() => {
|
|
originalEnv = { ...process.env };
|
|
testConfigPath = join(tmpdir(), `test-config-${Date.now()}.json`);
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env = originalEnv;
|
|
if (existsSync(testConfigPath)) {
|
|
unlinkSync(testConfigPath);
|
|
}
|
|
});
|
|
|
|
describe('constructor', () => {
|
|
it('should create config with defaults', () => {
|
|
const config = new Config({ loadEnv: false });
|
|
expect(config.values).toBeDefined();
|
|
expect(config.get('api.baseUrl')).toBeDefined();
|
|
});
|
|
|
|
it('should accept custom options', () => {
|
|
const config = new Config({
|
|
loadEnv: false,
|
|
api: { baseUrl: 'https://custom.com' }
|
|
});
|
|
expect(config.get('api.baseUrl')).toBe('https://custom.com');
|
|
});
|
|
|
|
it('should load from file if provided', () => {
|
|
writeFileSync(testConfigPath, JSON.stringify({
|
|
custom: { value: 'test' }
|
|
}));
|
|
|
|
const config = new Config({
|
|
loadEnv: false,
|
|
configPath: testConfigPath
|
|
});
|
|
|
|
expect(config.get('custom.value')).toBe('test');
|
|
});
|
|
});
|
|
|
|
describe('get', () => {
|
|
let config;
|
|
|
|
beforeEach(() => {
|
|
config = new Config({
|
|
loadEnv: false,
|
|
api: {
|
|
baseUrl: 'https://test.com',
|
|
timeout: 5000
|
|
},
|
|
nested: {
|
|
deep: {
|
|
value: 'found'
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
it('should get top-level value', () => {
|
|
expect(config.get('api')).toEqual({
|
|
baseUrl: 'https://test.com',
|
|
timeout: 5000
|
|
});
|
|
});
|
|
|
|
it('should get nested value with dot notation', () => {
|
|
expect(config.get('api.baseUrl')).toBe('https://test.com');
|
|
expect(config.get('nested.deep.value')).toBe('found');
|
|
});
|
|
|
|
it('should return default for non-existent key', () => {
|
|
expect(config.get('nonexistent', 'default')).toBe('default');
|
|
});
|
|
|
|
it('should return undefined for non-existent key without default', () => {
|
|
expect(config.get('nonexistent')).toBeUndefined();
|
|
});
|
|
|
|
it('should read from environment variables', () => {
|
|
process.env.AGENTIC_SYNTH_API_KEY = 'env-key-123';
|
|
const config = new Config({ loadEnv: false });
|
|
|
|
expect(config.get('api.key')).toBe('env-key-123');
|
|
});
|
|
|
|
it('should prioritize environment over config file', () => {
|
|
process.env.AGENTIC_SYNTH_CUSTOM_VALUE = 'from-env';
|
|
|
|
const config = new Config({
|
|
loadEnv: false,
|
|
custom: { value: 'from-config' }
|
|
});
|
|
|
|
expect(config.get('custom.value')).toBe('from-env');
|
|
});
|
|
});
|
|
|
|
describe('set', () => {
|
|
let config;
|
|
|
|
beforeEach(() => {
|
|
config = new Config({ loadEnv: false });
|
|
});
|
|
|
|
it('should set top-level value', () => {
|
|
config.set('newKey', 'newValue');
|
|
expect(config.get('newKey')).toBe('newValue');
|
|
});
|
|
|
|
it('should set nested value with dot notation', () => {
|
|
config.set('nested.deep.value', 'test');
|
|
expect(config.get('nested.deep.value')).toBe('test');
|
|
});
|
|
|
|
it('should create nested structure if not exists', () => {
|
|
config.set('a.b.c.d', 'deep');
|
|
expect(config.get('a.b.c.d')).toBe('deep');
|
|
});
|
|
|
|
it('should update existing value', () => {
|
|
config.set('api.baseUrl', 'https://new.com');
|
|
expect(config.get('api.baseUrl')).toBe('https://new.com');
|
|
});
|
|
});
|
|
|
|
describe('loadFromFile', () => {
|
|
it('should load JSON config', () => {
|
|
const configData = {
|
|
api: { baseUrl: 'https://json.com' }
|
|
};
|
|
writeFileSync(testConfigPath, JSON.stringify(configData));
|
|
|
|
const config = new Config({ loadEnv: false });
|
|
config.loadFromFile(testConfigPath);
|
|
|
|
expect(config.get('api.baseUrl')).toBe('https://json.com');
|
|
});
|
|
|
|
it('should load YAML config', () => {
|
|
const yamlPath = testConfigPath.replace('.json', '.yaml');
|
|
writeFileSync(yamlPath, 'api:\n baseUrl: https://yaml.com');
|
|
|
|
const config = new Config({ loadEnv: false });
|
|
config.loadFromFile(yamlPath);
|
|
|
|
expect(config.get('api.baseUrl')).toBe('https://yaml.com');
|
|
|
|
unlinkSync(yamlPath);
|
|
});
|
|
|
|
it('should throw error for invalid JSON', () => {
|
|
writeFileSync(testConfigPath, 'invalid json');
|
|
|
|
const config = new Config({ loadEnv: false });
|
|
expect(() => config.loadFromFile(testConfigPath)).toThrow();
|
|
});
|
|
|
|
it('should throw error for unsupported format', () => {
|
|
const txtPath = testConfigPath.replace('.json', '.txt');
|
|
writeFileSync(txtPath, 'text');
|
|
|
|
const config = new Config({ loadEnv: false });
|
|
expect(() => config.loadFromFile(txtPath)).toThrow('Unsupported config file format');
|
|
|
|
unlinkSync(txtPath);
|
|
});
|
|
|
|
it('should throw error for non-existent file', () => {
|
|
const config = new Config({ loadEnv: false });
|
|
expect(() => config.loadFromFile('/nonexistent/file.json')).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('validate', () => {
|
|
let config;
|
|
|
|
beforeEach(() => {
|
|
config = new Config({
|
|
loadEnv: false,
|
|
api: { baseUrl: 'https://test.com' },
|
|
cache: { maxSize: 100 }
|
|
});
|
|
});
|
|
|
|
it('should pass validation for existing keys', () => {
|
|
expect(() => config.validate(['api.baseUrl', 'cache.maxSize'])).not.toThrow();
|
|
});
|
|
|
|
it('should throw error for missing required keys', () => {
|
|
expect(() => config.validate(['nonexistent'])).toThrow('Missing required configuration: nonexistent');
|
|
});
|
|
|
|
it('should list all missing keys', () => {
|
|
expect(() => config.validate(['missing1', 'missing2'])).toThrow('missing1, missing2');
|
|
});
|
|
|
|
it('should return true on successful validation', () => {
|
|
expect(config.validate(['api.baseUrl'])).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('getAll', () => {
|
|
it('should return all configuration', () => {
|
|
const config = new Config({
|
|
loadEnv: false,
|
|
custom: { value: 'test' }
|
|
});
|
|
|
|
const all = config.getAll();
|
|
expect(all).toHaveProperty('custom');
|
|
expect(all.custom.value).toBe('test');
|
|
});
|
|
|
|
it('should return copy not reference', () => {
|
|
const config = new Config({ loadEnv: false });
|
|
const all = config.getAll();
|
|
|
|
all.modified = true;
|
|
expect(config.get('modified')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('_parseValue', () => {
|
|
let config;
|
|
|
|
beforeEach(() => {
|
|
config = new Config({ loadEnv: false });
|
|
});
|
|
|
|
it('should parse JSON strings', () => {
|
|
expect(config._parseValue('{"key":"value"}')).toEqual({ key: 'value' });
|
|
expect(config._parseValue('[1,2,3]')).toEqual([1, 2, 3]);
|
|
});
|
|
|
|
it('should parse booleans', () => {
|
|
expect(config._parseValue('true')).toBe(true);
|
|
expect(config._parseValue('false')).toBe(false);
|
|
});
|
|
|
|
it('should parse numbers', () => {
|
|
expect(config._parseValue('123')).toBe(123);
|
|
expect(config._parseValue('45.67')).toBe(45.67);
|
|
});
|
|
|
|
it('should return string for unparseable values', () => {
|
|
expect(config._parseValue('plain text')).toBe('plain text');
|
|
});
|
|
});
|
|
|
|
describe('default configuration', () => {
|
|
it('should have sensible defaults', () => {
|
|
const config = new Config({ loadEnv: false });
|
|
|
|
expect(config.get('api.timeout')).toBe(5000);
|
|
expect(config.get('cache.maxSize')).toBe(100);
|
|
expect(config.get('cache.ttl')).toBe(3600000);
|
|
expect(config.get('router.strategy')).toBe('round-robin');
|
|
});
|
|
});
|
|
});
|