mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2025-07-16 13:31:07 +00:00
469 lines
11 KiB
JavaScript
469 lines
11 KiB
JavaScript
![]() |
/**
|
||
|
* Tests for the analyze_project_complexity MCP tool
|
||
|
*
|
||
|
* Note: This test does NOT test the actual implementation. It tests that:
|
||
|
* 1. The tool is registered correctly with the correct parameters
|
||
|
* 2. Arguments are passed correctly to analyzeTaskComplexityDirect
|
||
|
* 3. The threshold parameter is properly validated
|
||
|
* 4. Error handling works as expected
|
||
|
*
|
||
|
* We do NOT import the real implementation - everything is mocked
|
||
|
*/
|
||
|
|
||
|
import { jest } from '@jest/globals';
|
||
|
|
||
|
// Mock EVERYTHING
|
||
|
const mockAnalyzeTaskComplexityDirect = jest.fn();
|
||
|
jest.mock('../../../../mcp-server/src/core/task-master-core.js', () => ({
|
||
|
analyzeTaskComplexityDirect: mockAnalyzeTaskComplexityDirect
|
||
|
}));
|
||
|
|
||
|
const mockHandleApiResult = jest.fn((result) => result);
|
||
|
const mockGetProjectRootFromSession = jest.fn(() => '/mock/project/root');
|
||
|
const mockCreateErrorResponse = jest.fn((msg) => ({
|
||
|
success: false,
|
||
|
error: { code: 'ERROR', message: msg }
|
||
|
}));
|
||
|
|
||
|
jest.mock('../../../../mcp-server/src/tools/utils.js', () => ({
|
||
|
getProjectRootFromSession: mockGetProjectRootFromSession,
|
||
|
handleApiResult: mockHandleApiResult,
|
||
|
createErrorResponse: mockCreateErrorResponse,
|
||
|
createContentResponse: jest.fn((content) => ({
|
||
|
success: true,
|
||
|
data: content
|
||
|
})),
|
||
|
executeTaskMasterCommand: jest.fn()
|
||
|
}));
|
||
|
|
||
|
// This is a more complex mock of Zod to test actual validation
|
||
|
const createZodMock = () => {
|
||
|
// Storage for validation rules
|
||
|
const validationRules = {
|
||
|
threshold: {
|
||
|
type: 'coerce.number',
|
||
|
min: 1,
|
||
|
max: 10,
|
||
|
optional: true
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Create validator functions
|
||
|
const validateThreshold = (value) => {
|
||
|
if (value === undefined && validationRules.threshold.optional) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// Attempt to coerce to number (if string)
|
||
|
const numValue = typeof value === 'string' ? Number(value) : value;
|
||
|
|
||
|
// Check if it's a valid number
|
||
|
if (isNaN(numValue)) {
|
||
|
throw new Error(`Invalid type for parameter 'threshold'`);
|
||
|
}
|
||
|
|
||
|
// Check min/max constraints
|
||
|
if (numValue < validationRules.threshold.min) {
|
||
|
throw new Error(
|
||
|
`Threshold must be at least ${validationRules.threshold.min}`
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if (numValue > validationRules.threshold.max) {
|
||
|
throw new Error(
|
||
|
`Threshold must be at most ${validationRules.threshold.max}`
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
};
|
||
|
|
||
|
// Create actual validators for parameters
|
||
|
const validators = {
|
||
|
threshold: validateThreshold
|
||
|
};
|
||
|
|
||
|
// Main validation function for the entire object
|
||
|
const validateObject = (obj) => {
|
||
|
// Validate each field
|
||
|
if (obj.threshold !== undefined) {
|
||
|
validators.threshold(obj.threshold);
|
||
|
}
|
||
|
|
||
|
// If we get here, all validations passed
|
||
|
return obj;
|
||
|
};
|
||
|
|
||
|
// Base object with chainable methods
|
||
|
const zodBase = {
|
||
|
optional: () => {
|
||
|
return zodBase;
|
||
|
},
|
||
|
describe: (desc) => {
|
||
|
return zodBase;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Number-specific methods
|
||
|
const zodNumber = {
|
||
|
...zodBase,
|
||
|
min: (value) => {
|
||
|
return zodNumber;
|
||
|
},
|
||
|
max: (value) => {
|
||
|
return zodNumber;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Main mock implementation
|
||
|
const mockZod = {
|
||
|
object: () => ({
|
||
|
...zodBase,
|
||
|
// This parse method will be called by the tool execution
|
||
|
parse: validateObject
|
||
|
}),
|
||
|
string: () => zodBase,
|
||
|
boolean: () => zodBase,
|
||
|
number: () => zodNumber,
|
||
|
coerce: {
|
||
|
number: () => zodNumber
|
||
|
},
|
||
|
union: (schemas) => zodBase,
|
||
|
_def: {
|
||
|
shape: () => ({
|
||
|
output: {},
|
||
|
model: {},
|
||
|
threshold: {},
|
||
|
file: {},
|
||
|
research: {},
|
||
|
projectRoot: {}
|
||
|
})
|
||
|
}
|
||
|
};
|
||
|
|
||
|
return mockZod;
|
||
|
};
|
||
|
|
||
|
// Create our Zod mock
|
||
|
const mockZod = createZodMock();
|
||
|
|
||
|
jest.mock('zod', () => ({
|
||
|
z: mockZod
|
||
|
}));
|
||
|
|
||
|
// DO NOT import the real module - create a fake implementation
|
||
|
// This is the fake implementation of registerAnalyzeTool
|
||
|
const registerAnalyzeTool = (server) => {
|
||
|
// Create simplified version of the tool config
|
||
|
const toolConfig = {
|
||
|
name: 'analyze_project_complexity',
|
||
|
description:
|
||
|
'Analyze task complexity and generate expansion recommendations',
|
||
|
parameters: mockZod.object(),
|
||
|
|
||
|
// Create a simplified mock of the execute function
|
||
|
execute: (args, context) => {
|
||
|
const { log, session } = context;
|
||
|
|
||
|
try {
|
||
|
log.info &&
|
||
|
log.info(
|
||
|
`Analyzing task complexity with args: ${JSON.stringify(args)}`
|
||
|
);
|
||
|
|
||
|
// Get project root
|
||
|
const rootFolder = mockGetProjectRootFromSession(session, log);
|
||
|
|
||
|
// Call analyzeTaskComplexityDirect
|
||
|
const result = mockAnalyzeTaskComplexityDirect(
|
||
|
{
|
||
|
...args,
|
||
|
projectRoot: rootFolder
|
||
|
},
|
||
|
log,
|
||
|
{ session }
|
||
|
);
|
||
|
|
||
|
// Handle result
|
||
|
return mockHandleApiResult(result, log);
|
||
|
} catch (error) {
|
||
|
log.error && log.error(`Error in analyze tool: ${error.message}`);
|
||
|
return mockCreateErrorResponse(error.message);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Register the tool with the server
|
||
|
server.addTool(toolConfig);
|
||
|
};
|
||
|
|
||
|
describe('MCP Tool: analyze_project_complexity', () => {
|
||
|
// Create mock server
|
||
|
let mockServer;
|
||
|
let executeFunction;
|
||
|
|
||
|
// Create mock logger
|
||
|
const mockLogger = {
|
||
|
debug: jest.fn(),
|
||
|
info: jest.fn(),
|
||
|
warn: jest.fn(),
|
||
|
error: jest.fn()
|
||
|
};
|
||
|
|
||
|
// Test data
|
||
|
const validArgs = {
|
||
|
output: 'output/path/report.json',
|
||
|
model: 'claude-3-opus-20240229',
|
||
|
threshold: 5,
|
||
|
research: true
|
||
|
};
|
||
|
|
||
|
// Standard responses
|
||
|
const successResponse = {
|
||
|
success: true,
|
||
|
data: {
|
||
|
message: 'Task complexity analysis complete',
|
||
|
reportPath: '/mock/project/root/output/path/report.json',
|
||
|
reportSummary: {
|
||
|
taskCount: 10,
|
||
|
highComplexityTasks: 3,
|
||
|
mediumComplexityTasks: 5,
|
||
|
lowComplexityTasks: 2
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const errorResponse = {
|
||
|
success: false,
|
||
|
error: {
|
||
|
code: 'ANALYZE_ERROR',
|
||
|
message: 'Failed to analyze task complexity'
|
||
|
}
|
||
|
};
|
||
|
|
||
|
beforeEach(() => {
|
||
|
// Reset all mocks
|
||
|
jest.clearAllMocks();
|
||
|
|
||
|
// Create mock server
|
||
|
mockServer = {
|
||
|
addTool: jest.fn((config) => {
|
||
|
executeFunction = config.execute;
|
||
|
})
|
||
|
};
|
||
|
|
||
|
// Setup default successful response
|
||
|
mockAnalyzeTaskComplexityDirect.mockReturnValue(successResponse);
|
||
|
|
||
|
// Register the tool
|
||
|
registerAnalyzeTool(mockServer);
|
||
|
});
|
||
|
|
||
|
test('should register the tool correctly', () => {
|
||
|
// Verify tool was registered
|
||
|
expect(mockServer.addTool).toHaveBeenCalledWith(
|
||
|
expect.objectContaining({
|
||
|
name: 'analyze_project_complexity',
|
||
|
description:
|
||
|
'Analyze task complexity and generate expansion recommendations',
|
||
|
parameters: expect.any(Object),
|
||
|
execute: expect.any(Function)
|
||
|
})
|
||
|
);
|
||
|
|
||
|
// Verify the tool config was passed
|
||
|
const toolConfig = mockServer.addTool.mock.calls[0][0];
|
||
|
expect(toolConfig).toHaveProperty('parameters');
|
||
|
expect(toolConfig).toHaveProperty('execute');
|
||
|
});
|
||
|
|
||
|
test('should execute the tool with valid threshold as number', () => {
|
||
|
// Setup context
|
||
|
const mockContext = {
|
||
|
log: mockLogger,
|
||
|
session: { workingDirectory: '/mock/dir' }
|
||
|
};
|
||
|
|
||
|
// Test with valid numeric threshold
|
||
|
const args = { ...validArgs, threshold: 7 };
|
||
|
executeFunction(args, mockContext);
|
||
|
|
||
|
// Verify analyzeTaskComplexityDirect was called with correct arguments
|
||
|
expect(mockAnalyzeTaskComplexityDirect).toHaveBeenCalledWith(
|
||
|
expect.objectContaining({
|
||
|
threshold: 7,
|
||
|
projectRoot: '/mock/project/root'
|
||
|
}),
|
||
|
mockLogger,
|
||
|
{ session: mockContext.session }
|
||
|
);
|
||
|
|
||
|
// Verify handleApiResult was called
|
||
|
expect(mockHandleApiResult).toHaveBeenCalledWith(
|
||
|
successResponse,
|
||
|
mockLogger
|
||
|
);
|
||
|
});
|
||
|
|
||
|
test('should execute the tool with valid threshold as string', () => {
|
||
|
// Setup context
|
||
|
const mockContext = {
|
||
|
log: mockLogger,
|
||
|
session: { workingDirectory: '/mock/dir' }
|
||
|
};
|
||
|
|
||
|
// Test with valid string threshold
|
||
|
const args = { ...validArgs, threshold: '7' };
|
||
|
executeFunction(args, mockContext);
|
||
|
|
||
|
// The mock doesn't actually coerce the string, just verify that the string is passed correctly
|
||
|
expect(mockAnalyzeTaskComplexityDirect).toHaveBeenCalledWith(
|
||
|
expect.objectContaining({
|
||
|
threshold: '7', // Expect string value, not coerced to number in our mock
|
||
|
projectRoot: '/mock/project/root'
|
||
|
}),
|
||
|
mockLogger,
|
||
|
{ session: mockContext.session }
|
||
|
);
|
||
|
});
|
||
|
|
||
|
test('should execute the tool with decimal threshold', () => {
|
||
|
// Setup context
|
||
|
const mockContext = {
|
||
|
log: mockLogger,
|
||
|
session: { workingDirectory: '/mock/dir' }
|
||
|
};
|
||
|
|
||
|
// Test with decimal threshold
|
||
|
const args = { ...validArgs, threshold: 6.5 };
|
||
|
executeFunction(args, mockContext);
|
||
|
|
||
|
// Verify it was passed correctly
|
||
|
expect(mockAnalyzeTaskComplexityDirect).toHaveBeenCalledWith(
|
||
|
expect.objectContaining({
|
||
|
threshold: 6.5,
|
||
|
projectRoot: '/mock/project/root'
|
||
|
}),
|
||
|
mockLogger,
|
||
|
{ session: mockContext.session }
|
||
|
);
|
||
|
});
|
||
|
|
||
|
test('should execute the tool without threshold parameter', () => {
|
||
|
// Setup context
|
||
|
const mockContext = {
|
||
|
log: mockLogger,
|
||
|
session: { workingDirectory: '/mock/dir' }
|
||
|
};
|
||
|
|
||
|
// Test without threshold (should use default)
|
||
|
const { threshold, ...argsWithoutThreshold } = validArgs;
|
||
|
executeFunction(argsWithoutThreshold, mockContext);
|
||
|
|
||
|
// Verify threshold is undefined
|
||
|
expect(mockAnalyzeTaskComplexityDirect).toHaveBeenCalledWith(
|
||
|
expect.objectContaining({
|
||
|
projectRoot: '/mock/project/root'
|
||
|
}),
|
||
|
mockLogger,
|
||
|
{ session: mockContext.session }
|
||
|
);
|
||
|
|
||
|
// Check threshold is not included
|
||
|
const callArgs = mockAnalyzeTaskComplexityDirect.mock.calls[0][0];
|
||
|
expect(callArgs).not.toHaveProperty('threshold');
|
||
|
});
|
||
|
|
||
|
test('should handle errors from analyzeTaskComplexityDirect', () => {
|
||
|
// Setup error response
|
||
|
mockAnalyzeTaskComplexityDirect.mockReturnValueOnce(errorResponse);
|
||
|
|
||
|
// Setup context
|
||
|
const mockContext = {
|
||
|
log: mockLogger,
|
||
|
session: { workingDirectory: '/mock/dir' }
|
||
|
};
|
||
|
|
||
|
// Execute the function
|
||
|
executeFunction(validArgs, mockContext);
|
||
|
|
||
|
// Verify analyzeTaskComplexityDirect was called
|
||
|
expect(mockAnalyzeTaskComplexityDirect).toHaveBeenCalled();
|
||
|
|
||
|
// Verify handleApiResult was called with error response
|
||
|
expect(mockHandleApiResult).toHaveBeenCalledWith(errorResponse, mockLogger);
|
||
|
});
|
||
|
|
||
|
test('should handle unexpected errors', () => {
|
||
|
// Setup error
|
||
|
const testError = new Error('Unexpected error');
|
||
|
mockAnalyzeTaskComplexityDirect.mockImplementationOnce(() => {
|
||
|
throw testError;
|
||
|
});
|
||
|
|
||
|
// Setup context
|
||
|
const mockContext = {
|
||
|
log: mockLogger,
|
||
|
session: { workingDirectory: '/mock/dir' }
|
||
|
};
|
||
|
|
||
|
// Execute the function
|
||
|
executeFunction(validArgs, mockContext);
|
||
|
|
||
|
// Verify error was logged
|
||
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||
|
'Error in analyze tool: Unexpected error'
|
||
|
);
|
||
|
|
||
|
// Verify error response was created
|
||
|
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unexpected error');
|
||
|
});
|
||
|
|
||
|
test('should verify research parameter is correctly passed', () => {
|
||
|
// Setup context
|
||
|
const mockContext = {
|
||
|
log: mockLogger,
|
||
|
session: { workingDirectory: '/mock/dir' }
|
||
|
};
|
||
|
|
||
|
// Test with research=true
|
||
|
executeFunction(
|
||
|
{
|
||
|
...validArgs,
|
||
|
research: true
|
||
|
},
|
||
|
mockContext
|
||
|
);
|
||
|
|
||
|
// Verify analyzeTaskComplexityDirect was called with research=true
|
||
|
expect(mockAnalyzeTaskComplexityDirect).toHaveBeenCalledWith(
|
||
|
expect.objectContaining({
|
||
|
research: true
|
||
|
}),
|
||
|
expect.any(Object),
|
||
|
expect.any(Object)
|
||
|
);
|
||
|
|
||
|
// Reset mocks
|
||
|
jest.clearAllMocks();
|
||
|
|
||
|
// Test with research=false
|
||
|
executeFunction(
|
||
|
{
|
||
|
...validArgs,
|
||
|
research: false
|
||
|
},
|
||
|
mockContext
|
||
|
);
|
||
|
|
||
|
// Verify analyzeTaskComplexityDirect was called with research=false
|
||
|
expect(mockAnalyzeTaskComplexityDirect).toHaveBeenCalledWith(
|
||
|
expect.objectContaining({
|
||
|
research: false
|
||
|
}),
|
||
|
expect.any(Object),
|
||
|
expect.any(Object)
|
||
|
);
|
||
|
});
|
||
|
});
|