import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { loadConfig } from '../../src/config/env.js'; const originalEnv = { ...process.env }; function setEnv(partial) { for (const key of Object.keys(process.env)) { if (key.startsWith('OPENAI_') || key.startsWith('CHATGPT_MCP_') || key.startsWith('OLLAMA_')) { delete process.env[key]; } } Object.assign(process.env, partial); } beforeEach(() => { setEnv({ OPENAI_API_KEY: 'test-key' }); }); afterEach(() => { for (const key of Object.keys(process.env)) { if (key.startsWith('OPENAI_') || key.startsWith('CHATGPT_MCP_') || key.startsWith('OLLAMA_')) { delete process.env[key]; } } Object.assign(process.env, originalEnv); }); describe('loadConfig', () => { it('throws when OPENAI_API_KEY is missing and provider is openai', () => { delete process.env.OPENAI_API_KEY; delete process.env.CHATGPT_MCP_PROVIDER; expect(() => loadConfig()).toThrow( 'Configuration error: OPENAI_API_KEY is required for the openai provider.' ); }); it('does not throw when OPENAI_API_KEY is missing and provider is manual', () => { delete process.env.OPENAI_API_KEY; setEnv({ CHATGPT_MCP_PROVIDER: 'manual' }); const cfg = loadConfig(); expect(cfg.chatgptMcpProvider).toBe('manual'); expect(cfg.openaiApiKey).toBe(undefined); }); it('does not throw when OPENAI_API_KEY is missing and provider is ollama', () => { delete process.env.OPENAI_API_KEY; setEnv({ CHATGPT_MCP_PROVIDER: 'ollama' }); const cfg = loadConfig(); expect(cfg.chatgptMcpProvider).toBe('ollama'); expect(cfg.openaiApiKey).toBe(undefined); }); it('still requires OPENAI_API_KEY for openai even when key is explicitly empty string', () => { setEnv({ CHATGPT_MCP_PROVIDER: 'openai', OPENAI_API_KEY: '' }); expect(() => loadConfig()).toThrow( 'Configuration error: OPENAI_API_KEY is required for the openai provider.' ); }); it('returns defaults when no optional vars are set', () => { const cfg = loadConfig(); expect(cfg.openaiApiKey).toBe('test-key'); expect(cfg.openaiModel).toBe('gpt-5.1'); expect(cfg.temperature).toBe(0.2); expect(cfg.maxOutputTokens).toBe(2000); expect(cfg.logLevel).toBe('info'); expect(cfg.enableFileContext).toBe(false); expect(cfg.contextDir).toBe('./context'); expect(cfg.maxInputChars).toBe(30000); expect(cfg.maxFileChars).toBe(12000); expect(cfg.maxFiles).toBe(5); expect(cfg.maxLogChars).toBe(10000); expect(cfg.redactSecrets).toBe(true); expect(cfg.ollamaBaseUrl).toBe('http://localhost:11434'); expect(cfg.ollamaModel).toBe('qwen3:latest'); expect(cfg.ollamaTemperature).toBe(0.2); expect(cfg.ollamaTimeout).toBe(60); }); it('applies custom string values', () => { setEnv({ OPENAI_API_KEY: 'test-key', OPENAI_MODEL: 'gpt-4', CHATGPT_MCP_LOG_LEVEL: 'debug', CHATGPT_MCP_CONTEXT_DIR: './custom', }); const cfg = loadConfig(); expect(cfg.openaiModel).toBe('gpt-4'); expect(cfg.logLevel).toBe('debug'); expect(cfg.contextDir).toBe('./custom'); }); it('applies custom numeric values', () => { setEnv({ OPENAI_API_KEY: 'test-key', OPENAI_TEMPERATURE: '0.5', OPENAI_MAX_OUTPUT_TOKENS: '4096', CHATGPT_MCP_MAX_INPUT_CHARS: '15000', CHATGPT_MCP_MAX_FILE_CHARS: '8000', CHATGPT_MCP_MAX_FILES: '3', CHATGPT_MCP_MAX_LOG_CHARS: '5000', }); const cfg = loadConfig(); expect(cfg.temperature).toBe(0.5); expect(cfg.maxOutputTokens).toBe(4096); expect(cfg.maxInputChars).toBe(15000); expect(cfg.maxFileChars).toBe(8000); expect(cfg.maxFiles).toBe(3); expect(cfg.maxLogChars).toBe(5000); }); it('applies custom boolean values', () => { setEnv({ OPENAI_API_KEY: 'test-key', CHATGPT_MCP_ENABLE_FILE_CONTEXT: 'true', CHATGPT_MCP_REDACT_SECRETS: 'false', }); const cfg = loadConfig(); expect(cfg.enableFileContext).toBe(true); expect(cfg.redactSecrets).toBe(false); }); it('throws on invalid numeric values', () => { setEnv({ OPENAI_API_KEY: 'test-key', CHATGPT_MCP_MAX_INPUT_CHARS: 'abc', }); expect(() => loadConfig()).toThrow( 'Configuration error: CHATGPT_MCP_MAX_INPUT_CHARS must be a positive number, got "abc".' ); }); it('throws on invalid boolean values', () => { setEnv({ OPENAI_API_KEY: 'test-key', CHATGPT_MCP_ENABLE_FILE_CONTEXT: 'yes', }); expect(() => loadConfig()).toThrow( 'Configuration error: CHATGPT_MCP_ENABLE_FILE_CONTEXT must be true or false, got "yes".' ); }); it('throws on negative numeric values', () => { setEnv({ OPENAI_API_KEY: 'test-key', CHATGPT_MCP_MAX_FILES: '-1', }); expect(() => loadConfig()).toThrow( 'Configuration error: CHATGPT_MCP_MAX_FILES must be a positive number, got "-1".' ); }); it('throws on NaN numeric values', () => { setEnv({ OPENAI_API_KEY: 'test-key', CHATGPT_MCP_MAX_FILES: 'NaN', }); expect(() => loadConfig()).toThrow( 'Configuration error: CHATGPT_MCP_MAX_FILES must be a positive number, got "NaN".' ); }); it('throws on Infinity numeric values', () => { setEnv({ OPENAI_API_KEY: 'test-key', CHATGPT_MCP_MAX_FILES: 'Infinity', }); expect(() => loadConfig()).toThrow( 'Configuration error: CHATGPT_MCP_MAX_FILES must be a positive number, got "Infinity".' ); }); }); describe('loadConfig chatgptMcpProvider', () => { it('defaults to "openai" when CHATGPT_MCP_PROVIDER is not set', () => { delete process.env.CHATGPT_MCP_PROVIDER; const cfg = loadConfig(); expect(cfg.chatgptMcpProvider).toBe('openai'); }); it('accepts "openai" value', () => { setEnv({ CHATGPT_MCP_PROVIDER: 'openai', OPENAI_API_KEY: 'sk-test' }); const cfg = loadConfig(); expect(cfg.chatgptMcpProvider).toBe('openai'); }); it('accepts any string value (validation is in factory)', () => { setEnv({ CHATGPT_MCP_PROVIDER: 'ollama', OPENAI_API_KEY: 'sk-test' }); const cfg = loadConfig(); expect(cfg.chatgptMcpProvider).toBe('ollama'); }); }); // --- Ollama env vars --- describe('loadConfig ollama env vars', () => { it('returns default ollama values when no OLLAMA_ vars set', () => { const cfg = loadConfig(); expect(cfg.ollamaBaseUrl).toBe('http://localhost:11434'); expect(cfg.ollamaModel).toBe('qwen3:latest'); expect(cfg.ollamaTemperature).toBe(0.2); expect(cfg.ollamaTimeout).toBe(60); }); it('applies custom OLLAMA_BASE_URL', () => { setEnv({ OPENAI_API_KEY: 'test-key', OLLAMA_BASE_URL: 'http://localhost:11435' }); const cfg = loadConfig(); expect(cfg.ollamaBaseUrl).toBe('http://localhost:11435'); }); it('returns raw OLLAMA_BASE_URL — trailing slash normalization is in the provider', () => { setEnv({ OPENAI_API_KEY: 'test-key', OLLAMA_BASE_URL: 'http://localhost:11434/' }); const cfg = loadConfig(); expect(cfg.ollamaBaseUrl).toBe('http://localhost:11434/'); }); it('applies custom OLLAMA_MODEL', () => { setEnv({ OPENAI_API_KEY: 'test-key', OLLAMA_MODEL: 'llama3' }); const cfg = loadConfig(); expect(cfg.ollamaModel).toBe('llama3'); }); it('applies custom OLLAMA_TEMPERATURE', () => { setEnv({ OPENAI_API_KEY: 'test-key', OLLAMA_TEMPERATURE: '0.7' }); const cfg = loadConfig(); expect(cfg.ollamaTemperature).toBe(0.7); }); it('applies custom OLLAMA_TIMEOUT', () => { setEnv({ OPENAI_API_KEY: 'test-key', OLLAMA_TIMEOUT: '120' }); const cfg = loadConfig(); expect(cfg.ollamaTimeout).toBe(120); }); it('throws on invalid OLLAMA_TEMPERATURE', () => { setEnv({ OPENAI_API_KEY: 'test-key', OLLAMA_TEMPERATURE: 'abc' }); expect(() => loadConfig()).toThrow( 'Configuration error: OLLAMA_TEMPERATURE must be a positive number, got "abc".' ); }); it('throws on negative OLLAMA_TIMEOUT', () => { setEnv({ OPENAI_API_KEY: 'test-key', OLLAMA_TIMEOUT: '-10' }); expect(() => loadConfig()).toThrow( 'Configuration error: OLLAMA_TIMEOUT must be a positive number, got "-10".' ); }); it('works with ollama provider and all ollama env vars', () => { setEnv({ OPENAI_API_KEY: 'sk-test', CHATGPT_MCP_PROVIDER: 'ollama', OLLAMA_BASE_URL: 'http://ollama-server:8080', OLLAMA_MODEL: 'mistral', OLLAMA_TEMPERATURE: '0.5', OLLAMA_TIMEOUT: '90', }); const cfg = loadConfig(); expect(cfg.chatgptMcpProvider).toBe('ollama'); expect(cfg.ollamaBaseUrl).toBe('http://ollama-server:8080'); expect(cfg.ollamaModel).toBe('mistral'); expect(cfg.ollamaTemperature).toBe(0.5); expect(cfg.ollamaTimeout).toBe(90); }); }); describe('loadConfig ollama env vars with trailing slash', () => { it('returns raw base URL — normalization happens in the provider', () => { setEnv({ OPENAI_API_KEY: 'test-key', OLLAMA_BASE_URL: 'http://localhost:11434//' }); const cfg = loadConfig(); expect(cfg.ollamaBaseUrl).toBe('http://localhost:11434//'); }); it('returns raw base URL with multiple slashes', () => { setEnv({ OPENAI_API_KEY: 'test-key', OLLAMA_BASE_URL: 'http://localhost:11434///' }); const cfg = loadConfig(); expect(cfg.ollamaBaseUrl).toBe('http://localhost:11434///'); }); it('handles base URL without trailing slash', () => { setEnv({ OPENAI_API_KEY: 'test-key', OLLAMA_BASE_URL: 'http://localhost:11434' }); const cfg = loadConfig(); expect(cfg.ollamaBaseUrl).toBe('http://localhost:11434'); }); it('handles empty base URL as default', () => { setEnv({ OPENAI_API_KEY: 'test-key', OLLAMA_BASE_URL: '' }); const cfg = loadConfig(); expect(cfg.ollamaBaseUrl).toBe('http://localhost:11434'); }); });