Skip to main content
Glama
config-manager.test.js37.4 kB
import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { jest } from '@jest/globals'; // Mock modules first before any imports jest.mock('fs', () => ({ existsSync: jest.fn((filePath) => { // Prevent Jest internal file access if ( filePath.includes('jest-message-util') || filePath.includes('node_modules') ) { return false; } return false; // Default to false for config discovery prevention }), readFileSync: jest.fn(() => '{}'), writeFileSync: jest.fn(), mkdirSync: jest.fn() })); jest.mock('path', () => ({ join: jest.fn((dir, file) => `${dir}/${file}`), dirname: jest.fn((filePath) => filePath.split('/').slice(0, -1).join('/')), resolve: jest.fn((...paths) => paths.join('/')), basename: jest.fn((filePath) => filePath.split('/').pop()) })); jest.mock('chalk', () => ({ red: jest.fn((text) => text), blue: jest.fn((text) => text), green: jest.fn((text) => text), yellow: jest.fn((text) => text), white: jest.fn(() => ({ bold: jest.fn((text) => text) })), reset: jest.fn((text) => text), dim: jest.fn((text) => text) // Add dim function to prevent chalk errors })); // Mock console to prevent Jest internal access const mockConsole = { log: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() }; global.console = mockConsole; // --- Define Mock Function Instances --- const mockFindConfigPath = jest.fn(() => null); // Default to null, can be overridden in tests // Mock path-utils to prevent config file path discovery and logging jest.mock('../../src/utils/path-utils.js', () => ({ __esModule: true, findProjectRoot: jest.fn(() => '/mock/project'), findConfigPath: mockFindConfigPath, // Use the mock function instance findTasksPath: jest.fn(() => '/mock/tasks.json'), findComplexityReportPath: jest.fn(() => null), resolveTasksOutputPath: jest.fn(() => '/mock/tasks.json'), resolveComplexityReportOutputPath: jest.fn(() => '/mock/report.json') })); // --- Read REAL supported-models.json data BEFORE mocks --- const __filename = fileURLToPath(import.meta.url); // Get current file path const __dirname = path.dirname(__filename); // Get current directory const realSupportedModelsPath = path.resolve( __dirname, '../../scripts/modules/supported-models.json' ); let REAL_SUPPORTED_MODELS_CONTENT; let REAL_SUPPORTED_MODELS_DATA; try { REAL_SUPPORTED_MODELS_CONTENT = fs.readFileSync( realSupportedModelsPath, 'utf-8' ); REAL_SUPPORTED_MODELS_DATA = JSON.parse(REAL_SUPPORTED_MODELS_CONTENT); } catch (err) { console.error( 'FATAL TEST SETUP ERROR: Could not read or parse real supported-models.json', err ); REAL_SUPPORTED_MODELS_CONTENT = '{}'; // Default to empty object on error REAL_SUPPORTED_MODELS_DATA = {}; process.exit(1); // Exit if essential test data can't be loaded } // --- Define Mock Function Instances --- const mockFindProjectRoot = jest.fn(); const mockLog = jest.fn(); // --- Mock Dependencies BEFORE importing the module under test --- // Mock the 'utils.js' module using a factory function jest.mock('../../scripts/modules/utils.js', () => ({ __esModule: true, // Indicate it's an ES module mock findProjectRoot: mockFindProjectRoot, // Use the mock function instance log: mockLog, // Use the mock function instance // Include other necessary exports from utils if config-manager uses them directly resolveEnvVariable: jest.fn() // Example if needed })); // Import the mocked 'fs' module to allow spying on its functions import fsMocked from 'fs'; // --- Import the module under test AFTER mocks are defined --- import * as configManager from '../../scripts/modules/config-manager.js'; // --- Test Data (Keep as is, ensure DEFAULT_CONFIG is accurate) --- const MOCK_PROJECT_ROOT = '/mock/project'; const MOCK_CONFIG_PATH = path.join( MOCK_PROJECT_ROOT, '.taskmaster/config.json' ); // Updated DEFAULT_CONFIG reflecting the implementation const DEFAULT_CONFIG = { models: { main: { provider: 'anthropic', modelId: 'claude-sonnet-4-20250514', maxTokens: 64000, temperature: 0.2 }, research: { provider: 'perplexity', modelId: 'sonar', maxTokens: 8700, temperature: 0.1 }, fallback: { provider: 'anthropic', modelId: 'claude-3-7-sonnet-20250219', maxTokens: 120000, temperature: 0.2 } }, global: { anonymousTelemetry: true, logLevel: 'info', debug: false, defaultNumTasks: 10, defaultSubtasks: 5, defaultPriority: 'medium', projectName: 'Task Master', ollamaBaseURL: 'http://localhost:11434/api', bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com', enableCodebaseAnalysis: true, enableProxy: false, responseLanguage: 'English' }, claudeCode: {}, codexCli: {}, grokCli: { timeout: 120000, workingDirectory: null, defaultModel: 'grok-4-latest' } }; // Other test data (VALID_CUSTOM_CONFIG, PARTIAL_CONFIG, INVALID_PROVIDER_CONFIG) const VALID_CUSTOM_CONFIG = { models: { main: { provider: 'openai', modelId: 'gpt-4o', maxTokens: 4096, temperature: 0.5 }, research: { provider: 'google', modelId: 'gemini-1.5-pro-latest', maxTokens: 8192, temperature: 0.3 }, fallback: { provider: 'anthropic', modelId: 'claude-3-opus-20240229', maxTokens: 100000, temperature: 0.4 } }, global: { logLevel: 'debug', defaultPriority: 'high', projectName: 'My Custom Project' } }; const PARTIAL_CONFIG = { models: { main: { provider: 'openai', modelId: 'gpt-4-turbo' } }, global: { projectName: 'Partial Project' } }; const INVALID_PROVIDER_CONFIG = { models: { main: { provider: 'invalid-provider', modelId: 'some-model' }, research: { provider: 'perplexity', modelId: 'llama-3-sonar-large-32k-online' } }, global: { logLevel: 'warn' } }; // Claude Code test data const VALID_CLAUDE_CODE_CONFIG = { maxTurns: 5, customSystemPrompt: 'You are a helpful coding assistant', appendSystemPrompt: 'Always follow best practices', permissionMode: 'acceptEdits', allowedTools: ['Read', 'LS', 'Edit'], disallowedTools: ['Write'], mcpServers: { 'test-server': { type: 'stdio', command: 'node', args: ['server.js'], env: { NODE_ENV: 'test' } } }, commandSpecific: { 'add-task': { maxTurns: 3, permissionMode: 'plan' }, research: { customSystemPrompt: 'You are a research assistant' } } }; const INVALID_CLAUDE_CODE_CONFIG = { maxTurns: 'invalid', // Should be number permissionMode: 'invalid-mode', // Invalid enum value allowedTools: 'not-an-array', // Should be array mcpServers: { 'invalid-server': { type: 'invalid-type', // Invalid enum value url: 'not-a-valid-url' // Invalid URL format } }, commandSpecific: { 'invalid-command': { // Invalid command name maxTurns: -1 // Invalid negative number } } }; const PARTIAL_CLAUDE_CODE_CONFIG = { maxTurns: 10, permissionMode: 'default', commandSpecific: { 'expand-task': { customSystemPrompt: 'Focus on task breakdown' } } }; // Define spies globally to be restored in afterAll let consoleErrorSpy; let consoleWarnSpy; let fsReadFileSyncSpy; let fsWriteFileSyncSpy; let fsExistsSyncSpy; beforeAll(() => { // Set up console spies consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); }); afterAll(() => { // Restore all spies jest.restoreAllMocks(); }); // Reset mocks before each test for isolation beforeEach(() => { // Clear all mock calls and reset implementations between tests jest.clearAllMocks(); // Reset the external mock instances for utils mockFindProjectRoot.mockReset(); mockLog.mockReset(); mockFindConfigPath.mockReset(); // --- Set up spies ON the imported 'fs' mock --- fsExistsSyncSpy = jest.spyOn(fsMocked, 'existsSync'); fsReadFileSyncSpy = jest.spyOn(fsMocked, 'readFileSync'); fsWriteFileSyncSpy = jest.spyOn(fsMocked, 'writeFileSync'); // --- Default Mock Implementations --- mockFindProjectRoot.mockReturnValue(MOCK_PROJECT_ROOT); // Default for utils.findProjectRoot mockFindConfigPath.mockReturnValue(null); // Default to no config file found fsExistsSyncSpy.mockReturnValue(true); // Assume files exist by default // Default readFileSync: Return REAL models content, mocked config, or throw error fsReadFileSyncSpy.mockImplementation((filePath) => { const baseName = path.basename(filePath); if (baseName === 'supported-models.json') { // Return the REAL file content stringified return REAL_SUPPORTED_MODELS_CONTENT; } else if (filePath === MOCK_CONFIG_PATH) { // Still mock the .taskmasterconfig reads return JSON.stringify(DEFAULT_CONFIG); // Default behavior } // For Jest internal files or other unexpected files, return empty string instead of throwing // This prevents Jest's internal file operations from breaking tests if ( filePath.includes('jest-message-util') || filePath.includes('node_modules') ) { return '{}'; // Return empty JSON for Jest internal files } // Throw for truly unexpected reads that should be caught in tests throw new Error(`Unexpected fs.readFileSync call in test: ${filePath}`); }); // Default writeFileSync: Do nothing, just allow calls fsWriteFileSyncSpy.mockImplementation(() => {}); }); // --- Validation Functions --- describe('Validation Functions', () => { // Tests for validateProvider and validateProviderModelCombination test('validateProvider should return true for valid providers', () => { expect(configManager.validateProvider('openai')).toBe(true); expect(configManager.validateProvider('anthropic')).toBe(true); expect(configManager.validateProvider('google')).toBe(true); expect(configManager.validateProvider('perplexity')).toBe(true); expect(configManager.validateProvider('ollama')).toBe(true); expect(configManager.validateProvider('openrouter')).toBe(true); expect(configManager.validateProvider('bedrock')).toBe(true); }); test('validateProvider should return false for invalid providers', () => { expect(configManager.validateProvider('invalid-provider')).toBe(false); expect(configManager.validateProvider('grok')).toBe(false); // Not in mock map expect(configManager.validateProvider('')).toBe(false); expect(configManager.validateProvider(null)).toBe(false); }); test('validateProviderModelCombination should validate known good combinations', () => { // Re-load config to ensure MODEL_MAP is populated from mock (now real data) configManager.getConfig(MOCK_PROJECT_ROOT, true); expect( configManager.validateProviderModelCombination('openai', 'gpt-4o') ).toBe(true); expect( configManager.validateProviderModelCombination( 'anthropic', 'claude-3-5-sonnet-20241022' ) ).toBe(true); }); test('validateProviderModelCombination should return false for known bad combinations', () => { // Re-load config to ensure MODEL_MAP is populated from mock (now real data) configManager.getConfig(MOCK_PROJECT_ROOT, true); expect( configManager.validateProviderModelCombination( 'openai', 'claude-3-opus-20240229' ) ).toBe(false); }); test('validateProviderModelCombination should return true for ollama/openrouter (empty lists in map)', () => { // Re-load config to ensure MODEL_MAP is populated from mock (now real data) configManager.getConfig(MOCK_PROJECT_ROOT, true); expect( configManager.validateProviderModelCombination('ollama', 'any-model') ).toBe(false); expect( configManager.validateProviderModelCombination('openrouter', 'any/model') ).toBe(false); }); test('validateProviderModelCombination should return true for providers not in map', () => { // Re-load config to ensure MODEL_MAP is populated from mock (now real data) configManager.getConfig(MOCK_PROJECT_ROOT, true); // The implementation returns true if the provider isn't in the map expect( configManager.validateProviderModelCombination( 'unknown-provider', 'some-model' ) ).toBe(true); }); }); // --- Claude Code Validation Tests --- describe('Claude Code Validation', () => { test('validateClaudeCodeSettings should return valid settings for correct input', () => { const result = configManager.validateClaudeCodeSettings( VALID_CLAUDE_CODE_CONFIG ); expect(result).toEqual(VALID_CLAUDE_CODE_CONFIG); expect(consoleWarnSpy).not.toHaveBeenCalled(); }); test('validateClaudeCodeSettings should return empty object for invalid input', () => { const result = configManager.validateClaudeCodeSettings( INVALID_CLAUDE_CODE_CONFIG ); expect(result).toEqual({}); expect(consoleWarnSpy).toHaveBeenCalledWith( expect.stringContaining('Warning: Invalid Claude Code settings in config') ); }); test('validateClaudeCodeSettings should handle partial valid configuration', () => { const result = configManager.validateClaudeCodeSettings( PARTIAL_CLAUDE_CODE_CONFIG ); expect(result).toEqual(PARTIAL_CLAUDE_CODE_CONFIG); expect(consoleWarnSpy).not.toHaveBeenCalled(); }); test('validateClaudeCodeSettings should return empty object for empty input', () => { const result = configManager.validateClaudeCodeSettings({}); expect(result).toEqual({}); expect(consoleWarnSpy).not.toHaveBeenCalled(); }); test('validateClaudeCodeSettings should handle null/undefined input', () => { expect(configManager.validateClaudeCodeSettings(null)).toEqual({}); expect(configManager.validateClaudeCodeSettings(undefined)).toEqual({}); expect(consoleWarnSpy).toHaveBeenCalledTimes(2); }); }); // --- Claude Code Getter Tests --- describe('Claude Code Getter Functions', () => { test('getClaudeCodeSettings should return default empty object when no config exists', () => { // No config file exists, should return empty object fsExistsSyncSpy.mockReturnValue(false); const settings = configManager.getClaudeCodeSettings(MOCK_PROJECT_ROOT); expect(settings).toEqual({}); }); test('getClaudeCodeSettings should return merged settings from config file', () => { // Config file with Claude Code settings const configWithClaudeCode = { ...VALID_CUSTOM_CONFIG, claudeCode: VALID_CLAUDE_CODE_CONFIG }; // Mock findConfigPath to return the mock config path mockFindConfigPath.mockReturnValue(MOCK_CONFIG_PATH); fsReadFileSyncSpy.mockImplementation((filePath) => { if (filePath === MOCK_CONFIG_PATH) return JSON.stringify(configWithClaudeCode); if (path.basename(filePath) === 'supported-models.json') { return JSON.stringify({ openai: [{ id: 'gpt-4o' }], google: [{ id: 'gemini-1.5-pro-latest' }], anthropic: [ { id: 'claude-3-opus-20240229' }, { id: 'claude-3-7-sonnet-20250219' }, { id: 'claude-3-5-sonnet' } ], perplexity: [{ id: 'sonar-pro' }], ollama: [], openrouter: [] }); } throw new Error(`Unexpected fs.readFileSync call: ${filePath}`); }); fsExistsSyncSpy.mockReturnValue(true); const settings = configManager.getClaudeCodeSettings( MOCK_PROJECT_ROOT, true ); // Force reload expect(settings).toEqual(VALID_CLAUDE_CODE_CONFIG); }); test('getClaudeCodeSettingsForCommand should return command-specific settings', () => { // Config with command-specific settings const configWithClaudeCode = { ...VALID_CUSTOM_CONFIG, claudeCode: VALID_CLAUDE_CODE_CONFIG }; // Mock findConfigPath to return the mock config path mockFindConfigPath.mockReturnValue(MOCK_CONFIG_PATH); fsReadFileSyncSpy.mockImplementation((filePath) => { if (path.basename(filePath) === 'supported-models.json') return '{}'; if (filePath === MOCK_CONFIG_PATH) return JSON.stringify(configWithClaudeCode); throw new Error(`Unexpected fs.readFileSync call: ${filePath}`); throw new Error(`Unexpected fs.readFileSync call: ${filePath}`); }); fsExistsSyncSpy.mockReturnValue(true); const settings = configManager.getClaudeCodeSettingsForCommand( 'add-task', MOCK_PROJECT_ROOT, true ); // Force reload // Should merge global settings with command-specific settings const expectedSettings = { ...VALID_CLAUDE_CODE_CONFIG, ...VALID_CLAUDE_CODE_CONFIG.commandSpecific['add-task'] }; expect(settings).toEqual(expectedSettings); }); test('getClaudeCodeSettingsForCommand should return global settings for unknown command', () => { // Config with Claude Code settings const configWithClaudeCode = { ...VALID_CUSTOM_CONFIG, claudeCode: PARTIAL_CLAUDE_CODE_CONFIG }; // Mock findConfigPath to return the mock config path mockFindConfigPath.mockReturnValue(MOCK_CONFIG_PATH); fsReadFileSyncSpy.mockImplementation((filePath) => { if (path.basename(filePath) === 'supported-models.json') return '{}'; if (filePath === MOCK_CONFIG_PATH) return JSON.stringify(configWithClaudeCode); throw new Error(`Unexpected fs.readFileSync call: ${filePath}`); }); fsExistsSyncSpy.mockReturnValue(true); const settings = configManager.getClaudeCodeSettingsForCommand( 'unknown-command', MOCK_PROJECT_ROOT, true ); // Force reload // Should return global settings only expect(settings).toEqual(PARTIAL_CLAUDE_CODE_CONFIG); }); }); // --- getConfig Tests --- describe('getConfig Tests', () => { test('should return default config if .taskmasterconfig does not exist', () => { // Arrange fsExistsSyncSpy.mockReturnValue(false); // findProjectRoot mock is set in beforeEach // Act: Call getConfig with explicit root const config = configManager.getConfig(MOCK_PROJECT_ROOT, true); // Force reload // Assert expect(config).toEqual(DEFAULT_CONFIG); expect(mockFindProjectRoot).not.toHaveBeenCalled(); // Explicit root provided // The implementation checks for .taskmaster directory first expect(fsExistsSyncSpy).toHaveBeenCalledWith( path.join(MOCK_PROJECT_ROOT, '.taskmaster') ); expect(fsReadFileSyncSpy).not.toHaveBeenCalled(); // No read if file doesn't exist expect(consoleWarnSpy).toHaveBeenCalledWith( expect.stringContaining('not found at provided project root') ); }); test.skip('should use findProjectRoot and return defaults if file not found', () => { // TODO: Fix mock interaction, findProjectRoot isn't being registered as called // Arrange fsExistsSyncSpy.mockReturnValue(false); // findProjectRoot mock is set in beforeEach // Act: Call getConfig without explicit root const config = configManager.getConfig(null, true); // Force reload // Assert expect(mockFindProjectRoot).toHaveBeenCalled(); // Should be called now expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH); expect(config).toEqual(DEFAULT_CONFIG); expect(fsReadFileSyncSpy).not.toHaveBeenCalled(); expect(consoleWarnSpy).toHaveBeenCalledWith( expect.stringContaining('not found at derived root') ); // Adjusted expected warning }); test('should read and merge valid config file with defaults', () => { // Arrange: Override readFileSync for this test fsReadFileSyncSpy.mockImplementation((filePath) => { if (filePath === MOCK_CONFIG_PATH) return JSON.stringify(VALID_CUSTOM_CONFIG); if (path.basename(filePath) === 'supported-models.json') { // Provide necessary models for validation within getConfig return JSON.stringify({ openai: [{ id: 'gpt-4o' }], google: [{ id: 'gemini-1.5-pro-latest' }], perplexity: [{ id: 'sonar-pro' }], anthropic: [ { id: 'claude-3-opus-20240229' }, { id: 'claude-3-5-sonnet' }, { id: 'claude-3-7-sonnet-20250219' }, { id: 'claude-3-5-sonnet' } ], ollama: [], openrouter: [] }); } throw new Error(`Unexpected fs.readFileSync call: ${filePath}`); }); fsExistsSyncSpy.mockReturnValue(true); // findProjectRoot mock set in beforeEach // Act const config = configManager.getConfig(MOCK_PROJECT_ROOT, true); // Force reload // Assert: Construct expected merged config const expectedMergedConfig = { models: { main: { ...DEFAULT_CONFIG.models.main, ...VALID_CUSTOM_CONFIG.models.main }, research: { ...DEFAULT_CONFIG.models.research, ...VALID_CUSTOM_CONFIG.models.research }, fallback: { ...DEFAULT_CONFIG.models.fallback, ...VALID_CUSTOM_CONFIG.models.fallback } }, global: { ...DEFAULT_CONFIG.global, ...VALID_CUSTOM_CONFIG.global }, claudeCode: { ...DEFAULT_CONFIG.claudeCode, ...VALID_CUSTOM_CONFIG.claudeCode }, grokCli: { ...DEFAULT_CONFIG.grokCli }, codexCli: { ...DEFAULT_CONFIG.codexCli } }; expect(config).toEqual(expectedMergedConfig); expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH); expect(fsReadFileSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH, 'utf-8'); }); test('should merge defaults for partial config file', () => { // Arrange fsReadFileSyncSpy.mockImplementation((filePath) => { if (filePath === MOCK_CONFIG_PATH) return JSON.stringify(PARTIAL_CONFIG); if (path.basename(filePath) === 'supported-models.json') { return JSON.stringify({ openai: [{ id: 'gpt-4-turbo' }], perplexity: [{ id: 'sonar-pro' }], anthropic: [ { id: 'claude-3-7-sonnet-20250219' }, { id: 'claude-3-5-sonnet' } ], ollama: [], openrouter: [] }); } throw new Error(`Unexpected fs.readFileSync call: ${filePath}`); }); fsExistsSyncSpy.mockReturnValue(true); // findProjectRoot mock set in beforeEach // Act const config = configManager.getConfig(MOCK_PROJECT_ROOT, true); // Assert: Construct expected merged config const expectedMergedConfig = { models: { main: { ...DEFAULT_CONFIG.models.main, ...PARTIAL_CONFIG.models.main }, research: { ...DEFAULT_CONFIG.models.research }, fallback: { ...DEFAULT_CONFIG.models.fallback } }, global: { ...DEFAULT_CONFIG.global, ...PARTIAL_CONFIG.global }, claudeCode: { ...DEFAULT_CONFIG.claudeCode, ...VALID_CUSTOM_CONFIG.claudeCode }, grokCli: { ...DEFAULT_CONFIG.grokCli }, codexCli: { ...DEFAULT_CONFIG.codexCli } }; expect(config).toEqual(expectedMergedConfig); expect(fsReadFileSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH, 'utf-8'); }); test('should handle JSON parsing error and return defaults', () => { // Arrange fsReadFileSyncSpy.mockImplementation((filePath) => { if (filePath === MOCK_CONFIG_PATH) return 'invalid json'; // Mock models read needed for initial load before parse error if (path.basename(filePath) === 'supported-models.json') { return JSON.stringify({ anthropic: [{ id: 'claude-3-7-sonnet-20250219' }], perplexity: [{ id: 'sonar-pro' }], fallback: [{ id: 'claude-3-5-sonnet' }], ollama: [], openrouter: [] }); } throw new Error(`Unexpected fs.readFileSync call: ${filePath}`); }); fsExistsSyncSpy.mockReturnValue(true); // findProjectRoot mock set in beforeEach // Act const config = configManager.getConfig(MOCK_PROJECT_ROOT, true); // Assert expect(config).toEqual(DEFAULT_CONFIG); expect(consoleErrorSpy).toHaveBeenCalledWith( expect.stringContaining('Error reading or parsing') ); }); test('should handle file read error and return defaults', () => { // Arrange const readError = new Error('Permission denied'); fsReadFileSyncSpy.mockImplementation((filePath) => { if (filePath === MOCK_CONFIG_PATH) throw readError; // Mock models read needed for initial load before read error if (path.basename(filePath) === 'supported-models.json') { return JSON.stringify({ anthropic: [{ id: 'claude-3-7-sonnet-20250219' }], perplexity: [{ id: 'sonar-pro' }], fallback: [{ id: 'claude-3-5-sonnet' }], ollama: [], openrouter: [] }); } throw new Error(`Unexpected fs.readFileSync call: ${filePath}`); }); fsExistsSyncSpy.mockReturnValue(true); // findProjectRoot mock set in beforeEach // Act const config = configManager.getConfig(MOCK_PROJECT_ROOT, true); // Assert expect(config).toEqual(DEFAULT_CONFIG); expect(consoleErrorSpy).toHaveBeenCalledWith( expect.stringContaining('Permission denied. Using default configuration.') ); }); test('should validate provider and fallback to default if invalid', () => { // Arrange fsReadFileSyncSpy.mockImplementation((filePath) => { if (filePath === MOCK_CONFIG_PATH) return JSON.stringify(INVALID_PROVIDER_CONFIG); if (path.basename(filePath) === 'supported-models.json') { return JSON.stringify({ perplexity: [{ id: 'llama-3-sonar-large-32k-online' }], anthropic: [ { id: 'claude-3-7-sonnet-20250219' }, { id: 'claude-3-5-sonnet' } ], ollama: [], openrouter: [] }); } throw new Error(`Unexpected fs.readFileSync call: ${filePath}`); }); fsExistsSyncSpy.mockReturnValue(true); // findProjectRoot mock set in beforeEach // Act const config = configManager.getConfig(MOCK_PROJECT_ROOT, true); // Assert expect(consoleWarnSpy).toHaveBeenCalledWith( expect.stringContaining( 'Warning: Invalid main provider "invalid-provider"' ) ); const expectedMergedConfig = { models: { main: { ...DEFAULT_CONFIG.models.main }, research: { ...DEFAULT_CONFIG.models.research, ...INVALID_PROVIDER_CONFIG.models.research }, fallback: { ...DEFAULT_CONFIG.models.fallback } }, global: { ...DEFAULT_CONFIG.global, ...INVALID_PROVIDER_CONFIG.global }, claudeCode: { ...DEFAULT_CONFIG.claudeCode, ...VALID_CUSTOM_CONFIG.claudeCode }, grokCli: { ...DEFAULT_CONFIG.grokCli }, codexCli: { ...DEFAULT_CONFIG.codexCli } }; expect(config).toEqual(expectedMergedConfig); }); }); // --- writeConfig Tests --- describe('writeConfig', () => { test('should write valid config to file', () => { // Arrange (Default mocks are sufficient) // findProjectRoot mock set in beforeEach fsWriteFileSyncSpy.mockImplementation(() => {}); // Ensure it doesn't throw // Act const success = configManager.writeConfig( VALID_CUSTOM_CONFIG, MOCK_PROJECT_ROOT ); // Assert expect(success).toBe(true); expect(fsWriteFileSyncSpy).toHaveBeenCalledWith( MOCK_CONFIG_PATH, JSON.stringify(VALID_CUSTOM_CONFIG, null, 2) // writeConfig stringifies ); expect(consoleErrorSpy).not.toHaveBeenCalled(); }); test('should return false and log error if write fails', () => { // Arrange const mockWriteError = new Error('Disk full'); fsWriteFileSyncSpy.mockImplementation(() => { throw mockWriteError; }); // findProjectRoot mock set in beforeEach // Act const success = configManager.writeConfig( VALID_CUSTOM_CONFIG, MOCK_PROJECT_ROOT ); // Assert expect(success).toBe(false); expect(fsWriteFileSyncSpy).toHaveBeenCalled(); expect(consoleErrorSpy).toHaveBeenCalledWith( expect.stringContaining('Disk full') ); }); test.skip('should return false if project root cannot be determined', () => { // TODO: Fix mock interaction or function logic, returns true unexpectedly in test // Arrange: Override mock for this specific test mockFindProjectRoot.mockReturnValue(null); // Act: Call without explicit root const success = configManager.writeConfig(VALID_CUSTOM_CONFIG); // Assert expect(success).toBe(false); // Function should return false if root is null expect(mockFindProjectRoot).toHaveBeenCalled(); expect(fsWriteFileSyncSpy).not.toHaveBeenCalled(); expect(consoleErrorSpy).toHaveBeenCalledWith( expect.stringContaining('Could not determine project root') ); }); }); // --- Getter Functions --- describe('Getter Functions', () => { test('getMainProvider should return provider from config', () => { // Arrange: Set up readFileSync to return VALID_CUSTOM_CONFIG fsReadFileSyncSpy.mockImplementation((filePath) => { if (filePath === MOCK_CONFIG_PATH) return JSON.stringify(VALID_CUSTOM_CONFIG); if (path.basename(filePath) === 'supported-models.json') { return JSON.stringify({ openai: [{ id: 'gpt-4o' }], google: [{ id: 'gemini-1.5-pro-latest' }], anthropic: [ { id: 'claude-3-opus-20240229' }, { id: 'claude-3-7-sonnet-20250219' }, { id: 'claude-3-5-sonnet' } ], perplexity: [{ id: 'sonar-pro' }], ollama: [], openrouter: [] }); // Added perplexity } throw new Error(`Unexpected fs.readFileSync call: ${filePath}`); }); fsExistsSyncSpy.mockReturnValue(true); // findProjectRoot mock set in beforeEach // Act const provider = configManager.getMainProvider(MOCK_PROJECT_ROOT); // Assert expect(provider).toBe(VALID_CUSTOM_CONFIG.models.main.provider); }); test('getLogLevel should return logLevel from config', () => { // Arrange: Set up readFileSync to return VALID_CUSTOM_CONFIG fsReadFileSyncSpy.mockImplementation((filePath) => { if (filePath === MOCK_CONFIG_PATH) return JSON.stringify(VALID_CUSTOM_CONFIG); if (path.basename(filePath) === 'supported-models.json') { // Provide enough mock model data for validation within getConfig return JSON.stringify({ openai: [{ id: 'gpt-4o' }], google: [{ id: 'gemini-1.5-pro-latest' }], anthropic: [ { id: 'claude-3-opus-20240229' }, { id: 'claude-3-7-sonnet-20250219' }, { id: 'claude-3-5-sonnet' } ], perplexity: [{ id: 'sonar-pro' }], ollama: [], openrouter: [] }); } throw new Error(`Unexpected fs.readFileSync call: ${filePath}`); }); fsExistsSyncSpy.mockReturnValue(true); // findProjectRoot mock set in beforeEach // Act const logLevel = configManager.getLogLevel(MOCK_PROJECT_ROOT); // Assert expect(logLevel).toBe(VALID_CUSTOM_CONFIG.global.logLevel); }); test('getResponseLanguage should return responseLanguage from config', () => { // Arrange // Prepare a config object with responseLanguage property for this test const configWithLanguage = JSON.stringify({ models: { main: { provider: 'openai', modelId: 'gpt-4-turbo' } }, global: { projectName: 'Test Project', responseLanguage: '中文' } }); // Set up fs.readFileSync to return our test config fsReadFileSyncSpy.mockImplementation((filePath) => { if (filePath === MOCK_CONFIG_PATH) { return configWithLanguage; } if (path.basename(filePath) === 'supported-models.json') { return JSON.stringify({ openai: [{ id: 'gpt-4-turbo' }] }); } throw new Error(`Unexpected fs.readFileSync call: ${filePath}`); }); fsExistsSyncSpy.mockReturnValue(true); // Ensure getConfig returns new values instead of cached ones configManager.getConfig(MOCK_PROJECT_ROOT, true); // Act const responseLanguage = configManager.getResponseLanguage(MOCK_PROJECT_ROOT); // Assert expect(responseLanguage).toBe('中文'); }); test('getResponseLanguage should return undefined when responseLanguage is not in config', () => { // Arrange const configWithoutLanguage = JSON.stringify({ models: { main: { provider: 'openai', modelId: 'gpt-4-turbo' } }, global: { projectName: 'Test Project' // No responseLanguage property } }); fsReadFileSyncSpy.mockImplementation((filePath) => { if (filePath === MOCK_CONFIG_PATH) { return configWithoutLanguage; } if (path.basename(filePath) === 'supported-models.json') { return JSON.stringify({ openai: [{ id: 'gpt-4-turbo' }] }); } throw new Error(`Unexpected fs.readFileSync call: ${filePath}`); }); fsExistsSyncSpy.mockReturnValue(true); // Ensure getConfig returns new values instead of cached ones configManager.getConfig(MOCK_PROJECT_ROOT, true); // Act const responseLanguage = configManager.getResponseLanguage(MOCK_PROJECT_ROOT); // Assert expect(responseLanguage).toBe('English'); }); // Add more tests for other getters (getResearchProvider, getProjectName, etc.) }); // --- isConfigFilePresent Tests --- describe('isConfigFilePresent', () => { test('should return true if config file exists', () => { fsExistsSyncSpy.mockReturnValue(true); // findProjectRoot mock set in beforeEach expect(configManager.isConfigFilePresent(MOCK_PROJECT_ROOT)).toBe(true); expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH); }); test('should return false if config file does not exist', () => { fsExistsSyncSpy.mockReturnValue(false); // findProjectRoot mock set in beforeEach expect(configManager.isConfigFilePresent(MOCK_PROJECT_ROOT)).toBe(false); expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH); }); test.skip('should use findProjectRoot if explicitRoot is not provided', () => { // TODO: Fix mock interaction, findProjectRoot isn't being registered as called fsExistsSyncSpy.mockReturnValue(true); // findProjectRoot mock set in beforeEach expect(configManager.isConfigFilePresent()).toBe(true); expect(mockFindProjectRoot).toHaveBeenCalled(); // Should be called now }); }); // --- getAllProviders Tests --- describe('getAllProviders', () => { test('should return all providers from ALL_PROVIDERS constant', () => { // Arrange: Ensure config is loaded with real data configManager.getConfig(null, true); // Force load using the mock that returns real data // Act const providers = configManager.getAllProviders(); // Assert // getAllProviders() should return the same as the ALL_PROVIDERS constant expect(providers).toEqual(configManager.ALL_PROVIDERS); expect(providers.length).toBe(configManager.ALL_PROVIDERS.length); // Verify it includes both validated and custom providers expect(providers).toEqual( expect.arrayContaining(configManager.VALIDATED_PROVIDERS) ); expect(providers).toEqual( expect.arrayContaining(Object.values(configManager.CUSTOM_PROVIDERS)) ); }); }); // Add tests for getParametersForRole if needed // --- defaultNumTasks Tests --- describe('Configuration Getters', () => { test('getDefaultNumTasks should return default value when config is valid', () => { // Arrange: Mock fs.readFileSync to return valid config when called with the expected path fsReadFileSyncSpy.mockImplementation((filePath) => { if (filePath === MOCK_CONFIG_PATH) { return JSON.stringify({ global: { defaultNumTasks: 15 } }); } throw new Error(`Unexpected fs.readFileSync call: ${filePath}`); }); fsExistsSyncSpy.mockReturnValue(true); // Force reload to clear cache configManager.getConfig(MOCK_PROJECT_ROOT, true); // Act: Call getDefaultNumTasks with explicit root const result = configManager.getDefaultNumTasks(MOCK_PROJECT_ROOT); // Assert expect(result).toBe(15); }); test('getDefaultNumTasks should return fallback when config value is invalid', () => { // Arrange: Mock fs.readFileSync to return invalid config fsReadFileSyncSpy.mockImplementation((filePath) => { if (filePath === MOCK_CONFIG_PATH) { return JSON.stringify({ global: { defaultNumTasks: 'invalid' } }); } throw new Error(`Unexpected fs.readFileSync call: ${filePath}`); }); fsExistsSyncSpy.mockReturnValue(true); // Force reload to clear cache configManager.getConfig(MOCK_PROJECT_ROOT, true); // Act: Call getDefaultNumTasks with explicit root const result = configManager.getDefaultNumTasks(MOCK_PROJECT_ROOT); // Assert expect(result).toBe(10); // Should fallback to DEFAULTS.global.defaultNumTasks }); test('getDefaultNumTasks should return fallback when config value is missing', () => { // Arrange: Mock fs.readFileSync to return config without defaultNumTasks fsReadFileSyncSpy.mockImplementation((filePath) => { if (filePath === MOCK_CONFIG_PATH) { return JSON.stringify({ global: {} }); } throw new Error(`Unexpected fs.readFileSync call: ${filePath}`); }); fsExistsSyncSpy.mockReturnValue(true); // Force reload to clear cache configManager.getConfig(MOCK_PROJECT_ROOT, true); // Act: Call getDefaultNumTasks with explicit root const result = configManager.getDefaultNumTasks(MOCK_PROJECT_ROOT); // Assert expect(result).toBe(10); // Should fallback to DEFAULTS.global.defaultNumTasks }); test('getDefaultNumTasks should handle non-existent config file', () => { // Arrange: Mock file not existing fsExistsSyncSpy.mockReturnValue(false); // Force reload to clear cache configManager.getConfig(MOCK_PROJECT_ROOT, true); // Act: Call getDefaultNumTasks with explicit root const result = configManager.getDefaultNumTasks(MOCK_PROJECT_ROOT); // Assert expect(result).toBe(10); // Should fallback to DEFAULTS.global.defaultNumTasks }); test('getDefaultNumTasks should accept explicit project root', () => { // Arrange: Mock fs.readFileSync to return valid config fsReadFileSyncSpy.mockImplementation((filePath) => { if (filePath === MOCK_CONFIG_PATH) { return JSON.stringify({ global: { defaultNumTasks: 20 } }); } throw new Error(`Unexpected fs.readFileSync call: ${filePath}`); }); fsExistsSyncSpy.mockReturnValue(true); // Force reload to clear cache configManager.getConfig(MOCK_PROJECT_ROOT, true); // Act: Call getDefaultNumTasks with explicit project root const result = configManager.getDefaultNumTasks(MOCK_PROJECT_ROOT); // Assert expect(result).toBe(20); }); }); // Note: Tests for setMainModel, setResearchModel were removed as the functions were removed in the implementation. // If similar setter functions exist, add tests for them following the writeConfig pattern.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/eyaltoledano/claude-task-master'

If you have feedback or need assistance with the MCP directory API, please join our Discord server