diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 9b4bb86..e633d4c 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -13,12 +13,14 @@ jobs: node-version: 'latest' - run: npm ci - run: npm install -g tsx - - run: npm run build # Add build step if needed - run: npm test publish: needs: test if: github.ref == 'refs/heads/main' # Only run this job on main branch + permissions: + packages: write + contents: write runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/index.ts b/index.ts index c9918e6..5c095ac 100644 --- a/index.ts +++ b/index.ts @@ -3,14 +3,14 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { TaskManager } from "./src/server/TaskManager.js"; -import { ALL_TOOLS } from "./src/server/tools.js"; +import { ALL_TOOLS, executeToolWithErrorHandling } from "./src/server/tools.js"; import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js"; // Create server with capabilities BEFORE setting up handlers const server = new Server( { name: "task-manager-server", - version: "1.0.9" + version: "1.1.0" }, { capabilities: { @@ -28,7 +28,7 @@ console.error('Server starting with env:', { NODE_ENV: process.env.NODE_ENV }); -// Initialize task manager +// Create task manager instance const taskManager = new TaskManager(); // Set up request handlers AFTER capabilities are configured @@ -39,210 +39,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { }); server.setRequestHandler(CallToolRequestSchema, async (request) => { - try { - const { name } = request.params; - const args = request.params.arguments || {}; - - // For validation, ensure args is an object when expected - if (name !== "list_projects" && name !== "list_tasks" && Object.keys(args).length === 0) { - throw new Error("Invalid arguments: expected object with parameters"); - } - - switch (name) { - // Project tools - case "list_projects": { - const result = await taskManager.listProjects(); - return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }], - }; - } - - case "read_project": { - const projectId = String(args.projectId); - if (!projectId) { - throw new Error("Missing required parameter: projectId"); - } - const result = await taskManager.getNextTask(projectId); - return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }], - }; - } - - case "create_project": { - const initialPrompt = String(args.initialPrompt || ""); - if (!initialPrompt || !args.tasks || !Array.isArray(args.tasks)) { - throw new Error("Missing required parameters: initialPrompt and/or tasks"); - } - const projectPlan = args.projectPlan ? String(args.projectPlan) : undefined; - - const result = await taskManager.createProject( - initialPrompt, - args.tasks, - projectPlan - ); - return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }], - }; - } - - case "delete_project": { - const projectId = String(args.projectId); - if (!projectId) { - throw new Error("Missing required parameter: projectId"); - } - // Use the private data and saveTasks via indexing since there's no explicit delete method - const projectIndex = taskManager["data"].projects.findIndex((p) => p.projectId === projectId); - if (projectIndex === -1) { - return { - content: [{ type: "text", text: JSON.stringify({ status: "error", message: "Project not found" }, null, 2) }], - }; - } - - taskManager["data"].projects.splice(projectIndex, 1); - await taskManager["saveTasks"](); - return { - content: [{ type: "text", text: JSON.stringify({ - status: "project_deleted", - message: `Project ${projectId} has been deleted.` - }, null, 2) }], - }; - } - - case "add_tasks_to_project": { - const projectId = String(args.projectId); - if (!projectId || !args.tasks || !Array.isArray(args.tasks)) { - throw new Error("Missing required parameters: projectId and/or tasks"); - } - const result = await taskManager.addTasksToProject(projectId, args.tasks); - return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }], - }; - } - - case "finalize_project": { - const projectId = String(args.projectId); - if (!projectId) { - throw new Error("Missing required parameter: projectId"); - } - const result = await taskManager.approveProjectCompletion(projectId); - return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }], - }; - } - - // Task tools - case "list_tasks": { - // No explicit list tasks method, so return a message - return { - content: [{ type: "text", text: JSON.stringify({ - status: "error", - message: "list_tasks functionality to be implemented in future version" - }, null, 2) }], - }; - } - - case "read_task": { - const taskId = String(args.taskId); - if (!taskId) { - throw new Error("Missing required parameter: taskId"); - } - const result = await taskManager.openTaskDetails(taskId); - return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }], - }; - } - - case "create_task": { - const projectId = String(args.projectId); - const title = String(args.title || ""); - const description = String(args.description || ""); - - if (!projectId || !title || !description) { - throw new Error("Missing required parameters: projectId, title, and/or description"); - } - - const result = await taskManager.addTasksToProject(projectId, [{ - title, - description - }]); - return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }], - }; - } - - case "update_task": { - const projectId = String(args.projectId); - const taskId = String(args.taskId); - - if (!projectId || !taskId) { - throw new Error("Missing required parameters: projectId and/or taskId"); - } - - 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); - - return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }], - }; - } - - case "delete_task": { - const projectId = String(args.projectId); - const taskId = String(args.taskId); - - if (!projectId || !taskId) { - throw new Error("Missing required parameters: projectId and/or taskId"); - } - const result = await taskManager.deleteTask(projectId, taskId); - return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }], - }; - } - - case "approve_task": { - const projectId = String(args.projectId); - const taskId = String(args.taskId); - - if (!projectId || !taskId) { - throw new Error("Missing required parameters: projectId and/or taskId"); - } - const result = await taskManager.approveTaskCompletion(projectId, taskId); - return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }], - }; - } - - case "get_next_task": { - const projectId = String(args.projectId); - if (!projectId) { - throw new Error("Missing required parameter: projectId"); - } - const result = await taskManager.getNextTask(projectId); - return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }], - }; - } - - default: - throw new Error(`Unknown tool: ${name}`); - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - return { - content: [{ type: "text", text: `Error: ${errorMessage}` }], - isError: true, - }; - } + return executeToolWithErrorHandling( + request.params.name, + request.params.arguments || {}, + taskManager + ); }); // Start the server diff --git a/package-lock.json b/package-lock.json index a99e8bd..b73956c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "taskqueue-mcp", - "version": "1.0.9", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "taskqueue-mcp", - "version": "1.0.9", + "version": "1.1.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.7.0", diff --git a/package.json b/package.json index c95ed31..44eb5cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "taskqueue-mcp", - "version": "1.0.9", + "version": "1.1.0", "description": "Task Queue MCP Server", "author": "Christopher C. Smith (christopher.smith@promptlytechnologies.com)", "main": "dist/index.js", diff --git a/src/client/cli.ts b/src/client/cli.ts index 72b08ad..a1d688a 100644 --- a/src/client/cli.ts +++ b/src/client/cli.ts @@ -4,8 +4,10 @@ import { Command } from "commander"; import * as fs from "node:fs/promises"; import * as path from "node:path"; import * as os from "node:os"; -import { TaskManagerFile } from "../types/index.js"; import chalk from "chalk"; +import { TaskManagerFile, ErrorCode } from "../types/index.js"; +import { createError, normalizeError } from "../utils/errors.js"; +import { formatCliError } from "./errors.js"; const program = new Command(); const DEFAULT_PATH = path.join(os.homedir(), "Documents", "tasks.json"); @@ -30,11 +32,21 @@ async function readData(): Promise { try { return JSON.parse(data); } catch (error) { - throw new Error(`Failed to parse JSON data: ${error instanceof Error ? error.message : String(error)}`); + throw createError( + ErrorCode.FileParseError, + "Failed to parse task file", + { originalError: error } + ); } } catch (error) { - console.error(chalk.red(`Error reading task file: ${error instanceof Error ? error.message : String(error)}`)); - return { projects: [] }; + if (error instanceof Error && error.message.includes("ENOENT")) { + return { projects: [] }; + } + throw createError( + ErrorCode.FileReadError, + "Failed to read task file", + { originalError: error } + ); } } @@ -54,8 +66,11 @@ async function writeData(data: TaskManagerFile): Promise { await fs.writeFile(TASK_FILE_PATH, JSON.stringify(data, null, 2), "utf-8"); console.log(chalk.green('Data saved successfully')); } catch (error) { - console.error(chalk.red(`Error writing to task file: ${error instanceof Error ? error.message : String(error)}`)); - throw error; + throw createError( + ErrorCode.FileWriteError, + "Failed to write task file", + { originalError: error } + ); } } @@ -65,10 +80,10 @@ program .version("1.0.0"); program - .command("approve-task") + .command("approve") .description("Approve a completed task") - .argument("", "ID of the project containing the task") - .argument("", "ID of the task to approve") + .argument("", "Project ID") + .argument("", "Task ID") .option('-f, --force', 'Force approval even if task is not marked as done') .action(async (projectId, taskId, options) => { try { @@ -145,15 +160,15 @@ program console.log(chalk.yellow(`${completedTasks - approvedTasks} tasks remaining to be approved.`)); } } catch (error) { - console.error(chalk.red(`An error occurred: ${error instanceof Error ? error.message : String(error)}`)); + console.error(chalk.red(formatCliError(normalizeError(error)))); process.exit(1); } }); program - .command("approve-project") - .description("Approve project completion") - .argument("", "ID of the project to approve") + .command("finalize") + .description("Mark a project as complete") + .argument("", "Project ID") .action(async (projectId) => { try { console.log(chalk.blue(`Approving project ${chalk.bold(projectId)}...`)); @@ -231,7 +246,7 @@ program console.log(chalk.blue(` task-manager-cli list -p ${projectId}`)); } catch (error) { - console.error(chalk.red(`An error occurred: ${error instanceof Error ? error.message : String(error)}`)); + console.error(chalk.red(formatCliError(normalizeError(error)))); process.exit(1); } }); @@ -366,7 +381,7 @@ program }); } } catch (error) { - console.error(chalk.red(`An error occurred: ${error instanceof Error ? error.message : String(error)}`)); + console.error(chalk.red(formatCliError(normalizeError(error)))); process.exit(1); } }); diff --git a/src/client/errors.ts b/src/client/errors.ts new file mode 100644 index 0000000..553409c --- /dev/null +++ b/src/client/errors.ts @@ -0,0 +1,8 @@ +import { StandardError } from "../types/index.js"; +/** + * Formats an error message for CLI output + */ +export function formatCliError(error: StandardError): string { + const details = error.details ? `: ${JSON.stringify(error.details)}` : ''; + return `[${error.code}] ${error.message}${details}`; + } \ No newline at end of file diff --git a/src/server/TaskManager.ts b/src/server/TaskManager.ts index 9b16339..5e3a6b1 100644 --- a/src/server/TaskManager.ts +++ b/src/server/TaskManager.ts @@ -1,7 +1,8 @@ import * as fs from "node:fs/promises"; import * as path from "node:path"; import * as os from "node:os"; -import { Task, TaskManagerFile, TaskState } from "../types/index.js"; +import { Task, TaskManagerFile, TaskState, StandardResponse, ErrorCode } from "../types/index.js"; +import { createError, createSuccessResponse } from "../utils/errors.js"; // Get platform-appropriate app data directory const getAppDataDir = () => { @@ -62,7 +63,11 @@ export class TaskManager { allProjectIds.length > 0 ? Math.max(...allProjectIds) : 0; this.taskCounter = allTaskIds.length > 0 ? Math.max(...allTaskIds) : 0; } catch (error) { + // Initialize with empty data for any initialization error + // This includes file not found, permission issues, invalid JSON, etc. this.data = { projects: [] }; + this.projectCounter = 0; + this.taskCounter = 0; } } @@ -83,10 +88,17 @@ export class TaskManager { ); } catch (error) { if (error instanceof Error && error.message.includes("EROFS")) { - console.error("EROFS: read-only file system. Cannot save tasks."); - throw error; + throw createError( + ErrorCode.ReadOnlyFileSystem, + "Cannot save tasks: read-only file system", + { originalError: error } + ); } - throw error; + throw createError( + ErrorCode.FileWriteError, + "Failed to save tasks file", + { originalError: error } + ); } } @@ -164,8 +176,7 @@ export class TaskManager { const progressTable = this.formatTaskProgressTable(projectId); - return { - status: "planned", + return createSuccessResponse({ projectId, totalTasks: newTasks.length, tasks: newTasks.map((t) => ({ @@ -174,20 +185,23 @@ export class TaskManager { description: t.description, })), message: `Tasks have been successfully added. Please use the task tool with 'read' action to retrieve tasks.\n${progressTable}`, - }; + }); } - public async getNextTask(projectId: string) { + public async getNextTask(projectId: string): Promise { await this.ensureInitialized(); const proj = this.data.projects.find((p) => p.projectId === projectId); if (!proj) { - return { status: "error", message: "Project not found" }; + throw createError( + ErrorCode.ProjectNotFound, + `Project ${projectId} not found` + ); } if (proj.completed) { - return { - status: "already_completed", - message: "Project already completed.", - }; + throw createError( + ErrorCode.ProjectAlreadyCompleted, + "Project is already completed" + ); } const nextTask = proj.tasks.find((t) => t.status !== "done"); if (!nextTask) { @@ -197,38 +211,58 @@ export class TaskManager { const progressTable = this.formatTaskProgressTable(projectId); return { status: "all_tasks_done", - message: `All tasks have been completed. Awaiting project completion approval.\n${progressTable}`, + data: { + message: `All tasks have been completed. Awaiting project completion approval.\n${progressTable}` + } }; } - return { status: "no_next_task", message: "No undone tasks found." }; + throw createError( + ErrorCode.TaskNotFound, + "No undone tasks found" + ); } const progressTable = this.formatTaskProgressTable(projectId); return { status: "next_task", - task: { + data: { id: nextTask.id, title: nextTask.title, description: nextTask.description, - }, - message: `Next task is ready. Task approval will be required after completion.\n${progressTable}`, + message: `Next task is ready. Task approval will be required after completion.\n${progressTable}` + } }; } public async approveTaskCompletion(projectId: string, taskId: string) { await this.ensureInitialized(); const proj = this.data.projects.find((p) => p.projectId === projectId); - if (!proj) return { status: "error", message: "Project not found" }; + if (!proj) { + throw createError( + ErrorCode.ProjectNotFound, + `Project ${projectId} 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: "error", message: "Task not done yet." }; - if (task.approved) - return { status: "already_approved", message: "Task already approved." }; + if (!task) { + throw createError( + ErrorCode.TaskNotFound, + `Task ${taskId} not found` + ); + } + if (task.status !== "done") { + throw createError( + ErrorCode.TaskNotDone, + "Task not done yet" + ); + } + if (task.approved) { + return createSuccessResponse({ message: "Task already approved." }); + } task.approved = true; await this.saveTasks(); - return { - status: "task_approved", + return createSuccessResponse({ projectId: proj.projectId, task: { id: task.id, @@ -237,36 +271,49 @@ export class TaskManager { completedDetails: task.completedDetails, approved: task.approved, }, - }; + }); } public async approveProjectCompletion(projectId: string) { await this.ensureInitialized(); const proj = this.data.projects.find((p) => p.projectId === projectId); - if (!proj) return { status: "error", message: "Project not found" }; + if (!proj) { + throw createError( + ErrorCode.ProjectNotFound, + `Project ${projectId} not found` + ); + } // Check if project is already completed if (proj.completed) { - return { status: "error", message: "Project is already completed." }; + throw createError( + ErrorCode.ProjectAlreadyCompleted, + "Project is already completed" + ); } // Check if all tasks are done and approved const allDone = proj.tasks.every((t) => t.status === "done"); if (!allDone) { - return { status: "error", message: "Not all tasks are done." }; + throw createError( + ErrorCode.TasksNotAllDone, + "Not all tasks are done" + ); } const allApproved = proj.tasks.every((t) => t.status === "done" && t.approved); if (!allApproved) { - return { status: "error", message: "Not all done tasks are approved." }; + throw createError( + ErrorCode.TasksNotAllApproved, + "Not all done tasks are approved" + ); } proj.completed = true; await this.saveTasks(); - return { - status: "project_approved_complete", + return createSuccessResponse({ projectId: proj.projectId, message: "Project is fully completed and approved.", - }; + }); } public async openTaskDetails(taskId: string) { @@ -274,8 +321,7 @@ export class TaskManager { for (const proj of this.data.projects) { const target = proj.tasks.find((t) => t.id === taskId); if (target) { - return { - status: "task_details", + return createSuccessResponse({ projectId: proj.projectId, initialPrompt: proj.initialPrompt, projectPlan: proj.projectPlan, @@ -288,10 +334,13 @@ export class TaskManager { approved: target.approved, completedDetails: target.completedDetails, }, - }; + }); } } - return { status: "task_not_found", message: "No such task found" }; + throw createError( + ErrorCode.TaskNotFound, + `Task ${taskId} not found` + ); } public async listProjects(state?: TaskState) { @@ -315,8 +364,7 @@ export class TaskManager { } const projectsList = this.formatProjectsList(); - return { - status: "projects_listed", + return createSuccessResponse({ message: `Current projects in the system:\n${projectsList}`, projects: filteredProjects.map((proj) => ({ projectId: proj.projectId, @@ -325,7 +373,7 @@ export class TaskManager { completedTasks: proj.tasks.filter((task) => task.status === "done").length, approvedTasks: proj.tasks.filter((task) => task.approved).length, })), - }; + }); } public async listTasks(projectId?: string, state?: TaskState) { @@ -335,10 +383,10 @@ export class TaskManager { if (projectId) { const project = this.data.projects.find((p) => p.projectId === projectId); if (!project) { - return { - status: "error", - message: "Project not found" - }; + throw createError( + ErrorCode.ProjectNotFound, + `Project ${projectId} not found` + ); } } @@ -363,8 +411,7 @@ export class TaskManager { }); } - return { - status: "tasks_listed", + return createSuccessResponse({ message: `Tasks in the system${projectId ? ` for project ${projectId}` : ""}:\n${tasks.length} tasks found.`, tasks: tasks.map(task => ({ id: task.id, @@ -376,7 +423,7 @@ export class TaskManager { toolRecommendations: task.toolRecommendations, ruleRecommendations: task.ruleRecommendations })) - }; + }); } public async addTasksToProject( @@ -386,7 +433,10 @@ export class TaskManager { await this.ensureInitialized(); const proj = this.data.projects.find((p) => p.projectId === projectId); if (!proj) { - return { status: "error", message: "Project not found" }; + throw createError( + ErrorCode.ProjectNotFound, + `Project ${projectId} not found` + ); } const newTasks: Task[] = []; @@ -408,15 +458,14 @@ export class TaskManager { await this.saveTasks(); const progressTable = this.formatTaskProgressTable(projectId); - return { - status: "tasks_added", + return createSuccessResponse({ message: `Added ${newTasks.length} new tasks to project.\n${progressTable}`, newTasks: newTasks.map((t) => ({ id: t.id, title: t.title, description: t.description, })), - }; + }); } public async updateTask( @@ -434,12 +483,18 @@ export class TaskManager { await this.ensureInitialized(); const project = this.data.projects.find((p) => p.projectId === projectId); if (!project) { - throw new Error(`Project not found: ${projectId}`); + throw createError( + ErrorCode.ProjectNotFound, + `Project ${projectId} not found` + ); } const taskIndex = project.tasks.findIndex((t) => t.id === taskId); if (taskIndex === -1) { - throw new Error(`Task not found: ${taskId}`); + throw createError( + ErrorCode.TaskNotFound, + `Task ${taskId} not found` + ); } // Update the task with the provided updates @@ -451,26 +506,63 @@ export class TaskManager { } await this.saveTasks(); - return project.tasks[taskIndex]; + return createSuccessResponse(project.tasks[taskIndex]); } public async deleteTask(projectId: string, taskId: string) { await this.ensureInitialized(); const proj = this.data.projects.find((p) => p.projectId === projectId); - if (!proj) return { status: "error", message: "Project not found" }; + if (!proj) { + throw createError( + ErrorCode.ProjectNotFound, + `Project ${projectId} not found` + ); + } const taskIndex = proj.tasks.findIndex((t) => t.id === taskId); - if (taskIndex === -1) return { status: "error", message: "Task not found" }; - if (proj.tasks[taskIndex].status === "done") - return { status: "error", message: "Cannot delete completed task" }; + if (taskIndex === -1) { + throw createError( + ErrorCode.TaskNotFound, + `Task ${taskId} not found` + ); + } + if (proj.tasks[taskIndex].status === "done") { + throw createError( + ErrorCode.CannotDeleteCompletedTask, + "Cannot delete completed task" + ); + } proj.tasks.splice(taskIndex, 1); await this.saveTasks(); const progressTable = this.formatTaskProgressTable(projectId); - return { - status: "task_deleted", + return createSuccessResponse({ message: `Task ${taskId} has been deleted.\n${progressTable}`, - }; + }); + } + + public async readProject(projectId: string): Promise> { + await this.ensureInitialized(); + const project = this.data.projects.find(p => p.projectId === projectId); + if (!project) { + throw createError( + ErrorCode.ProjectNotFound, + `Project ${projectId} not found` + ); + } + return createSuccessResponse({ + projectId: project.projectId, + initialPrompt: project.initialPrompt, + projectPlan: project.projectPlan, + completed: project.completed, + tasks: project.tasks + }); } } \ No newline at end of file diff --git a/src/server/tools.ts b/src/server/tools.ts index b509b41..4453b4f 100644 --- a/src/server/tools.ts +++ b/src/server/tools.ts @@ -1,4 +1,7 @@ import { Tool } from "@modelcontextprotocol/sdk/types.js"; +import { TaskManager } from "./TaskManager.js"; +import { ErrorCode } from "../types/index.js"; +import { createError, normalizeError } from "../utils/errors.js"; // ---------------------- PROJECT TOOLS ---------------------- @@ -332,7 +335,7 @@ const getNextTaskTool: Tool = { }; // Export all tools as an array -export const ALL_TOOLS = [ +export const ALL_TOOLS: Tool[] = [ listProjectsTool, readProjectTool, createProjectTool, @@ -347,4 +350,259 @@ export const ALL_TOOLS = [ deleteTaskTool, approveTaskTool, getNextTaskTool, -]; \ No newline at end of file +]; + +// Error handling wrapper for tool execution +export async function executeToolWithErrorHandling( + toolName: string, + args: Record, + taskManager: TaskManager +): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> { + try { + switch (toolName) { + case "list_projects": { + const result = await taskManager.listProjects(); + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + }; + } + + case "read_project": { + const projectId = String(args.projectId); + if (!projectId) { + throw createError( + ErrorCode.MissingParameter, + "Missing required parameter: projectId" + ); + } + const result = await taskManager.readProject(projectId); + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + }; + } + + case "create_project": { + const initialPrompt = String(args.initialPrompt || ""); + if (!initialPrompt || !args.tasks || !Array.isArray(args.tasks)) { + throw createError( + ErrorCode.MissingParameter, + "Missing required parameters: initialPrompt and/or tasks" + ); + } + const projectPlan = args.projectPlan ? String(args.projectPlan) : undefined; + const autoApprove = args.autoApprove === true; + + const result = await taskManager.createProject( + initialPrompt, + args.tasks, + projectPlan, + autoApprove + ); + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + }; + } + + case "delete_project": { + const projectId = String(args.projectId); + if (!projectId) { + throw createError( + ErrorCode.MissingParameter, + "Missing required parameter: projectId" + ); + } + // Use the private data and saveTasks via indexing since there's no explicit delete method + const projectIndex = taskManager["data"].projects.findIndex((p) => p.projectId === projectId); + if (projectIndex === -1) { + return { + content: [{ type: "text", text: JSON.stringify({ status: "error", message: "Project not found" }, null, 2) }], + }; + } + + taskManager["data"].projects.splice(projectIndex, 1); + await taskManager["saveTasks"](); + return { + content: [{ type: "text", text: JSON.stringify({ + status: "project_deleted", + message: `Project ${projectId} has been deleted.` + }, null, 2) }], + }; + } + + case "add_tasks_to_project": { + const projectId = String(args.projectId); + if (!projectId || !args.tasks || !Array.isArray(args.tasks)) { + throw createError( + ErrorCode.MissingParameter, + "Missing required parameters: projectId and/or tasks" + ); + } + const result = await taskManager.addTasksToProject(projectId, args.tasks); + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + }; + } + + case "finalize_project": { + const projectId = String(args.projectId); + if (!projectId) { + throw createError( + ErrorCode.MissingParameter, + "Missing required parameter: projectId" + ); + } + const result = await taskManager.approveProjectCompletion(projectId); + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + }; + } + + // Task tools + case "list_tasks": { + const projectId = args.projectId ? String(args.projectId) : undefined; + const state = args.state ? String(args.state as string) : undefined; + const result = await taskManager.listTasks(projectId, state as any); + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + }; + } + + case "read_task": { + const taskId = String(args.taskId); + if (!taskId) { + throw createError( + ErrorCode.MissingParameter, + "Missing required parameter: taskId" + ); + } + const result = await taskManager.openTaskDetails(taskId); + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + }; + } + + case "create_task": { + const projectId = String(args.projectId); + const title = String(args.title || ""); + const description = String(args.description || ""); + + if (!projectId || !title || !description) { + throw createError( + ErrorCode.MissingParameter, + "Missing required parameters: projectId, title, and/or description" + ); + } + + const result = await taskManager.addTasksToProject(projectId, [{ + title, + description, + toolRecommendations: args.toolRecommendations ? String(args.toolRecommendations) : undefined, + ruleRecommendations: args.ruleRecommendations ? String(args.ruleRecommendations) : undefined + }]); + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + }; + } + + case "update_task": { + const projectId = String(args.projectId); + const taskId = String(args.taskId); + + if (!projectId || !taskId) { + throw createError( + ErrorCode.MissingParameter, + "Missing required parameters: projectId and/or taskId" + ); + } + + 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); + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + }; + } + + case "delete_task": { + const projectId = String(args.projectId); + const taskId = String(args.taskId); + + if (!projectId || !taskId) { + throw createError( + ErrorCode.MissingParameter, + "Missing required parameters: projectId and/or taskId" + ); + } + const result = await taskManager.deleteTask(projectId, taskId); + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + }; + } + + case "approve_task": { + const projectId = String(args.projectId); + const taskId = String(args.taskId); + + if (!projectId || !taskId) { + throw createError( + ErrorCode.MissingParameter, + "Missing required parameters: projectId and/or taskId" + ); + } + const result = await taskManager.approveTaskCompletion(projectId, taskId); + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + }; + } + + case "get_next_task": { + const projectId = String(args.projectId); + if (!projectId) { + throw createError( + ErrorCode.MissingParameter, + "Missing required parameter: projectId" + ); + } + const result = await taskManager.getNextTask(projectId); + + // Ensure backward compatibility with integration tests + // by adding a task property that refers to the data + if (result.status === "next_task" && result.data) { + return { + content: [{ type: "text", text: JSON.stringify({ + status: "next_task", + task: result.data, + message: result.data.message + }, null, 2) }], + }; + } + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + }; + } + + default: + throw createError( + ErrorCode.InvalidArgument, + `Unknown tool: ${toolName}` + ); + } + } catch (error) { + const standardError = normalizeError(error); + return { + content: [{ type: "text", text: `Error: ${standardError.message}` }], + isError: true, + }; + } +} \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 20fca41..a23917e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -154,3 +154,96 @@ export const TaskToolSchema = z.object({ tool: z.literal("task"), params: TaskActionSchema }); + +// Error Types +export enum ErrorCategory { + Validation = 'VALIDATION', + ResourceNotFound = 'RESOURCE_NOT_FOUND', + StateTransition = 'STATE_TRANSITION', + FileSystem = 'FILE_SYSTEM', + TestAssertion = 'TEST_ASSERTION', + Unknown = 'UNKNOWN' +} + +export enum ErrorCode { + // Validation Errors (1000-1999) + MissingParameter = 'ERR_1000', + InvalidState = 'ERR_1001', + InvalidArgument = 'ERR_1002', + + // Resource Not Found Errors (2000-2999) + ProjectNotFound = 'ERR_2000', + TaskNotFound = 'ERR_2001', + EmptyTaskFile = 'ERR_2002', + + // State Transition Errors (3000-3999) + TaskNotDone = 'ERR_3000', + ProjectAlreadyCompleted = 'ERR_3001', + CannotDeleteCompletedTask = 'ERR_3002', + TasksNotAllDone = 'ERR_3003', + TasksNotAllApproved = 'ERR_3004', + + // File System Errors (4000-4999) + FileReadError = 'ERR_4000', + FileWriteError = 'ERR_4001', + FileParseError = 'ERR_4002', + ReadOnlyFileSystem = 'ERR_4003', + + // Test Assertion Errors (5000-5999) + MissingExpectedData = 'ERR_5000', + InvalidResponseFormat = 'ERR_5001', + + // Unknown Error (9999) + Unknown = 'ERR_9999' +} + +export interface StandardError { + status: "error"; + code: ErrorCode; + category: ErrorCategory; + message: string; + details?: unknown; +} + +// Generic success response +export interface SuccessResponse { + status: "success"; + data: T; + message?: string; +} + +// Error response +export interface ErrorResponse { + status: "error"; + error: { + code: ErrorCode; + message: string; + details?: unknown; + }; +} + +// Next task response +export interface NextTaskResponse { + status: "next_task"; + data: { + id: string; + title: string; + description: string; + message?: string; + }; +} + +// All tasks done response +export interface AllTasksDoneResponse { + status: "all_tasks_done"; + data: { + message: string; + }; +} + +// Combined union type for all response types +export type StandardResponse = + | SuccessResponse + | ErrorResponse + | NextTaskResponse + | AllTasksDoneResponse; diff --git a/src/utils/errors.ts b/src/utils/errors.ts new file mode 100644 index 0000000..93904a9 --- /dev/null +++ b/src/utils/errors.ts @@ -0,0 +1,72 @@ +import { ErrorCategory, ErrorCode, StandardError, SuccessResponse } from '../types/index.js'; + +/** + * Creates a standardized error object + */ +export function createError( + code: ErrorCode, + message: string, + details?: unknown +): StandardError { + const category = getCategoryFromCode(code); + return { + status: "error", + code, + category, + message, + details + }; +} + +/** + * Creates a standardized success response + */ +export function createSuccessResponse(data: T): SuccessResponse { + return { + status: "success", + data + }; +} + +/** + * Gets the error category from an error code + */ +function getCategoryFromCode(code: ErrorCode): ErrorCategory { + const codeNum = parseInt(code.split('_')[1]); + if (codeNum >= 1000 && codeNum < 2000) return ErrorCategory.Validation; + if (codeNum >= 2000 && codeNum < 3000) return ErrorCategory.ResourceNotFound; + if (codeNum >= 3000 && codeNum < 4000) return ErrorCategory.StateTransition; + if (codeNum >= 4000 && codeNum < 5000) return ErrorCategory.FileSystem; + if (codeNum >= 5000 && codeNum < 6000) return ErrorCategory.TestAssertion; + return ErrorCategory.Unknown; +} + +/** + * Converts any error to a StandardError + */ +export function normalizeError(error: unknown): StandardError { + if (error instanceof Error) { + // Try to parse error code from message if it exists + const codeMatch = error.message.match(/\[([A-Z_]+)\]/); + if (codeMatch && Object.values(ErrorCode).includes(codeMatch[1] as ErrorCode)) { + return createError( + codeMatch[1] as ErrorCode, + error.message.replace(`[${codeMatch[1]}]`, '').trim(), + { stack: error.stack } + ); + } + + // Default to unknown error + return createError( + ErrorCode.InvalidArgument, + error.message, + { stack: error.stack } + ); + } + + return createError( + ErrorCode.Unknown, + typeof error === 'string' ? error : 'An unknown error occurred', + { originalError: error } + ); +} \ No newline at end of file diff --git a/tests/integration/TaskManagertest.ts b/tests/integration/TaskManagertest.ts index cadf16b..330af98 100644 --- a/tests/integration/TaskManagertest.ts +++ b/tests/integration/TaskManagertest.ts @@ -39,13 +39,14 @@ describe('TaskManager Integration', () => { // 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); + expect(result.status).toBe("success"); + expect(result.data.projects.length).toBe(1); + expect(result.data.projects[0].projectId).toBe(project.data.projectId); // Modify task state in new server await newServer.updateTask( - project.projectId, - project.tasks[0].id, + project.data.projectId, + project.data.tasks[0].id, { status: "done", completedDetails: "Completed task details" @@ -54,8 +55,9 @@ describe('TaskManager Integration', () => { // 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); + const pendingResult = await thirdServer.listTasks(project.data.projectId, "pending_approval"); + expect(pendingResult.status).toBe("success"); + expect(pendingResult.data.tasks!.length).toBe(1); }); it('should execute a complete project workflow', async () => { @@ -75,19 +77,19 @@ describe('TaskManager Integration', () => { 'Detailed plan for complete workflow' ); - expect(createResult.status).toBe('planned'); - expect(createResult.projectId).toBeDefined(); - expect(createResult.totalTasks).toBe(2); + expect(createResult.status).toBe('success'); + expect(createResult.data.projectId).toBeDefined(); + expect(createResult.data.totalTasks).toBe(2); - const projectId = createResult.projectId; - const taskId1 = createResult.tasks[0].id; - const taskId2 = createResult.tasks[1].id; + const projectId = createResult.data.projectId; + const taskId1 = createResult.data.tasks[0].id; + const taskId2 = createResult.data.tasks[1].id; // 2. Get the next task (first 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(taskId1); + if (nextTaskResult.status === 'next_task' && nextTaskResult.data) { + expect(nextTaskResult.data.id).toBe(taskId1); } // 3. Mark the first task as in progress @@ -100,17 +102,17 @@ describe('TaskManager Integration', () => { status: 'done', completedDetails: 'Task 1 completed details' }); - expect(markDoneResult.status).toBe('done'); + expect(markDoneResult.status).toBe('success'); // 5. Approve the first task const approveResult = await server.approveTaskCompletion(projectId, taskId1); - expect(approveResult.status).toBe('task_approved'); + expect(approveResult.status).toBe('success'); // 6. Get the next task (second task) const nextTaskResult2 = await server.getNextTask(projectId); expect(nextTaskResult2.status).toBe('next_task'); - if (nextTaskResult2.status === 'next_task' && nextTaskResult2.task) { - expect(nextTaskResult2.task.id).toBe(taskId2); + if (nextTaskResult2.status === 'next_task' && nextTaskResult2.data) { + expect(nextTaskResult2.data.id).toBe(taskId2); } // 7. Mark the second task as in progress @@ -123,24 +125,28 @@ describe('TaskManager Integration', () => { status: 'done', completedDetails: 'Task 2 completed details' }); - expect(markDoneResult2.status).toBe('done'); + expect(markDoneResult2.status).toBe('success'); // 9. Approve the second task const approveResult2 = await server.approveTaskCompletion(projectId, taskId2); - expect(approveResult2.status).toBe('task_approved'); + expect(approveResult2.status).toBe('success'); // 10. Now all tasks should be done, check with getNextTask const allDoneResult = await server.getNextTask(projectId); expect(allDoneResult.status).toBe('all_tasks_done'); + if (allDoneResult.status === 'all_tasks_done') { + expect(allDoneResult.data.message).toContain('All tasks have been completed'); + } // 11. Finalize the project const finalizeResult = await server.approveProjectCompletion(projectId); - expect(finalizeResult.status).toBe('project_approved_complete'); + expect(finalizeResult.status).toBe('success'); // 12. Verify the project is marked as completed const projectState = await server.listProjects("completed"); - expect(projectState.projects.length).toBe(1); - expect(projectState.projects[0].projectId).toBe(projectId); + expect(projectState.status).toBe('success'); + expect(projectState.data.projects.length).toBe(1); + expect(projectState.data.projects[0].projectId).toBe(projectId); }); it('should handle project approval workflow', async () => { @@ -157,28 +163,22 @@ describe('TaskManager Integration', () => { description: 'Description of task 2' } ] - ) as { - projectId: string; - tasks: { id: string }[]; - }; + ); - const projectId = createResult.projectId; - const taskId1 = createResult.tasks[0].id; - const taskId2 = createResult.tasks[1].id; + expect(createResult.status).toBe('success'); + const projectId = createResult.data.projectId; + const taskId1 = createResult.data.tasks[0].id; + const taskId2 = createResult.data.tasks[1].id; // 2. Try to approve project before tasks are done (should fail) - const earlyApprovalResult = await server.approveProjectCompletion(projectId); - expect(earlyApprovalResult.status).toBe('error'); - expect(earlyApprovalResult.message).toContain('Not all tasks are done'); + await expect(server.approveProjectCompletion(projectId)).rejects.toThrow('Not all tasks are done'); // 3. Mark tasks as done 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); - expect(preApprovalResult.status).toBe('error'); - expect(preApprovalResult.message).toContain('Not all done tasks are approved'); + await expect(server.approveProjectCompletion(projectId)).rejects.toThrow('Not all done tasks are approved'); // 5. Approve tasks await server.approveTaskCompletion(projectId, taskId1); @@ -186,17 +186,16 @@ describe('TaskManager Integration', () => { // 6. Now approve the project (should succeed) const approvalResult = await server.approveProjectCompletion(projectId); - expect(approvalResult.status).toBe('project_approved_complete'); + expect(approvalResult.status).toBe('success'); // 7. Verify project state const projectAfterApproval = await server.listProjects("completed"); - const completedProject = projectAfterApproval.projects.find(p => p.projectId === projectId); + expect(projectAfterApproval.status).toBe('success'); + const completedProject = projectAfterApproval.data.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'); + await expect(server.approveProjectCompletion(projectId)).rejects.toThrow('Project is already completed'); }); it("should handle complex project and task state transitions", async () => { @@ -207,46 +206,54 @@ describe('TaskManager Integration', () => { { title: "Task 3", description: "Third task" } ]); + expect(project.status).toBe('success'); + // Initially all tasks should be open - const initialOpenTasks = await server.listTasks(project.projectId, "open"); - expect(initialOpenTasks.tasks!.length).toBe(3); + const initialOpenTasks = await server.listTasks(project.data.projectId, "open"); + expect(initialOpenTasks.status).toBe('success'); + expect(initialOpenTasks.data.tasks!.length).toBe(3); // Mark first task as done and approved - await server.updateTask(project.projectId, project.tasks[0].id, { + await server.updateTask(project.data.projectId, project.data.tasks[0].id, { status: 'done', completedDetails: 'Task 1 completed' }); - await server.approveTaskCompletion(project.projectId, project.tasks[0].id); + await server.approveTaskCompletion(project.data.projectId, project.data.tasks[0].id); // Should now have 2 open tasks and 1 completed - const openTasks = await server.listTasks(project.projectId, "open"); - expect(openTasks.tasks!.length).toBe(2); + const openTasks = await server.listTasks(project.data.projectId, "open"); + expect(openTasks.status).toBe('success'); + expect(openTasks.data.tasks!.length).toBe(2); - const completedTasks = await server.listTasks(project.projectId, "completed"); - expect(completedTasks.tasks!.length).toBe(1); + const completedTasks = await server.listTasks(project.data.projectId, "completed"); + expect(completedTasks.status).toBe('success'); + expect(completedTasks.data.tasks!.length).toBe(1); // Mark second task as done but not approved - await server.updateTask(project.projectId, project.tasks[1].id, { + await server.updateTask(project.data.projectId, project.data.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"); - expect(finalOpenTasks.tasks!.length).toBe(1); + const finalOpenTasks = await server.listTasks(project.data.projectId, "open"); + expect(finalOpenTasks.status).toBe('success'); + expect(finalOpenTasks.data.tasks!.length).toBe(1); - const pendingTasks = await server.listTasks(project.projectId, "pending_approval"); - expect(pendingTasks.tasks!.length).toBe(1); + const pendingTasks = await server.listTasks(project.data.projectId, "pending_approval"); + expect(pendingTasks.status).toBe('success'); + expect(pendingTasks.data.tasks!.length).toBe(1); - const finalCompletedTasks = await server.listTasks(project.projectId, "completed"); - expect(finalCompletedTasks.tasks!.length).toBe(1); + const finalCompletedTasks = await server.listTasks(project.data.projectId, "completed"); + expect(finalCompletedTasks.status).toBe('success'); + expect(finalCompletedTasks.data.tasks!.length).toBe(1); }); it("should handle tool/rule recommendations end-to-end", async () => { const server = new TaskManager(testFilePath); // Create a project with tasks that have recommendations - const { projectId } = await server.createProject("Test Project", [ + const response = await server.createProject("Test Project", [ { title: "Task with Recommendations", description: "Test Description", @@ -259,12 +266,13 @@ describe('TaskManager Integration', () => { } ]); + expect(response.status).toBe('success'); + const { projectId } = response.data; + // Verify initial state 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[]; + expect(tasksResponse.status).toBe('success'); + const tasks = tasksResponse.data.tasks as Task[]; const taskWithRecs = tasks.find(t => t.title === "Task with Recommendations"); const taskWithoutRecs = tasks.find(t => t.title === "Task without Recommendations"); @@ -284,20 +292,19 @@ describe('TaskManager Integration', () => { // Update task recommendations if (taskWithoutRecs) { - const updatedTask = await server.updateTask(projectId, taskWithoutRecs.id, { + const updateResponse = await server.updateTask(projectId, taskWithoutRecs.id, { toolRecommendations: "Use tool X", ruleRecommendations: "Review rule Y" }); - expect(updatedTask.toolRecommendations).toBe("Use tool X"); - expect(updatedTask.ruleRecommendations).toBe("Review rule Y"); + expect(updateResponse.status).toBe('success'); + expect(updateResponse.data.toolRecommendations).toBe("Use tool X"); + expect(updateResponse.data.ruleRecommendations).toBe("Review rule Y"); // Verify the update persisted const updatedTasksResponse = await server.listTasks(projectId); - if (!('tasks' in updatedTasksResponse) || !updatedTasksResponse.tasks?.length) { - throw new Error('Expected tasks in response'); - } - const updatedTasks = updatedTasksResponse.tasks as Task[]; + expect(updatedTasksResponse.status).toBe('success'); + const updatedTasks = updatedTasksResponse.data.tasks as Task[]; const verifyTask = updatedTasks.find(t => t.id === taskWithoutRecs.id); expect(verifyTask).toBeDefined(); if (verifyTask) { @@ -307,7 +314,7 @@ describe('TaskManager Integration', () => { } // Add new tasks with recommendations - await server.addTasksToProject(projectId, [ + const addResponse = await server.addTasksToProject(projectId, [ { title: "New Task", description: "With recommendations", @@ -316,11 +323,11 @@ describe('TaskManager Integration', () => { } ]); + expect(addResponse.status).toBe('success'); + const finalTasksResponse = await server.listTasks(projectId); - if (!('tasks' in finalTasksResponse) || !finalTasksResponse.tasks?.length) { - throw new Error('Expected tasks in response'); - } - const finalTasks = finalTasksResponse.tasks as Task[]; + expect(finalTasksResponse.status).toBe('success'); + const finalTasks = finalTasksResponse.data.tasks as Task[]; const newTask = finalTasks.find(t => t.title === "New Task"); expect(newTask).toBeDefined(); if (newTask) { @@ -331,7 +338,7 @@ describe('TaskManager Integration', () => { it("should handle auto-approval in end-to-end workflow", async () => { // Create a project with autoApprove enabled - const project = await server.createProject( + const projectResponse = await server.createProject( "Auto-approval Project", [ { title: "Task 1", description: "First auto-approved task" }, @@ -341,6 +348,9 @@ describe('TaskManager Integration', () => { true // Enable auto-approval ); + expect(projectResponse.status).toBe('success'); + const project = projectResponse.data; + // Mark tasks as done - they should be auto-approved await server.updateTask(project.projectId, project.tasks[0].id, { status: 'done', @@ -354,18 +364,20 @@ describe('TaskManager Integration', () => { // Verify tasks are approved const tasksResponse = await server.listTasks(project.projectId); - const tasks = tasksResponse.tasks as Task[]; + expect(tasksResponse.status).toBe('success'); + const tasks = tasksResponse.data.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'); + expect(completionResult.status).toBe('success'); // 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(); + expect(projectState.status).toBe('success'); + expect(projectState.data.projects.find(p => p.projectId === project.projectId)).toBeDefined(); }); it("should handle multiple concurrent server instances", async () => { @@ -374,11 +386,14 @@ describe('TaskManager Integration', () => { const server2 = new TaskManager(testFilePath); // Create a project with server1 - const project = await server1.createProject( + const projectResponse = await server1.createProject( "Concurrent Test Project", [{ title: "Test Task", description: "Description" }] ); + expect(projectResponse.status).toBe('success'); + const project = projectResponse.data; + // Update the task with server2 await server2.updateTask(project.projectId, project.tasks[0].id, { status: 'in progress' @@ -386,11 +401,8 @@ describe('TaskManager Integration', () => { // 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'); - } + expect(taskDetails.status).toBe('success'); + expect(taskDetails.data.task.status).toBe('in progress'); // Complete and approve the task with server1 await server1.updateTask(project.projectId, project.tasks[0].id, { @@ -401,15 +413,17 @@ describe('TaskManager Integration', () => { // Verify completion with server2 const completedTasks = await server2.listTasks(project.projectId, "completed"); - expect(completedTasks.tasks!.length).toBe(1); + expect(completedTasks.status).toBe('success'); + expect(completedTasks.data.tasks!.length).toBe(1); // Complete the project with server2 const completionResult = await server2.approveProjectCompletion(project.projectId); - expect(completionResult.status).toBe('project_approved_complete'); + expect(completionResult.status).toBe('success'); // Verify with server1 const projectState = await server1.listProjects("completed"); - expect(projectState.projects.find(p => p.projectId === project.projectId)).toBeDefined(); + expect(projectState.status).toBe('success'); + expect(projectState.data.projects.find(p => p.projectId === project.projectId)).toBeDefined(); }); }); diff --git a/tests/integration/mcp-client.test.ts b/tests/integration/mcp-client.test.ts index 60c27ec..bc163dd 100644 --- a/tests/integration/mcp-client.test.ts +++ b/tests/integration/mcp-client.test.ts @@ -130,7 +130,7 @@ describe('MCP Client Integration', () => { // Parse the project ID from the response const responseData = JSON.parse((createResult.content[0] as { text: string }).text); - const projectId = responseData.projectId; + const projectId = responseData.data.projectId; expect(projectId).toBeDefined(); console.log('Created project with ID:', projectId); @@ -141,7 +141,7 @@ describe('MCP Client Integration', () => { }) as ToolResponse; expect(listResult.isError).toBeFalsy(); const projects = JSON.parse((listResult.content[0] as { text: string }).text); - expect(projects.projects.some((p: any) => p.projectId === projectId)).toBe(true); + expect(projects.data.projects.some((p: any) => p.projectId === projectId)).toBe(true); console.log('Project verified in list'); // Get next task @@ -223,7 +223,7 @@ describe('MCP Client Integration', () => { // Get the project ID const responseData = JSON.parse((createResult.content[0] as { text: string }).text); - const projectId = responseData.projectId; + const projectId = responseData.data.projectId; expect(projectId).toBeDefined(); console.log('Created auto-approve project with ID:', projectId); @@ -237,6 +237,7 @@ describe('MCP Client Integration', () => { expect(nextTaskResult.isError).toBeFalsy(); const nextTask = JSON.parse((nextTaskResult.content[0] as { text: string }).text); expect(nextTask.status).toBe("next_task"); + expect(nextTask.task).toBeDefined(); const taskId = nextTask.task.id; // Mark task as done - we need to mark it as done using the update_task tool @@ -270,9 +271,9 @@ describe('MCP Client Integration', () => { }) 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); + expect(taskDetails.data.task.status).toBe("done"); + expect(taskDetails.data.task.approved).toBe(true); + console.log('Task was manually approved:', taskDetails.data.task.approved); // Verify we can finalize the project after explicit approval const finalizeResult = await client.callTool({ diff --git a/tests/unit/TaskManager.test.ts b/tests/unit/TaskManager.test.ts index afb22c9..5d5828e 100644 --- a/tests/unit/TaskManager.test.ts +++ b/tests/unit/TaskManager.test.ts @@ -83,11 +83,11 @@ describe('TaskManager', () => { } ], '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('success'); + expect(result.data.projectId).toBeDefined(); + expect(result.data.totalTasks).toBe(1); }); it('should handle project listing', async () => { @@ -103,9 +103,9 @@ describe('TaskManager', () => { '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(); + expect(result.status).toBe('success'); + expect(result.data.projects).toHaveLength(1); }); it('should handle project deletion', async () => { @@ -119,16 +119,16 @@ describe('TaskManager', () => { } ], '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); + const projectIndex = server["data"].projects.findIndex((p) => p.projectId === createResult.data.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); + const listResult = await server.listProjects(); + expect(listResult.data.projects).toHaveLength(0); }); }); @@ -144,16 +144,16 @@ describe('TaskManager', () => { } ], 'Test plan' - ) as { status: string; projectId: string; totalTasks: number; tasks: { id: string }[]; message: string }; + ); - const projectId = createResult.projectId; - const taskId = createResult.tasks[0].id; + const projectId = createResult.data.projectId; + const taskId = createResult.data.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); + expect(readResult.status).toBe('success'); + if (readResult.status === 'success' && readResult.data.task) { + expect(readResult.data.task.id).toBe(taskId); } // Test task updating @@ -161,22 +161,24 @@ describe('TaskManager', () => { title: "Updated task", description: "Updated description" }); - expect(updatedTask.title).toBe("Updated task"); - expect(updatedTask.description).toBe("Updated description"); - expect(updatedTask.status).toBe("not started"); + expect(updatedTask.status).toBe('success'); + expect(updatedTask.data.title).toBe("Updated task"); + expect(updatedTask.data.description).toBe("Updated description"); + expect(updatedTask.data.status).toBe("not started"); // Test status update const updatedStatusTask = await server.updateTask(projectId, taskId, { status: 'in progress' }); - expect(updatedStatusTask.status).toBe('in progress'); + expect(updatedStatusTask.status).toBe('success'); + expect(updatedStatusTask.data.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'); + ); + expect(deleteResult.status).toBe('success'); }); it('should get the next task', async () => { @@ -193,19 +195,16 @@ describe('TaskManager', () => { description: 'Description 2' } ] - ) as { - projectId: string; - tasks: { id: string }[]; - }; + ); - const projectId = createResult.projectId; + const projectId = createResult.data.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); + if (nextTaskResult.status === 'next_task') { + expect(nextTaskResult.data.id).toBe(createResult.data.tasks[0].id); } }); }); @@ -229,20 +228,18 @@ describe('TaskManager', () => { description: 'Description for task 2' } ] - ) as { - projectId: string; - tasks: { id: string }[]; - }; + ); - projectId = createResult.projectId; - taskId1 = createResult.tasks[0].id; - taskId2 = createResult.tasks[1].id; + projectId = createResult.data.projectId; + taskId1 = createResult.data.tasks[0].id; + taskId2 = createResult.data.tasks[1].id; }); it('should not approve project if tasks are not done', async () => { - const result = await server.approveProjectCompletion(projectId); - expect(result.status).toBe('error'); - expect(result.message).toContain('Not all tasks are done'); + await expect(server.approveProjectCompletion(projectId)).rejects.toMatchObject({ + code: 'ERR_3003', + message: 'Not all tasks are done' + }); }); it('should not approve project if tasks are done but not approved', async () => { @@ -256,9 +253,10 @@ describe('TaskManager', () => { completedDetails: 'Task 2 completed details' }); - const result = await server.approveProjectCompletion(projectId); - expect(result.status).toBe('error'); - expect(result.message).toContain('Not all done tasks are approved'); + await expect(server.approveProjectCompletion(projectId)).rejects.toMatchObject({ + code: 'ERR_3004', + message: 'Not all done tasks are approved' + }); }); it('should approve project when all tasks are done and approved', async () => { @@ -277,7 +275,7 @@ describe('TaskManager', () => { await server.approveTaskCompletion(projectId, taskId2); const result = await server.approveProjectCompletion(projectId); - expect(result.status).toBe('project_approved_complete'); + expect(result.status).toBe('success'); // Verify project is marked as completed const project = server["data"].projects.find(p => p.projectId === projectId); @@ -300,9 +298,10 @@ describe('TaskManager', () => { await server.approveProjectCompletion(projectId); // Try to approve again - const result = await server.approveProjectCompletion(projectId); - expect(result.status).toBe('error'); - expect(result.message).toContain('Project is already completed'); + await expect(server.approveProjectCompletion(projectId)).rejects.toMatchObject({ + code: 'ERR_3001', + message: 'Project is already completed' + }); }); }); @@ -312,22 +311,23 @@ describe('TaskManager', () => { // 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; + const proj1Id = project1.data.projectId; + const proj2Id = project2.data.projectId; // Complete tasks in project 2 - await server.updateTask(proj2Id, project2.tasks[0].id, { + await server.updateTask(proj2Id, project2.data.tasks[0].id, { status: 'done', completedDetails: 'Completed task details' }); - await server.approveTaskCompletion(proj2Id, project2.tasks[0].id); + await server.approveTaskCompletion(proj2Id, project2.data.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); + expect(result.status).toBe('success'); + expect(result.data.projects.length).toBe(1); + expect(result.data.projects[0].projectId).toBe(proj1Id); }); it('should list only pending approval projects', async () => { @@ -337,22 +337,23 @@ describe('TaskManager', () => { 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, { + await server.updateTask(project1.data.projectId, project1.data.tasks[0].id, { status: 'done', completedDetails: 'Completed task details' }); // Complete project 2 fully - await server.updateTask(project2.projectId, project2.tasks[0].id, { + await server.updateTask(project2.data.projectId, project2.data.tasks[0].id, { status: 'done', completedDetails: 'Completed task details' }); - await server.approveTaskCompletion(project2.projectId, project2.tasks[0].id); - await server.approveProjectCompletion(project2.projectId); + await server.approveTaskCompletion(project2.data.projectId, project2.data.tasks[0].id); + await server.approveProjectCompletion(project2.data.projectId); const result = await server.listProjects("pending_approval"); - expect(result.projects.length).toBe(1); - expect(result.projects[0].projectId).toBe(project1.projectId); + expect(result.status).toBe('success'); + expect(result.data.projects.length).toBe(1); + expect(result.data.projects[0].projectId).toBe(project1.data.projectId); }); it('should list only completed projects', async () => { @@ -362,22 +363,23 @@ describe('TaskManager', () => { 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, { + await server.updateTask(project2.data.projectId, project2.data.tasks[0].id, { status: 'done', completedDetails: 'Completed task details' }); - await server.approveTaskCompletion(project2.projectId, project2.tasks[0].id); - await server.approveProjectCompletion(project2.projectId); + await server.approveTaskCompletion(project2.data.projectId, project2.data.tasks[0].id); + await server.approveProjectCompletion(project2.data.projectId); // Mark project 3's task as done but not approved - await server.updateTask(project3.projectId, project3.tasks[0].id, { + await server.updateTask(project3.data.projectId, project3.data.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); + expect(result.status).toBe('success'); + expect(result.data.projects.length).toBe(1); + expect(result.data.projects[0].projectId).toBe(project2.data.projectId); }); it('should list all projects when state is \'all\'', async () => { @@ -387,12 +389,14 @@ describe('TaskManager', () => { const project3 = await server.createProject("Pending Project", [{ title: "Task 3", description: "Desc" }]); const result = await server.listProjects("all"); - expect(result.projects.length).toBe(3); + expect(result.status).toBe('success'); + expect(result.data.projects.length).toBe(3); }); it('should handle empty project list', async () => { const result = await server.listProjects("open"); - expect(result.projects.length).toBe(0); + expect(result.status).toBe('success'); + expect(result.data.projects.length).toBe(0); }); }); @@ -408,31 +412,34 @@ describe('TaskManager', () => { ]); // Set task states - await server.updateTask(project1.projectId, project1.tasks[1].id, { + await server.updateTask(project1.data.projectId, project1.data.tasks[1].id, { status: 'done', completedDetails: 'Task 2 completed details' }); - await server.approveTaskCompletion(project1.projectId, project1.tasks[1].id); + await server.approveTaskCompletion(project1.data.projectId, project1.data.tasks[1].id); - await server.updateTask(project2.projectId, project2.tasks[0].id, { + await server.updateTask(project2.data.projectId, project2.data.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"); + expect(openResult.status).toBe('success'); + expect(openResult.data.tasks!.length).toBe(1); + expect(openResult.data.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"); + expect(pendingResult.status).toBe('success'); + expect(pendingResult.data.tasks!.length).toBe(1); + expect(pendingResult.data.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"); + expect(completedResult.status).toBe('success'); + expect(completedResult.data.tasks!.length).toBe(1); + expect(completedResult.data.tasks![0].title).toBe("Task 2"); }); it('should list tasks for specific project filtered by state', async () => { @@ -444,50 +451,55 @@ describe('TaskManager', () => { ]); // Set task states - await server.updateTask(project.projectId, project.tasks[1].id, { + await server.updateTask(project.data.projectId, project.data.tasks[1].id, { status: 'done', completedDetails: 'Task 2 completed details' }); - await server.approveTaskCompletion(project.projectId, project.tasks[1].id); + await server.approveTaskCompletion(project.data.projectId, project.data.tasks[1].id); - await server.updateTask(project.projectId, project.tasks[2].id, { + await server.updateTask(project.data.projectId, project.data.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"); + const openResult = await server.listTasks(project.data.projectId, "open"); + expect(openResult.status).toBe('success'); + expect(openResult.data.tasks!.length).toBe(1); + expect(openResult.data.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"); + const pendingResult = await server.listTasks(project.data.projectId, "pending_approval"); + expect(pendingResult.status).toBe('success'); + expect(pendingResult.data.tasks!.length).toBe(1); + expect(pendingResult.data.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"); + const completedResult = await server.listTasks(project.data.projectId, "completed"); + expect(completedResult.status).toBe('success'); + expect(completedResult.data.tasks!.length).toBe(1); + expect(completedResult.data.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"); + await expect(server.listTasks("non-existent-project", "open")).rejects.toMatchObject({ + code: 'ERR_2000', + message: 'Project non-existent-project not found' + }); }); 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); + const result = await server.listTasks(project.data.projectId, "open"); + expect(result.status).toBe('success'); + expect(result.data.tasks!.length).toBe(0); }); }); }); describe('Task Recommendations', () => { it("should handle tasks with tool and rule recommendations", async () => { - const { projectId } = await server.createProject("Test Project", [ + const createResult = await server.createProject("Test Project", [ { title: "Test Task", description: "Test Description", @@ -495,11 +507,12 @@ describe('TaskManager', () => { ruleRecommendations: "Review rule Y" }, ]); + const projectId = createResult.data.projectId; const tasksResponse = await server.listTasks(projectId); - if (!('tasks' in tasksResponse) || !tasksResponse.tasks?.length) { + if (tasksResponse.status !== 'success' || !tasksResponse.data.tasks?.length) { throw new Error('Expected tasks in response'); } - const tasks = tasksResponse.tasks as Task[]; + const tasks = tasksResponse.data.tasks as Task[]; const taskId = tasks[0].id; // Verify initial recommendations @@ -512,8 +525,9 @@ describe('TaskManager', () => { ruleRecommendations: "Review rule W", }); - expect(updatedTask.toolRecommendations).toBe("Use tool Z"); - expect(updatedTask.ruleRecommendations).toBe("Review rule W"); + expect(updatedTask.status).toBe('success'); + expect(updatedTask.data.toolRecommendations).toBe("Use tool Z"); + expect(updatedTask.data.ruleRecommendations).toBe("Review rule W"); // Add new task with recommendations await server.addTasksToProject(projectId, [ @@ -526,10 +540,10 @@ describe('TaskManager', () => { ]); const allTasksResponse = await server.listTasks(projectId); - if (!('tasks' in allTasksResponse) || !allTasksResponse.tasks?.length) { + if (allTasksResponse.status !== 'success' || !allTasksResponse.data.tasks?.length) { throw new Error('Expected tasks in response'); } - const allTasks = allTasksResponse.tasks as Task[]; + const allTasks = allTasksResponse.data.tasks as Task[]; const newTask = allTasks.find(t => t.title === "Added Task"); expect(newTask).toBeDefined(); if (newTask) { @@ -538,15 +552,16 @@ describe('TaskManager', () => { } }); - it("should allow tasks with no recommendations", async () => { - const { projectId } = await server.createProject("Test Project", [ + it("should handle tasks with no recommendations", async () => { + const createResult = await server.createProject("Test Project", [ { title: "Test Task", description: "Test Description" }, ]); + const projectId = createResult.data.projectId; const tasksResponse = await server.listTasks(projectId); - if (!('tasks' in tasksResponse) || !tasksResponse.tasks?.length) { + if (tasksResponse.status !== 'success' || !tasksResponse.data.tasks?.length) { throw new Error('Expected tasks in response'); } - const tasks = tasksResponse.tasks as Task[]; + const tasks = tasksResponse.data.tasks as Task[]; const taskId = tasks[0].id; // Verify no recommendations @@ -559,10 +574,10 @@ describe('TaskManager', () => { ]); const allTasksResponse = await server.listTasks(projectId); - if (!('tasks' in allTasksResponse) || !allTasksResponse.tasks?.length) { + if (allTasksResponse.status !== 'success' || !allTasksResponse.data.tasks?.length) { throw new Error('Expected tasks in response'); } - const allTasks = allTasksResponse.tasks as Task[]; + const allTasks = allTasksResponse.data.tasks as Task[]; const newTask = allTasks.find(t => t.title === "Added Task"); expect(newTask).toBeDefined(); if (newTask) { @@ -585,13 +600,10 @@ describe('TaskManager', () => { ], 'Test plan', true // autoApprove parameter - ) as { - projectId: string; - tasks: { id: string }[]; - }; + ); - const projectId = createResult.projectId; - const taskId = createResult.tasks[0].id; + const projectId = createResult.data.projectId; + const taskId = createResult.data.tasks[0].id; // Update the task status to done const updatedTask = await server.updateTask(projectId, taskId, { @@ -600,12 +612,13 @@ describe('TaskManager', () => { }); // The task should be automatically approved - expect(updatedTask.status).toBe('done'); - expect(updatedTask.approved).toBe(true); + expect(updatedTask.status).toBe('success'); + expect(updatedTask.data.status).toBe('done'); + expect(updatedTask.data.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'); + expect(approveResult.status).toBe('success'); }); it('should not auto-approve tasks when updating status to done and autoApprove is disabled', async () => { @@ -620,13 +633,10 @@ describe('TaskManager', () => { ], 'Test plan', false // autoApprove parameter - ) as { - projectId: string; - tasks: { id: string }[]; - }; + ); - const projectId = createResult.projectId; - const taskId = createResult.tasks[0].id; + const projectId = createResult.data.projectId; + const taskId = createResult.data.tasks[0].id; // Update the task status to done const updatedTask = await server.updateTask(projectId, taskId, { @@ -635,12 +645,15 @@ describe('TaskManager', () => { }); // The task should not be automatically approved - expect(updatedTask.status).toBe('done'); - expect(updatedTask.approved).toBe(false); + expect(updatedTask.status).toBe('success'); + expect(updatedTask.data.status).toBe('done'); + expect(updatedTask.data.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'); + await expect(server.approveProjectCompletion(projectId)).rejects.toMatchObject({ + code: 'ERR_3004', + message: 'Not all done tasks are approved' + }); }); it('should make autoApprove false by default if not specified', async () => { @@ -653,13 +666,10 @@ describe('TaskManager', () => { 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; + const projectId = createResult.data.projectId; + const taskId = createResult.data.tasks[0].id; // Update the task status to done const updatedTask = await server.updateTask(projectId, taskId, { @@ -668,8 +678,9 @@ describe('TaskManager', () => { }); // The task should not be automatically approved by default - expect(updatedTask.status).toBe('done'); - expect(updatedTask.approved).toBe(false); + expect(updatedTask.status).toBe('success'); + expect(updatedTask.data.status).toBe('done'); + expect(updatedTask.data.approved).toBe(false); }); }); }); \ No newline at end of file