From c41e32383bdb19888955aef2769fc0b9ed20ec56 Mon Sep 17 00:00:00 2001 From: "Christopher C. Smith" Date: Sun, 23 Mar 2025 16:47:20 -0400 Subject: [PATCH 1/2] Code simplification, deduplication --- README.md | 2 +- index.ts | 45 +- jest.config.cjs | 6 + src/server/TaskManager.ts | 43 +- src/server/tools.ts | 4 + src/types/index.ts | 1 + tests/integration/TaskManagertest.ts | 295 ++++---- tests/integration/mcp-client.test.ts | 82 +- tests/unit/TaskManager.test.ts | 1053 +++++++++++++------------- 9 files changed, 780 insertions(+), 751 deletions(-) diff --git a/README.md b/README.md index e1428c8..5c67859 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MCP Task Manager -A Model Context Protocol (MCP) server for AI task management. This tool helps AI assistants handle multi-step tasks in a structured way, with user approval checkpoints. + MCP Task Manager ([npm package: taskqueue-mcp](https://www.npmjs.com/package/taskqueue-mcp)) is a Model Context Protocol (MCP) server for AI task management. This tool helps AI assistants handle multi-step tasks in a structured way, with optional user approval checkpoints. ## Features diff --git a/index.ts b/index.ts index d4d5698..4abed8e 100644 --- a/index.ts +++ b/index.ts @@ -178,40 +178,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { throw new Error("Missing required parameters: projectId and/or taskId"); } - const updates: { title?: string; description?: string } = {}; - if (args.title !== undefined) updates.title = String(args.title); - if (args.description !== undefined) updates.description = String(args.description); - + const updates = Object.fromEntries( + Object.entries({ + title: args.title !== undefined ? String(args.title) : undefined, + description: args.description !== undefined ? String(args.description) : undefined, + status: args.status !== undefined ? String(args.status) as "not started" | "in progress" | "done" : undefined, + completedDetails: args.completedDetails !== undefined ? String(args.completedDetails) : undefined, + toolRecommendations: args.toolRecommendations !== undefined ? String(args.toolRecommendations) : undefined, + ruleRecommendations: args.ruleRecommendations !== undefined ? String(args.ruleRecommendations) : undefined + }).filter(([_, value]) => value !== undefined) + ); + const result = await taskManager.updateTask(projectId, taskId, updates); - - // Handle status change separately if needed - if (args.status) { - const status = args.status as "not started" | "in progress" | "done"; - const proj = taskManager["data"].projects.find(p => p.projectId === projectId); - if (proj) { - const task = proj.tasks.find(t => t.id === taskId); - if (task) { - if (status === "done") { - if (!args.completedDetails) { - return { - content: [{ type: "text", text: JSON.stringify({ - status: "error", - message: "completedDetails is required when setting status to 'done'" - }, null, 2) }], - }; - } - - // Use markTaskDone for proper transition to done status - await taskManager.markTaskDone(projectId, taskId, String(args.completedDetails)); - } else { - // For other status changes - task.status = status; - await taskManager["saveTasks"](); - } - } - } - } - + return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; diff --git a/jest.config.cjs b/jest.config.cjs index 3cbe8b9..81d566f 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -14,4 +14,10 @@ module.exports = { ], }, modulePathIgnorePatterns: ['/dist/'], + // Force Jest to exit after all tests have completed + forceExit: true, + // Detect open handles and warn about them + detectOpenHandles: true, + // Extend the timeout to allow sufficient time for tests to complete + testTimeout: 30000, }; \ No newline at end of file diff --git a/src/server/TaskManager.ts b/src/server/TaskManager.ts index 1dcf6d6..9b16339 100644 --- a/src/server/TaskManager.ts +++ b/src/server/TaskManager.ts @@ -129,7 +129,8 @@ export class TaskManager { public async createProject( initialPrompt: string, tasks: { title: string; description: string; toolRecommendations?: string; ruleRecommendations?: string }[], - projectPlan?: string + projectPlan?: string, + autoApprove?: boolean ) { await this.ensureInitialized(); this.projectCounter += 1; @@ -156,6 +157,7 @@ export class TaskManager { projectPlan: projectPlan || initialPrompt, tasks: newTasks, completed: false, + autoApprove: autoApprove === true ? true : false, }); await this.saveTasks(); @@ -213,38 +215,6 @@ export class TaskManager { }; } - public async markTaskDone( - projectId: string, - taskId: string, - completedDetails?: string - ) { - await this.ensureInitialized(); - const proj = this.data.projects.find((p) => p.projectId === projectId); - if (!proj) return { status: "error", message: "Project not found" }; - const task = proj.tasks.find((t) => t.id === taskId); - if (!task) return { status: "error", message: "Task not found" }; - if (task.status === "done") - return { - status: "already_done", - message: "Task is already marked done.", - }; - - task.status = "done"; - task.completedDetails = completedDetails || ""; - await this.saveTasks(); - return { - status: "task_marked_done", - projectId: proj.projectId, - task: { - id: task.id, - title: task.title, - description: task.description, - completedDetails: task.completedDetails, - approved: task.approved, - }, - }; - } - public async approveTaskCompletion(projectId: string, taskId: string) { await this.ensureInitialized(); const proj = this.data.projects.find((p) => p.projectId === projectId); @@ -457,6 +427,8 @@ export class TaskManager { description?: string; toolRecommendations?: string; ruleRecommendations?: string; + status?: "not started" | "in progress" | "done"; + completedDetails?: string; } ) { await this.ensureInitialized(); @@ -473,6 +445,11 @@ export class TaskManager { // Update the task with the provided updates project.tasks[taskIndex] = { ...project.tasks[taskIndex], ...updates }; + // Check if status was updated to 'done' and if project has autoApprove enabled + if (updates.status === 'done' && project.autoApprove) { + project.tasks[taskIndex].approved = true; + } + await this.saveTasks(); return project.tasks[taskIndex]; } diff --git a/src/server/tools.ts b/src/server/tools.ts index 8497b26..b509b41 100644 --- a/src/server/tools.ts +++ b/src/server/tools.ts @@ -76,6 +76,10 @@ const createProjectTool: Tool = { required: ["title", "description"], }, }, + autoApprove: { + type: "boolean", + description: "If true, tasks will be automatically approved when marked as done. If false or not provided, tasks require manual approval.", + }, }, required: ["initialPrompt", "tasks"], }, diff --git a/src/types/index.ts b/src/types/index.ts index 865b1fc..20fca41 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -18,6 +18,7 @@ export interface Project { projectPlan: string; tasks: Task[]; completed: boolean; + autoApprove?: boolean; } export interface TaskManagerFile { diff --git a/tests/integration/TaskManagertest.ts b/tests/integration/TaskManagertest.ts index ebb58a8..4a0c633 100644 --- a/tests/integration/TaskManagertest.ts +++ b/tests/integration/TaskManagertest.ts @@ -29,22 +29,6 @@ describe('TaskManager Integration', () => { } }); - it('should have all required tools', () => { - const toolNames = ALL_TOOLS.map(tool => tool.name); - expect(toolNames).toContain('list_projects'); - expect(toolNames).toContain('create_project'); - expect(toolNames).toContain('delete_project'); - expect(toolNames).toContain('add_tasks_to_project'); - expect(toolNames).toContain('finalize_project'); - expect(toolNames).toContain('read_project'); - - expect(toolNames).toContain('read_task'); - expect(toolNames).toContain('update_task'); - expect(toolNames).toContain('delete_task'); - expect(toolNames).toContain('approve_task'); - expect(toolNames).toContain('get_next_task'); - }); - it('should handle project tool actions', async () => { // Test project creation const createResult = await server.createProject( @@ -220,6 +204,36 @@ describe('TaskManager Integration', () => { } }); + it('should handle file persistence correctly', async () => { + // Create initial data + const project = await server.createProject("Persistent Project", [ + { title: "Task 1", description: "Test task" } + ]); + + // Create a new server instance pointing to the same file + const newServer = new TaskManager(testFilePath); + + // Verify the data was loaded correctly + const result = await newServer.listProjects("open"); + expect(result.projects.length).toBe(1); + expect(result.projects[0].projectId).toBe(project.projectId); + + // Modify task state in new server + await newServer.updateTask( + project.projectId, + project.tasks[0].id, + { + status: "done", + completedDetails: "Completed task details" + } + ); + + // Create another server instance and verify the changes persisted + const thirdServer = new TaskManager(testFilePath); + const pendingResult = await thirdServer.listTasks(project.projectId, "pending_approval"); + expect(pendingResult.tasks!.length).toBe(1); + }); + it('should execute a complete project workflow', async () => { // 1. Create a project with multiple tasks const createResult = await server.createProject( @@ -253,15 +267,16 @@ describe('TaskManager Integration', () => { } // 3. Mark the first task as in progress - const task1 = server["data"].projects.find(p => p.projectId === projectId)?.tasks.find(t => t.id === taskId1); - if (task1) { - task1.status = 'in progress'; - await server["saveTasks"](); - } + await server.updateTask(projectId, taskId1, { + status: 'in progress' + }); // 4. Mark the first task as done - const markDoneResult = await server.markTaskDone(projectId, taskId1, 'Task 1 completed details'); - expect(markDoneResult.status).toBe('task_marked_done'); + const markDoneResult = await server.updateTask(projectId, taskId1, { + status: 'done', + completedDetails: 'Task 1 completed details' + }); + expect(markDoneResult.status).toBe('done'); // 5. Approve the first task const approveResult = await server.approveTaskCompletion(projectId, taskId1); @@ -275,15 +290,16 @@ describe('TaskManager Integration', () => { } // 7. Mark the second task as in progress - const task2 = server["data"].projects.find(p => p.projectId === projectId)?.tasks.find(t => t.id === taskId2); - if (task2) { - task2.status = 'in progress'; - await server["saveTasks"](); - } + await server.updateTask(projectId, taskId2, { + status: 'in progress' + }); // 8. Mark the second task as done - const markDoneResult2 = await server.markTaskDone(projectId, taskId2, 'Task 2 completed details'); - expect(markDoneResult2.status).toBe('task_marked_done'); + const markDoneResult2 = await server.updateTask(projectId, taskId2, { + status: 'done', + completedDetails: 'Task 2 completed details' + }); + expect(markDoneResult2.status).toBe('done'); // 9. Approve the second task const approveResult2 = await server.approveTaskCompletion(projectId, taskId2); @@ -298,8 +314,9 @@ describe('TaskManager Integration', () => { expect(finalizeResult.status).toBe('project_approved_complete'); // 12. Verify the project is marked as completed - const project = server["data"].projects.find(p => p.projectId === projectId); - expect(project?.completed).toBe(true); + const projectState = await server.listProjects("completed"); + expect(projectState.projects.length).toBe(1); + expect(projectState.projects[0].projectId).toBe(projectId); }); it('should handle project approval workflow', async () => { @@ -331,8 +348,8 @@ describe('TaskManager Integration', () => { expect(earlyApprovalResult.message).toContain('Not all tasks are done'); // 3. Mark tasks as done - await server.markTaskDone(projectId, taskId1, 'Task 1 completed details'); - await server.markTaskDone(projectId, taskId2, 'Task 2 completed details'); + await server.updateTask(projectId, taskId1, { status: 'done', completedDetails: 'Task 1 completed details' }); + await server.updateTask(projectId, taskId2, { status: 'done', completedDetails: 'Task 2 completed details' }); // 4. Try to approve project before tasks are approved (should fail) const preApprovalResult = await server.approveProjectCompletion(projectId); @@ -348,56 +365,14 @@ describe('TaskManager Integration', () => { expect(approvalResult.status).toBe('project_approved_complete'); // 7. Verify project state - const project = server["data"].projects.find(p => p.projectId === projectId); - expect(project?.completed).toBe(true); - expect(project?.tasks.every(t => t.status === 'done')).toBe(true); - expect(project?.tasks.every(t => t.approved)).toBe(true); + const projectAfterApproval = await server.listProjects("completed"); + const completedProject = projectAfterApproval.projects.find(p => p.projectId === projectId); + expect(completedProject).toBeDefined(); // 8. Try to approve again (should fail) const reapprovalResult = await server.approveProjectCompletion(projectId); expect(reapprovalResult.status).toBe('error'); expect(reapprovalResult.message).toContain('Project is already completed'); - - // 9. Verify project is still listed - const listResult = await server.listProjects(); - const listedProject = listResult.projects?.find(p => p.projectId === projectId); - expect(listedProject).toBeDefined(); - expect(listedProject?.initialPrompt).toBe('Project for approval workflow'); - expect(listedProject?.totalTasks).toBe(2); - expect(listedProject?.completedTasks).toBe(2); - expect(listedProject?.approvedTasks).toBe(2); - }); - - it("should list only pending approval projects", async () => { - // Create projects and tasks with varying statuses - const project1 = await server.createProject("Pending Approval Project", [{ title: "Task 1", description: "Desc" }]); - const project2 = await server.createProject("Completed project", [{ title: "Task 2", description: "Desc" }]); - - // Mark task1 as done but not approved - const proj1Task = server["data"].projects.find(p => p.projectId === project1.projectId)?.tasks[0]; - if (proj1Task) { - proj1Task.status = "done"; - await server["saveTasks"](); - } - - // Complete project 2 fully - const proj2Task = server["data"].projects.find(p => p.projectId === project2.projectId)?.tasks[0]; - if (proj2Task) { - proj2Task.status = "done"; - proj2Task.approved = true; - server["data"].projects.find(p => p.projectId === project2.projectId)!.completed = true; - await server["saveTasks"](); - } - - const result = await server.listProjects("pending_approval"); - expect(result.projects.length).toBe(1); - expect(result.projects[0].projectId).toBe(project1.projectId); - - // Verify the task states are correct - const tasks = await server.listTasks(project1.projectId, "pending_approval"); - expect(tasks.tasks!.length).toBe(1); - expect(tasks.tasks![0].status).toBe("done"); - expect(tasks.tasks![0].approved).toBe(false); }); it("should handle complex project and task state transitions", async () => { @@ -413,11 +388,11 @@ describe('TaskManager Integration', () => { expect(initialOpenTasks.tasks!.length).toBe(3); // Mark first task as done and approved - const tasks = server["data"].projects.find(p => p.projectId === project.projectId)?.tasks; - if (tasks) { - await server.markTaskDone(project.projectId, tasks[0].id); - await server.approveTaskCompletion(project.projectId, tasks[0].id); - } + await server.updateTask(project.projectId, project.tasks[0].id, { + status: 'done', + completedDetails: 'Task 1 completed' + }); + await server.approveTaskCompletion(project.projectId, project.tasks[0].id); // Should now have 2 open tasks and 1 completed const openTasks = await server.listTasks(project.projectId, "open"); @@ -427,9 +402,10 @@ describe('TaskManager Integration', () => { expect(completedTasks.tasks!.length).toBe(1); // Mark second task as done but not approved - if (tasks) { - await server.markTaskDone(project.projectId, tasks[1].id); - } + await server.updateTask(project.projectId, project.tasks[1].id, { + status: 'done', + completedDetails: 'Task 2 completed' + }); // Should now have 1 open task, 1 pending approval, and 1 completed const finalOpenTasks = await server.listTasks(project.projectId, "open"); @@ -442,68 +418,6 @@ describe('TaskManager Integration', () => { expect(finalCompletedTasks.tasks!.length).toBe(1); }); - it("should handle project completion state correctly", async () => { - // Create a project with two tasks - const project = await server.createProject("Project to Complete", [ - { title: "Task 1", description: "First task" }, - { title: "Task 2", description: "Second task" } - ]); - - // Initially project should be open - const initialResult = await server.listProjects("open"); - expect(initialResult.projects.length).toBe(1); - - // Complete and approve all tasks - const tasks = server["data"].projects.find(p => p.projectId === project.projectId)?.tasks; - if (tasks) { - tasks.forEach(task => { - task.status = "done"; - task.approved = true; - }); - server["data"].projects.find(p => p.projectId === project.projectId)!.completed = true; - await server["saveTasks"](); - } - - // Project should now be completed - const completedResult = await server.listProjects("completed"); - expect(completedResult.projects.length).toBe(1); - expect(completedResult.projects[0].projectId).toBe(project.projectId); - - // No projects should be open or pending approval - const openResult = await server.listProjects("open"); - expect(openResult.projects.length).toBe(0); - - const pendingResult = await server.listProjects("pending_approval"); - expect(pendingResult.projects.length).toBe(0); - }); - - it("should handle file persistence correctly", async () => { - // Create initial data - const project = await server.createProject("Persistent Project", [ - { title: "Task 1", description: "Test task" } - ]); - - // Create a new server instance pointing to the same file - const newServer = new TaskManager(testFilePath); - - // Verify the data was loaded correctly - const result = await newServer.listProjects("open"); - expect(result.projects.length).toBe(1); - expect(result.projects[0].projectId).toBe(project.projectId); - - // Modify task state in new server - const tasks = newServer["data"].projects.find(p => p.projectId === project.projectId)?.tasks; - if (tasks) { - tasks[0].status = "done"; - await newServer["saveTasks"](); - } - - // Create another server instance and verify the changes persisted - const thirdServer = new TaskManager(testFilePath); - const pendingResult = await thirdServer.listTasks(project.projectId, "pending_approval"); - expect(pendingResult.tasks!.length).toBe(1); - }); - it("should handle tool/rule recommendations end-to-end", async () => { const server = new TaskManager(testFilePath); @@ -590,4 +504,87 @@ describe('TaskManager Integration', () => { expect(newTask.ruleRecommendations).toBe("Review rule D"); } }); + + it("should handle auto-approval in end-to-end workflow", async () => { + // Create a project with autoApprove enabled + const project = await server.createProject( + "Auto-approval Project", + [ + { title: "Task 1", description: "First auto-approved task" }, + { title: "Task 2", description: "Second auto-approved task" } + ], + "Auto approval plan", + true // Enable auto-approval + ); + + // Mark tasks as done - they should be auto-approved + await server.updateTask(project.projectId, project.tasks[0].id, { + status: 'done', + completedDetails: 'Task 1 completed' + }); + + await server.updateTask(project.projectId, project.tasks[1].id, { + status: 'done', + completedDetails: 'Task 2 completed' + }); + + // Verify tasks are approved + const tasksResponse = await server.listTasks(project.projectId); + const tasks = tasksResponse.tasks as Task[]; + expect(tasks[0].approved).toBe(true); + expect(tasks[1].approved).toBe(true); + + // Project should be able to be completed without explicit task approval + const completionResult = await server.approveProjectCompletion(project.projectId); + expect(completionResult.status).toBe('project_approved_complete'); + + // Create a new server instance and verify persistence + const newServer = new TaskManager(testFilePath); + const projectState = await newServer.listProjects("completed"); + expect(projectState.projects.find(p => p.projectId === project.projectId)).toBeDefined(); + }); + + it("should handle multiple concurrent server instances", async () => { + // Create two server instances pointing to the same file + const server1 = new TaskManager(testFilePath); + const server2 = new TaskManager(testFilePath); + + // Create a project with server1 + const project = await server1.createProject( + "Concurrent Test Project", + [{ title: "Test Task", description: "Description" }] + ); + + // Update the task with server2 + await server2.updateTask(project.projectId, project.tasks[0].id, { + status: 'in progress' + }); + + // Verify the update with server1 + const taskDetails = await server1.openTaskDetails(project.tasks[0].id); + if (taskDetails.status === 'task_details' && taskDetails.task) { + expect(taskDetails.task.status).toBe('in progress'); + } else { + throw new Error('Expected task details'); + } + + // Complete and approve the task with server1 + await server1.updateTask(project.projectId, project.tasks[0].id, { + status: 'done', + completedDetails: 'Task completed' + }); + await server1.approveTaskCompletion(project.projectId, project.tasks[0].id); + + // Verify completion with server2 + const completedTasks = await server2.listTasks(project.projectId, "completed"); + expect(completedTasks.tasks!.length).toBe(1); + + // Complete the project with server2 + const completionResult = await server2.approveProjectCompletion(project.projectId); + expect(completionResult.status).toBe('project_approved_complete'); + + // Verify with server1 + const projectState = await server1.listProjects("completed"); + expect(projectState.projects.find(p => p.projectId === project.projectId)).toBeDefined(); + }); }); \ No newline at end of file diff --git a/tests/integration/mcp-client.test.ts b/tests/integration/mcp-client.test.ts index 7ad60c4..60c27ec 100644 --- a/tests/integration/mcp-client.test.ts +++ b/tests/integration/mcp-client.test.ts @@ -82,8 +82,6 @@ describe('MCP Client Integration', () => { if (transport) { transport.close(); console.log('Transport closed'); - // Give it a moment to clean up - await new Promise(resolve => setTimeout(resolve, 100)); } } catch (err) { console.error('Error closing transport:', err); @@ -206,4 +204,84 @@ describe('MCP Client Integration', () => { ); expect(response?.version).toBe(packageJson.version); }); + + it('should auto-approve tasks when autoApprove is enabled', async () => { + console.log('Testing autoApprove feature...'); + + // Create a project with autoApprove enabled + const createResult = await client.callTool({ + name: "create_project", + arguments: { + initialPrompt: "Auto-Approval Project", + tasks: [ + { title: "Auto Task", description: "This task should be auto-approved" } + ], + autoApprove: true + } + }) as ToolResponse; + expect(createResult.isError).toBeFalsy(); + + // Get the project ID + const responseData = JSON.parse((createResult.content[0] as { text: string }).text); + const projectId = responseData.projectId; + expect(projectId).toBeDefined(); + console.log('Created auto-approve project with ID:', projectId); + + // Get the task ID + const nextTaskResult = await client.callTool({ + name: "get_next_task", + arguments: { + projectId + } + }) as ToolResponse; + expect(nextTaskResult.isError).toBeFalsy(); + const nextTask = JSON.parse((nextTaskResult.content[0] as { text: string }).text); + expect(nextTask.status).toBe("next_task"); + const taskId = nextTask.task.id; + + // Mark task as done - we need to mark it as done using the update_task tool + const markDoneResult = await client.callTool({ + name: "update_task", + arguments: { + projectId, + taskId, + status: "done", + completedDetails: "Auto-approved task completed" + } + }) as ToolResponse; + expect(markDoneResult.isError).toBeFalsy(); + + // Now manually approve the task with approve_task + const approveResult = await client.callTool({ + name: "approve_task", + arguments: { + projectId, + taskId + } + }) as ToolResponse; + expect(approveResult.isError).toBeFalsy(); + + // Read the task and verify it was approved + const readTaskResult = await client.callTool({ + name: "read_task", + arguments: { + taskId + } + }) as ToolResponse; + expect(readTaskResult.isError).toBeFalsy(); + const taskDetails = JSON.parse((readTaskResult.content[0] as { text: string }).text); + expect(taskDetails.task.status).toBe("done"); + expect(taskDetails.task.approved).toBe(true); + console.log('Task was manually approved:', taskDetails.task.approved); + + // Verify we can finalize the project after explicit approval + const finalizeResult = await client.callTool({ + name: "finalize_project", + arguments: { + projectId + } + }) as ToolResponse; + expect(finalizeResult.isError).toBeFalsy(); + console.log('Project was successfully finalized after explicit task approval'); + }); }); \ No newline at end of file diff --git a/tests/unit/TaskManager.test.ts b/tests/unit/TaskManager.test.ts index e384701..afb22c9 100644 --- a/tests/unit/TaskManager.test.ts +++ b/tests/unit/TaskManager.test.ts @@ -2,7 +2,6 @@ import { describe, it, expect } from '@jest/globals'; import { ALL_TOOLS } from '../../src/server/tools.js'; import { VALID_STATUS_TRANSITIONS, Task } from '../../src/types/index.js'; import { TaskManager } from '../../src/server/TaskManager.js'; -import { mockTaskManagerData } from '../helpers/mocks.js'; import * as os from 'node:os'; import * as path from 'node:path'; import * as fs from 'node:fs/promises'; @@ -23,310 +22,191 @@ describe('TaskManager', () => { await fs.rm(tempDir, { recursive: true, force: true }); }); - describe('Tools Configuration', () => { - it('should have the required tools', () => { - expect(ALL_TOOLS.length).toBeGreaterThan(2); // Now we have many more tools - - const projectToolCount = ALL_TOOLS.filter(tool => - tool.name.includes('project') - ).length; - expect(projectToolCount).toBeGreaterThanOrEqual(5); + describe('Configuration and Constants', () => { + describe('Tools Configuration', () => { + it('should have the required tools', () => { + const toolNames = ALL_TOOLS.map(tool => tool.name); + expect(toolNames).toContain('list_projects'); + expect(toolNames).toContain('create_project'); + expect(toolNames).toContain('delete_project'); + expect(toolNames).toContain('add_tasks_to_project'); + expect(toolNames).toContain('finalize_project'); + expect(toolNames).toContain('read_project'); + + expect(toolNames).toContain('read_task'); + expect(toolNames).toContain('update_task'); + expect(toolNames).toContain('delete_task'); + expect(toolNames).toContain('approve_task'); + expect(toolNames).toContain('get_next_task'); + }); - const taskToolCount = ALL_TOOLS.filter(tool => - tool.name.includes('task') - ).length; - expect(taskToolCount).toBeGreaterThanOrEqual(5); - }); - - it('should have proper tool schemas', () => { - ALL_TOOLS.forEach(tool => { - expect(tool).toHaveProperty('name'); - expect(tool).toHaveProperty('description'); - expect(tool).toHaveProperty('inputSchema'); - expect(tool.inputSchema).toHaveProperty('type', 'object'); + it('should have proper tool schemas', () => { + ALL_TOOLS.forEach(tool => { + expect(tool).toHaveProperty('name'); + expect(tool).toHaveProperty('description'); + expect(tool).toHaveProperty('inputSchema'); + expect(tool.inputSchema).toHaveProperty('type', 'object'); + }); }); }); - }); - - describe('Status Transition Rules', () => { - it('should define valid transitions from not started status', () => { - expect(VALID_STATUS_TRANSITIONS['not started']).toEqual(['in progress']); - }); - it('should define valid transitions from in progress status', () => { - expect(VALID_STATUS_TRANSITIONS['in progress']).toContain('done'); - expect(VALID_STATUS_TRANSITIONS['in progress']).toContain('not started'); - expect(VALID_STATUS_TRANSITIONS['in progress'].length).toBe(2); - }); - - it('should define valid transitions from done status', () => { - expect(VALID_STATUS_TRANSITIONS['done']).toEqual(['in progress']); - }); - - it('should not allow direct transition from not started to done', () => { - const notStartedTransitions = VALID_STATUS_TRANSITIONS['not started']; - expect(notStartedTransitions).not.toContain('done'); + describe('Status Transition Rules', () => { + it('should define valid transitions from not started status', () => { + expect(VALID_STATUS_TRANSITIONS['not started']).toEqual(['in progress']); + }); + + it('should define valid transitions from in progress status', () => { + expect(VALID_STATUS_TRANSITIONS['in progress']).toContain('done'); + expect(VALID_STATUS_TRANSITIONS['in progress']).toContain('not started'); + expect(VALID_STATUS_TRANSITIONS['in progress'].length).toBe(2); + }); + + it('should define valid transitions from done status', () => { + expect(VALID_STATUS_TRANSITIONS['done']).toEqual(['in progress']); + }); + + it('should not allow direct transition from not started to done', () => { + const notStartedTransitions = VALID_STATUS_TRANSITIONS['not started']; + expect(notStartedTransitions).not.toContain('done'); + }); }); }); - it('should handle project creation', async () => { - const result = await server.createProject( - 'Test project', - [ - { - title: 'Test task', - description: 'Test description' - } - ], - 'Test plan' - ) as { status: string; projectId: string; totalTasks: number; tasks: any[]; message: string }; + describe('Basic Project Operations', () => { + it('should handle project creation', async () => { + const result = await server.createProject( + 'Test project', + [ + { + title: 'Test task', + description: 'Test description' + } + ], + 'Test plan' + ) as { status: string; projectId: string; totalTasks: number; tasks: any[]; message: string }; - expect(result.status).toBe('planned'); - expect(result.projectId).toBeDefined(); - expect(result.totalTasks).toBe(1); - }); + expect(result.status).toBe('planned'); + expect(result.projectId).toBeDefined(); + expect(result.totalTasks).toBe(1); + }); - it('should handle project listing', async () => { - // Create a project first - await server.createProject( - 'Test project', - [ - { - title: 'Test task', - description: 'Test description' - } - ], - 'Test plan' - ); + it('should handle project listing', async () => { + // Create a project first + await server.createProject( + 'Test project', + [ + { + title: 'Test task', + description: 'Test description' + } + ], + 'Test plan' + ); - const result = await server.listProjects() as { status: string; projects: any[]; message: string }; - expect(result.status).toBe('projects_listed'); - expect(result.projects).toHaveLength(1); - }); + const result = await server.listProjects() as { status: string; projects: any[]; message: string }; + expect(result.status).toBe('projects_listed'); + expect(result.projects).toHaveLength(1); + }); - it('should handle project deletion', async () => { - // Create a project first - const createResult = await server.createProject( - 'Test project', - [ - { - title: 'Test task', - description: 'Test description' - } - ], - 'Test plan' - ) as { status: string; projectId: string; totalTasks: number; tasks: any[]; message: string }; - - // Delete the project directly using data model access - const projectIndex = server["data"].projects.findIndex((p) => p.projectId === createResult.projectId); - server["data"].projects.splice(projectIndex, 1); - await server["saveTasks"](); - - // Verify deletion - const listResult = await server.listProjects() as { status: string; projects: any[]; message: string }; - expect(listResult.projects).toHaveLength(0); - }); + it('should handle project deletion', async () => { + // Create a project first + const createResult = await server.createProject( + 'Test project', + [ + { + title: 'Test task', + description: 'Test description' + } + ], + 'Test plan' + ) as { status: string; projectId: string; totalTasks: number; tasks: any[]; message: string }; - it('should handle task operations', async () => { - // Create a project first - const createResult = await server.createProject( - 'Test project', - [ - { - title: 'Test task', - description: 'Test description' - } - ], - 'Test plan' - ) as { status: string; projectId: string; totalTasks: number; tasks: { id: string }[]; message: string }; - - const projectId = createResult.projectId; - const taskId = createResult.tasks[0].id; - - // Test task reading - const readResult = await server.openTaskDetails(taskId); - expect(readResult.status).toBe('task_details'); - if (readResult.status === 'task_details' && readResult.task) { - expect(readResult.task.id).toBe(taskId); - } - - // Test task updating - const updatedTask = await server.updateTask(projectId, taskId, { - title: "Updated task", - description: "Updated description" - }); - expect(updatedTask.title).toBe("Updated task"); - expect(updatedTask.description).toBe("Updated description"); - expect(updatedTask.status).toBe("not started"); - - // Update status separately - const task = server["data"].projects.find(p => p.projectId === projectId)?.tasks.find(t => t.id === taskId); - if (task) { - task.status = 'in progress'; + // Delete the project directly using data model access + const projectIndex = server["data"].projects.findIndex((p) => p.projectId === createResult.projectId); + server["data"].projects.splice(projectIndex, 1); await server["saveTasks"](); - } - - // Test task deletion - const deleteResult = await server.deleteTask( - projectId, - taskId - ) as { status: string; message: string }; - expect(deleteResult.status).toBe('task_deleted'); + + // Verify deletion + const listResult = await server.listProjects() as { status: string; projects: any[]; message: string }; + expect(listResult.projects).toHaveLength(0); + }); }); - - it('should get the next task', async () => { - // Create a project with multiple tasks - const createResult = await server.createProject( - 'Test project with multiple tasks', - [ - { - title: 'Task 1', - description: 'Description 1' - }, - { - title: 'Task 2', - description: 'Description 2' - } - ] - ) as { - projectId: string; - tasks: { id: string }[]; - }; - const projectId = createResult.projectId; - - // Get the next task - const nextTaskResult = await server.getNextTask(projectId); - - expect(nextTaskResult.status).toBe('next_task'); - if (nextTaskResult.status === 'next_task' && nextTaskResult.task) { - expect(nextTaskResult.task.id).toBe(createResult.tasks[0].id); - } - }); - - it('should mark a task as done and approve it', async () => { - // Create a project with a task - const createResult = await server.createProject( - 'Test project for approval', - [ - { - title: 'Task to approve', - description: 'Description of task to approve' - } - ] - ) as { - projectId: string; - tasks: { id: string }[]; - }; - - const projectId = createResult.projectId; - const taskId = createResult.tasks[0].id; - - // Mark the task as done - const markDoneResult = await server.markTaskDone( - projectId, - taskId, - 'Completed task details' - ); - - expect(markDoneResult.status).toBe('task_marked_done'); - - // Check the task status from the data model - const project = server["data"].projects.find(p => p.projectId === projectId); - const task = project?.tasks.find(t => t.id === taskId); - expect(task?.status).toBe('done'); - - // Approve the task - const approveResult = await server.approveTaskCompletion(projectId, taskId); - - expect(approveResult.status).toBe('task_approved'); - if (approveResult.status === 'task_approved' && approveResult.task) { - expect(approveResult.task.approved).toBe(true); - } - }); - - describe('Conditional validation for completedDetails', () => { - let projectId: string; - let taskId: string; - - beforeEach(async () => { - // Create a project with a task for each test in this group + describe('Basic Task Operations', () => { + it('should handle task operations', async () => { + // Create a project first const createResult = await server.createProject( - 'Test project for completedDetails validation', + 'Test project', [ { - title: 'Task for validation', - description: 'Task used to test completedDetails validation' + title: 'Test task', + description: 'Test description' } - ] - ) as { - projectId: string; - tasks: { id: string }[]; - }; - - projectId = createResult.projectId; - taskId = createResult.tasks[0].id; - - // Set task to in_progress - const task = server["data"].projects.find(p => p.projectId === projectId)?.tasks.find(t => t.id === taskId); - if (task) { - task.status = 'in progress'; - await server["saveTasks"](); + ], + 'Test plan' + ) as { status: string; projectId: string; totalTasks: number; tasks: { id: string }[]; message: string }; + + const projectId = createResult.projectId; + const taskId = createResult.tasks[0].id; + + // Test task reading + const readResult = await server.openTaskDetails(taskId); + expect(readResult.status).toBe('task_details'); + if (readResult.status === 'task_details' && readResult.task) { + expect(readResult.task.id).toBe(taskId); } - }); - - it('should require completedDetails when marking task as done', async () => { - const result = await server.markTaskDone(projectId, taskId); - - // Even without completedDetails, markTaskDone should still work but set completedDetails to empty string - expect(result.status).toBe('task_marked_done'); - - const task = server["data"].projects.find(p => p.projectId === projectId)?.tasks.find(t => t.id === taskId); - expect(task?.completedDetails).toBe(''); - }); - - it('should save completedDetails when provided', async () => { - const details = 'These are the completed details'; - const result = await server.markTaskDone(projectId, taskId, details); - - expect(result.status).toBe('task_marked_done'); + + // Test task updating + const updatedTask = await server.updateTask(projectId, taskId, { + title: "Updated task", + description: "Updated description" + }); + expect(updatedTask.title).toBe("Updated task"); + expect(updatedTask.description).toBe("Updated description"); + expect(updatedTask.status).toBe("not started"); - const task = server["data"].projects.find(p => p.projectId === projectId)?.tasks.find(t => t.id === taskId); - expect(task?.completedDetails).toBe(details); + // Test status update + const updatedStatusTask = await server.updateTask(projectId, taskId, { + status: 'in progress' + }); + expect(updatedStatusTask.status).toBe('in progress'); + + // Test task deletion + const deleteResult = await server.deleteTask( + projectId, + taskId + ) as { status: string; message: string }; + expect(deleteResult.status).toBe('task_deleted'); }); - it('should validate status transitions', async () => { - // Create a new task that's in "not started" state - const newProjectResult = await server.createProject( - 'Project for status transition', + it('should get the next task', async () => { + // Create a project with multiple tasks + const createResult = await server.createProject( + 'Test project with multiple tasks', [ { - title: 'Task for status transition', - description: 'Testing that we cannot go directly from not started to done' + title: 'Task 1', + description: 'Description 1' + }, + { + title: 'Task 2', + description: 'Description 2' } ] - ); - - const newProjectId = newProjectResult.projectId; - const newTaskId = newProjectResult.tasks[0].id; + ) as { + projectId: string; + tasks: { id: string }[]; + }; + + const projectId = createResult.projectId; - // Attempt to mark as done directly (should work, but would ideally validate in the future) - await server.markTaskDone(newProjectId, newTaskId, 'Details'); + // Get the next task + const nextTaskResult = await server.getNextTask(projectId); - const task = server["data"].projects.find(p => p.projectId === newProjectId)?.tasks.find(t => t.id === newTaskId); - expect(task?.status).toBe('done'); - }); - - it('should handle invalid project and task IDs when marking task as done', async () => { - // Test with invalid project ID - const invalidProjectResult = await server.markTaskDone('invalid-project', taskId, 'Details'); - expect(invalidProjectResult.status).toBe('error'); - expect(invalidProjectResult.message).toBe('Project not found'); - - // Test with invalid task ID - const invalidTaskResult = await server.markTaskDone(projectId, 'invalid-task', 'Details'); - expect(invalidTaskResult.status).toBe('error'); - expect(invalidTaskResult.message).toBe('Task not found'); + expect(nextTaskResult.status).toBe('next_task'); + if (nextTaskResult.status === 'next_task' && nextTaskResult.task) { + expect(nextTaskResult.task.id).toBe(createResult.tasks[0].id); + } }); }); @@ -367,13 +247,14 @@ describe('TaskManager', () => { it('should not approve project if tasks are done but not approved', async () => { // Mark both tasks as done - const task1 = server["data"].projects.find(p => p.projectId === projectId)?.tasks.find(t => t.id === taskId1); - const task2 = server["data"].projects.find(p => p.projectId === projectId)?.tasks.find(t => t.id === taskId2); - if (task1 && task2) { - task1.status = 'done'; - task2.status = 'done'; - await server["saveTasks"](); - } + await server.updateTask(projectId, taskId1, { + status: 'done', + completedDetails: 'Task 1 completed details' + }); + await server.updateTask(projectId, taskId2, { + status: 'done', + completedDetails: 'Task 2 completed details' + }); const result = await server.approveProjectCompletion(projectId); expect(result.status).toBe('error'); @@ -382,15 +263,18 @@ describe('TaskManager', () => { it('should approve project when all tasks are done and approved', async () => { // Mark both tasks as done and approved - const task1 = server["data"].projects.find(p => p.projectId === projectId)?.tasks.find(t => t.id === taskId1); - const task2 = server["data"].projects.find(p => p.projectId === projectId)?.tasks.find(t => t.id === taskId2); - if (task1 && task2) { - task1.status = 'done'; - task2.status = 'done'; - task1.approved = true; - task2.approved = true; - await server["saveTasks"](); - } + await server.updateTask(projectId, taskId1, { + status: 'done', + completedDetails: 'Task 1 completed details' + }); + await server.updateTask(projectId, taskId2, { + status: 'done', + completedDetails: 'Task 2 completed details' + }); + + // Approve tasks + await server.approveTaskCompletion(projectId, taskId1); + await server.approveTaskCompletion(projectId, taskId2); const result = await server.approveProjectCompletion(projectId); expect(result.status).toBe('project_approved_complete'); @@ -402,15 +286,16 @@ describe('TaskManager', () => { it('should not allow approving an already completed project', async () => { // First approve the project - const task1 = server["data"].projects.find(p => p.projectId === projectId)?.tasks.find(t => t.id === taskId1); - const task2 = server["data"].projects.find(p => p.projectId === projectId)?.tasks.find(t => t.id === taskId2); - if (task1 && task2) { - task1.status = 'done'; - task2.status = 'done'; - task1.approved = true; - task2.approved = true; - await server["saveTasks"](); - } + await server.updateTask(projectId, taskId1, { + status: 'done', + completedDetails: 'Task 1 completed details' + }); + await server.updateTask(projectId, taskId2, { + status: 'done', + completedDetails: 'Task 2 completed details' + }); + await server.approveTaskCompletion(projectId, taskId1); + await server.approveTaskCompletion(projectId, taskId2); await server.approveProjectCompletion(projectId); @@ -421,268 +306,370 @@ describe('TaskManager', () => { }); }); - describe('listProjects', () => { - it('should list only open projects', async () => { - // Create some projects. One open and one complete - const project1 = await server.createProject("Open Project", [{ title: "Task 1", description: "Desc" }]); - const project2 = await server.createProject("Completed project", [{ title: "Task 2", description: "Desc" }]); - const proj1Id = project1.projectId; - const proj2Id = project2.projectId; - - // Complete tasks in project 2 - const proj2Task = server["data"].projects.find(p => p.projectId === proj2Id)?.tasks[0]; - if (proj2Task) { - proj2Task.status = "done"; - proj2Task.approved = true; - server["data"].projects.find(p => p.projectId === proj2Id)!.completed = true; - await server["saveTasks"](); - } - - const result = await server.listProjects("open"); - expect(result.projects.length).toBe(1); - expect(result.projects[0].projectId).toBe(proj1Id); - }); - - it('should list only pending approval projects', async () => { - // Create projects and tasks with varying statuses - const project1 = await server.createProject("Pending Approval Project", [{ title: "Task 1", description: "Desc" }]); - const project2 = await server.createProject("Completed project", [{ title: "Task 2", description: "Desc" }]); - const project3 = await server.createProject("In Progress Project", [{ title: "Task 3", description: "Desc" }]); - - // Mark task1 as done but not approved - const proj1Task = server["data"].projects.find(p => p.projectId === project1.projectId)?.tasks[0]; - if (proj1Task) { - proj1Task.status = "done"; - await server["saveTasks"](); - } + describe('Task and Project Filtering', () => { + describe('listProjects', () => { + it('should list only open projects', async () => { + // Create some projects. One open and one complete + const project1 = await server.createProject("Open Project", [{ title: "Task 1", description: "Desc" }]); + const project2 = await server.createProject("Completed project", [{ title: "Task 2", description: "Desc" }]); + const proj1Id = project1.projectId; + const proj2Id = project2.projectId; + + // Complete tasks in project 2 + await server.updateTask(proj2Id, project2.tasks[0].id, { + status: 'done', + completedDetails: 'Completed task details' + }); + await server.approveTaskCompletion(proj2Id, project2.tasks[0].id); + + // Approve project 2 + await server.approveProjectCompletion(proj2Id); + + const result = await server.listProjects("open"); + expect(result.projects.length).toBe(1); + expect(result.projects[0].projectId).toBe(proj1Id); + }); - // Complete project 2 fully - const proj2Task = server["data"].projects.find(p => p.projectId === project2.projectId)?.tasks[0]; - if (proj2Task) { - proj2Task.status = "done"; - proj2Task.approved = true; - server["data"].projects.find(p => p.projectId === project2.projectId)!.completed = true; - await server["saveTasks"](); - } + it('should list only pending approval projects', async () => { + // Create projects and tasks with varying statuses + const project1 = await server.createProject("Pending Approval Project", [{ title: "Task 1", description: "Desc" }]); + const project2 = await server.createProject("Completed project", [{ title: "Task 2", description: "Desc" }]); + const project3 = await server.createProject("In Progress Project", [{ title: "Task 3", description: "Desc" }]); + + // Mark task1 as done but not approved + await server.updateTask(project1.projectId, project1.tasks[0].id, { + status: 'done', + completedDetails: 'Completed task details' + }); + + // Complete project 2 fully + await server.updateTask(project2.projectId, project2.tasks[0].id, { + status: 'done', + completedDetails: 'Completed task details' + }); + await server.approveTaskCompletion(project2.projectId, project2.tasks[0].id); + await server.approveProjectCompletion(project2.projectId); + + const result = await server.listProjects("pending_approval"); + expect(result.projects.length).toBe(1); + expect(result.projects[0].projectId).toBe(project1.projectId); + }); - const result = await server.listProjects("pending_approval"); - expect(result.projects.length).toBe(1); - expect(result.projects[0].projectId).toBe(project1.projectId); - }); + it('should list only completed projects', async () => { + // Create projects with different states + const project1 = await server.createProject("Open Project", [{ title: "Task 1", description: "Desc" }]); + const project2 = await server.createProject("Completed project", [{ title: "Task 2", description: "Desc" }]); + const project3 = await server.createProject("Pending Project", [{ title: "Task 3", description: "Desc" }]); + + // Complete project 2 fully + await server.updateTask(project2.projectId, project2.tasks[0].id, { + status: 'done', + completedDetails: 'Completed task details' + }); + await server.approveTaskCompletion(project2.projectId, project2.tasks[0].id); + await server.approveProjectCompletion(project2.projectId); + + // Mark project 3's task as done but not approved + await server.updateTask(project3.projectId, project3.tasks[0].id, { + status: 'done', + completedDetails: 'Completed task details' + }); + + const result = await server.listProjects("completed"); + expect(result.projects.length).toBe(1); + expect(result.projects[0].projectId).toBe(project2.projectId); + }); - it('should list only completed projects', async () => { - // Create projects with different states - const project1 = await server.createProject("Open Project", [{ title: "Task 1", description: "Desc" }]); - const project2 = await server.createProject("Completed project", [{ title: "Task 2", description: "Desc" }]); - const project3 = await server.createProject("Pending Project", [{ title: "Task 3", description: "Desc" }]); - - // Complete project 2 fully - const proj2Task = server["data"].projects.find(p => p.projectId === project2.projectId)?.tasks[0]; - if (proj2Task) { - proj2Task.status = "done"; - proj2Task.approved = true; - server["data"].projects.find(p => p.projectId === project2.projectId)!.completed = true; - await server["saveTasks"](); - } + it('should list all projects when state is \'all\'', async () => { + // Create projects with different states + const project1 = await server.createProject("Open Project", [{ title: "Task 1", description: "Desc" }]); + const project2 = await server.createProject("Completed project", [{ title: "Task 2", description: "Desc" }]); + const project3 = await server.createProject("Pending Project", [{ title: "Task 3", description: "Desc" }]); - // Mark project 3's task as done but not approved - const proj3Task = server["data"].projects.find(p => p.projectId === project3.projectId)?.tasks[0]; - if (proj3Task) { - proj3Task.status = "done"; - await server["saveTasks"](); - } + const result = await server.listProjects("all"); + expect(result.projects.length).toBe(3); + }); - const result = await server.listProjects("completed"); - expect(result.projects.length).toBe(1); - expect(result.projects[0].projectId).toBe(project2.projectId); + it('should handle empty project list', async () => { + const result = await server.listProjects("open"); + expect(result.projects.length).toBe(0); + }); }); - it('should list all projects when state is \'all\'', async () => { - // Create projects with different states - const project1 = await server.createProject("Open Project", [{ title: "Task 1", description: "Desc" }]); - const project2 = await server.createProject("Completed project", [{ title: "Task 2", description: "Desc" }]); - const project3 = await server.createProject("Pending Project", [{ title: "Task 3", description: "Desc" }]); + describe('listTasks', () => { + it('should list tasks across all projects filtered by state', async () => { + // Create two projects with tasks in different states + const project1 = await server.createProject("Project 1", [ + { title: "Task 1", description: "Open task" }, + { title: "Task 2", description: "Done task" } + ]); + const project2 = await server.createProject("Project 2", [ + { title: "Task 3", description: "Pending approval task" } + ]); + + // Set task states + await server.updateTask(project1.projectId, project1.tasks[1].id, { + status: 'done', + completedDetails: 'Task 2 completed details' + }); + await server.approveTaskCompletion(project1.projectId, project1.tasks[1].id); + + await server.updateTask(project2.projectId, project2.tasks[0].id, { + status: 'done', + completedDetails: 'Task 3 completed details' + }); + + // Test open tasks + const openResult = await server.listTasks(undefined, "open"); + expect(openResult.tasks!.length).toBe(1); + expect(openResult.tasks![0].title).toBe("Task 1"); + + // Test pending approval tasks + const pendingResult = await server.listTasks(undefined, "pending_approval"); + expect(pendingResult.tasks!.length).toBe(1); + expect(pendingResult.tasks![0].title).toBe("Task 3"); + + // Test completed tasks + const completedResult = await server.listTasks(undefined, "completed"); + expect(completedResult.tasks!.length).toBe(1); + expect(completedResult.tasks![0].title).toBe("Task 2"); + }); - const result = await server.listProjects("all"); - expect(result.projects.length).toBe(3); - }); + it('should list tasks for specific project filtered by state', async () => { + // Create a project with tasks in different states + const project = await server.createProject("Test Project", [ + { title: "Task 1", description: "Open task" }, + { title: "Task 2", description: "Done and approved task" }, + { title: "Task 3", description: "Done but not approved task" } + ]); + + // Set task states + await server.updateTask(project.projectId, project.tasks[1].id, { + status: 'done', + completedDetails: 'Task 2 completed details' + }); + await server.approveTaskCompletion(project.projectId, project.tasks[1].id); + + await server.updateTask(project.projectId, project.tasks[2].id, { + status: 'done', + completedDetails: 'Task 3 completed details' + }); + + // Test open tasks + const openResult = await server.listTasks(project.projectId, "open"); + expect(openResult.tasks!.length).toBe(1); + expect(openResult.tasks![0].title).toBe("Task 1"); + + // Test pending approval tasks + const pendingResult = await server.listTasks(project.projectId, "pending_approval"); + expect(pendingResult.tasks!.length).toBe(1); + expect(pendingResult.tasks![0].title).toBe("Task 3"); + + // Test completed tasks + const completedResult = await server.listTasks(project.projectId, "completed"); + expect(completedResult.tasks!.length).toBe(1); + expect(completedResult.tasks![0].title).toBe("Task 2"); + }); + + it('should handle non-existent project ID', async () => { + const result = await server.listTasks("non-existent-project", "open"); + expect(result.status).toBe("error"); + expect(result.message).toBe("Project not found"); + }); - it('should handle empty project list', async () => { - const result = await server.listProjects("open"); - expect(result.projects.length).toBe(0); + it('should handle empty task list', async () => { + const project = await server.createProject("Empty Project", []); + const result = await server.listTasks(project.projectId, "open"); + expect(result.tasks!.length).toBe(0); + }); }); }); - describe('listTasks', () => { - it('should list tasks across all projects filtered by state', async () => { - // Create two projects with tasks in different states - const project1 = await server.createProject("Project 1", [ - { title: "Task 1", description: "Open task" }, - { title: "Task 2", description: "Done task" } - ]); - const project2 = await server.createProject("Project 2", [ - { title: "Task 3", description: "Pending approval task" } + describe('Task Recommendations', () => { + it("should handle tasks with tool and rule recommendations", async () => { + const { projectId } = await server.createProject("Test Project", [ + { + title: "Test Task", + description: "Test Description", + toolRecommendations: "Use tool X", + ruleRecommendations: "Review rule Y" + }, ]); - - // Set task states - const proj1Tasks = server["data"].projects.find(p => p.projectId === project1.projectId)?.tasks; - if (proj1Tasks) { - proj1Tasks[1].status = "done"; - proj1Tasks[1].approved = true; + const tasksResponse = await server.listTasks(projectId); + if (!('tasks' in tasksResponse) || !tasksResponse.tasks?.length) { + throw new Error('Expected tasks in response'); } + const tasks = tasksResponse.tasks as Task[]; + const taskId = tasks[0].id; - const proj2Tasks = server["data"].projects.find(p => p.projectId === project2.projectId)?.tasks; - if (proj2Tasks) { - proj2Tasks[0].status = "done"; - } + // Verify initial recommendations + expect(tasks[0].toolRecommendations).toBe("Use tool X"); + expect(tasks[0].ruleRecommendations).toBe("Review rule Y"); - await server["saveTasks"](); + // Update recommendations + const updatedTask = await server.updateTask(projectId, taskId, { + toolRecommendations: "Use tool Z", + ruleRecommendations: "Review rule W", + }); - // Test open tasks - const openResult = await server.listTasks(undefined, "open"); - expect(openResult.tasks!.length).toBe(1); - expect(openResult.tasks![0].title).toBe("Task 1"); + expect(updatedTask.toolRecommendations).toBe("Use tool Z"); + expect(updatedTask.ruleRecommendations).toBe("Review rule W"); - // Test pending approval tasks - const pendingResult = await server.listTasks(undefined, "pending_approval"); - expect(pendingResult.tasks!.length).toBe(1); - expect(pendingResult.tasks![0].title).toBe("Task 3"); + // Add new task with recommendations + await server.addTasksToProject(projectId, [ + { + title: "Added Task", + description: "With recommendations", + toolRecommendations: "Tool A", + ruleRecommendations: "Rule B" + } + ]); - // Test completed tasks - const completedResult = await server.listTasks(undefined, "completed"); - expect(completedResult.tasks!.length).toBe(1); - expect(completedResult.tasks![0].title).toBe("Task 2"); + const allTasksResponse = await server.listTasks(projectId); + if (!('tasks' in allTasksResponse) || !allTasksResponse.tasks?.length) { + throw new Error('Expected tasks in response'); + } + const allTasks = allTasksResponse.tasks as Task[]; + const newTask = allTasks.find(t => t.title === "Added Task"); + expect(newTask).toBeDefined(); + if (newTask) { + expect(newTask.toolRecommendations).toBe("Tool A"); + expect(newTask.ruleRecommendations).toBe("Rule B"); + } }); - it('should list tasks for specific project filtered by state', async () => { - // Create a project with tasks in different states - const project = await server.createProject("Test Project", [ - { title: "Task 1", description: "Open task" }, - { title: "Task 2", description: "Done and approved task" }, - { title: "Task 3", description: "Done but not approved task" } + it("should allow tasks with no recommendations", async () => { + const { projectId } = await server.createProject("Test Project", [ + { title: "Test Task", description: "Test Description" }, ]); - - // Set task states - const tasks = server["data"].projects.find(p => p.projectId === project.projectId)?.tasks; - if (tasks) { - tasks[1].status = "done"; - tasks[1].approved = true; - tasks[2].status = "done"; + const tasksResponse = await server.listTasks(projectId); + if (!('tasks' in tasksResponse) || !tasksResponse.tasks?.length) { + throw new Error('Expected tasks in response'); } + const tasks = tasksResponse.tasks as Task[]; + const taskId = tasks[0].id; - await server["saveTasks"](); - - // Test open tasks - const openResult = await server.listTasks(project.projectId, "open"); - expect(openResult.tasks!.length).toBe(1); - expect(openResult.tasks![0].title).toBe("Task 1"); + // Verify no recommendations + expect(tasks[0].toolRecommendations).toBeUndefined(); + expect(tasks[0].ruleRecommendations).toBeUndefined(); - // Test pending approval tasks - const pendingResult = await server.listTasks(project.projectId, "pending_approval"); - expect(pendingResult.tasks!.length).toBe(1); - expect(pendingResult.tasks![0].title).toBe("Task 3"); + // Add task without recommendations + await server.addTasksToProject(projectId, [ + { title: "Added Task", description: "No recommendations" } + ]); - // Test completed tasks - const completedResult = await server.listTasks(project.projectId, "completed"); - expect(completedResult.tasks!.length).toBe(1); - expect(completedResult.tasks![0].title).toBe("Task 2"); + const allTasksResponse = await server.listTasks(projectId); + if (!('tasks' in allTasksResponse) || !allTasksResponse.tasks?.length) { + throw new Error('Expected tasks in response'); + } + const allTasks = allTasksResponse.tasks as Task[]; + const newTask = allTasks.find(t => t.title === "Added Task"); + expect(newTask).toBeDefined(); + if (newTask) { + expect(newTask.toolRecommendations).toBeUndefined(); + expect(newTask.ruleRecommendations).toBeUndefined(); + } }); + }); - it('should handle non-existent project ID', async () => { - const result = await server.listTasks("non-existent-project", "open"); - expect(result.status).toBe("error"); - expect(result.message).toBe("Project not found"); + describe('Auto-approval of tasks', () => { + it('should auto-approve tasks when updating status to done and autoApprove is enabled', async () => { + // Create a project with autoApprove enabled + const createResult = await server.createProject( + 'Auto-approval for updateTask', + [ + { + title: 'Task to update', + description: 'This task should be auto-approved when status is updated to done' + } + ], + 'Test plan', + true // autoApprove parameter + ) as { + projectId: string; + tasks: { id: string }[]; + }; + + const projectId = createResult.projectId; + const taskId = createResult.tasks[0].id; + + // Update the task status to done + const updatedTask = await server.updateTask(projectId, taskId, { + status: 'done', + completedDetails: 'Task completed via updateTask' + }); + + // The task should be automatically approved + expect(updatedTask.status).toBe('done'); + expect(updatedTask.approved).toBe(true); + + // Verify that we can complete the project without explicitly approving the task + const approveResult = await server.approveProjectCompletion(projectId); + expect(approveResult.status).toBe('project_approved_complete'); }); - - it('should handle empty task list', async () => { - const project = await server.createProject("Empty Project", []); - const result = await server.listTasks(project.projectId, "open"); - expect(result.tasks!.length).toBe(0); + + it('should not auto-approve tasks when updating status to done and autoApprove is disabled', async () => { + // Create a project with autoApprove disabled + const createResult = await server.createProject( + 'Manual-approval for updateTask', + [ + { + title: 'Task to update manually', + description: 'This task should not be auto-approved when status is updated to done' + } + ], + 'Test plan', + false // autoApprove parameter + ) as { + projectId: string; + tasks: { id: string }[]; + }; + + const projectId = createResult.projectId; + const taskId = createResult.tasks[0].id; + + // Update the task status to done + const updatedTask = await server.updateTask(projectId, taskId, { + status: 'done', + completedDetails: 'Task completed via updateTask' + }); + + // The task should not be automatically approved + expect(updatedTask.status).toBe('done'); + expect(updatedTask.approved).toBe(false); + + // Verify that we cannot complete the project without explicitly approving the task + const approveResult = await server.approveProjectCompletion(projectId); + expect(approveResult.status).toBe('error'); }); - }); - - it("should handle tasks with tool and rule recommendations", async () => { - const { projectId } = await server.createProject("Test Project", [ - { - title: "Test Task", - description: "Test Description", - toolRecommendations: "Use tool X", - ruleRecommendations: "Review rule Y" - }, - ]); - const tasksResponse = await server.listTasks(projectId); - if (!('tasks' in tasksResponse) || !tasksResponse.tasks?.length) { - throw new Error('Expected tasks in response'); - } - const tasks = tasksResponse.tasks as Task[]; - const taskId = tasks[0].id; - - // Verify initial recommendations - expect(tasks[0].toolRecommendations).toBe("Use tool X"); - expect(tasks[0].ruleRecommendations).toBe("Review rule Y"); - - // Update recommendations - const updatedTask = await server.updateTask(projectId, taskId, { - toolRecommendations: "Use tool Z", - ruleRecommendations: "Review rule W", + + it('should make autoApprove false by default if not specified', async () => { + // Create a project without specifying autoApprove + const createResult = await server.createProject( + 'Default-approval Project', + [ + { + title: 'Default-approved task', + description: 'This task should follow the default approval behavior' + } + ] + ) as { + projectId: string; + tasks: { id: string }[]; + }; + + const projectId = createResult.projectId; + const taskId = createResult.tasks[0].id; + + // Update the task status to done + const updatedTask = await server.updateTask(projectId, taskId, { + status: 'done', + completedDetails: 'Task completed via updateTask' + }); + + // The task should not be automatically approved by default + expect(updatedTask.status).toBe('done'); + expect(updatedTask.approved).toBe(false); }); - - expect(updatedTask.toolRecommendations).toBe("Use tool Z"); - expect(updatedTask.ruleRecommendations).toBe("Review rule W"); - - // Add new task with recommendations - await server.addTasksToProject(projectId, [ - { - title: "Added Task", - description: "With recommendations", - toolRecommendations: "Tool A", - ruleRecommendations: "Rule B" - } - ]); - - const allTasksResponse = await server.listTasks(projectId); - if (!('tasks' in allTasksResponse) || !allTasksResponse.tasks?.length) { - throw new Error('Expected tasks in response'); - } - const allTasks = allTasksResponse.tasks as Task[]; - const newTask = allTasks.find(t => t.title === "Added Task"); - expect(newTask).toBeDefined(); - if (newTask) { - expect(newTask.toolRecommendations).toBe("Tool A"); - expect(newTask.ruleRecommendations).toBe("Rule B"); - } - }); - - it("should allow tasks with no recommendations", async () => { - const { projectId } = await server.createProject("Test Project", [ - { title: "Test Task", description: "Test Description" }, - ]); - const tasksResponse = await server.listTasks(projectId); - if (!('tasks' in tasksResponse) || !tasksResponse.tasks?.length) { - throw new Error('Expected tasks in response'); - } - const tasks = tasksResponse.tasks as Task[]; - const taskId = tasks[0].id; - - // Verify no recommendations - expect(tasks[0].toolRecommendations).toBeUndefined(); - expect(tasks[0].ruleRecommendations).toBeUndefined(); - - // Add task without recommendations - await server.addTasksToProject(projectId, [ - { title: "Added Task", description: "No recommendations" } - ]); - - const allTasksResponse = await server.listTasks(projectId); - if (!('tasks' in allTasksResponse) || !allTasksResponse.tasks?.length) { - throw new Error('Expected tasks in response'); - } - const allTasks = allTasksResponse.tasks as Task[]; - const newTask = allTasks.find(t => t.title === "Added Task"); - expect(newTask).toBeDefined(); - if (newTask) { - expect(newTask.toolRecommendations).toBeUndefined(); - expect(newTask.ruleRecommendations).toBeUndefined(); - } }); }); \ No newline at end of file From da1ae977a8dd5e06af50ebd7ce8d0f006e496086 Mon Sep 17 00:00:00 2001 From: "Christopher C. Smith" Date: Sun, 23 Mar 2025 16:51:52 -0400 Subject: [PATCH 2/2] Version bump --- index.ts | 2 +- package-lock.json | 4 +- package.json | 2 +- tests/integration/TaskManagertest.ts | 179 +-------------------------- 4 files changed, 6 insertions(+), 181 deletions(-) diff --git a/index.ts b/index.ts index 4abed8e..8332761 100644 --- a/index.ts +++ b/index.ts @@ -10,7 +10,7 @@ import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprot const server = new Server( { name: "task-manager-server", - version: "1.0.7" + version: "1.0.8" }, { capabilities: { diff --git a/package-lock.json b/package-lock.json index 76f49d5..23ed788 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "taskqueue-mcp", - "version": "1.0.7", + "version": "1.0.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "taskqueue-mcp", - "version": "1.0.7", + "version": "1.0.8", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.7.0", diff --git a/package.json b/package.json index 78388e4..2ecf958 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "taskqueue-mcp", - "version": "1.0.7", + "version": "1.0.8", "description": "Task Queue MCP Server", "author": "Christopher C. Smith (christopher.smith@promptlytechnologies.com)", "main": "dist/index.js", diff --git a/tests/integration/TaskManagertest.ts b/tests/integration/TaskManagertest.ts index 4a0c633..cadf16b 100644 --- a/tests/integration/TaskManagertest.ts +++ b/tests/integration/TaskManagertest.ts @@ -1,4 +1,3 @@ -import { ALL_TOOLS } from '../../src/server/tools.js'; import { TaskManager } from '../../src/server/TaskManager.js'; import * as os from 'node:os'; import * as path from 'node:path'; @@ -29,181 +28,6 @@ describe('TaskManager Integration', () => { } }); - it('should handle project tool actions', async () => { - // Test project creation - const createResult = await server.createProject( - 'Test project', - [ - { - title: 'Test task', - description: 'Test description' - } - ], - 'Test plan' - ) as { - status: string; - projectId: string; - totalTasks: number; - tasks: { id: string; title: string; description: string }[]; - message: string - }; - - expect(createResult.status).toBe('planned'); - expect(createResult.projectId).toBeDefined(); - expect(createResult.totalTasks).toBe(1); - - // Test project listing - const listResult = await server.listProjects() as { - status: string; - message: string; - projects: { projectId: string; initialPrompt: string; totalTasks: number; completedTasks: number; approvedTasks: number }[]; - }; - expect(listResult.status).toBe('projects_listed'); - expect(listResult.projects).toHaveLength(1); - - // Test project deletion - const projectId = createResult.projectId; - const projectIndex = server["data"].projects.findIndex((p) => p.projectId === projectId); - server["data"].projects.splice(projectIndex, 1); - await server["saveTasks"](); - - // Verify deletion - const listAfterDelete = await server.listProjects() as { - status: string; - message: string; - projects: { projectId: string; initialPrompt: string; totalTasks: number; completedTasks: number; approvedTasks: number }[]; - }; - expect(listAfterDelete.projects).toHaveLength(0); - }); - - it('should handle task tool actions', async () => { - // Create a project first - const createResult = await server.createProject( - 'Test project', - [ - { - title: 'Test task', - description: 'Test description' - } - ], - 'Test plan' - ) as { - status: string; - projectId: string; - totalTasks: number; - tasks: { id: string; title: string; description: string }[]; - message: string - }; - - const projectId = createResult.projectId; - const taskId = createResult.tasks[0].id; - - // Test task reading - const readResult = await server.openTaskDetails(taskId); - expect(readResult.status).toBe('task_details'); - if (readResult.status === 'task_details' && readResult.task) { - expect(readResult.task.id).toBe(taskId); - } - - // Test task updating - const updatedTask = await server.updateTask(projectId, taskId, { - title: "Updated task", - description: "Updated description" - }); - expect(updatedTask.title).toBe("Updated task"); - expect(updatedTask.description).toBe("Updated description"); - expect(updatedTask.status).toBe("not started"); - - // Also update the status directly - const task = server["data"].projects.find(p => p.projectId === projectId)?.tasks.find(t => t.id === taskId); - if (task) { - task.status = 'in progress'; - await server["saveTasks"](); - } - - // Test task deletion - const deleteResult = await server.deleteTask( - projectId, - taskId - ) as { - status: string; - message: string; - }; - expect(deleteResult.status).toBe('task_deleted'); - - // Verify deletion - const readAfterDelete = await server.openTaskDetails(taskId) as { - status: string; - message?: string; - }; - expect(readAfterDelete.status).toBe('task_not_found'); - }); - - it('should get the next task in a project', async () => { - // Create a project with multiple tasks - const createResult = await server.createProject( - 'Test project with multiple tasks', - [ - { - title: 'Task 1', - description: 'Description 1' - }, - { - title: 'Task 2', - description: 'Description 2' - } - ] - ) as { - projectId: string; - tasks: { id: string }[]; - }; - - const projectId = createResult.projectId; - - // Get the next task - const nextTaskResult = await server.getNextTask(projectId); - - expect(nextTaskResult.status).toBe('next_task'); - if (nextTaskResult.status === 'next_task' && nextTaskResult.task) { - expect(nextTaskResult.task.id).toBe(createResult.tasks[0].id); - } - }); - - it('should approve a completed task', async () => { - // Create a project with a task - const createResult = await server.createProject( - 'Test project for approval', - [ - { - title: 'Task to approve', - description: 'Description of task to approve' - } - ] - ) as { - projectId: string; - tasks: { id: string }[]; - }; - - const projectId = createResult.projectId; - const taskId = createResult.tasks[0].id; - - // Mark the task as done - const task = server["data"].projects.find(p => p.projectId === projectId)?.tasks.find(t => t.id === taskId); - if (task) { - task.status = 'done'; - task.completedDetails = 'Completed task details'; - await server["saveTasks"](); - } - - // Approve the task - const approveResult = await server.approveTaskCompletion(projectId, taskId); - - expect(approveResult.status).toBe('task_approved'); - if (approveResult.status === 'task_approved' && approveResult.task) { - expect(approveResult.task.approved).toBe(true); - } - }); - it('should handle file persistence correctly', async () => { // Create initial data const project = await server.createProject("Persistent Project", [ @@ -587,4 +411,5 @@ describe('TaskManager Integration', () => { const projectState = await server1.listProjects("completed"); expect(projectState.projects.find(p => p.projectId === project.projectId)).toBeDefined(); }); -}); \ No newline at end of file +}); +