mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2025-07-18 06:23:04 +00:00
460 lines
10 KiB
JavaScript
460 lines
10 KiB
JavaScript
![]() |
/**
|
||
|
* Tests for the get-tasks MCP tool
|
||
|
*
|
||
|
* This test verifies the MCP tool properly handles comma-separated status filtering
|
||
|
* and passes arguments correctly to the underlying direct function.
|
||
|
*/
|
||
|
|
||
|
import { jest } from '@jest/globals';
|
||
|
import {
|
||
|
sampleTasks,
|
||
|
emptySampleTasks
|
||
|
} from '../../../fixtures/sample-tasks.js';
|
||
|
|
||
|
// Mock EVERYTHING
|
||
|
const mockListTasksDirect = jest.fn();
|
||
|
jest.mock('../../../../mcp-server/src/core/task-master-core.js', () => ({
|
||
|
listTasksDirect: mockListTasksDirect
|
||
|
}));
|
||
|
|
||
|
const mockHandleApiResult = jest.fn((result) => result);
|
||
|
const mockWithNormalizedProjectRoot = jest.fn((executeFn) => executeFn);
|
||
|
const mockCreateErrorResponse = jest.fn((msg) => ({
|
||
|
success: false,
|
||
|
error: { code: 'ERROR', message: msg }
|
||
|
}));
|
||
|
|
||
|
const mockResolveTasksPath = jest.fn(() => '/mock/project/tasks.json');
|
||
|
const mockResolveComplexityReportPath = jest.fn(
|
||
|
() => '/mock/project/complexity-report.json'
|
||
|
);
|
||
|
|
||
|
jest.mock('../../../../mcp-server/src/tools/utils.js', () => ({
|
||
|
withNormalizedProjectRoot: mockWithNormalizedProjectRoot,
|
||
|
handleApiResult: mockHandleApiResult,
|
||
|
createErrorResponse: mockCreateErrorResponse,
|
||
|
createContentResponse: jest.fn((content) => ({
|
||
|
success: true,
|
||
|
data: content
|
||
|
}))
|
||
|
}));
|
||
|
|
||
|
jest.mock('../../../../mcp-server/src/core/utils/path-utils.js', () => ({
|
||
|
resolveTasksPath: mockResolveTasksPath,
|
||
|
resolveComplexityReportPath: mockResolveComplexityReportPath
|
||
|
}));
|
||
|
|
||
|
// Mock the z object from zod
|
||
|
const mockZod = {
|
||
|
object: jest.fn(() => mockZod),
|
||
|
string: jest.fn(() => mockZod),
|
||
|
boolean: jest.fn(() => mockZod),
|
||
|
optional: jest.fn(() => mockZod),
|
||
|
describe: jest.fn(() => mockZod),
|
||
|
_def: {
|
||
|
shape: () => ({
|
||
|
status: {},
|
||
|
withSubtasks: {},
|
||
|
file: {},
|
||
|
complexityReport: {},
|
||
|
projectRoot: {}
|
||
|
})
|
||
|
}
|
||
|
};
|
||
|
|
||
|
jest.mock('zod', () => ({
|
||
|
z: mockZod
|
||
|
}));
|
||
|
|
||
|
// DO NOT import the real module - create a fake implementation
|
||
|
const registerListTasksTool = (server) => {
|
||
|
const toolConfig = {
|
||
|
name: 'get_tasks',
|
||
|
description:
|
||
|
'Get all tasks from Task Master, optionally filtering by status and including subtasks.',
|
||
|
parameters: mockZod,
|
||
|
|
||
|
execute: (args, context) => {
|
||
|
const { log, session } = context;
|
||
|
|
||
|
try {
|
||
|
log.info &&
|
||
|
log.info(`Getting tasks with filters: ${JSON.stringify(args)}`);
|
||
|
|
||
|
// Resolve paths using mock functions
|
||
|
let tasksJsonPath;
|
||
|
try {
|
||
|
tasksJsonPath = mockResolveTasksPath(args, log);
|
||
|
} catch (error) {
|
||
|
log.error && log.error(`Error finding tasks.json: ${error.message}`);
|
||
|
return mockCreateErrorResponse(
|
||
|
`Failed to find tasks.json: ${error.message}`
|
||
|
);
|
||
|
}
|
||
|
|
||
|
let complexityReportPath;
|
||
|
try {
|
||
|
complexityReportPath = mockResolveComplexityReportPath(args, session);
|
||
|
} catch (error) {
|
||
|
log.error &&
|
||
|
log.error(`Error finding complexity report: ${error.message}`);
|
||
|
complexityReportPath = null;
|
||
|
}
|
||
|
|
||
|
const result = mockListTasksDirect(
|
||
|
{
|
||
|
tasksJsonPath: tasksJsonPath,
|
||
|
status: args.status,
|
||
|
withSubtasks: args.withSubtasks,
|
||
|
reportPath: complexityReportPath
|
||
|
},
|
||
|
log
|
||
|
);
|
||
|
|
||
|
log.info &&
|
||
|
log.info(
|
||
|
`Retrieved ${result.success ? result.data?.tasks?.length || 0 : 0} tasks`
|
||
|
);
|
||
|
return mockHandleApiResult(result, log, 'Error getting tasks');
|
||
|
} catch (error) {
|
||
|
log.error && log.error(`Error getting tasks: ${error.message}`);
|
||
|
return mockCreateErrorResponse(error.message);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
server.addTool(toolConfig);
|
||
|
};
|
||
|
|
||
|
describe('MCP Tool: get-tasks', () => {
|
||
|
let mockServer;
|
||
|
let executeFunction;
|
||
|
|
||
|
const mockLogger = {
|
||
|
debug: jest.fn(),
|
||
|
info: jest.fn(),
|
||
|
warn: jest.fn(),
|
||
|
error: jest.fn()
|
||
|
};
|
||
|
|
||
|
// Sample response data with different statuses for testing
|
||
|
const tasksResponse = {
|
||
|
success: true,
|
||
|
data: {
|
||
|
tasks: [
|
||
|
{ id: 1, title: 'Task 1', status: 'done' },
|
||
|
{ id: 2, title: 'Task 2', status: 'pending' },
|
||
|
{ id: 3, title: 'Task 3', status: 'in-progress' },
|
||
|
{ id: 4, title: 'Task 4', status: 'blocked' },
|
||
|
{ id: 5, title: 'Task 5', status: 'deferred' },
|
||
|
{ id: 6, title: 'Task 6', status: 'review' }
|
||
|
],
|
||
|
filter: 'all',
|
||
|
stats: {
|
||
|
total: 6,
|
||
|
completed: 1,
|
||
|
inProgress: 1,
|
||
|
pending: 1,
|
||
|
blocked: 1,
|
||
|
deferred: 1,
|
||
|
review: 1
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
beforeEach(() => {
|
||
|
jest.clearAllMocks();
|
||
|
|
||
|
mockServer = {
|
||
|
addTool: jest.fn((config) => {
|
||
|
executeFunction = config.execute;
|
||
|
})
|
||
|
};
|
||
|
|
||
|
// Setup default successful response
|
||
|
mockListTasksDirect.mockReturnValue(tasksResponse);
|
||
|
|
||
|
// Register the tool
|
||
|
registerListTasksTool(mockServer);
|
||
|
});
|
||
|
|
||
|
test('should register the tool correctly', () => {
|
||
|
expect(mockServer.addTool).toHaveBeenCalledWith(
|
||
|
expect.objectContaining({
|
||
|
name: 'get_tasks',
|
||
|
description: expect.stringContaining('Get all tasks from Task Master'),
|
||
|
parameters: expect.any(Object),
|
||
|
execute: expect.any(Function)
|
||
|
})
|
||
|
);
|
||
|
});
|
||
|
|
||
|
test('should handle single status filter', () => {
|
||
|
const mockContext = {
|
||
|
log: mockLogger,
|
||
|
session: { workingDirectory: '/mock/dir' }
|
||
|
};
|
||
|
|
||
|
const args = {
|
||
|
status: 'pending',
|
||
|
withSubtasks: false,
|
||
|
projectRoot: '/mock/project'
|
||
|
};
|
||
|
|
||
|
executeFunction(args, mockContext);
|
||
|
|
||
|
expect(mockListTasksDirect).toHaveBeenCalledWith(
|
||
|
expect.objectContaining({
|
||
|
status: 'pending'
|
||
|
}),
|
||
|
mockLogger
|
||
|
);
|
||
|
});
|
||
|
|
||
|
test('should handle comma-separated status filter', () => {
|
||
|
const mockContext = {
|
||
|
log: mockLogger,
|
||
|
session: { workingDirectory: '/mock/dir' }
|
||
|
};
|
||
|
|
||
|
const args = {
|
||
|
status: 'done,pending,in-progress',
|
||
|
withSubtasks: false,
|
||
|
projectRoot: '/mock/project'
|
||
|
};
|
||
|
|
||
|
executeFunction(args, mockContext);
|
||
|
|
||
|
expect(mockListTasksDirect).toHaveBeenCalledWith(
|
||
|
expect.objectContaining({
|
||
|
status: 'done,pending,in-progress'
|
||
|
}),
|
||
|
mockLogger
|
||
|
);
|
||
|
});
|
||
|
|
||
|
test('should handle comma-separated status with spaces', () => {
|
||
|
const mockContext = {
|
||
|
log: mockLogger,
|
||
|
session: { workingDirectory: '/mock/dir' }
|
||
|
};
|
||
|
|
||
|
const args = {
|
||
|
status: 'blocked, deferred , review',
|
||
|
withSubtasks: true,
|
||
|
projectRoot: '/mock/project'
|
||
|
};
|
||
|
|
||
|
executeFunction(args, mockContext);
|
||
|
|
||
|
expect(mockListTasksDirect).toHaveBeenCalledWith(
|
||
|
expect.objectContaining({
|
||
|
status: 'blocked, deferred , review',
|
||
|
withSubtasks: true
|
||
|
}),
|
||
|
mockLogger
|
||
|
);
|
||
|
});
|
||
|
|
||
|
test('should handle withSubtasks parameter correctly', () => {
|
||
|
const mockContext = {
|
||
|
log: mockLogger,
|
||
|
session: { workingDirectory: '/mock/dir' }
|
||
|
};
|
||
|
|
||
|
// Test with withSubtasks=true
|
||
|
executeFunction(
|
||
|
{
|
||
|
status: 'pending',
|
||
|
withSubtasks: true,
|
||
|
projectRoot: '/mock/project'
|
||
|
},
|
||
|
mockContext
|
||
|
);
|
||
|
|
||
|
expect(mockListTasksDirect).toHaveBeenCalledWith(
|
||
|
expect.objectContaining({
|
||
|
withSubtasks: true
|
||
|
}),
|
||
|
mockLogger
|
||
|
);
|
||
|
|
||
|
jest.clearAllMocks();
|
||
|
|
||
|
// Test with withSubtasks=false
|
||
|
executeFunction(
|
||
|
{
|
||
|
status: 'pending',
|
||
|
withSubtasks: false,
|
||
|
projectRoot: '/mock/project'
|
||
|
},
|
||
|
mockContext
|
||
|
);
|
||
|
|
||
|
expect(mockListTasksDirect).toHaveBeenCalledWith(
|
||
|
expect.objectContaining({
|
||
|
withSubtasks: false
|
||
|
}),
|
||
|
mockLogger
|
||
|
);
|
||
|
});
|
||
|
|
||
|
test('should handle path resolution errors gracefully', () => {
|
||
|
mockResolveTasksPath.mockImplementationOnce(() => {
|
||
|
throw new Error('Tasks file not found');
|
||
|
});
|
||
|
|
||
|
const mockContext = {
|
||
|
log: mockLogger,
|
||
|
session: { workingDirectory: '/mock/dir' }
|
||
|
};
|
||
|
|
||
|
const args = {
|
||
|
status: 'pending',
|
||
|
projectRoot: '/mock/project'
|
||
|
};
|
||
|
|
||
|
const result = executeFunction(args, mockContext);
|
||
|
|
||
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||
|
'Error finding tasks.json: Tasks file not found'
|
||
|
);
|
||
|
expect(mockCreateErrorResponse).toHaveBeenCalledWith(
|
||
|
'Failed to find tasks.json: Tasks file not found'
|
||
|
);
|
||
|
});
|
||
|
|
||
|
test('should handle complexity report path resolution errors gracefully', () => {
|
||
|
mockResolveComplexityReportPath.mockImplementationOnce(() => {
|
||
|
throw new Error('Complexity report not found');
|
||
|
});
|
||
|
|
||
|
const mockContext = {
|
||
|
log: mockLogger,
|
||
|
session: { workingDirectory: '/mock/dir' }
|
||
|
};
|
||
|
|
||
|
const args = {
|
||
|
status: 'pending',
|
||
|
projectRoot: '/mock/project'
|
||
|
};
|
||
|
|
||
|
executeFunction(args, mockContext);
|
||
|
|
||
|
// Should not fail the operation but set complexityReportPath to null
|
||
|
expect(mockListTasksDirect).toHaveBeenCalledWith(
|
||
|
expect.objectContaining({
|
||
|
reportPath: null
|
||
|
}),
|
||
|
mockLogger
|
||
|
);
|
||
|
});
|
||
|
|
||
|
test('should handle listTasksDirect errors', () => {
|
||
|
const errorResponse = {
|
||
|
success: false,
|
||
|
error: {
|
||
|
code: 'LIST_TASKS_ERROR',
|
||
|
message: 'Failed to list tasks'
|
||
|
}
|
||
|
};
|
||
|
|
||
|
mockListTasksDirect.mockReturnValueOnce(errorResponse);
|
||
|
|
||
|
const mockContext = {
|
||
|
log: mockLogger,
|
||
|
session: { workingDirectory: '/mock/dir' }
|
||
|
};
|
||
|
|
||
|
const args = {
|
||
|
status: 'pending',
|
||
|
projectRoot: '/mock/project'
|
||
|
};
|
||
|
|
||
|
executeFunction(args, mockContext);
|
||
|
|
||
|
expect(mockHandleApiResult).toHaveBeenCalledWith(
|
||
|
errorResponse,
|
||
|
mockLogger,
|
||
|
'Error getting tasks'
|
||
|
);
|
||
|
});
|
||
|
|
||
|
test('should handle unexpected errors', () => {
|
||
|
const testError = new Error('Unexpected error');
|
||
|
mockListTasksDirect.mockImplementationOnce(() => {
|
||
|
throw testError;
|
||
|
});
|
||
|
|
||
|
const mockContext = {
|
||
|
log: mockLogger,
|
||
|
session: { workingDirectory: '/mock/dir' }
|
||
|
};
|
||
|
|
||
|
const args = {
|
||
|
status: 'pending',
|
||
|
projectRoot: '/mock/project'
|
||
|
};
|
||
|
|
||
|
executeFunction(args, mockContext);
|
||
|
|
||
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||
|
'Error getting tasks: Unexpected error'
|
||
|
);
|
||
|
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unexpected error');
|
||
|
});
|
||
|
|
||
|
test('should pass all parameters correctly', () => {
|
||
|
const mockContext = {
|
||
|
log: mockLogger,
|
||
|
session: { workingDirectory: '/mock/dir' }
|
||
|
};
|
||
|
|
||
|
const args = {
|
||
|
status: 'done,pending',
|
||
|
withSubtasks: true,
|
||
|
file: 'custom-tasks.json',
|
||
|
complexityReport: 'custom-report.json',
|
||
|
projectRoot: '/mock/project'
|
||
|
};
|
||
|
|
||
|
executeFunction(args, mockContext);
|
||
|
|
||
|
// Verify path resolution functions were called with correct arguments
|
||
|
expect(mockResolveTasksPath).toHaveBeenCalledWith(args, mockLogger);
|
||
|
expect(mockResolveComplexityReportPath).toHaveBeenCalledWith(
|
||
|
args,
|
||
|
mockContext.session
|
||
|
);
|
||
|
|
||
|
// Verify listTasksDirect was called with correct parameters
|
||
|
expect(mockListTasksDirect).toHaveBeenCalledWith(
|
||
|
{
|
||
|
tasksJsonPath: '/mock/project/tasks.json',
|
||
|
status: 'done,pending',
|
||
|
withSubtasks: true,
|
||
|
reportPath: '/mock/project/complexity-report.json'
|
||
|
},
|
||
|
mockLogger
|
||
|
);
|
||
|
});
|
||
|
|
||
|
test('should log task count after successful retrieval', () => {
|
||
|
const mockContext = {
|
||
|
log: mockLogger,
|
||
|
session: { workingDirectory: '/mock/dir' }
|
||
|
};
|
||
|
|
||
|
const args = {
|
||
|
status: 'pending',
|
||
|
projectRoot: '/mock/project'
|
||
|
};
|
||
|
|
||
|
executeFunction(args, mockContext);
|
||
|
|
||
|
expect(mockLogger.info).toHaveBeenCalledWith(
|
||
|
`Retrieved ${tasksResponse.data.tasks.length} tasks`
|
||
|
);
|
||
|
});
|
||
|
});
|