Skip to main content
Glama
update-task-by-id.test.js21.1 kB
import { jest } from '@jest/globals'; jest.unstable_mockModule('fs', () => { const mockFs = { existsSync: jest.fn(() => true), writeFileSync: jest.fn(), readFileSync: jest.fn(), unlinkSync: jest.fn() }; return { default: mockFs, ...mockFs }; }); jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({ readJSON: jest.fn(), writeJSON: jest.fn(), log: jest.fn(), isSilentMode: jest.fn(() => false), findProjectRoot: jest.fn(() => '/project'), flattenTasksWithSubtasks: jest.fn(() => []), truncate: jest.fn((t) => t), isEmpty: jest.fn(() => false), resolveEnvVariable: jest.fn(), findTaskById: jest.fn(), getCurrentTag: jest.fn(() => 'master'), resolveTag: jest.fn(() => 'master'), addComplexityToTask: jest.fn((task, complexity) => ({ ...task, complexity })), getTasksForTag: jest.fn((data, tag) => data[tag]?.tasks || []), setTasksForTag: jest.fn(), ensureTagMetadata: jest.fn((tagObj) => tagObj) })); jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({ displayBanner: jest.fn(), getStatusWithColor: jest.fn((s) => s), startLoadingIndicator: jest.fn(() => ({ stop: jest.fn() })), stopLoadingIndicator: jest.fn(), succeedLoadingIndicator: jest.fn(), failLoadingIndicator: jest.fn(), warnLoadingIndicator: jest.fn(), infoLoadingIndicator: jest.fn(), displayAiUsageSummary: jest.fn(), displayContextAnalysis: jest.fn() })); jest.unstable_mockModule( '../../../../../scripts/modules/ai-services-unified.js', () => ({ generateTextService: jest .fn() .mockResolvedValue({ mainResult: { content: '{}' }, telemetryData: {} }), generateObjectService: jest.fn().mockResolvedValue({ mainResult: { task: { id: 1, title: 'Updated Task', description: 'Updated description', status: 'pending', dependencies: [], priority: 'medium', details: null, testStrategy: null, subtasks: [] } }, telemetryData: {} }) }) ); jest.unstable_mockModule( '../../../../../scripts/modules/config-manager.js', () => ({ getDebugFlag: jest.fn(() => false), isApiKeySet: jest.fn(() => true), hasCodebaseAnalysis: jest.fn(() => false) }) ); // Mock @tm/bridge module jest.unstable_mockModule('@tm/bridge', () => ({ tryUpdateViaRemote: jest.fn().mockResolvedValue(null) })); // Mock bridge-utils module jest.unstable_mockModule( '../../../../../scripts/modules/bridge-utils.js', () => ({ createBridgeLogger: jest.fn(() => ({ logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() }, report: jest.fn(), isMCP: false })) }) ); // Mock prompt-manager module jest.unstable_mockModule( '../../../../../scripts/modules/prompt-manager.js', () => ({ getPromptManager: jest.fn().mockReturnValue({ loadPrompt: jest.fn((promptId, params) => ({ systemPrompt: 'You are an AI assistant that helps update a software development task with new requirements and information.', userPrompt: `Update the following task based on the provided information: ${params?.updatePrompt || 'User prompt for task update'}`, metadata: { templateId: 'update-task', version: '1.0.0', variant: 'default', parameters: params || {} } })) }) }) ); // Mock contextGatherer module jest.unstable_mockModule( '../../../../../scripts/modules/utils/contextGatherer.js', () => ({ ContextGatherer: jest.fn().mockImplementation(() => ({ gather: jest.fn().mockResolvedValue({ fullContext: '', summary: '' }) })) }) ); // Mock fuzzyTaskSearch module jest.unstable_mockModule( '../../../../../scripts/modules/utils/fuzzyTaskSearch.js', () => ({ FuzzyTaskSearch: jest.fn().mockImplementation(() => ({ search: jest.fn().mockReturnValue([]), findRelevantTasks: jest.fn().mockReturnValue([]), getTaskIds: jest.fn().mockReturnValue([]) })) }) ); const { readJSON, log } = await import( '../../../../../scripts/modules/utils.js' ); const { tryUpdateViaRemote } = await import('@tm/bridge'); const { createBridgeLogger } = await import( '../../../../../scripts/modules/bridge-utils.js' ); const { getPromptManager } = await import( '../../../../../scripts/modules/prompt-manager.js' ); const { ContextGatherer } = await import( '../../../../../scripts/modules/utils/contextGatherer.js' ); const { FuzzyTaskSearch } = await import( '../../../../../scripts/modules/utils/fuzzyTaskSearch.js' ); const { default: updateTaskById } = await import( '../../../../../scripts/modules/task-manager/update-task-by-id.js' ); // Import test fixtures for consistent sample data import { taggedEmptyTasks, taggedOneTask } from '../../../../fixtures/sample-tasks.js'; describe('updateTaskById validation', () => { beforeEach(() => { jest.clearAllMocks(); jest.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit called'); }); }); test('throws error if prompt is empty', async () => { await expect( updateTaskById( 'tasks/tasks.json', 1, '', false, { tag: 'master' }, 'json' ) ).rejects.toThrow('Prompt cannot be empty'); }); test('throws error if task file missing', async () => { const fs = await import('fs'); fs.existsSync.mockReturnValue(false); await expect( updateTaskById( 'tasks/tasks.json', 1, 'prompt', false, { tag: 'master' }, 'json' ) ).rejects.toThrow('Tasks file not found'); }); test('throws error when task ID not found', async () => { const fs = await import('fs'); fs.existsSync.mockReturnValue(true); readJSON.mockReturnValue(taggedEmptyTasks); await expect( updateTaskById( 'tasks/tasks.json', 42, 'prompt', false, { tag: 'master' }, 'json' ) ).rejects.toThrow('Task with ID 42 not found'); // Note: The error is reported through the bridge logger (report), // not the log function, so we don't assert on log being called }); }); describe('updateTaskById success path with generateObjectService', () => { let fs; let generateObjectService; beforeEach(async () => { jest.clearAllMocks(); jest.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit called'); }); fs = await import('fs'); const aiServices = await import( '../../../../../scripts/modules/ai-services-unified.js' ); generateObjectService = aiServices.generateObjectService; }); test('successfully updates task with all fields from generateObjectService', async () => { fs.existsSync.mockReturnValue(true); readJSON.mockReturnValue({ tag: 'master', tasks: [ { id: 1, title: 'Original Task', description: 'Original description', status: 'pending', dependencies: [], priority: 'low', details: null, testStrategy: null, subtasks: [] } ] }); const updatedTaskData = { id: 1, title: 'Updated Task', description: 'Updated description', status: 'pending', dependencies: [2], priority: 'high', details: 'New implementation details', testStrategy: 'Unit tests required', subtasks: [ { id: 1, title: 'Subtask 1', description: 'First subtask', status: 'pending', dependencies: [] } ] }; generateObjectService.mockResolvedValue({ mainResult: { task: updatedTaskData }, telemetryData: { model: 'claude-3-5-sonnet-20241022', inputTokens: 100, outputTokens: 200 } }); const result = await updateTaskById( 'tasks/tasks.json', 1, 'Update task with new requirements', false, { tag: 'master' }, 'json' ); // Verify generateObjectService was called (not generateTextService) expect(generateObjectService).toHaveBeenCalled(); const callArgs = generateObjectService.mock.calls[0][0]; // Verify correct arguments were passed expect(callArgs).toMatchObject({ role: 'main', commandName: 'update-task', objectName: 'task' }); expect(callArgs.schema).toBeDefined(); expect(callArgs.systemPrompt).toContain( 'update a software development task' ); expect(callArgs.prompt).toContain('Update task with new requirements'); // Verify the returned task contains all expected fields expect(result).toEqual({ updatedTask: expect.objectContaining({ id: 1, title: 'Updated Task', description: 'Updated description', status: 'pending', dependencies: [2], priority: 'high', details: 'New implementation details', testStrategy: 'Unit tests required', subtasks: expect.arrayContaining([ expect.objectContaining({ id: 1, title: 'Subtask 1', description: 'First subtask', status: 'pending' }) ]) }), telemetryData: expect.objectContaining({ model: 'claude-3-5-sonnet-20241022', inputTokens: 100, outputTokens: 200 }), tagInfo: undefined }); }); test('handles generateObjectService with malformed mainResult', async () => { fs.existsSync.mockReturnValue(true); readJSON.mockReturnValue({ tag: 'master', tasks: [ { id: 1, title: 'Task', description: 'Description', status: 'pending', dependencies: [], priority: 'medium', details: null, testStrategy: null, subtasks: [] } ] }); generateObjectService.mockResolvedValue({ mainResult: { task: null // Malformed: task is null }, telemetryData: {} }); await expect( updateTaskById( 'tasks/tasks.json', 1, 'Update task', false, { tag: 'master' }, 'json' ) ).rejects.toThrow('Received invalid task object from AI'); }); test('handles generateObjectService with missing required fields', async () => { fs.existsSync.mockReturnValue(true); readJSON.mockReturnValue({ tag: 'master', tasks: [ { id: 1, title: 'Task', description: 'Description', status: 'pending', dependencies: [], priority: 'medium', details: null, testStrategy: null, subtasks: [] } ] }); generateObjectService.mockResolvedValue({ mainResult: { task: { id: 1, // Missing title and description status: 'pending', dependencies: [], priority: 'medium' } }, telemetryData: {} }); await expect( updateTaskById( 'tasks/tasks.json', 1, 'Update task', false, { tag: 'master' }, 'json' ) ).rejects.toThrow('Updated task missing required fields'); }); }); describe('Remote Update via Bridge', () => { let fs; let generateObjectService; beforeEach(async () => { jest.clearAllMocks(); jest.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit called'); }); fs = await import('fs'); const aiServices = await import( '../../../../../scripts/modules/ai-services-unified.js' ); generateObjectService = aiServices.generateObjectService; }); test('should use remote update result when tryUpdateViaRemote succeeds', async () => { // Arrange - Mock successful remote update const remoteResult = { success: true, message: 'Task updated successfully via remote', data: { task: { id: 1, title: 'Updated via Remote', description: 'Updated description from remote', status: 'in-progress', dependencies: [], priority: 'high', details: 'Remote update details', testStrategy: 'Remote test strategy', subtasks: [] } } }; tryUpdateViaRemote.mockResolvedValue(remoteResult); fs.existsSync.mockReturnValue(true); readJSON.mockReturnValue({ tag: 'master', tasks: [ { id: 1, title: 'Original Task', description: 'Original description', status: 'pending', dependencies: [], priority: 'medium', details: 'Original details', testStrategy: 'Original test strategy', subtasks: [] } ] }); // Act const result = await updateTaskById( 'tasks/tasks.json', 1, 'Update this task', false, { tag: 'master' }, 'json' ); // Assert - Should use remote result and NOT call local AI service expect(tryUpdateViaRemote).toHaveBeenCalled(); expect(generateObjectService).not.toHaveBeenCalled(); expect(result).toEqual(remoteResult); }); test('should fallback to local update when tryUpdateViaRemote returns null', async () => { // Arrange - Mock remote returning null (no remote available) tryUpdateViaRemote.mockResolvedValue(null); fs.existsSync.mockReturnValue(true); readJSON.mockReturnValue({ tag: 'master', tasks: [ { id: 1, title: 'Task', description: 'Description', status: 'pending', dependencies: [], priority: 'medium', details: 'Details', testStrategy: 'Test strategy', subtasks: [] } ] }); generateObjectService.mockResolvedValue({ mainResult: { task: { id: 1, title: 'Updated Task', description: 'Updated description', status: 'in-progress', dependencies: [], priority: 'high', details: 'Updated details', testStrategy: 'Updated test strategy', subtasks: [] } }, telemetryData: {} }); // Act await updateTaskById( 'tasks/tasks.json', 1, 'Update this task', false, { tag: 'master' }, 'json' ); // Assert - Should fallback to local update expect(tryUpdateViaRemote).toHaveBeenCalled(); expect(generateObjectService).toHaveBeenCalled(); }); test('should propagate error when tryUpdateViaRemote throws error', async () => { // Arrange - Mock remote throwing error (it re-throws, doesn't return null) tryUpdateViaRemote.mockImplementation(() => Promise.reject(new Error('Remote update service unavailable')) ); fs.existsSync.mockReturnValue(true); readJSON.mockReturnValue({ tag: 'master', tasks: [ { id: 1, title: 'Task', description: 'Description', status: 'pending', dependencies: [], priority: 'medium', details: 'Details', testStrategy: 'Test strategy', subtasks: [] } ] }); generateObjectService.mockResolvedValue({ mainResult: { task: { id: 1, title: 'Updated Task', description: 'Updated description', status: 'in-progress', dependencies: [], priority: 'high', details: 'Updated details', testStrategy: 'Updated test strategy', subtasks: [] } }, telemetryData: {} }); // Act & Assert - Should propagate the error (not fallback to local) await expect( updateTaskById( 'tasks/tasks.json', 1, 'Update this task', false, { tag: 'master' }, 'json' ) ).rejects.toThrow('Remote update service unavailable'); expect(tryUpdateViaRemote).toHaveBeenCalled(); // Local update should NOT be called when remote throws expect(generateObjectService).not.toHaveBeenCalled(); }); }); describe('Prompt Manager Integration', () => { let fs; let generateObjectService; beforeEach(async () => { jest.clearAllMocks(); jest.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit called'); }); fs = await import('fs'); const aiServices = await import( '../../../../../scripts/modules/ai-services-unified.js' ); generateObjectService = aiServices.generateObjectService; tryUpdateViaRemote.mockResolvedValue(null); // No remote }); test('should use prompt manager to load update prompts', async () => { // Arrange fs.existsSync.mockReturnValue(true); readJSON.mockReturnValue({ tag: 'master', tasks: [ { id: 1, title: 'Task', description: 'Description', status: 'pending', dependencies: [], priority: 'medium', details: 'Details', testStrategy: 'Test strategy', subtasks: [] } ] }); generateObjectService.mockResolvedValue({ mainResult: { task: { id: 1, title: 'Updated Task', description: 'Updated description', status: 'in-progress', dependencies: [], priority: 'high', details: 'Updated details', testStrategy: 'Updated test strategy', subtasks: [] } }, telemetryData: {} }); // Act await updateTaskById( 'tasks/tasks.json', 1, 'Update this task with new requirements', false, { tag: 'master', projectRoot: '/mock/project' }, 'json' ); // Assert - Prompt manager should be called expect(getPromptManager).toHaveBeenCalled(); const promptManagerInstance = getPromptManager.mock.results[0].value; expect(promptManagerInstance.loadPrompt).toHaveBeenCalled(); }); }); describe('Context Gathering Integration', () => { let fs; let generateObjectService; beforeEach(async () => { jest.clearAllMocks(); jest.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit called'); }); fs = await import('fs'); const aiServices = await import( '../../../../../scripts/modules/ai-services-unified.js' ); generateObjectService = aiServices.generateObjectService; tryUpdateViaRemote.mockResolvedValue(null); // No remote }); test('should gather project context when projectRoot is provided', async () => { // Arrange const mockContextGatherer = { gather: jest.fn().mockResolvedValue({ fullContext: 'Project context from files', summary: 'Context summary' }) }; ContextGatherer.mockImplementation(() => mockContextGatherer); fs.existsSync.mockReturnValue(true); readJSON.mockReturnValue({ tag: 'master', tasks: [ { id: 1, title: 'Task', description: 'Description', status: 'pending', dependencies: [], priority: 'medium', details: 'Details', testStrategy: 'Test strategy', subtasks: [] } ] }); generateObjectService.mockResolvedValue({ mainResult: { task: { id: 1, title: 'Updated Task', description: 'Updated description', status: 'in-progress', dependencies: [], priority: 'high', details: 'Updated details', testStrategy: 'Updated test strategy', subtasks: [] } }, telemetryData: {} }); // Act await updateTaskById( 'tasks/tasks.json', 1, 'Update with context', false, { tag: 'master', projectRoot: '/mock/project' }, 'json' ); // Assert - Context gatherer should be instantiated and used expect(ContextGatherer).toHaveBeenCalledWith('/mock/project', 'master'); expect(mockContextGatherer.gather).toHaveBeenCalled(); }); }); describe('Fuzzy Task Search Integration', () => { let fs; let generateObjectService; beforeEach(async () => { jest.clearAllMocks(); jest.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit called'); }); fs = await import('fs'); const aiServices = await import( '../../../../../scripts/modules/ai-services-unified.js' ); generateObjectService = aiServices.generateObjectService; tryUpdateViaRemote.mockResolvedValue(null); // No remote }); test('should use fuzzy search to find related tasks for context', async () => { // Arrange const mockFuzzySearch = { findRelevantTasks: jest.fn().mockReturnValue([ { id: 2, title: 'Related Task 1', score: 0.9 }, { id: 3, title: 'Related Task 2', score: 0.85 } ]), getTaskIds: jest.fn().mockReturnValue(['2', '3']) }; FuzzyTaskSearch.mockImplementation(() => mockFuzzySearch); fs.existsSync.mockReturnValue(true); readJSON.mockReturnValue({ tag: 'master', tasks: [ { id: 1, title: 'Task to update', description: 'Description', status: 'pending', dependencies: [], priority: 'medium', details: 'Details', testStrategy: 'Test strategy', subtasks: [] }, { id: 2, title: 'Related Task 1', description: 'Related description', status: 'done', dependencies: [], priority: 'medium', details: 'Related details', testStrategy: 'Related test strategy', subtasks: [] }, { id: 3, title: 'Related Task 2', description: 'Another related description', status: 'pending', dependencies: [], priority: 'low', details: 'More details', testStrategy: 'Test strategy', subtasks: [] } ] }); generateObjectService.mockResolvedValue({ mainResult: { task: { id: 1, title: 'Updated Task', description: 'Updated description', status: 'in-progress', dependencies: [], priority: 'high', details: 'Updated details', testStrategy: 'Updated test strategy', subtasks: [] } }, telemetryData: {} }); // Act await updateTaskById( 'tasks/tasks.json', 1, 'Update with related task context', false, { tag: 'master' }, 'json' ); // Assert - Fuzzy search should be instantiated and used expect(FuzzyTaskSearch).toHaveBeenCalled(); expect(mockFuzzySearch.findRelevantTasks).toHaveBeenCalledWith( expect.stringContaining('Task to update'), expect.objectContaining({ maxResults: 5, includeSelf: true }) ); expect(mockFuzzySearch.getTaskIds).toHaveBeenCalled(); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/eyaltoledano/claude-task-master'

If you have feedback or need assistance with the MCP directory API, please join our Discord server