Skip to main content
Glama
task-manager.test.ts18.5 kB
/** * Tests for Task Manager - MCP Tasks Integration * * Validates the TaskManager implementation for ADR-020: MCP Tasks Integration Strategy * * @see ADR-020: MCP Tasks Integration Strategy * @see https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks */ import { TaskManager, getTaskManager, resetTaskManager, isTerminalStatus, type CreateAdrTaskOptions, type TaskResult, type McpTaskStatus, } from '../../src/utils/task-manager.js'; describe('TaskManager', () => { let taskManager: TaskManager; beforeEach(async () => { // Reset global task manager and create fresh instance for each test await resetTaskManager(); taskManager = new TaskManager({ enablePersistence: false }); await taskManager.initialize(); }); afterEach(async () => { await taskManager.shutdown(); }); describe('Task Creation', () => { it('should create a task with required options', async () => { const options: CreateAdrTaskOptions = { type: 'bootstrap', tool: 'bootstrap_validation_loop', }; const task = await taskManager.createTask(options); expect(task).toBeDefined(); expect(task.taskId).toBeDefined(); expect(task.taskId.length).toBe(32); // UUID hex format expect(task.status).toBe('working'); expect(task.metadata?.type).toBe('bootstrap'); expect(task.metadata?.tool).toBe('bootstrap_validation_loop'); expect(task.createdAt).toBeDefined(); expect(task.lastUpdatedAt).toBeDefined(); }); it('should create a task with phases', async () => { const options: CreateAdrTaskOptions = { type: 'bootstrap', tool: 'bootstrap_validation_loop', phases: ['platform_detection', 'infrastructure_setup', 'validation', 'cleanup'], }; const task = await taskManager.createTask(options); expect(task.metadata?.phases).toBeDefined(); expect(task.metadata?.phases?.length).toBe(4); expect(task.metadata?.phases?.[0]?.name).toBe('platform_detection'); expect(task.metadata?.phases?.[0]?.status).toBe('pending'); expect(task.metadata?.phases?.[0]?.progress).toBe(0); }); it('should create a task with custom TTL', async () => { const customTtl = 60000; // 1 minute const options: CreateAdrTaskOptions = { type: 'validation', tool: 'validate_adr', ttl: customTtl, }; const task = await taskManager.createTask(options); expect(task.ttl).toBe(customTtl); }); it('should create a task with project context', async () => { const options: CreateAdrTaskOptions = { type: 'planning', tool: 'interactive_adr_planning', projectPath: '/path/to/project', adrDirectory: 'docs/adrs', }; const task = await taskManager.createTask(options); expect(task.metadata?.projectPath).toBe('/path/to/project'); expect(task.metadata?.adrDirectory).toBe('docs/adrs'); }); it('should create a task with dependencies', async () => { const options: CreateAdrTaskOptions = { type: 'orchestration', tool: 'tool_chain_orchestrator', dependencies: ['task-1', 'task-2'], }; const task = await taskManager.createTask(options); expect(task.metadata?.dependencies).toEqual(['task-1', 'task-2']); }); }); describe('Task Retrieval', () => { it('should retrieve a task by ID', async () => { const task = await taskManager.createTask({ type: 'bootstrap', tool: 'bootstrap_validation_loop', }); const retrieved = await taskManager.getTask(task.taskId); expect(retrieved).toBeDefined(); expect(retrieved?.taskId).toBe(task.taskId); }); it('should return null for non-existent task', async () => { const retrieved = await taskManager.getTask('non-existent-id'); expect(retrieved).toBeNull(); }); it('should list all tasks', async () => { await taskManager.createTask({ type: 'bootstrap', tool: 'tool1' }); await taskManager.createTask({ type: 'validation', tool: 'tool2' }); await taskManager.createTask({ type: 'planning', tool: 'tool3' }); const { tasks } = await taskManager.listTasks(); expect(tasks.length).toBe(3); }); it('should paginate task list', async () => { // Create many tasks for (let i = 0; i < 60; i++) { await taskManager.createTask({ type: 'validation', tool: `tool-${i}` }); } const firstPage = await taskManager.listTasks(); expect(firstPage.tasks.length).toBe(50); expect(firstPage.nextCursor).toBeDefined(); const secondPage = await taskManager.listTasks(firstPage.nextCursor); expect(secondPage.tasks.length).toBe(10); expect(secondPage.nextCursor).toBeUndefined(); }); it('should get tasks by tool', async () => { await taskManager.createTask({ type: 'bootstrap', tool: 'bootstrap_validation_loop' }); await taskManager.createTask({ type: 'bootstrap', tool: 'bootstrap_validation_loop' }); await taskManager.createTask({ type: 'validation', tool: 'validate_adr' }); const bootstrapTasks = await taskManager.getTasksByTool('bootstrap_validation_loop'); expect(bootstrapTasks.length).toBe(2); }); it('should get tasks by type', async () => { await taskManager.createTask({ type: 'bootstrap', tool: 'tool1' }); await taskManager.createTask({ type: 'bootstrap', tool: 'tool2' }); await taskManager.createTask({ type: 'validation', tool: 'tool3' }); const bootstrapTasks = await taskManager.getTasksByType('bootstrap'); expect(bootstrapTasks.length).toBe(2); }); }); describe('Progress Updates', () => { it('should update task progress', async () => { const task = await taskManager.createTask({ type: 'bootstrap', tool: 'bootstrap_validation_loop', }); await taskManager.updateProgress({ taskId: task.taskId, progress: 50, message: 'Halfway done', }); const progress = taskManager.getProgress(task.taskId); expect(progress).toBe(50); const updated = await taskManager.getTask(task.taskId); expect(updated?.statusMessage).toBe('Halfway done'); }); it('should update phase progress', async () => { const task = await taskManager.createTask({ type: 'bootstrap', tool: 'bootstrap_validation_loop', phases: ['phase1', 'phase2', 'phase3'], }); await taskManager.updateProgress({ taskId: task.taskId, progress: 33, phase: 'phase1', phaseProgress: 100, }); const updated = await taskManager.getTask(task.taskId); const phase1 = updated?.metadata?.phases?.find(p => p.name === 'phase1'); expect(phase1?.status).toBe('completed'); expect(phase1?.progress).toBe(100); // Next phase should start const phase2 = updated?.metadata?.phases?.find(p => p.name === 'phase2'); expect(phase2?.status).toBe('running'); }); }); describe('Phase Management', () => { it('should start a phase', async () => { const task = await taskManager.createTask({ type: 'bootstrap', tool: 'bootstrap_validation_loop', phases: ['init', 'execute', 'validate'], }); await taskManager.startPhase(task.taskId, 'init'); const updated = await taskManager.getTask(task.taskId); const initPhase = updated?.metadata?.phases?.find(p => p.name === 'init'); expect(initPhase?.status).toBe('running'); expect(initPhase?.startTime).toBeDefined(); }); it('should complete a phase', async () => { const task = await taskManager.createTask({ type: 'bootstrap', tool: 'bootstrap_validation_loop', phases: ['init', 'execute', 'validate'], }); await taskManager.startPhase(task.taskId, 'init'); await taskManager.completePhase(task.taskId, 'init'); const updated = await taskManager.getTask(task.taskId); const initPhase = updated?.metadata?.phases?.find(p => p.name === 'init'); expect(initPhase?.status).toBe('completed'); expect(initPhase?.progress).toBe(100); expect(initPhase?.endTime).toBeDefined(); // Next phase should auto-start const executePhase = updated?.metadata?.phases?.find(p => p.name === 'execute'); expect(executePhase?.status).toBe('running'); // Overall progress should be 33% (1 of 3 phases) const progress = taskManager.getProgress(task.taskId); expect(progress).toBe(33); }); it('should fail a phase with error', async () => { const task = await taskManager.createTask({ type: 'bootstrap', tool: 'bootstrap_validation_loop', phases: ['init', 'execute'], }); await taskManager.startPhase(task.taskId, 'init'); await taskManager.failPhase(task.taskId, 'init', 'Configuration error'); const updated = await taskManager.getTask(task.taskId); const initPhase = updated?.metadata?.phases?.find(p => p.name === 'init'); expect(initPhase?.status).toBe('failed'); expect(initPhase?.error).toBe('Configuration error'); }); }); describe('Task Completion', () => { it('should complete a task successfully', async () => { const task = await taskManager.createTask({ type: 'validation', tool: 'validate_adr', }); const result: TaskResult = { success: true, data: { validationPassed: true, issues: [] }, }; await taskManager.completeTask(task.taskId, result); const updated = await taskManager.getTask(task.taskId); expect(updated?.status).toBe('completed'); expect(updated?.result).toEqual(result); const progress = taskManager.getProgress(task.taskId); expect(progress).toBe(100); }); it('should complete a task with phases', async () => { const task = await taskManager.createTask({ type: 'bootstrap', tool: 'bootstrap_validation_loop', phases: ['init', 'execute'], }); await taskManager.completeTask(task.taskId, { success: true }); const updated = await taskManager.getTask(task.taskId); expect(updated?.metadata?.phases?.every(p => p.status === 'completed')).toBe(true); }); it('should fail a task', async () => { const task = await taskManager.createTask({ type: 'bootstrap', tool: 'bootstrap_validation_loop', }); await taskManager.failTask(task.taskId, 'Infrastructure deployment failed'); const updated = await taskManager.getTask(task.taskId); expect(updated?.status).toBe('failed'); expect(updated?.statusMessage).toBe('Infrastructure deployment failed'); expect((updated?.result as TaskResult)?.success).toBe(false); }); it('should get task result', async () => { const task = await taskManager.createTask({ type: 'validation', tool: 'validate_adr', }); const result: TaskResult = { success: true, data: { validated: true }, }; await taskManager.completeTask(task.taskId, result); const retrieved = await taskManager.getTaskResult(task.taskId); expect(retrieved).toEqual(result); }); }); describe('Task Cancellation', () => { it('should cancel a running task', async () => { const task = await taskManager.createTask({ type: 'bootstrap', tool: 'bootstrap_validation_loop', }); await taskManager.cancelTask(task.taskId, 'User requested cancellation'); const updated = await taskManager.getTask(task.taskId); expect(updated?.status).toBe('cancelled'); expect(updated?.statusMessage).toBe('User requested cancellation'); }); it('should cancel a task with phases and mark them as skipped', async () => { const task = await taskManager.createTask({ type: 'bootstrap', tool: 'bootstrap_validation_loop', phases: ['init', 'execute', 'validate'], }); await taskManager.startPhase(task.taskId, 'init'); await taskManager.cancelTask(task.taskId); const updated = await taskManager.getTask(task.taskId); const phases = updated?.metadata?.phases; // Running phase should be skipped expect(phases?.find(p => p.name === 'init')?.status).toBe('skipped'); // Pending phases should be skipped expect(phases?.find(p => p.name === 'execute')?.status).toBe('skipped'); expect(phases?.find(p => p.name === 'validate')?.status).toBe('skipped'); }); it('should throw when cancelling a completed task', async () => { const task = await taskManager.createTask({ type: 'validation', tool: 'validate_adr', }); await taskManager.completeTask(task.taskId, { success: true }); await expect(taskManager.cancelTask(task.taskId)).rejects.toThrow( 'already in terminal state' ); }); it('should throw when cancelling non-existent task', async () => { await expect(taskManager.cancelTask('non-existent-id')).rejects.toThrow('not found'); }); }); describe('Task Status Helpers', () => { it('should identify terminal states', () => { expect(isTerminalStatus('completed')).toBe(true); expect(isTerminalStatus('failed')).toBe(true); expect(isTerminalStatus('cancelled')).toBe(true); expect(isTerminalStatus('working')).toBe(false); expect(isTerminalStatus('input_required')).toBe(false); }); it('should check if task is running', async () => { const task = await taskManager.createTask({ type: 'bootstrap', tool: 'bootstrap_validation_loop', }); expect(await taskManager.isTaskRunning(task.taskId)).toBe(true); await taskManager.completeTask(task.taskId, { success: true }); expect(await taskManager.isTaskRunning(task.taskId)).toBe(false); }); }); describe('Task Cleanup', () => { it('should cleanup old completed tasks', async () => { const task1 = await taskManager.createTask({ type: 'validation', tool: 'tool1' }); const task2 = await taskManager.createTask({ type: 'validation', tool: 'tool2' }); // Complete both tasks await taskManager.completeTask(task1.taskId, { success: true }); await taskManager.completeTask(task2.taskId, { success: true }); // Manually update lastUpdatedAt to simulate old task const t1 = await taskManager.getTask(task1.taskId); if (t1) { t1.lastUpdatedAt = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(); // 2 days ago } // Cleanup with 1 day max age const cleaned = await taskManager.cleanup(24 * 60 * 60 * 1000); expect(cleaned).toBe(1); // task1 should be cleaned, task2 should remain expect(await taskManager.getTask(task1.taskId)).toBeNull(); expect(await taskManager.getTask(task2.taskId)).not.toBeNull(); }); it('should not cleanup running tasks', async () => { const task = await taskManager.createTask({ type: 'bootstrap', tool: 'bootstrap_validation_loop', }); // Try to cleanup with 0 max age (should cleanup everything old) const cleaned = await taskManager.cleanup(0); expect(cleaned).toBe(0); expect(await taskManager.getTask(task.taskId)).not.toBeNull(); }); }); describe('Global TaskManager', () => { it('should return singleton instance', async () => { await resetTaskManager(); const manager1 = getTaskManager(); const manager2 = getTaskManager(); expect(manager1).toBe(manager2); }); it('should reset global instance', async () => { const manager1 = getTaskManager(); await manager1.initialize(); await resetTaskManager(); const manager2 = getTaskManager(); expect(manager1).not.toBe(manager2); }); }); describe('Task Metadata', () => { it('should get task metadata', async () => { const task = await taskManager.createTask({ type: 'planning', tool: 'interactive_adr_planning', phases: ['research', 'design', 'implement'], projectPath: '/project', }); const metadata = taskManager.getMetadata(task.taskId); expect(metadata?.type).toBe('planning'); expect(metadata?.tool).toBe('interactive_adr_planning'); expect(metadata?.phases?.length).toBe(3); expect(metadata?.projectPath).toBe('/project'); }); it('should return undefined for non-existent task metadata', () => { const metadata = taskManager.getMetadata('non-existent'); expect(metadata).toBeUndefined(); }); }); }); describe('TaskManager Integration with MCP Spec', () => { let taskManager: TaskManager; beforeEach(async () => { await resetTaskManager(); taskManager = new TaskManager({ enablePersistence: false }); await taskManager.initialize(); }); afterEach(async () => { await taskManager.shutdown(); }); it('should support all MCP task status values', async () => { const statuses: McpTaskStatus[] = [ 'working', 'input_required', 'completed', 'failed', 'cancelled', ]; for (const status of statuses) { // Verify these are valid status types expect(['working', 'input_required', 'completed', 'failed', 'cancelled']).toContain(status); } }); it('should support ADR-020 task types', async () => { const taskTypes = [ 'bootstrap', 'deployment', 'research', 'orchestration', 'troubleshooting', 'validation', 'planning', ] as const; for (const type of taskTypes) { const task = await taskManager.createTask({ type, tool: `${type}_tool`, }); expect(task.metadata?.type).toBe(type); } }); it('should provide poll interval for clients', async () => { const task = await taskManager.createTask({ type: 'bootstrap', tool: 'bootstrap_validation_loop', }); expect(task.pollInterval).toBeDefined(); expect(task.pollInterval).toBeGreaterThan(0); }); it('should track TTL for automatic cleanup', async () => { const ttl = 5000; // 5 seconds const task = await taskManager.createTask({ type: 'validation', tool: 'validate_adr', ttl, }); expect(task.ttl).toBe(ttl); }); });

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/tosin2013/mcp-adr-analysis-server'

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