Implements updateTask command to update a single task instead of all tasks as of a certain one. Useful when iterating and R&D'ing bit by bit and needing more research after what has been done.

This commit is contained in:
Eyal Toledano
2025-03-27 01:33:20 -04:00
parent 1abcf69ecd
commit 707618ca5d
9 changed files with 1395 additions and 28 deletions

View File

@@ -22,6 +22,8 @@ const mockValidateAndFixDependencies = jest.fn();
const mockReadJSON = jest.fn();
const mockLog = jest.fn();
const mockIsTaskDependentOn = jest.fn().mockReturnValue(false);
const mockCreate = jest.fn(); // Mock for Anthropic messages.create
const mockChatCompletionsCreate = jest.fn(); // Mock for Perplexity chat.completions.create
// Mock fs module
jest.mock('fs', () => ({
@@ -63,6 +65,30 @@ jest.mock('../../scripts/modules/ai-services.js', () => ({
callPerplexity: mockCallPerplexity
}));
// Mock Anthropic SDK
jest.mock('@anthropic-ai/sdk', () => {
return {
Anthropic: jest.fn().mockImplementation(() => ({
messages: {
create: mockCreate
}
}))
};
});
// Mock Perplexity using OpenAI
jest.mock('openai', () => {
return {
default: jest.fn().mockImplementation(() => ({
chat: {
completions: {
create: mockChatCompletionsCreate
}
}
}))
};
});
// Mock the task-manager module itself to control what gets imported
jest.mock('../../scripts/modules/task-manager.js', () => {
// Get the original module to preserve function implementations
@@ -227,7 +253,7 @@ import { sampleClaudeResponse } from '../fixtures/sample-claude-response.js';
import { sampleTasks, emptySampleTasks } from '../fixtures/sample-tasks.js';
// Destructure the required functions for convenience
const { findNextTask, generateTaskFiles, clearSubtasks } = taskManager;
const { findNextTask, generateTaskFiles, clearSubtasks, updateTaskById } = taskManager;
describe('Task Manager Module', () => {
beforeEach(() => {
@@ -1697,4 +1723,294 @@ const testRemoveSubtask = (tasksPath, subtaskId, convertToTask = false, generate
}
return convertedTask;
};
};
describe.skip('updateTaskById function', () => {
let mockConsoleLog;
let mockConsoleError;
let mockProcess;
beforeEach(() => {
// Reset all mocks
jest.clearAllMocks();
// Set up default mock values
mockExistsSync.mockReturnValue(true);
mockWriteJSON.mockImplementation(() => {});
mockGenerateTaskFiles.mockResolvedValue(undefined);
// Create a deep copy of sample tasks for tests - use imported ES module instead of require
const sampleTasksDeepCopy = JSON.parse(JSON.stringify(sampleTasks));
mockReadJSON.mockReturnValue(sampleTasksDeepCopy);
// Mock console and process.exit
mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => {});
mockConsoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
mockProcess = jest.spyOn(process, 'exit').mockImplementation(() => {});
});
afterEach(() => {
// Restore console and process.exit
mockConsoleLog.mockRestore();
mockConsoleError.mockRestore();
mockProcess.mockRestore();
});
test('should update a task successfully', async () => {
// Mock the return value of messages.create and Anthropic
const mockTask = {
id: 2,
title: "Updated Core Functionality",
description: "Updated description",
status: "in-progress",
dependencies: [1],
priority: "high",
details: "Updated details",
testStrategy: "Updated test strategy"
};
// Mock streaming for successful response
const mockStream = {
[Symbol.asyncIterator]: jest.fn().mockImplementation(() => {
return {
next: jest.fn()
.mockResolvedValueOnce({
done: false,
value: {
type: 'content_block_delta',
delta: { text: '{"id": 2, "title": "Updated Core Functionality",' }
}
})
.mockResolvedValueOnce({
done: false,
value: {
type: 'content_block_delta',
delta: { text: '"description": "Updated description", "status": "in-progress",' }
}
})
.mockResolvedValueOnce({
done: false,
value: {
type: 'content_block_delta',
delta: { text: '"dependencies": [1], "priority": "high", "details": "Updated details",' }
}
})
.mockResolvedValueOnce({
done: false,
value: {
type: 'content_block_delta',
delta: { text: '"testStrategy": "Updated test strategy"}' }
}
})
.mockResolvedValueOnce({ done: true })
};
})
};
mockCreate.mockResolvedValue(mockStream);
// Call the function
const result = await updateTaskById('test-tasks.json', 2, 'Update task 2 with new information');
// Verify the task was updated
expect(result).toBeDefined();
expect(result.title).toBe("Updated Core Functionality");
expect(result.description).toBe("Updated description");
// Verify the correct functions were called
expect(mockReadJSON).toHaveBeenCalledWith('test-tasks.json');
expect(mockCreate).toHaveBeenCalled();
expect(mockWriteJSON).toHaveBeenCalled();
expect(mockGenerateTaskFiles).toHaveBeenCalled();
// Verify the task was updated in the tasks data
const tasksData = mockWriteJSON.mock.calls[0][1];
const updatedTask = tasksData.tasks.find(task => task.id === 2);
expect(updatedTask).toEqual(mockTask);
});
test('should return null when task is already completed', async () => {
// Call the function with a completed task
const result = await updateTaskById('test-tasks.json', 1, 'Update task 1 with new information');
// Verify the result is null
expect(result).toBeNull();
// Verify the correct functions were called
expect(mockReadJSON).toHaveBeenCalledWith('test-tasks.json');
expect(mockCreate).not.toHaveBeenCalled();
expect(mockWriteJSON).not.toHaveBeenCalled();
expect(mockGenerateTaskFiles).not.toHaveBeenCalled();
});
test('should handle task not found error', async () => {
// Call the function with a non-existent task
const result = await updateTaskById('test-tasks.json', 999, 'Update non-existent task');
// Verify the result is null
expect(result).toBeNull();
// Verify the error was logged
expect(mockLog).toHaveBeenCalledWith('error', expect.stringContaining('Task with ID 999 not found'));
expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Task with ID 999 not found'));
// Verify the correct functions were called
expect(mockReadJSON).toHaveBeenCalledWith('test-tasks.json');
expect(mockCreate).not.toHaveBeenCalled();
expect(mockWriteJSON).not.toHaveBeenCalled();
expect(mockGenerateTaskFiles).not.toHaveBeenCalled();
});
test('should preserve completed subtasks', async () => {
// Modify the sample data to have a task with completed subtasks
const tasksData = mockReadJSON();
const task = tasksData.tasks.find(t => t.id === 3);
if (task && task.subtasks && task.subtasks.length > 0) {
// Mark the first subtask as completed
task.subtasks[0].status = 'done';
task.subtasks[0].title = 'Completed Header Component';
mockReadJSON.mockReturnValue(tasksData);
}
// Mock a response that tries to modify the completed subtask
const mockStream = {
[Symbol.asyncIterator]: jest.fn().mockImplementation(() => {
return {
next: jest.fn()
.mockResolvedValueOnce({
done: false,
value: {
type: 'content_block_delta',
delta: { text: '{"id": 3, "title": "Updated UI Components",' }
}
})
.mockResolvedValueOnce({
done: false,
value: {
type: 'content_block_delta',
delta: { text: '"description": "Updated description", "status": "pending",' }
}
})
.mockResolvedValueOnce({
done: false,
value: {
type: 'content_block_delta',
delta: { text: '"dependencies": [2], "priority": "medium", "subtasks": [' }
}
})
.mockResolvedValueOnce({
done: false,
value: {
type: 'content_block_delta',
delta: { text: '{"id": 1, "title": "Modified Header Component", "status": "pending"},' }
}
})
.mockResolvedValueOnce({
done: false,
value: {
type: 'content_block_delta',
delta: { text: '{"id": 2, "title": "Create Footer Component", "status": "pending"}]}' }
}
})
.mockResolvedValueOnce({ done: true })
};
})
};
mockCreate.mockResolvedValue(mockStream);
// Call the function
const result = await updateTaskById('test-tasks.json', 3, 'Update UI components task');
// Verify the subtasks were preserved
expect(result).toBeDefined();
expect(result.subtasks[0].title).toBe('Completed Header Component');
expect(result.subtasks[0].status).toBe('done');
// Verify the correct functions were called
expect(mockReadJSON).toHaveBeenCalledWith('test-tasks.json');
expect(mockCreate).toHaveBeenCalled();
expect(mockWriteJSON).toHaveBeenCalled();
expect(mockGenerateTaskFiles).toHaveBeenCalled();
});
test('should handle missing tasks file', async () => {
// Mock file not existing
mockExistsSync.mockReturnValue(false);
// Call the function
const result = await updateTaskById('missing-tasks.json', 2, 'Update task');
// Verify the result is null
expect(result).toBeNull();
// Verify the error was logged
expect(mockLog).toHaveBeenCalledWith('error', expect.stringContaining('Tasks file not found'));
expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Tasks file not found'));
// Verify the correct functions were called
expect(mockReadJSON).not.toHaveBeenCalled();
expect(mockCreate).not.toHaveBeenCalled();
expect(mockWriteJSON).not.toHaveBeenCalled();
expect(mockGenerateTaskFiles).not.toHaveBeenCalled();
});
test('should handle API errors', async () => {
// Mock API error
mockCreate.mockRejectedValue(new Error('API error'));
// Call the function
const result = await updateTaskById('test-tasks.json', 2, 'Update task');
// Verify the result is null
expect(result).toBeNull();
// Verify the error was logged
expect(mockLog).toHaveBeenCalledWith('error', expect.stringContaining('API error'));
expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('API error'));
// Verify the correct functions were called
expect(mockReadJSON).toHaveBeenCalledWith('test-tasks.json');
expect(mockCreate).toHaveBeenCalled();
expect(mockWriteJSON).not.toHaveBeenCalled(); // Should not write on error
expect(mockGenerateTaskFiles).not.toHaveBeenCalled(); // Should not generate on error
});
test('should use Perplexity AI when research flag is true', async () => {
// Mock Perplexity API response
const mockPerplexityResponse = {
choices: [
{
message: {
content: '{"id": 2, "title": "Researched Core Functionality", "description": "Research-backed description", "status": "in-progress", "dependencies": [1], "priority": "high", "details": "Research-backed details", "testStrategy": "Research-backed test strategy"}'
}
}
]
};
mockChatCompletionsCreate.mockResolvedValue(mockPerplexityResponse);
// Set the Perplexity API key in environment
process.env.PERPLEXITY_API_KEY = 'dummy-key';
// Call the function with research flag
const result = await updateTaskById('test-tasks.json', 2, 'Update task with research', true);
// Verify the task was updated with research-backed information
expect(result).toBeDefined();
expect(result.title).toBe("Researched Core Functionality");
expect(result.description).toBe("Research-backed description");
// Verify the Perplexity API was called
expect(mockChatCompletionsCreate).toHaveBeenCalled();
expect(mockCreate).not.toHaveBeenCalled(); // Claude should not be called
// Verify the correct functions were called
expect(mockReadJSON).toHaveBeenCalledWith('test-tasks.json');
expect(mockWriteJSON).toHaveBeenCalled();
expect(mockGenerateTaskFiles).toHaveBeenCalled();
// Clean up
delete process.env.PERPLEXITY_API_KEY;
});
});