chore: prettier formatting

This commit is contained in:
Eyal Toledano
2025-04-09 18:20:47 -04:00
parent 12519946b4
commit 4f68bf3b47
7 changed files with 487 additions and 376 deletions

View File

@@ -50,13 +50,16 @@ export async function addTaskDirect(args, log, context = {}) {
// Check required parameters
if (!args.prompt && !isManualCreation) {
log.error('Missing required parameters: either prompt or title+description must be provided');
log.error(
'Missing required parameters: either prompt or title+description must be provided'
);
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_PARAMETER',
message: 'Either the prompt parameter or both title and description parameters are required for adding a task'
message:
'Either the prompt parameter or both title and description parameters are required for adding a task'
}
};
}

View File

@@ -22,11 +22,28 @@ export function registerAddTaskTool(server) {
name: 'add_task',
description: 'Add a new task using AI',
parameters: z.object({
prompt: z.string().optional().describe('Description of the task to add (required if not using manual fields)'),
title: z.string().optional().describe('Task title (for manual task creation)'),
description: z.string().optional().describe('Task description (for manual task creation)'),
details: z.string().optional().describe('Implementation details (for manual task creation)'),
testStrategy: z.string().optional().describe('Test strategy (for manual task creation)'),
prompt: z
.string()
.optional()
.describe(
'Description of the task to add (required if not using manual fields)'
),
title: z
.string()
.optional()
.describe('Task title (for manual task creation)'),
description: z
.string()
.optional()
.describe('Task description (for manual task creation)'),
details: z
.string()
.optional()
.describe('Implementation details (for manual task creation)'),
testStrategy: z
.string()
.optional()
.describe('Test strategy (for manual task creation)'),
dependencies: z
.string()
.optional()
@@ -35,11 +52,16 @@ export function registerAddTaskTool(server) {
.string()
.optional()
.describe('Task priority (high, medium, low)'),
file: z.string().optional().describe('Path to the tasks file (default: tasks/tasks.json)'),
file: z
.string()
.optional()
.describe('Path to the tasks file (default: tasks/tasks.json)'),
projectRoot: z
.string()
.optional()
.describe('Root directory of the project (default: current working directory)'),
.describe(
'Root directory of the project (default: current working directory)'
),
research: z
.boolean()
.optional()

View File

@@ -791,20 +791,46 @@ function registerCommands(programInstance) {
.command('add-task')
.description('Add a new task using AI or manual input')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-p, --prompt <prompt>', 'Description of the task to add (required if not using manual fields)')
.option(
'-p, --prompt <prompt>',
'Description of the task to add (required if not using manual fields)'
)
.option('-t, --title <title>', 'Task title (for manual task creation)')
.option('-d, --description <description>', 'Task description (for manual task creation)')
.option('--details <details>', 'Implementation details (for manual task creation)')
.option('--test-strategy <testStrategy>', 'Test strategy (for manual task creation)')
.option('--dependencies <dependencies>', 'Comma-separated list of task IDs this task depends on')
.option('--priority <priority>', 'Task priority (high, medium, low)', 'medium')
.option('-r, --research', 'Whether to use research capabilities for task creation')
.option(
'-d, --description <description>',
'Task description (for manual task creation)'
)
.option(
'--details <details>',
'Implementation details (for manual task creation)'
)
.option(
'--test-strategy <testStrategy>',
'Test strategy (for manual task creation)'
)
.option(
'--dependencies <dependencies>',
'Comma-separated list of task IDs this task depends on'
)
.option(
'--priority <priority>',
'Task priority (high, medium, low)',
'medium'
)
.option(
'-r, --research',
'Whether to use research capabilities for task creation'
)
.action(async (options) => {
const isManualCreation = options.title && options.description;
// Validate that either prompt or title+description are provided
if (!options.prompt && !isManualCreation) {
console.error(chalk.red('Error: Either --prompt or both --title and --description must be provided'));
console.error(
chalk.red(
'Error: Either --prompt or both --title and --description must be provided'
)
);
process.exit(1);
}
@@ -812,7 +838,9 @@ function registerCommands(programInstance) {
// Prepare dependencies if provided
let dependencies = [];
if (options.dependencies) {
dependencies = options.dependencies.split(',').map(id => parseInt(id.trim(), 10));
dependencies = options.dependencies
.split(',')
.map((id) => parseInt(id.trim(), 10));
}
// Create manual task data if title and description are provided
@@ -825,17 +853,27 @@ function registerCommands(programInstance) {
testStrategy: options.testStrategy || ''
};
console.log(chalk.blue(`Creating task manually with title: "${options.title}"`));
console.log(
chalk.blue(`Creating task manually with title: "${options.title}"`)
);
if (dependencies.length > 0) {
console.log(chalk.blue(`Dependencies: [${dependencies.join(', ')}]`));
console.log(
chalk.blue(`Dependencies: [${dependencies.join(', ')}]`)
);
}
if (options.priority) {
console.log(chalk.blue(`Priority: ${options.priority}`));
}
} else {
console.log(chalk.blue(`Creating task with AI using prompt: "${options.prompt}"`));
console.log(
chalk.blue(
`Creating task with AI using prompt: "${options.prompt}"`
)
);
if (dependencies.length > 0) {
console.log(chalk.blue(`Dependencies: [${dependencies.join(', ')}]`));
console.log(
chalk.blue(`Dependencies: [${dependencies.join(', ')}]`)
);
}
if (options.priority) {
console.log(chalk.blue(`Priority: ${options.priority}`));

View File

@@ -3506,7 +3506,9 @@ async function addTask(
'\n' +
chalk.white(`Status: ${getStatusWithColor(newTask.status)}`) +
'\n' +
chalk.white(`Priority: ${chalk.keyword(getPriorityColor(newTask.priority))(newTask.priority)}`) +
chalk.white(
`Priority: ${chalk.keyword(getPriorityColor(newTask.priority))(newTask.priority)}`
) +
'\n' +
(dependencies.length > 0
? chalk.white(`Dependencies: ${dependencies.join(', ')}`) + '\n'
@@ -3514,11 +3516,17 @@ async function addTask(
'\n' +
chalk.white.bold('Next Steps:') +
'\n' +
chalk.cyan(`1. Run ${chalk.yellow(`task-master show ${newTaskId}`)} to see complete task details`) +
chalk.cyan(
`1. Run ${chalk.yellow(`task-master show ${newTaskId}`)} to see complete task details`
) +
'\n' +
chalk.cyan(`2. Run ${chalk.yellow(`task-master set-status --id=${newTaskId} --status=in-progress`)} to start working on it`) +
chalk.cyan(
`2. Run ${chalk.yellow(`task-master set-status --id=${newTaskId} --status=in-progress`)} to start working on it`
) +
'\n' +
chalk.cyan(`3. Run ${chalk.yellow(`task-master expand --id=${newTaskId}`)} to break it down into subtasks`),
chalk.cyan(
`3. Run ${chalk.yellow(`task-master expand --id=${newTaskId}`)} to break it down into subtasks`
),
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
)
);

View File

@@ -1,14 +1,14 @@
{
"tasks": [
{
"id": 1,
"dependencies": [],
"subtasks": [
{
"id": 1,
"dependencies": []
}
]
}
]
"tasks": [
{
"id": 1,
"dependencies": [],
"subtasks": [
{
"id": 1,
"dependencies": []
}
]
}
]
}

View File

@@ -3,7 +3,10 @@
*/
import { jest } from '@jest/globals';
import { sampleTasks, emptySampleTasks } from '../../tests/fixtures/sample-tasks.js';
import {
sampleTasks,
emptySampleTasks
} from '../../tests/fixtures/sample-tasks.js';
// Mock functions that need jest.fn methods
const mockParsePRD = jest.fn().mockResolvedValue(undefined);
@@ -658,11 +661,24 @@ describe('Commands Module', () => {
// Create a mock task manager with an addTask function that resolves to taskId 5
mockTaskManager = {
addTask: jest.fn().mockImplementation((file, prompt, dependencies, priority, session, research, generateFiles, manualTaskData) => {
// Return the next ID after the last one in sample tasks
const newId = sampleTasks.tasks.length + 1;
return Promise.resolve(newId.toString());
})
addTask: jest
.fn()
.mockImplementation(
(
file,
prompt,
dependencies,
priority,
session,
research,
generateFiles,
manualTaskData
) => {
// Return the next ID after the last one in sample tasks
const newId = sampleTasks.tasks.length + 1;
return Promise.resolve(newId.toString());
}
)
};
// Create a simplified version of the add-task action function for testing
@@ -676,13 +692,15 @@ describe('Commands Module', () => {
// Validate that either prompt or title+description are provided
if (!prompt && !isManualCreation) {
throw new Error('Either --prompt or both --title and --description must be provided');
throw new Error(
'Either --prompt or both --title and --description must be provided'
);
}
// Prepare dependencies if provided
let dependencies = [];
if (options.dependencies) {
dependencies = options.dependencies.split(',').map(id => id.trim());
dependencies = options.dependencies.split(',').map((id) => id.trim());
}
// Create manual task data if title and description are provided
@@ -716,7 +734,9 @@ describe('Commands Module', () => {
await expect(async () => {
await addTaskAction(undefined, options);
}).rejects.toThrow('Either --prompt or both --title and --description must be provided');
}).rejects.toThrow(
'Either --prompt or both --title and --description must be provided'
);
});
test('should handle short-hand flag -p for prompt', async () => {
@@ -737,7 +757,7 @@ describe('Commands Module', () => {
{ session: process.env },
false, // Research flag
null, // Generate files parameter
null // Manual task data
null // Manual task data
);
});
@@ -759,7 +779,7 @@ describe('Commands Module', () => {
{ session: process.env },
true, // Research flag should be true
null, // Generate files parameter
null // Manual task data
null // Manual task data
);
});
@@ -782,7 +802,8 @@ describe('Commands Module', () => {
{ session: process.env },
false,
null, // Generate files parameter
{ // Manual task data
{
// Manual task data
title: 'Login Component',
description: 'Create a reusable login form',
details: 'Implementation details here',
@@ -809,7 +830,7 @@ describe('Commands Module', () => {
{ session: process.env },
false,
null, // Generate files parameter
null // Manual task data
null // Manual task data
);
});
@@ -831,7 +852,7 @@ describe('Commands Module', () => {
{ session: process.env },
false,
null, // Generate files parameter
null // Manual task data
null // Manual task data
);
});
@@ -852,7 +873,7 @@ describe('Commands Module', () => {
{ session: process.env },
false, // Research is false by default
null, // Generate files parameter
null // Manual task data
null // Manual task data
);
});
});

View File

@@ -10,317 +10,336 @@
*/
import { jest } from '@jest/globals';
import { sampleTasks, emptySampleTasks } from '../../../fixtures/sample-tasks.js';
import {
sampleTasks,
emptySampleTasks
} from '../../../fixtures/sample-tasks.js';
// Mock EVERYTHING
const mockAddTaskDirect = jest.fn();
jest.mock('../../../../mcp-server/src/core/task-master-core.js', () => ({
addTaskDirect: mockAddTaskDirect
addTaskDirect: mockAddTaskDirect
}));
const mockHandleApiResult = jest.fn(result => result);
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 }
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()
getProjectRootFromSession: mockGetProjectRootFromSession,
handleApiResult: mockHandleApiResult,
createErrorResponse: mockCreateErrorResponse,
createContentResponse: jest.fn((content) => ({
success: true,
data: content
})),
executeTaskMasterCommand: jest.fn()
}));
// 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: () => ({
prompt: {},
dependencies: {},
priority: {},
research: {},
file: {},
projectRoot: {}
})}
object: jest.fn(() => mockZod),
string: jest.fn(() => mockZod),
boolean: jest.fn(() => mockZod),
optional: jest.fn(() => mockZod),
describe: jest.fn(() => mockZod),
_def: {
shape: () => ({
prompt: {},
dependencies: {},
priority: {},
research: {},
file: {},
projectRoot: {}
})
}
};
jest.mock('zod', () => ({
z: mockZod
z: mockZod
}));
// DO NOT import the real module - create a fake implementation
// This is the fake implementation of registerAddTaskTool
const registerAddTaskTool = (server) => {
// Create simplified version of the tool config
const toolConfig = {
name: 'add_task',
description: 'Add a new task using AI',
parameters: mockZod,
// Create simplified version of the tool config
const toolConfig = {
name: 'add_task',
description: 'Add a new task using AI',
parameters: mockZod,
// Create a simplified mock of the execute function
execute: (args, context) => {
const { log, reportProgress, session } = context;
// Create a simplified mock of the execute function
execute: (args, context) => {
const { log, reportProgress, session } = context;
try {
log.info && log.info(`Starting add-task with args: ${JSON.stringify(args)}`);
try {
log.info &&
log.info(`Starting add-task with args: ${JSON.stringify(args)}`);
// Get project root
const rootFolder = mockGetProjectRootFromSession(session, log);
// Get project root
const rootFolder = mockGetProjectRootFromSession(session, log);
// Call addTaskDirect
const result = mockAddTaskDirect({
...args,
projectRoot: rootFolder
}, log, { reportProgress, session });
// Call addTaskDirect
const result = mockAddTaskDirect(
{
...args,
projectRoot: rootFolder
},
log,
{ reportProgress, session }
);
// Handle result
return mockHandleApiResult(result, log);
} catch (error) {
log.error && log.error(`Error in add-task tool: ${error.message}`);
return mockCreateErrorResponse(error.message);
}
}
};
// Handle result
return mockHandleApiResult(result, log);
} catch (error) {
log.error && log.error(`Error in add-task tool: ${error.message}`);
return mockCreateErrorResponse(error.message);
}
}
};
// Register the tool with the server
server.addTool(toolConfig);
// Register the tool with the server
server.addTool(toolConfig);
};
describe('MCP Tool: add-task', () => {
// Create mock server
let mockServer;
let executeFunction;
// Create mock server
let mockServer;
let executeFunction;
// Create mock logger
const mockLogger = {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn()
};
// Create mock logger
const mockLogger = {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn()
};
// Test data
const validArgs = {
prompt: 'Create a new task',
dependencies: '1,2',
priority: 'high',
research: true
};
// Test data
const validArgs = {
prompt: 'Create a new task',
dependencies: '1,2',
priority: 'high',
research: true
};
// Standard responses
const successResponse = {
success: true,
data: {
taskId: '5',
message: 'Successfully added new task #5'
}
};
// Standard responses
const successResponse = {
success: true,
data: {
taskId: '5',
message: 'Successfully added new task #5'
}
};
const errorResponse = {
success: false,
error: {
code: 'ADD_TASK_ERROR',
message: 'Failed to add task'
}
};
const errorResponse = {
success: false,
error: {
code: 'ADD_TASK_ERROR',
message: 'Failed to add task'
}
};
beforeEach(() => {
// Reset all mocks
jest.clearAllMocks();
beforeEach(() => {
// Reset all mocks
jest.clearAllMocks();
// Create mock server
mockServer = {
addTool: jest.fn(config => {
executeFunction = config.execute;
})
};
// Create mock server
mockServer = {
addTool: jest.fn((config) => {
executeFunction = config.execute;
})
};
// Setup default successful response
mockAddTaskDirect.mockReturnValue(successResponse);
// Setup default successful response
mockAddTaskDirect.mockReturnValue(successResponse);
// Register the tool
registerAddTaskTool(mockServer);
});
// Register the tool
registerAddTaskTool(mockServer);
});
test('should register the tool correctly', () => {
// Verify tool was registered
expect(mockServer.addTool).toHaveBeenCalledWith(
expect.objectContaining({
name: 'add_task',
description: 'Add a new task using AI',
parameters: expect.any(Object),
execute: expect.any(Function)
})
);
test('should register the tool correctly', () => {
// Verify tool was registered
expect(mockServer.addTool).toHaveBeenCalledWith(
expect.objectContaining({
name: 'add_task',
description: 'Add a new task using AI',
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');
});
// 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 parameters', () => {
// Setup context
const mockContext = {
log: mockLogger,
reportProgress: jest.fn(),
session: { workingDirectory: '/mock/dir' }
};
test('should execute the tool with valid parameters', () => {
// Setup context
const mockContext = {
log: mockLogger,
reportProgress: jest.fn(),
session: { workingDirectory: '/mock/dir' }
};
// Execute the function
executeFunction(validArgs, mockContext);
// Execute the function
executeFunction(validArgs, mockContext);
// Verify getProjectRootFromSession was called
expect(mockGetProjectRootFromSession).toHaveBeenCalledWith(
mockContext.session,
mockLogger
);
// Verify getProjectRootFromSession was called
expect(mockGetProjectRootFromSession).toHaveBeenCalledWith(
mockContext.session,
mockLogger
);
// Verify addTaskDirect was called with correct arguments
expect(mockAddTaskDirect).toHaveBeenCalledWith(
expect.objectContaining({
...validArgs,
projectRoot: '/mock/project/root'
}),
mockLogger,
{
reportProgress: mockContext.reportProgress,
session: mockContext.session
}
);
// Verify addTaskDirect was called with correct arguments
expect(mockAddTaskDirect).toHaveBeenCalledWith(
expect.objectContaining({
...validArgs,
projectRoot: '/mock/project/root'
}),
mockLogger,
{
reportProgress: mockContext.reportProgress,
session: mockContext.session
}
);
// Verify handleApiResult was called
expect(mockHandleApiResult).toHaveBeenCalledWith(
successResponse,
mockLogger
);
});
// Verify handleApiResult was called
expect(mockHandleApiResult).toHaveBeenCalledWith(
successResponse,
mockLogger
);
});
test('should handle errors from addTaskDirect', () => {
// Setup error response
mockAddTaskDirect.mockReturnValueOnce(errorResponse);
test('should handle errors from addTaskDirect', () => {
// Setup error response
mockAddTaskDirect.mockReturnValueOnce(errorResponse);
// Setup context
const mockContext = {
log: mockLogger,
reportProgress: jest.fn(),
session: { workingDirectory: '/mock/dir' }
};
// Setup context
const mockContext = {
log: mockLogger,
reportProgress: jest.fn(),
session: { workingDirectory: '/mock/dir' }
};
// Execute the function
executeFunction(validArgs, mockContext);
// Execute the function
executeFunction(validArgs, mockContext);
// Verify addTaskDirect was called
expect(mockAddTaskDirect).toHaveBeenCalled();
// Verify addTaskDirect was called
expect(mockAddTaskDirect).toHaveBeenCalled();
// Verify handleApiResult was called with error response
expect(mockHandleApiResult).toHaveBeenCalledWith(
errorResponse,
mockLogger
);
});
// 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');
mockAddTaskDirect.mockImplementationOnce(() => {
throw testError;
});
test('should handle unexpected errors', () => {
// Setup error
const testError = new Error('Unexpected error');
mockAddTaskDirect.mockImplementationOnce(() => {
throw testError;
});
// Setup context
const mockContext = {
log: mockLogger,
reportProgress: jest.fn(),
session: { workingDirectory: '/mock/dir' }
};
// Setup context
const mockContext = {
log: mockLogger,
reportProgress: jest.fn(),
session: { workingDirectory: '/mock/dir' }
};
// Execute the function
executeFunction(validArgs, mockContext);
// Execute the function
executeFunction(validArgs, mockContext);
// Verify error was logged
expect(mockLogger.error).toHaveBeenCalledWith(
'Error in add-task tool: Unexpected error'
);
// Verify error was logged
expect(mockLogger.error).toHaveBeenCalledWith(
'Error in add-task tool: Unexpected error'
);
// Verify error response was created
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unexpected error');
});
// Verify error response was created
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unexpected error');
});
test('should pass research parameter correctly', () => {
// Setup context
const mockContext = {
log: mockLogger,
reportProgress: jest.fn(),
session: { workingDirectory: '/mock/dir' }
};
test('should pass research parameter correctly', () => {
// Setup context
const mockContext = {
log: mockLogger,
reportProgress: jest.fn(),
session: { workingDirectory: '/mock/dir' }
};
// Test with research=true
executeFunction({
...validArgs,
research: true
}, mockContext);
// Test with research=true
executeFunction(
{
...validArgs,
research: true
},
mockContext
);
// Verify addTaskDirect was called with research=true
expect(mockAddTaskDirect).toHaveBeenCalledWith(
expect.objectContaining({
research: true
}),
expect.any(Object),
expect.any(Object)
);
// Verify addTaskDirect was called with research=true
expect(mockAddTaskDirect).toHaveBeenCalledWith(
expect.objectContaining({
research: true
}),
expect.any(Object),
expect.any(Object)
);
// Reset mocks
jest.clearAllMocks();
// Reset mocks
jest.clearAllMocks();
// Test with research=false
executeFunction({
...validArgs,
research: false
}, mockContext);
// Test with research=false
executeFunction(
{
...validArgs,
research: false
},
mockContext
);
// Verify addTaskDirect was called with research=false
expect(mockAddTaskDirect).toHaveBeenCalledWith(
expect.objectContaining({
research: false
}),
expect.any(Object),
expect.any(Object)
);
});
// Verify addTaskDirect was called with research=false
expect(mockAddTaskDirect).toHaveBeenCalledWith(
expect.objectContaining({
research: false
}),
expect.any(Object),
expect.any(Object)
);
});
test('should pass priority parameter correctly', () => {
// Setup context
const mockContext = {
log: mockLogger,
reportProgress: jest.fn(),
session: { workingDirectory: '/mock/dir' }
};
test('should pass priority parameter correctly', () => {
// Setup context
const mockContext = {
log: mockLogger,
reportProgress: jest.fn(),
session: { workingDirectory: '/mock/dir' }
};
// Test different priority values
['high', 'medium', 'low'].forEach(priority => {
// Reset mocks
jest.clearAllMocks();
// Test different priority values
['high', 'medium', 'low'].forEach((priority) => {
// Reset mocks
jest.clearAllMocks();
// Execute with specific priority
executeFunction({
...validArgs,
priority
}, mockContext);
// Execute with specific priority
executeFunction(
{
...validArgs,
priority
},
mockContext
);
// Verify addTaskDirect was called with correct priority
expect(mockAddTaskDirect).toHaveBeenCalledWith(
expect.objectContaining({
priority
}),
expect.any(Object),
expect.any(Object)
);
});
});
// Verify addTaskDirect was called with correct priority
expect(mockAddTaskDirect).toHaveBeenCalledWith(
expect.objectContaining({
priority
}),
expect.any(Object),
expect.any(Object)
);
});
});
});