Feat: Implemented advanced settings for Claude Code AI provider (#872)
* 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>
This commit is contained in:
5
.changeset/sweet-ties-argue.md
Normal file
5
.changeset/sweet-ties-argue.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Add advanced settings for Claude Code AI Provider
|
||||
@@ -31,5 +31,6 @@
|
||||
"azureBaseURL": "https://your-endpoint.azure.com/",
|
||||
"defaultTag": "master",
|
||||
"responseLanguage": "English"
|
||||
}
|
||||
},
|
||||
"claudeCode": {}
|
||||
}
|
||||
|
||||
@@ -64,100 +64,81 @@ task-master set-status --id=task-001 --status=in-progress
|
||||
```bash
|
||||
npm install @anthropic-ai/claude-code
|
||||
```
|
||||
3. No API key is required in your environment variables or MCP configuration
|
||||
3. Run Claude Code for the first time and authenticate with your Anthropic account:
|
||||
```bash
|
||||
claude
|
||||
```
|
||||
4. No API key is required in your environment variables or MCP configuration
|
||||
|
||||
## Advanced Settings
|
||||
|
||||
The Claude Code SDK supports additional settings that provide fine-grained control over Claude's behavior. While these settings are implemented in the underlying SDK (`src/ai-providers/custom-sdk/claude-code/`), they are not currently exposed through Task Master's standard API due to architectural constraints.
|
||||
The Claude Code SDK supports additional settings that provide fine-grained control over Claude's behavior. These settings are implemented in the underlying SDK (`src/ai-providers/custom-sdk/claude-code/`), and can be managed through Task Master's configuration file.
|
||||
|
||||
### Supported Settings
|
||||
### Advanced Settings Usage
|
||||
|
||||
To update settings for Claude Code, update your `.taskmaster/config.json`:
|
||||
|
||||
The Claude Code settings can be specified globally in the `claudeCode` section of the config, or on a per-command basis in the `commandSpecific` section:
|
||||
|
||||
```javascript
|
||||
const settings = {
|
||||
// Maximum conversation turns Claude can make in a single request
|
||||
maxTurns: 5,
|
||||
|
||||
// Custom system prompt to override Claude Code's default behavior
|
||||
customSystemPrompt: "You are a helpful assistant focused on code quality",
|
||||
|
||||
// Permission mode for file system operations
|
||||
permissionMode: 'default', // Options: 'default', 'restricted', 'permissive'
|
||||
|
||||
// Explicitly allow only certain tools
|
||||
allowedTools: ['Read', 'LS'], // Claude can only read files and list directories
|
||||
|
||||
// Explicitly disallow certain tools
|
||||
disallowedTools: ['Write', 'Edit'], // Prevent Claude from modifying files
|
||||
|
||||
// MCP servers for additional tool integrations
|
||||
mcpServers: []
|
||||
};
|
||||
```
|
||||
{
|
||||
// "models" and "global" config...
|
||||
|
||||
### Current Limitations
|
||||
"claudeCode": {
|
||||
// Maximum conversation turns Claude can make in a single request
|
||||
"maxTurns": 5,
|
||||
|
||||
// Custom system prompt to override Claude Code's default behavior
|
||||
"customSystemPrompt": "You are a helpful assistant focused on code quality",
|
||||
|
||||
Task Master uses a standardized `BaseAIProvider` interface that only passes through common parameters (modelId, messages, maxTokens, temperature) to maintain consistency across all providers. The Claude Code advanced settings are implemented in the SDK but not accessible through Task Master's high-level commands.
|
||||
// Append additional content to the system prompt
|
||||
"appendSystemPrompt": "Always follow coding best practices",
|
||||
|
||||
// Permission mode for file system operations
|
||||
"permissionMode": "default", // Options: "default", "acceptEdits", "plan", "bypassPermissions"
|
||||
|
||||
// Explicitly allow only certain tools
|
||||
"allowedTools": ["Read", "LS"], // Claude can only read files and list directories
|
||||
|
||||
// Explicitly disallow certain tools
|
||||
"disallowedTools": ["Write", "Edit"], // Prevent Claude from modifying files
|
||||
|
||||
// MCP servers for additional tool integrations
|
||||
"mcpServers": {
|
||||
"mcp-server-name": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-serve"],
|
||||
"env": {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
### Future Integration Options
|
||||
|
||||
For developers who need to use these advanced settings, there are three potential approaches:
|
||||
|
||||
#### Option 1: Extend BaseAIProvider
|
||||
Modify the core Task Master architecture to support provider-specific settings:
|
||||
|
||||
```javascript
|
||||
// In BaseAIProvider
|
||||
const result = await generateText({
|
||||
model: client(params.modelId),
|
||||
messages: params.messages,
|
||||
maxTokens: params.maxTokens,
|
||||
temperature: params.temperature,
|
||||
...params.providerSettings // New: pass through provider-specific settings
|
||||
});
|
||||
```
|
||||
|
||||
#### Option 2: Override Methods in ClaudeCodeProvider
|
||||
Create custom implementations that extract and use Claude-specific settings:
|
||||
|
||||
```javascript
|
||||
// In ClaudeCodeProvider
|
||||
async generateText(params) {
|
||||
const { maxTurns, allowedTools, disallowedTools, ...baseParams } = params;
|
||||
|
||||
const client = this.getClient({
|
||||
...baseParams,
|
||||
settings: { maxTurns, allowedTools, disallowedTools }
|
||||
});
|
||||
|
||||
// Continue with generation...
|
||||
// Command-specific settings override global settings
|
||||
"commandSpecific": {
|
||||
"parse-prd": {
|
||||
// Settings specific to the 'parse-prd' command
|
||||
"maxTurns": 10,
|
||||
"customSystemPrompt": "You are a task breakdown specialist"
|
||||
},
|
||||
"analyze-complexity": {
|
||||
// Settings specific to the 'analyze-complexity' command
|
||||
"maxTurns": 3,
|
||||
"appendSystemPrompt": "Focus on identifying bottlenecks"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Option 3: Direct SDK Usage
|
||||
For immediate access to advanced features, developers can use the Claude Code SDK directly:
|
||||
|
||||
```javascript
|
||||
import { createClaudeCode } from 'task-master-ai/ai-providers/custom-sdk/claude-code';
|
||||
|
||||
const claude = createClaudeCode({
|
||||
defaultSettings: {
|
||||
maxTurns: 5,
|
||||
allowedTools: ['Read', 'LS'],
|
||||
disallowedTools: ['Write', 'Edit']
|
||||
}
|
||||
});
|
||||
|
||||
const model = claude('sonnet');
|
||||
const result = await generateText({
|
||||
model,
|
||||
messages: [{ role: 'user', content: 'Analyze this code...' }]
|
||||
});
|
||||
```
|
||||
- For a full list of Cluaude Code settings, see the [Claude Code Settings documentation](https://docs.anthropic.com/en/docs/claude-code/settings).
|
||||
- For a full list of AI powered command names, see this file: `src/constants/commands.js`
|
||||
|
||||
### Why These Settings Matter
|
||||
|
||||
- **maxTurns**: Useful for complex refactoring tasks that require multiple iterations
|
||||
- **customSystemPrompt**: Allows specializing Claude for specific domains or coding standards
|
||||
- **appendSystemPrompt**: Useful for enforcing coding standards or providing additional context
|
||||
- **permissionMode**: Critical for security in production environments
|
||||
- **allowedTools/disallowedTools**: Enable read-only analysis modes or restrict access to sensitive operations
|
||||
- **mcpServers**: Future extensibility for custom tool integrations
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import { z } from 'zod';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { log, findProjectRoot, resolveEnvVariable } from './utils.js';
|
||||
import { log, findProjectRoot, resolveEnvVariable, isEmpty } from './utils.js';
|
||||
import { LEGACY_CONFIG_FILE } from '../../src/constants/paths.js';
|
||||
import { findConfigPath } from '../../src/utils/path-utils.js';
|
||||
import {
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
CUSTOM_PROVIDERS_ARRAY,
|
||||
ALL_PROVIDERS
|
||||
} from '../../src/constants/providers.js';
|
||||
import { AI_COMMAND_NAMES } from '../../src/constants/commands.js';
|
||||
|
||||
// Calculate __dirname in ESM
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -68,7 +70,8 @@ const DEFAULTS = {
|
||||
ollamaBaseURL: 'http://localhost:11434/api',
|
||||
bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com',
|
||||
responseLanguage: 'English'
|
||||
}
|
||||
},
|
||||
claudeCode: {}
|
||||
};
|
||||
|
||||
// --- Internal Config Loading ---
|
||||
@@ -129,7 +132,8 @@ function _loadAndValidateConfig(explicitRoot = null) {
|
||||
? { ...defaults.models.fallback, ...parsedConfig.models.fallback }
|
||||
: { ...defaults.models.fallback }
|
||||
},
|
||||
global: { ...defaults.global, ...parsedConfig?.global }
|
||||
global: { ...defaults.global, ...parsedConfig?.global },
|
||||
claudeCode: { ...defaults.claudeCode, ...parsedConfig?.claudeCode }
|
||||
};
|
||||
configSource = `file (${configPath})`; // Update source info
|
||||
|
||||
@@ -172,6 +176,9 @@ function _loadAndValidateConfig(explicitRoot = null) {
|
||||
config.models.fallback.provider = undefined;
|
||||
config.models.fallback.modelId = undefined;
|
||||
}
|
||||
if (config.claudeCode && !isEmpty(config.claudeCode)) {
|
||||
config.claudeCode = validateClaudeCodeSettings(config.claudeCode);
|
||||
}
|
||||
} catch (error) {
|
||||
// Use console.error for actual errors during parsing
|
||||
console.error(
|
||||
@@ -279,6 +286,83 @@ function validateProviderModelCombination(providerName, modelId) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates Claude Code AI provider custom settings
|
||||
* @param {object} settings The settings to validate
|
||||
* @returns {object} The validated settings
|
||||
*/
|
||||
function validateClaudeCodeSettings(settings) {
|
||||
// Define the base settings schema without commandSpecific first
|
||||
const BaseSettingsSchema = z.object({
|
||||
maxTurns: z.number().int().positive().optional(),
|
||||
customSystemPrompt: z.string().optional(),
|
||||
appendSystemPrompt: z.string().optional(),
|
||||
permissionMode: z
|
||||
.enum(['default', 'acceptEdits', 'plan', 'bypassPermissions'])
|
||||
.optional(),
|
||||
allowedTools: z.array(z.string()).optional(),
|
||||
disallowedTools: z.array(z.string()).optional(),
|
||||
mcpServers: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
type: z.enum(['stdio', 'sse']).optional(),
|
||||
command: z.string(),
|
||||
args: z.array(z.string()).optional(),
|
||||
env: z.record(z.string()).optional(),
|
||||
url: z.string().url().optional(),
|
||||
headers: z.record(z.string()).optional()
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
});
|
||||
|
||||
// Define CommandSpecificSchema using the base schema
|
||||
const CommandSpecificSchema = z.record(
|
||||
z.enum(AI_COMMAND_NAMES),
|
||||
BaseSettingsSchema
|
||||
);
|
||||
|
||||
// Define the full settings schema with commandSpecific
|
||||
const SettingsSchema = BaseSettingsSchema.extend({
|
||||
commandSpecific: CommandSpecificSchema.optional()
|
||||
});
|
||||
|
||||
let validatedSettings = {};
|
||||
|
||||
try {
|
||||
validatedSettings = SettingsSchema.parse(settings);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
`Warning: Invalid Claude Code settings in config: ${error.message}. Falling back to default.`
|
||||
)
|
||||
);
|
||||
|
||||
validatedSettings = {};
|
||||
}
|
||||
|
||||
return validatedSettings;
|
||||
}
|
||||
|
||||
// --- Claude Code Settings Getters ---
|
||||
|
||||
function getClaudeCodeSettings(explicitRoot = null, forceReload = false) {
|
||||
const config = getConfig(explicitRoot, forceReload);
|
||||
// Ensure Claude Code defaults are applied if Claude Code section is missing
|
||||
return { ...DEFAULTS.claudeCode, ...(config?.claudeCode || {}) };
|
||||
}
|
||||
|
||||
function getClaudeCodeSettingsForCommand(
|
||||
commandName,
|
||||
explicitRoot = null,
|
||||
forceReload = false
|
||||
) {
|
||||
const settings = getClaudeCodeSettings(explicitRoot, forceReload);
|
||||
const commandSpecific = settings?.commandSpecific || {};
|
||||
return { ...settings, ...commandSpecific[commandName] };
|
||||
}
|
||||
|
||||
// --- Role-Specific Getters ---
|
||||
|
||||
function getModelConfigForRole(role, explicitRoot = null) {
|
||||
@@ -815,9 +899,13 @@ export {
|
||||
writeConfig,
|
||||
ConfigurationError,
|
||||
isConfigFilePresent,
|
||||
// Claude Code settings
|
||||
getClaudeCodeSettings,
|
||||
getClaudeCodeSettingsForCommand,
|
||||
// Validation
|
||||
validateProvider,
|
||||
validateProviderModelCombination,
|
||||
validateClaudeCodeSettings,
|
||||
VALIDATED_PROVIDERS,
|
||||
CUSTOM_PROVIDERS,
|
||||
ALL_PROVIDERS,
|
||||
|
||||
@@ -1012,6 +1012,21 @@ function truncate(text, maxLength) {
|
||||
return `${text.slice(0, maxLength - 3)}...`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if array or object are empty
|
||||
* @param {*} value - The value to check
|
||||
* @returns {boolean} True if empty, false otherwise
|
||||
*/
|
||||
function isEmpty(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.length === 0;
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
return Object.keys(value).length === 0;
|
||||
}
|
||||
|
||||
return false; // Not an array or object, or is null
|
||||
}
|
||||
|
||||
/**
|
||||
* Find cycles in a dependency graph using DFS
|
||||
* @param {string} subtaskId - Current subtask ID
|
||||
@@ -1373,6 +1388,7 @@ export {
|
||||
formatTaskId,
|
||||
findTaskById,
|
||||
truncate,
|
||||
isEmpty,
|
||||
findCycles,
|
||||
toKebabCase,
|
||||
detectCamelCaseFlags,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import { createClaudeCode } from './custom-sdk/claude-code/index.js';
|
||||
import { BaseAIProvider } from './base-provider.js';
|
||||
import { getClaudeCodeSettingsForCommand } from '../../scripts/modules/config-manager.js';
|
||||
|
||||
export class ClaudeCodeProvider extends BaseAIProvider {
|
||||
constructor() {
|
||||
@@ -26,6 +27,7 @@ export class ClaudeCodeProvider extends BaseAIProvider {
|
||||
/**
|
||||
* Creates and returns a Claude Code client instance.
|
||||
* @param {object} params - Parameters for client initialization
|
||||
* @param {string} [params.commandName] - Name of the command invoking the service
|
||||
* @param {string} [params.baseURL] - Optional custom API endpoint (not used by Claude Code)
|
||||
* @returns {Function} Claude Code client function
|
||||
* @throws {Error} If initialization fails
|
||||
@@ -35,10 +37,7 @@ export class ClaudeCodeProvider extends BaseAIProvider {
|
||||
// Claude Code doesn't use API keys or base URLs
|
||||
// Just return the provider factory
|
||||
return createClaudeCode({
|
||||
defaultSettings: {
|
||||
// Add any default settings if needed
|
||||
// These can be overridden per request
|
||||
}
|
||||
defaultSettings: getClaudeCodeSettingsForCommand(params?.commandName)
|
||||
});
|
||||
} catch (error) {
|
||||
this.handleError('client initialization', error);
|
||||
|
||||
17
src/constants/commands.js
Normal file
17
src/constants/commands.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Command related constants
|
||||
* Defines which commands trigger AI processing
|
||||
*/
|
||||
|
||||
// Command names that trigger AI processing
|
||||
export const AI_COMMAND_NAMES = [
|
||||
'add-task',
|
||||
'analyze-complexity',
|
||||
'expand-task',
|
||||
'parse-prd',
|
||||
'research',
|
||||
'research-save',
|
||||
'update-subtask',
|
||||
'update-task',
|
||||
'update-tasks'
|
||||
];
|
||||
@@ -48,11 +48,14 @@ const mockConsole = {
|
||||
};
|
||||
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: jest.fn(() => null), // Always return null to prevent config discovery
|
||||
findConfigPath: mockFindConfigPath, // Use the mock function instance
|
||||
findTasksPath: jest.fn(() => '/mock/tasks.json'),
|
||||
findComplexityReportPath: jest.fn(() => null),
|
||||
resolveTasksOutputPath: jest.fn(() => '/mock/tasks.json'),
|
||||
@@ -143,7 +146,8 @@ const DEFAULT_CONFIG = {
|
||||
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)
|
||||
@@ -197,6 +201,61 @@ const INVALID_PROVIDER_CONFIG = {
|
||||
}
|
||||
};
|
||||
|
||||
// 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;
|
||||
@@ -222,6 +281,7 @@ beforeEach(() => {
|
||||
// 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');
|
||||
@@ -230,6 +290,7 @@ beforeEach(() => {
|
||||
|
||||
// --- 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
|
||||
@@ -327,6 +388,162 @@ describe('Validation Functions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// --- 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', () => {
|
||||
@@ -411,7 +628,11 @@ describe('getConfig Tests', () => {
|
||||
...VALID_CUSTOM_CONFIG.models.fallback
|
||||
}
|
||||
},
|
||||
global: { ...DEFAULT_CONFIG.global, ...VALID_CUSTOM_CONFIG.global }
|
||||
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);
|
||||
@@ -449,7 +670,11 @@ describe('getConfig Tests', () => {
|
||||
research: { ...DEFAULT_CONFIG.models.research },
|
||||
fallback: { ...DEFAULT_CONFIG.models.fallback }
|
||||
},
|
||||
global: { ...DEFAULT_CONFIG.global, ...PARTIAL_CONFIG.global }
|
||||
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');
|
||||
@@ -553,7 +778,11 @@ describe('getConfig Tests', () => {
|
||||
},
|
||||
fallback: { ...DEFAULT_CONFIG.models.fallback }
|
||||
},
|
||||
global: { ...DEFAULT_CONFIG.global, ...INVALID_PROVIDER_CONFIG.global }
|
||||
global: { ...DEFAULT_CONFIG.global, ...INVALID_PROVIDER_CONFIG.global },
|
||||
claudeCode: {
|
||||
...DEFAULT_CONFIG.claudeCode,
|
||||
...VALID_CUSTOM_CONFIG.claudeCode
|
||||
}
|
||||
};
|
||||
expect(config).toEqual(expectedMergedConfig);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user