200 lines
6.7 KiB
TypeScript
200 lines
6.7 KiB
TypeScript
/**
|
|
* Unit tests for config.ts — required field validation, defaults, range checks.
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { loadConfig } from '../../src/config.js';
|
|
|
|
// Suppress logger output during tests
|
|
vi.mock('../../src/utils/logger.js', () => ({
|
|
logInfo: vi.fn(),
|
|
logWarn: vi.fn(),
|
|
logError: vi.fn(),
|
|
logDebug: vi.fn(),
|
|
}));
|
|
|
|
describe('loadConfig', () => {
|
|
const originalEnv = { ...process.env };
|
|
|
|
beforeEach(() => {
|
|
// Clear all GUI_AGENT_* env vars
|
|
Object.keys(process.env).forEach(key => {
|
|
if (key.startsWith('GUI_AGENT_')) delete process.env[key];
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env = { ...originalEnv };
|
|
});
|
|
|
|
// --- Required field validation ---
|
|
|
|
it('throws ConfigError when API_KEY is missing', () => {
|
|
expect(() => loadConfig()).toThrow('GUI_AGENT_API_KEY is required');
|
|
});
|
|
|
|
it('accepts API_KEY from env', () => {
|
|
process.env.GUI_AGENT_API_KEY = 'test-key';
|
|
const config = loadConfig();
|
|
expect(config.apiKey).toBe('test-key');
|
|
});
|
|
|
|
it('accepts API_KEY from overrides', () => {
|
|
const config = loadConfig({ apiKey: 'override-key' });
|
|
expect(config.apiKey).toBe('override-key');
|
|
});
|
|
|
|
// --- Default values ---
|
|
|
|
it('has correct defaults', () => {
|
|
const config = loadConfig({ apiKey: 'test-key' });
|
|
expect(config.provider).toBe('anthropic');
|
|
expect(config.apiProtocol).toBe('anthropic');
|
|
expect(config.model).toBe('claude-sonnet-4-20250514');
|
|
expect(config.port).toBe(60008);
|
|
expect(config.transport).toBe('http');
|
|
expect(config.maxSteps).toBe(50);
|
|
expect(config.stepDelayMs).toBe(1500);
|
|
expect(config.stuckThreshold).toBe(3);
|
|
expect(config.jpegQuality).toBe(75);
|
|
expect(config.displayIndex).toBe(0);
|
|
expect(config.coordinateMode).toBeUndefined();
|
|
expect(config.baseUrl).toBeUndefined();
|
|
expect(config.memoryProvider).toBeUndefined();
|
|
expect(config.memoryModel).toBeUndefined();
|
|
expect(config.logFile).toBeUndefined();
|
|
});
|
|
|
|
// --- Transport validation ---
|
|
|
|
it('throws on invalid transport', () => {
|
|
expect(() => loadConfig({ apiKey: 'test-key', transport: 'websocket' as any }))
|
|
.toThrow('GUI_AGENT_TRANSPORT must be "http" or "stdio"');
|
|
});
|
|
|
|
it('accepts stdio transport', () => {
|
|
const config = loadConfig({ apiKey: 'test-key', transport: 'stdio' });
|
|
expect(config.transport).toBe('stdio');
|
|
});
|
|
|
|
// --- Numeric range validation ---
|
|
|
|
it('throws when maxSteps exceeds range', () => {
|
|
process.env.GUI_AGENT_API_KEY = 'test-key';
|
|
process.env.GUI_AGENT_MAX_STEPS = '999';
|
|
expect(() => loadConfig()).toThrow('GUI_AGENT_MAX_STEPS must be between 1 and 200');
|
|
});
|
|
|
|
it('throws when maxSteps is below range', () => {
|
|
process.env.GUI_AGENT_API_KEY = 'test-key';
|
|
process.env.GUI_AGENT_MAX_STEPS = '0';
|
|
expect(() => loadConfig()).toThrow('GUI_AGENT_MAX_STEPS must be between 1 and 200');
|
|
});
|
|
|
|
it('throws when jpegQuality exceeds range', () => {
|
|
process.env.GUI_AGENT_API_KEY = 'test-key';
|
|
process.env.GUI_AGENT_JPEG_QUALITY = '101';
|
|
expect(() => loadConfig()).toThrow('GUI_AGENT_JPEG_QUALITY must be between 1 and 100');
|
|
});
|
|
|
|
it('throws when stepDelayMs exceeds range', () => {
|
|
process.env.GUI_AGENT_API_KEY = 'test-key';
|
|
process.env.GUI_AGENT_STEP_DELAY_MS = '50000';
|
|
expect(() => loadConfig()).toThrow('GUI_AGENT_STEP_DELAY_MS must be between 100 and 30000');
|
|
});
|
|
|
|
it('throws when port is invalid', () => {
|
|
process.env.GUI_AGENT_API_KEY = 'test-key';
|
|
process.env.GUI_AGENT_PORT = '99999';
|
|
expect(() => loadConfig()).toThrow('GUI_AGENT_PORT must be between 1 and 65535');
|
|
});
|
|
|
|
it('accepts valid numeric values', () => {
|
|
const config = loadConfig({
|
|
apiKey: 'test-key',
|
|
maxSteps: 100,
|
|
jpegQuality: 50,
|
|
stepDelayMs: 2000,
|
|
port: 8080,
|
|
});
|
|
expect(config.maxSteps).toBe(100);
|
|
expect(config.jpegQuality).toBe(50);
|
|
expect(config.stepDelayMs).toBe(2000);
|
|
expect(config.port).toBe(8080);
|
|
});
|
|
|
|
// --- apiProtocol validation ---
|
|
|
|
it('throws on invalid apiProtocol', () => {
|
|
expect(() => loadConfig({ apiKey: 'test-key', apiProtocol: 'grpc' as any }))
|
|
.toThrow('GUI_AGENT_API_PROTOCOL must be "anthropic" or "openai"');
|
|
});
|
|
|
|
it('accepts openai apiProtocol', () => {
|
|
const config = loadConfig({ apiKey: 'test-key', apiProtocol: 'openai' });
|
|
expect(config.apiProtocol).toBe('openai');
|
|
});
|
|
|
|
it('reads apiProtocol from env', () => {
|
|
process.env.GUI_AGENT_API_KEY = 'test-key';
|
|
process.env.GUI_AGENT_API_PROTOCOL = 'openai';
|
|
const config = loadConfig();
|
|
expect(config.apiProtocol).toBe('openai');
|
|
});
|
|
|
|
it('overrides take precedence for apiProtocol', () => {
|
|
process.env.GUI_AGENT_API_KEY = 'test-key';
|
|
process.env.GUI_AGENT_API_PROTOCOL = 'openai';
|
|
const config = loadConfig({ apiKey: 'test-key', apiProtocol: 'anthropic' });
|
|
expect(config.apiProtocol).toBe('anthropic');
|
|
});
|
|
|
|
// --- coordinateMode validation ---
|
|
|
|
it('throws on invalid coordinateMode', () => {
|
|
expect(() => loadConfig({ apiKey: 'test-key', coordinateMode: 'foobar' as any }))
|
|
.toThrow('GUI_AGENT_COORDINATE_MODE must be one of');
|
|
});
|
|
|
|
it('accepts valid coordinateMode', () => {
|
|
const config = loadConfig({ apiKey: 'test-key', coordinateMode: 'normalized-1000' });
|
|
expect(config.coordinateMode).toBe('normalized-1000');
|
|
});
|
|
|
|
it('treats "auto" coordinateMode as undefined (auto-detect)', () => {
|
|
const config = loadConfig({ apiKey: 'test-key', coordinateMode: 'auto' as any });
|
|
expect(config.coordinateMode).toBeUndefined();
|
|
});
|
|
|
|
// --- Environment variable overrides ---
|
|
|
|
it('reads all env vars', () => {
|
|
process.env.GUI_AGENT_API_KEY = 'env-key';
|
|
process.env.GUI_AGENT_PROVIDER = 'openai';
|
|
process.env.GUI_AGENT_MODEL = 'gpt-4o';
|
|
process.env.GUI_AGENT_BASE_URL = 'https://example.com';
|
|
process.env.GUI_AGENT_MEMORY_PROVIDER = 'anthropic';
|
|
process.env.GUI_AGENT_MEMORY_MODEL = 'haiku';
|
|
process.env.GUI_AGENT_LOG_FILE = '/tmp/gui.log';
|
|
|
|
const config = loadConfig();
|
|
expect(config.provider).toBe('openai');
|
|
expect(config.model).toBe('gpt-4o');
|
|
expect(config.baseUrl).toBe('https://example.com');
|
|
expect(config.memoryProvider).toBe('anthropic');
|
|
expect(config.memoryModel).toBe('haiku');
|
|
expect(config.logFile).toBe('/tmp/gui.log');
|
|
});
|
|
|
|
// --- Overrides take precedence over env ---
|
|
|
|
it('overrides take precedence over env vars', () => {
|
|
process.env.GUI_AGENT_API_KEY = 'env-key';
|
|
process.env.GUI_AGENT_MODEL = 'env-model';
|
|
|
|
const config = loadConfig({ apiKey: 'override-key', model: 'override-model' });
|
|
expect(config.apiKey).toBe('override-key');
|
|
expect(config.model).toBe('override-model');
|
|
});
|
|
});
|