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

Added detailed next_step guidance to the initialize-project MCP tool response, providing clear instructions about creating a PRD file and using parse-prd after initialization. This helps users understand the workflow better after project initialization. Also added comprehensive unit tests for the initialize-project MCP tool that: - Verify tool registration with correct parameters - Test command construction with proper argument formatting - Check special character escaping in command arguments - Validate success response formatting including the new next_step field - Test error handling and fallback mechanisms - Verify logging behavior The tests follow the same pattern as other MCP tool tests in the codebase.
343 lines
10 KiB
JavaScript
343 lines
10 KiB
JavaScript
/**
|
|
* Tests for the initialize-project 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. Command construction works correctly with various arguments
|
|
* 3. Error handling works as expected
|
|
* 4. Response formatting is correct
|
|
*
|
|
* We do NOT import the real implementation - everything is mocked
|
|
*/
|
|
|
|
import { jest } from '@jest/globals';
|
|
|
|
// Mock child_process.execSync
|
|
const mockExecSync = jest.fn();
|
|
jest.mock('child_process', () => ({
|
|
execSync: mockExecSync
|
|
}));
|
|
|
|
// Mock the utility functions
|
|
const mockCreateContentResponse = jest.fn((content) => ({
|
|
content
|
|
}));
|
|
|
|
const mockCreateErrorResponse = jest.fn((message, details) => ({
|
|
error: { message, details }
|
|
}));
|
|
|
|
jest.mock('../../../../mcp-server/src/tools/utils.js', () => ({
|
|
createContentResponse: mockCreateContentResponse,
|
|
createErrorResponse: mockCreateErrorResponse
|
|
}));
|
|
|
|
// Mock the z object from zod
|
|
const mockZod = {
|
|
object: jest.fn(() => mockZod),
|
|
string: jest.fn(() => mockZod),
|
|
boolean: jest.fn(() => mockZod),
|
|
optional: jest.fn(() => mockZod),
|
|
default: jest.fn(() => mockZod),
|
|
describe: jest.fn(() => mockZod),
|
|
_def: {
|
|
shape: () => ({
|
|
projectName: {},
|
|
projectDescription: {},
|
|
projectVersion: {},
|
|
authorName: {},
|
|
skipInstall: {},
|
|
addAliases: {},
|
|
yes: {}
|
|
})
|
|
}
|
|
};
|
|
|
|
jest.mock('zod', () => ({
|
|
z: mockZod
|
|
}));
|
|
|
|
// Create our own simplified version of the registerInitializeProjectTool function
|
|
const registerInitializeProjectTool = (server) => {
|
|
server.addTool({
|
|
name: 'initialize_project',
|
|
description:
|
|
"Initializes a new Task Master project structure in the current working directory by running 'task-master init'.",
|
|
parameters: mockZod,
|
|
execute: async (args, { log }) => {
|
|
try {
|
|
log.info(
|
|
`Executing initialize_project with args: ${JSON.stringify(args)}`
|
|
);
|
|
|
|
// Construct the command arguments
|
|
let command = 'npx task-master init';
|
|
const cliArgs = [];
|
|
if (args.projectName) {
|
|
cliArgs.push(`--name "${args.projectName.replace(/"/g, '\\"')}"`);
|
|
}
|
|
if (args.projectDescription) {
|
|
cliArgs.push(
|
|
`--description "${args.projectDescription.replace(/"/g, '\\"')}"`
|
|
);
|
|
}
|
|
if (args.projectVersion) {
|
|
cliArgs.push(
|
|
`--version "${args.projectVersion.replace(/"/g, '\\"')}"`
|
|
);
|
|
}
|
|
if (args.authorName) {
|
|
cliArgs.push(`--author "${args.authorName.replace(/"/g, '\\"')}"`);
|
|
}
|
|
if (args.skipInstall) cliArgs.push('--skip-install');
|
|
if (args.addAliases) cliArgs.push('--aliases');
|
|
if (args.yes) cliArgs.push('--yes');
|
|
|
|
command += ' ' + cliArgs.join(' ');
|
|
|
|
log.info(`Constructed command: ${command}`);
|
|
|
|
// Execute the command
|
|
const output = mockExecSync(command, {
|
|
encoding: 'utf8',
|
|
stdio: 'pipe',
|
|
timeout: 300000
|
|
});
|
|
|
|
log.info(`Initialization output:\n${output}`);
|
|
|
|
// Return success response
|
|
return mockCreateContentResponse({
|
|
message: 'Project initialized successfully.',
|
|
next_step:
|
|
'Now that the project is initialized, the next step is to create the tasks by parsing a PRD. This will create the tasks folder and the initial task files. The parse-prd tool will required a PRD file',
|
|
output: output
|
|
});
|
|
} catch (error) {
|
|
// Catch errors
|
|
const errorMessage = `Project initialization failed: ${error.message}`;
|
|
const errorDetails =
|
|
error.stderr?.toString() || error.stdout?.toString() || error.message;
|
|
log.error(`${errorMessage}\nDetails: ${errorDetails}`);
|
|
|
|
// Return error response
|
|
return mockCreateErrorResponse(errorMessage, { details: errorDetails });
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
describe('Initialize Project MCP Tool', () => {
|
|
// Mock server and logger
|
|
let mockServer;
|
|
let executeFunction;
|
|
|
|
const mockLogger = {
|
|
debug: jest.fn(),
|
|
info: jest.fn(),
|
|
warn: jest.fn(),
|
|
error: jest.fn()
|
|
};
|
|
|
|
beforeEach(() => {
|
|
// Clear all mocks before each test
|
|
jest.clearAllMocks();
|
|
|
|
// Create mock server
|
|
mockServer = {
|
|
addTool: jest.fn((config) => {
|
|
executeFunction = config.execute;
|
|
})
|
|
};
|
|
|
|
// Default mock behavior
|
|
mockExecSync.mockReturnValue('Project initialized successfully.');
|
|
|
|
// Register the tool to capture the tool definition
|
|
registerInitializeProjectTool(mockServer);
|
|
});
|
|
|
|
test('registers the tool with correct name and parameters', () => {
|
|
// Check that addTool was called
|
|
expect(mockServer.addTool).toHaveBeenCalledTimes(1);
|
|
|
|
// Extract the tool definition from the mock call
|
|
const toolDefinition = mockServer.addTool.mock.calls[0][0];
|
|
|
|
// Verify tool properties
|
|
expect(toolDefinition.name).toBe('initialize_project');
|
|
expect(toolDefinition.description).toContain(
|
|
'Initializes a new Task Master project'
|
|
);
|
|
expect(toolDefinition).toHaveProperty('parameters');
|
|
expect(toolDefinition).toHaveProperty('execute');
|
|
});
|
|
|
|
test('constructs command with proper arguments', async () => {
|
|
// Create arguments with all parameters
|
|
const args = {
|
|
projectName: 'Test Project',
|
|
projectDescription: 'A project for testing',
|
|
projectVersion: '1.0.0',
|
|
authorName: 'Test Author',
|
|
skipInstall: true,
|
|
addAliases: true,
|
|
yes: true
|
|
};
|
|
|
|
// Execute the tool
|
|
await executeFunction(args, { log: mockLogger });
|
|
|
|
// Verify execSync was called with the expected command
|
|
expect(mockExecSync).toHaveBeenCalledTimes(1);
|
|
|
|
const command = mockExecSync.mock.calls[0][0];
|
|
|
|
// Check that the command includes npx task-master init
|
|
expect(command).toContain('npx task-master init');
|
|
|
|
// Verify each argument is correctly formatted in the command
|
|
expect(command).toContain('--name "Test Project"');
|
|
expect(command).toContain('--description "A project for testing"');
|
|
expect(command).toContain('--version "1.0.0"');
|
|
expect(command).toContain('--author "Test Author"');
|
|
expect(command).toContain('--skip-install');
|
|
expect(command).toContain('--aliases');
|
|
expect(command).toContain('--yes');
|
|
});
|
|
|
|
test('properly escapes special characters in arguments', async () => {
|
|
// Create arguments with special characters
|
|
const args = {
|
|
projectName: 'Test "Quoted" Project',
|
|
projectDescription: 'A "special" project for testing'
|
|
};
|
|
|
|
// Execute the tool
|
|
await executeFunction(args, { log: mockLogger });
|
|
|
|
// Get the command that was executed
|
|
const command = mockExecSync.mock.calls[0][0];
|
|
|
|
// Verify quotes were properly escaped
|
|
expect(command).toContain('--name "Test \\"Quoted\\" Project"');
|
|
expect(command).toContain(
|
|
'--description "A \\"special\\" project for testing"'
|
|
);
|
|
});
|
|
|
|
test('returns success response when command succeeds', async () => {
|
|
// Set up the mock to return specific output
|
|
const outputMessage = 'Project initialized successfully.';
|
|
mockExecSync.mockReturnValueOnce(outputMessage);
|
|
|
|
// Execute the tool
|
|
const result = await executeFunction({}, { log: mockLogger });
|
|
|
|
// Verify createContentResponse was called with the right arguments
|
|
expect(mockCreateContentResponse).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
message: 'Project initialized successfully.',
|
|
next_step: expect.any(String),
|
|
output: outputMessage
|
|
})
|
|
);
|
|
|
|
// Verify the returned result has the expected structure
|
|
expect(result).toHaveProperty('content');
|
|
expect(result.content).toHaveProperty('message');
|
|
expect(result.content).toHaveProperty('next_step');
|
|
expect(result.content).toHaveProperty('output');
|
|
expect(result.content.output).toBe(outputMessage);
|
|
});
|
|
|
|
test('returns error response when command fails', async () => {
|
|
// Create an error to be thrown
|
|
const error = new Error('Command failed');
|
|
error.stdout = 'Some standard output';
|
|
error.stderr = 'Some error output';
|
|
|
|
// Make the mock throw the error
|
|
mockExecSync.mockImplementationOnce(() => {
|
|
throw error;
|
|
});
|
|
|
|
// Execute the tool
|
|
const result = await executeFunction({}, { log: mockLogger });
|
|
|
|
// Verify createErrorResponse was called with the right arguments
|
|
expect(mockCreateErrorResponse).toHaveBeenCalledWith(
|
|
'Project initialization failed: Command failed',
|
|
expect.objectContaining({
|
|
details: 'Some error output'
|
|
})
|
|
);
|
|
|
|
// Verify the returned result has the expected structure
|
|
expect(result).toHaveProperty('error');
|
|
expect(result.error).toHaveProperty('message');
|
|
expect(result.error.message).toContain('Project initialization failed');
|
|
});
|
|
|
|
test('logs information about the execution', async () => {
|
|
// Execute the tool
|
|
await executeFunction({}, { log: mockLogger });
|
|
|
|
// Verify that logging occurred
|
|
expect(mockLogger.info).toHaveBeenCalledWith(
|
|
expect.stringContaining('Executing initialize_project')
|
|
);
|
|
expect(mockLogger.info).toHaveBeenCalledWith(
|
|
expect.stringContaining('Constructed command')
|
|
);
|
|
expect(mockLogger.info).toHaveBeenCalledWith(
|
|
expect.stringContaining('Initialization output')
|
|
);
|
|
});
|
|
|
|
test('uses fallback to stdout if stderr is not available in error', async () => {
|
|
// Create an error with only stdout
|
|
const error = new Error('Command failed');
|
|
error.stdout = 'Some standard output with error details';
|
|
// No stderr property
|
|
|
|
// Make the mock throw the error
|
|
mockExecSync.mockImplementationOnce(() => {
|
|
throw error;
|
|
});
|
|
|
|
// Execute the tool
|
|
await executeFunction({}, { log: mockLogger });
|
|
|
|
// Verify createErrorResponse was called with stdout as details
|
|
expect(mockCreateErrorResponse).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
expect.objectContaining({
|
|
details: 'Some standard output with error details'
|
|
})
|
|
);
|
|
});
|
|
|
|
test('logs error details when command fails', async () => {
|
|
// Create an error
|
|
const error = new Error('Command failed');
|
|
error.stderr = 'Some detailed error message';
|
|
|
|
// Make the mock throw the error
|
|
mockExecSync.mockImplementationOnce(() => {
|
|
throw error;
|
|
});
|
|
|
|
// Execute the tool
|
|
await executeFunction({}, { log: mockLogger });
|
|
|
|
// Verify error logging
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
expect.stringContaining('Project initialization failed')
|
|
);
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
expect.stringContaining('Some detailed error message')
|
|
);
|
|
});
|
|
});
|