mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2025-07-05 16:11:02 +00:00

* Feat: Implemented advanced settings for Claude Code AI provider - Added new 'claudeCode' property to default config - Added getters and validation functions to 'config-manager.js' - Added new 'isEmpty' utility to 'utils.js' - Added new constants file 'commands.js' for AI_COMMAND_NAMES - Updated Claude Code AI provider to use new config functions - Updated 'claude-code-usage.md' documentation - Added 'config-manager.test.js' tests to cover new settings * chore: run format --------- Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
1161 lines
36 KiB
JavaScript
1161 lines
36 KiB
JavaScript
import fs from 'fs';
|
|
import path from 'path';
|
|
import { jest } from '@jest/globals';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
// 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((text) => ({
|
|
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 module under test AFTER mocks are defined ---
|
|
import * as configManager from '../../scripts/modules/config-manager.js';
|
|
// Import the mocked 'fs' module to allow spying on its functions
|
|
import fsMocked from 'fs';
|
|
|
|
// --- 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-3-7-sonnet-20250219',
|
|
maxTokens: 64000,
|
|
temperature: 0.2
|
|
},
|
|
research: {
|
|
provider: 'perplexity',
|
|
modelId: 'sonar-pro',
|
|
maxTokens: 8700,
|
|
temperature: 0.1
|
|
},
|
|
fallback: {
|
|
provider: 'anthropic',
|
|
modelId: 'claude-3-5-sonnet',
|
|
maxTokens: 8192,
|
|
temperature: 0.2
|
|
}
|
|
},
|
|
global: {
|
|
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',
|
|
responseLanguage: 'English'
|
|
},
|
|
claudeCode: {}
|
|
};
|
|
|
|
// 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
|
|
expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
|
|
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
|
|
}
|
|
};
|
|
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
|
|
}
|
|
};
|
|
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
|
|
}
|
|
};
|
|
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.
|