Skip to main content
Glama
blizzy78
by blizzy78
decompose_task.test.ts18.1 kB
import { beforeEach, describe, expect, it } from 'vitest' import { TaskDB } from '../task_db.js' import { DoneStatus, InProgressStatus, newTaskID, TodoStatus, type Task } from '../tasks.js' import { handleDecomposeTask } from './decompose_task.js' describe('decompose_task tool handler', () => { let taskDB: TaskDB beforeEach(() => { taskDB = new TaskDB() }) describe('basic task decomposition', () => { it('should decompose task into subtasks with sequence ordering', async () => { // Create parent task const parentTaskID = newTaskID() const parentTask: Task = { taskID: parentTaskID, status: TodoStatus, dependsOnTaskIDs: [], title: 'Parent Task', description: 'Task to be decomposed', goal: 'Parent goal', definitionsOfDone: ['Parent done'], criticalPath: true, uncertaintyAreas: [], estimatedComplexity: { level: 'medium, must decompose before execution', description: 'Needs decomposition', }, lessonsLearned: [], verificationEvidence: [], } taskDB.set(parentTaskID, parentTask) const args = { taskID: parentTaskID, decompositionReason: 'Task is too complex', subtasks: [ { title: 'Subtask 1', description: 'First subtask', goal: 'First goal', definitionsOfDone: ['First done'], criticalPath: true, uncertaintyAreas: [], estimatedComplexity: { level: 'trivial' as const, description: 'Simple subtask', }, sequenceOrder: 1, }, { title: 'Subtask 2', description: 'Second subtask', goal: 'Second goal', definitionsOfDone: ['Second done'], criticalPath: false, uncertaintyAreas: [], estimatedComplexity: { level: 'low, may benefit from decomposition before execution' as const, description: 'Low complexity subtask', }, sequenceOrder: 2, }, ], } const result = await handleDecomposeTask(args, taskDB, false) expect(result.structuredContent).toMatchObject({ taskUpdated: { taskID: parentTaskID, title: 'Parent Task', }, tasksCreated: expect.arrayContaining([ expect.objectContaining({ title: 'Subtask 1', mustDecomposeBeforeExecution: undefined, }), expect.objectContaining({ title: 'Subtask 2', mustDecomposeBeforeExecution: undefined, }), ]), }) // Verify parent task now depends on the last sequence subtasks const updatedParent = taskDB.get(parentTaskID)! expect(updatedParent.dependsOnTaskIDs).toHaveLength(1) // Verify subtasks were created with proper dependencies const createdTaskIDs = result.structuredContent.tasksCreated.map((t: any) => t.taskID) expect(createdTaskIDs).toHaveLength(2) const subtask1 = taskDB.get(createdTaskIDs[0])! const subtask2 = taskDB.get(createdTaskIDs[1])! // First subtask should have no dependencies expect(subtask1.dependsOnTaskIDs).toHaveLength(0) // Second subtask should depend on first subtask expect(subtask2.dependsOnTaskIDs).toContain(subtask1.taskID) }) it('should handle parallel subtasks with same sequence order', async () => { const parentTaskID = newTaskID() const parentTask: Task = { taskID: parentTaskID, status: TodoStatus, dependsOnTaskIDs: [], title: 'Parallel Parent', description: 'Task with parallel subtasks', goal: 'Parallel goal', definitionsOfDone: ['Parallel done'], criticalPath: true, uncertaintyAreas: [], lessonsLearned: [], verificationEvidence: [], } taskDB.set(parentTaskID, parentTask) const args = { taskID: parentTaskID, decompositionReason: 'Can be done in parallel', subtasks: [ { title: 'Parallel Task A', description: 'First parallel task', goal: 'Parallel A goal', definitionsOfDone: ['A done'], criticalPath: true, uncertaintyAreas: [], estimatedComplexity: { level: 'trivial' as const, description: 'Simple parallel task', }, sequenceOrder: 1, }, { title: 'Parallel Task B', description: 'Second parallel task', goal: 'Parallel B goal', definitionsOfDone: ['B done'], criticalPath: true, uncertaintyAreas: [], estimatedComplexity: { level: 'trivial' as const, description: 'Simple parallel task', }, sequenceOrder: 1, }, ], } const result = await handleDecomposeTask(args, taskDB, false) const createdTaskIDs = result.structuredContent.tasksCreated.map((t: any) => t.taskID) const taskA = taskDB.get(createdTaskIDs[0])! const taskB = taskDB.get(createdTaskIDs[1])! // Both tasks should have no dependencies (parallel execution) expect(taskA.dependsOnTaskIDs).toHaveLength(0) expect(taskB.dependsOnTaskIDs).toHaveLength(0) // Parent should depend on both tasks const updatedParent = taskDB.get(parentTaskID)! expect(updatedParent.dependsOnTaskIDs).toHaveLength(2) expect(updatedParent.dependsOnTaskIDs).toContain(taskA.taskID) expect(updatedParent.dependsOnTaskIDs).toContain(taskB.taskID) }) it('should create complex dependency chains', async () => { const parentTaskID = newTaskID() const parentTask: Task = { taskID: parentTaskID, status: TodoStatus, dependsOnTaskIDs: [], title: 'Complex Parent', description: 'Complex decomposition', goal: 'Complex goal', definitionsOfDone: ['Complex done'], criticalPath: true, uncertaintyAreas: [], lessonsLearned: [], verificationEvidence: [], } taskDB.set(parentTaskID, parentTask) const args = { taskID: parentTaskID, decompositionReason: 'Complex workflow', subtasks: [ { title: 'Setup', description: 'Initial setup', goal: 'Setup complete', definitionsOfDone: ['Setup done'], criticalPath: true, uncertaintyAreas: [], estimatedComplexity: { level: 'trivial' as const, description: 'Setup task', }, sequenceOrder: 1, }, { title: 'Process A', description: 'First process', goal: 'Process A complete', definitionsOfDone: ['A processed'], criticalPath: true, uncertaintyAreas: [], estimatedComplexity: { level: 'trivial' as const, description: 'Process task', }, sequenceOrder: 2, }, { title: 'Process B', description: 'Second process', goal: 'Process B complete', definitionsOfDone: ['B processed'], criticalPath: false, uncertaintyAreas: [], estimatedComplexity: { level: 'trivial' as const, description: 'Process task', }, sequenceOrder: 2, }, { title: 'Finalize', description: 'Final step', goal: 'Finalization complete', definitionsOfDone: ['Finalized'], criticalPath: true, uncertaintyAreas: [], estimatedComplexity: { level: 'trivial' as const, description: 'Final task', }, sequenceOrder: 3, }, ], } const result = await handleDecomposeTask(args, taskDB, false) const createdTasks = result.structuredContent.tasksCreated const setupTask = createdTasks.find((t: any) => t.title === 'Setup')! const processATask = createdTasks.find((t: any) => t.title === 'Process A')! const processBTask = createdTasks.find((t: any) => t.title === 'Process B')! const finalizeTask = createdTasks.find((t: any) => t.title === 'Finalize')! const setup = taskDB.get(setupTask.taskID)! const processA = taskDB.get(processATask.taskID)! const processB = taskDB.get(processBTask.taskID)! const finalize = taskDB.get(finalizeTask.taskID)! // Setup has no dependencies expect(setup.dependsOnTaskIDs).toHaveLength(0) // Both processes depend on setup expect(processA.dependsOnTaskIDs).toContain(setup.taskID) expect(processB.dependsOnTaskIDs).toContain(setup.taskID) // Finalize depends on both processes expect(finalize.dependsOnTaskIDs).toContain(processA.taskID) expect(finalize.dependsOnTaskIDs).toContain(processB.taskID) // Parent depends on finalize const updatedParent = taskDB.get(parentTaskID)! expect(updatedParent.dependsOnTaskIDs).toContain(finalize.taskID) }) }) describe('error handling', () => { it('should throw error for non-existent parent task', async () => { const nonExistentID = newTaskID() const args = { taskID: nonExistentID, decompositionReason: 'Testing error', subtasks: [ { title: 'Test Subtask', description: 'Test description', goal: 'Test goal', definitionsOfDone: ['Test done'], criticalPath: false, uncertaintyAreas: [], estimatedComplexity: { level: 'trivial' as const, description: 'Test', }, sequenceOrder: 1, }, ], } await expect(handleDecomposeTask(args, taskDB, false)).rejects.toThrow(`Task not found: ${nonExistentID}`) }) it('should throw error when trying to decompose non-todo task', async () => { const parentTaskID = newTaskID() const parentTask: Task = { taskID: parentTaskID, status: InProgressStatus, dependsOnTaskIDs: [], title: 'In Progress Task', description: 'Cannot be decomposed', goal: 'Should fail', definitionsOfDone: ['Will not work'], criticalPath: true, uncertaintyAreas: [], lessonsLearned: [], verificationEvidence: [], } taskDB.set(parentTaskID, parentTask) const args = { taskID: parentTaskID, decompositionReason: 'Should fail', subtasks: [ { title: 'Should Not Work', description: 'This should fail', goal: 'Failure', definitionsOfDone: ['Failed'], criticalPath: false, uncertaintyAreas: [], estimatedComplexity: { level: 'trivial' as const, description: 'Should fail', }, sequenceOrder: 1, }, ], } await expect(handleDecomposeTask(args, taskDB, false)).rejects.toThrow( `Can't decompose task ${parentTaskID} in status: in-progress` ) }) it('should throw error when trying to decompose done task', async () => { const parentTaskID = newTaskID() const parentTask: Task = { taskID: parentTaskID, status: DoneStatus, dependsOnTaskIDs: [], title: 'Done Task', description: 'Already completed', goal: 'Completed', definitionsOfDone: ['Done'], criticalPath: true, uncertaintyAreas: [], lessonsLearned: [], verificationEvidence: [], } taskDB.set(parentTaskID, parentTask) const args = { taskID: parentTaskID, decompositionReason: 'Should fail', subtasks: [ { title: 'Should Not Work', description: 'This should fail', goal: 'Failure', definitionsOfDone: ['Failed'], criticalPath: false, uncertaintyAreas: [], estimatedComplexity: { level: 'trivial' as const, description: 'Should fail', }, sequenceOrder: 1, }, ], } await expect(handleDecomposeTask(args, taskDB, false)).rejects.toThrow( `Can't decompose task ${parentTaskID} in status: done` ) }) }) describe('content messages', () => { it('should return empty content when no subtasks need decomposition', async () => { const parentTaskID = newTaskID() const parentTask: Task = { taskID: parentTaskID, status: TodoStatus, dependsOnTaskIDs: [], title: 'Simple Parent', description: 'Simple decomposition', goal: 'Simple goal', definitionsOfDone: ['Simple done'], criticalPath: true, uncertaintyAreas: [], lessonsLearned: [], verificationEvidence: [], } taskDB.set(parentTaskID, parentTask) const args = { taskID: parentTaskID, decompositionReason: 'Simple breakdown', subtasks: [ { title: 'Simple Subtask', description: 'Simple subtask', goal: 'Simple subtask goal', definitionsOfDone: ['Simple subtask done'], criticalPath: false, uncertaintyAreas: [], estimatedComplexity: { level: 'trivial' as const, description: 'Simple', }, sequenceOrder: 1, }, ], } const result = await handleDecomposeTask(args, taskDB, false) expect(result.content).toEqual([]) }) it('should indicate when some subtasks need decomposition', async () => { const parentTaskID = newTaskID() const parentTask: Task = { taskID: parentTaskID, status: TodoStatus, dependsOnTaskIDs: [], title: 'Mixed Parent', description: 'Mixed complexity', goal: 'Mixed goal', definitionsOfDone: ['Mixed done'], criticalPath: true, uncertaintyAreas: [], lessonsLearned: [], verificationEvidence: [], } taskDB.set(parentTaskID, parentTask) const args = { taskID: parentTaskID, decompositionReason: 'Mixed complexity', subtasks: [ { title: 'Simple Subtask', description: 'Simple subtask', goal: 'Simple goal', definitionsOfDone: ['Simple done'], criticalPath: false, uncertaintyAreas: [], estimatedComplexity: { level: 'trivial' as const, description: 'Simple', }, sequenceOrder: 1, }, { title: 'Complex Subtask', description: 'Complex subtask', goal: 'Complex goal', definitionsOfDone: ['Complex done'], criticalPath: true, uncertaintyAreas: [], estimatedComplexity: { level: 'medium, must decompose before execution' as const, description: 'Complex', }, sequenceOrder: 2, }, ], } const result = await handleDecomposeTask(args, taskDB, false) expect(result.content).toHaveLength(1) expect(result.content[0]).toMatchObject({ type: 'text', text: "Some tasks must be decomposed before execution, use 'decompose_task' tool", audience: ['assistant'], }) }) }) describe('existing dependencies preservation', () => { it('should preserve existing parent task dependencies', async () => { const existingDepID = newTaskID() const existingDep: Task = { taskID: existingDepID, status: TodoStatus, dependsOnTaskIDs: [], title: 'Existing Dependency', description: 'Already exists', goal: 'Existing goal', definitionsOfDone: ['Existing done'], criticalPath: true, uncertaintyAreas: [], lessonsLearned: [], verificationEvidence: [], } taskDB.set(existingDepID, existingDep) const parentTaskID = newTaskID() const parentTask: Task = { taskID: parentTaskID, status: TodoStatus, dependsOnTaskIDs: [existingDepID], // Existing dependency title: 'Parent with Dependencies', description: 'Has existing deps', goal: 'Preserve deps', definitionsOfDone: ['Deps preserved'], criticalPath: true, uncertaintyAreas: [], lessonsLearned: [], verificationEvidence: [], } taskDB.set(parentTaskID, parentTask) const args = { taskID: parentTaskID, decompositionReason: 'Add more subtasks', subtasks: [ { title: 'New Subtask', description: 'New subtask', goal: 'New goal', definitionsOfDone: ['New done'], criticalPath: true, uncertaintyAreas: [], estimatedComplexity: { level: 'trivial' as const, description: 'New task', }, sequenceOrder: 1, }, ], } const result = await handleDecomposeTask(args, taskDB, false) const updatedParent = taskDB.get(parentTaskID)! const newSubtaskID = result.structuredContent.tasksCreated[0].taskID // Should have both existing and new dependencies expect(updatedParent.dependsOnTaskIDs).toContain(existingDepID) expect(updatedParent.dependsOnTaskIDs).toContain(newSubtaskID) expect(updatedParent.dependsOnTaskIDs).toHaveLength(2) }) }) })

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/blizzy78/mcp-task-manager'

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