Skip to main content
Glama

Task Trellis MCP

updateObject.e2e.test.ts55.5 kB
import { McpTestClient, TestEnvironment, createObjectContent, createObjectFile, parseUpdateObjectResponse, readObjectFile, type ObjectData, } from "../utils"; describe("E2E CRUD - updateObject", () => { let testEnv: TestEnvironment; let client: McpTestClient; beforeEach(async () => { testEnv = new TestEnvironment(); testEnv.setup(); client = new McpTestClient(testEnv.projectRoot); await client.connect(); // Activate server in local mode await client.callTool("activate", { mode: "local", projectRoot: testEnv.projectRoot, }); }, 30000); afterEach(async () => { await client?.disconnect(); testEnv?.cleanup(); }); describe("Individual Field Updates", () => { it("should update priority field and persist to file", async () => { // Create initial task const taskData: ObjectData = { id: "T-priority-test", title: "Priority Test Task", status: "open", priority: "low", body: "Original body content", }; await createObjectFile( testEnv.projectRoot, "task", "T-priority-test", createObjectContent(taskData), ); // Update priority const result = await client.callTool("update_issue", { id: "T-priority-test", priority: "high", }); expect(result.content[0].type).toBe("text"); const updatedObject = parseUpdateObjectResponse( result.content[0].text as string, ); expect(updatedObject.priority).toBe("high"); expect(updatedObject.title).toBe("Priority Test Task"); expect(updatedObject.body).toBe("Original body content"); // Verify file persistence const file = await readObjectFile( testEnv.projectRoot, "t/open/T-priority-test.md", ); expect(file.yaml.priority).toBe("high"); expect(file.body).toBe("Original body content"); }); it("should update status field and persist to file", async () => { // Create initial project const projectData: ObjectData = { id: "P-status-test", title: "Status Test Project", status: "draft", priority: "medium", }; await createObjectFile( testEnv.projectRoot, "project", "P-status-test", createObjectContent(projectData), ); // Update status const result = await client.callTool("update_issue", { id: "P-status-test", status: "open", }); const updatedObject = parseUpdateObjectResponse( result.content[0].text as string, ); expect(updatedObject.status).toBe("open"); // Verify file persistence const file = await readObjectFile( testEnv.projectRoot, "p/P-status-test/P-status-test.md", ); expect(file.yaml.status).toBe("open"); }); it("should update body content", async () => { // Create initial feature const featureData: ObjectData = { id: "F-body-test", title: "Body Test Feature", body: "Original description", }; await createObjectFile( testEnv.projectRoot, "feature", "F-body-test", createObjectContent(featureData), ); // Update body const newBody = `# Updated Feature Description ## Overview This is the new body content with markdown formatting. ## Details - Point 1 - Point 2`; const result = await client.callTool("update_issue", { id: "F-body-test", body: newBody, }); const updatedObject = parseUpdateObjectResponse( result.content[0].text as string, ); expect(updatedObject.body).toBe(newBody); // Verify file persistence const file = await readObjectFile( testEnv.projectRoot, "f/F-body-test/F-body-test.md", ); expect(file.body).toBe(newBody); }); it("should update prerequisites array for tasks", async () => { // Create prerequisite tasks await createObjectFile( testEnv.projectRoot, "task", "T-prereq-1", createObjectContent({ id: "T-prereq-1", title: "Prerequisite 1", status: "done", }), ); await createObjectFile( testEnv.projectRoot, "task", "T-prereq-2", createObjectContent({ id: "T-prereq-2", title: "Prerequisite 2", status: "done", }), ); // Create main task const taskData: ObjectData = { id: "T-deps-test", title: "Dependencies Test Task", prerequisites: ["T-prereq-1"], }; await createObjectFile( testEnv.projectRoot, "task", "T-deps-test", createObjectContent(taskData), ); // Update prerequisites const result = await client.callTool("update_issue", { id: "T-deps-test", prerequisites: ["T-prereq-1", "T-prereq-2", "F-external-dep"], }); const updatedObject = parseUpdateObjectResponse( result.content[0].text as string, ); expect(updatedObject.prerequisites).toEqual([ "T-prereq-1", "T-prereq-2", "F-external-dep", ]); // Verify file persistence const file = await readObjectFile( testEnv.projectRoot, "t/open/T-deps-test.md", ); expect(file.yaml.prerequisites).toEqual([ "T-prereq-1", "T-prereq-2", "F-external-dep", ]); }); }); describe("Multiple Field Updates", () => { it("should update multiple fields simultaneously", async () => { // Create initial epic const epicData: ObjectData = { id: "E-multi-update", title: "Multi Update Epic", status: "draft", priority: "low", parent: "P-parent-project", body: "Initial content", }; await createObjectFile( testEnv.projectRoot, "epic", "E-multi-update", createObjectContent(epicData), { projectId: "P-parent-project" }, ); // Update multiple fields const result = await client.callTool("update_issue", { id: "E-multi-update", priority: "high", status: "open", body: "Completely new content with updated priority and status", prerequisites: ["P-dep-1", "E-dep-2"], }); const updatedObject = parseUpdateObjectResponse( result.content[0].text as string, ); expect(updatedObject.priority).toBe("high"); expect(updatedObject.status).toBe("open"); expect(updatedObject.body).toBe( "Completely new content with updated priority and status", ); expect(updatedObject.prerequisites).toEqual(["P-dep-1", "E-dep-2"]); expect(updatedObject.title).toBe("Multi Update Epic"); // Unchanged expect(updatedObject.parent).toBe("P-parent-project"); // Unchanged // Verify file persistence const file = await readObjectFile( testEnv.projectRoot, "p/P-parent-project/e/E-multi-update/E-multi-update.md", ); expect(file.yaml.priority).toBe("high"); expect(file.yaml.status).toBe("open"); expect(file.yaml.prerequisites).toEqual(["P-dep-1", "E-dep-2"]); expect(file.body).toBe( "Completely new content with updated priority and status", ); }); it("should preserve unchanged fields during update", async () => { // Create parent hierarchy first await createObjectFile( testEnv.projectRoot, "project", "P-project", createObjectContent({ id: "P-project", title: "Test Project", }), ); await createObjectFile( testEnv.projectRoot, "epic", "E-parent-epic", createObjectContent({ id: "E-parent-epic", title: "Parent Epic", parent: "P-project", }), { projectId: "P-project" }, ); const featureData: ObjectData = { id: "F-preserve-test", title: "Preserve Test Feature", status: "in-progress", priority: "medium", parent: "E-parent-epic", prerequisites: ["F-dep-1", "T-dep-2"], childrenIds: ["T-child-1", "T-child-2"], affectedFiles: { "src/main.ts": "Added feature" }, log: ["Created feature", "Updated status"], schema: "1.1", body: "Detailed feature description", }; // Create feature in hierarchy await createObjectFile( testEnv.projectRoot, "feature", "F-preserve-test", createObjectContent(featureData), { projectId: "P-project", epicId: "E-parent-epic" }, ); // Create the expected child tasks await createObjectFile( testEnv.projectRoot, "task", "T-child-1", createObjectContent({ id: "T-child-1", title: "Child Task 1", parent: "F-preserve-test", }), { projectId: "P-project", epicId: "E-parent-epic", featureId: "F-preserve-test", status: "open", }, ); await createObjectFile( testEnv.projectRoot, "task", "T-child-2", createObjectContent({ id: "T-child-2", title: "Child Task 2", parent: "F-preserve-test", }), { projectId: "P-project", epicId: "E-parent-epic", featureId: "F-preserve-test", status: "open", }, ); // Update only priority const result = await client.callTool("update_issue", { id: "F-preserve-test", priority: "high", }); const updatedObject = parseUpdateObjectResponse( result.content[0].text as string, ); // Verify only priority changed expect(updatedObject.priority).toBe("high"); // Verify all other fields preserved expect(updatedObject.title).toBe("Preserve Test Feature"); expect(updatedObject.status).toBe("in-progress"); expect(updatedObject.parent).toBe("E-parent-epic"); expect(updatedObject.prerequisites).toEqual(["F-dep-1", "T-dep-2"]); expect(updatedObject.childrenIds).toEqual(["T-child-1", "T-child-2"]); expect(updatedObject.log).toEqual(["Created feature", "Updated status"]); expect(updatedObject.body).toBe("Detailed feature description"); }); }); describe("Status Transition Validation", () => { it("should allow status change to in-progress when prerequisites are complete", async () => { // Create completed prerequisite await createObjectFile( testEnv.projectRoot, "task", "T-prereq-done", createObjectContent({ id: "T-prereq-done", title: "Completed Prerequisite", status: "done", }), ); // Create task with prerequisite await createObjectFile( testEnv.projectRoot, "task", "T-can-progress", createObjectContent({ id: "T-can-progress", title: "Can Progress Task", status: "open", prerequisites: ["T-prereq-done"], }), ); // Update status to in-progress const result = await client.callTool("update_issue", { id: "T-can-progress", status: "in-progress", }); expect(result.content[0].text).toContain("Successfully updated object"); const updatedObject = parseUpdateObjectResponse( result.content[0].text as string, ); expect(updatedObject.status).toBe("in-progress"); }); it("should reject status change to in-progress when prerequisites incomplete", async () => { // Create incomplete prerequisite await createObjectFile( testEnv.projectRoot, "task", "T-prereq-incomplete", createObjectContent({ id: "T-prereq-incomplete", title: "Incomplete Prerequisite", status: "open", }), ); // Create task with incomplete prerequisite await createObjectFile( testEnv.projectRoot, "task", "T-blocked", createObjectContent({ id: "T-blocked", title: "Blocked Task", status: "open", prerequisites: ["T-prereq-incomplete"], }), ); // Attempt to update status to in-progress const result = await client.callTool("update_issue", { id: "T-blocked", status: "in-progress", }); expect(result.content[0].type).toBe("text"); expect(result.content[0].text).toBe( "Error updating object: Cannot update status to 'in-progress' - prerequisites are not complete. Use force=true to override.", ); // Verify file was not updated const file = await readObjectFile( testEnv.projectRoot, "t/open/T-blocked.md", ); expect(file.yaml.status).toBe("open"); }); it("should reject status change to done when prerequisites incomplete", async () => { // Create mixed prerequisites await createObjectFile( testEnv.projectRoot, "task", "T-prereq-done-2", createObjectContent({ id: "T-prereq-done-2", title: "Done Prerequisite", status: "done", }), ); await createObjectFile( testEnv.projectRoot, "task", "T-prereq-open", createObjectContent({ id: "T-prereq-open", title: "Open Prerequisite", status: "open", }), ); // Create feature with mixed prerequisites await createObjectFile( testEnv.projectRoot, "feature", "F-blocked-done", createObjectContent({ id: "F-blocked-done", title: "Feature Blocked from Done", status: "in-progress", prerequisites: ["T-prereq-done-2", "T-prereq-open"], }), ); // Attempt to update status to done const result = await client.callTool("update_issue", { id: "F-blocked-done", status: "done", }); expect(result.content[0].text).toBe( "Error updating object: Cannot update status to 'done' - prerequisites are not complete. Use force=true to override.", ); }); it("should allow force update bypassing validation", async () => { // Create incomplete prerequisite await createObjectFile( testEnv.projectRoot, "task", "T-prereq-incomplete-2", createObjectContent({ id: "T-prereq-incomplete-2", title: "Incomplete Prerequisite 2", status: "in-progress", }), ); // Create task with incomplete prerequisite await createObjectFile( testEnv.projectRoot, "task", "T-force-update", createObjectContent({ id: "T-force-update", title: "Force Update Task", status: "open", prerequisites: ["T-prereq-incomplete-2"], }), ); // Force update status to done const result = await client.callTool("update_issue", { id: "T-force-update", status: "done", force: true, }); expect(result.content[0].text).toContain("Successfully updated object"); const updatedObject = parseUpdateObjectResponse( result.content[0].text as string, ); expect(updatedObject.status).toBe("done"); // Verify file moved to closed folder const file = await readObjectFile( testEnv.projectRoot, "t/closed/T-force-update.md", ); expect(file.yaml.status).toBe("done"); }); it("should allow status changes to draft without validation", async () => { // Create task with incomplete prerequisites await createObjectFile( testEnv.projectRoot, "task", "T-to-draft", createObjectContent({ id: "T-to-draft", title: "To Draft Task", status: "open", prerequisites: ["T-nonexistent"], }), ); // Update to draft (no validation required) const result = await client.callTool("update_issue", { id: "T-to-draft", status: "draft", }); expect(result.content[0].text).toContain("Successfully updated object"); const updatedObject = parseUpdateObjectResponse( result.content[0].text as string, ); expect(updatedObject.status).toBe("draft"); }); it("should allow status changes to wont-do without validation", async () => { // Create task with incomplete prerequisites await createObjectFile( testEnv.projectRoot, "task", "T-to-wontdo", createObjectContent({ id: "T-to-wontdo", title: "To Wont-Do Task", status: "open", prerequisites: ["T-incomplete"], }), ); // Update to wont-do (no validation required) const result = await client.callTool("update_issue", { id: "T-to-wontdo", status: "wont-do", }); expect(result.content[0].text).toContain("Successfully updated object"); const updatedObject = parseUpdateObjectResponse( result.content[0].text as string, ); expect(updatedObject.status).toBe("wont-do"); }); it("should consider wont-do prerequisites as complete", async () => { // Create wont-do prerequisite await createObjectFile( testEnv.projectRoot, "task", "T-wontdo-prereq", createObjectContent({ id: "T-wontdo-prereq", title: "Wont-Do Prerequisite", status: "wont-do", }), ); // Create task with wont-do prerequisite await createObjectFile( testEnv.projectRoot, "task", "T-with-wontdo", createObjectContent({ id: "T-with-wontdo", title: "Task with Wont-Do Prerequisite", status: "open", prerequisites: ["T-wontdo-prereq"], }), ); // Should allow progression since wont-do is considered complete const result = await client.callTool("update_issue", { id: "T-with-wontdo", status: "done", }); expect(result.content[0].text).toContain("Successfully updated object"); const updatedObject = parseUpdateObjectResponse( result.content[0].text as string, ); expect(updatedObject.status).toBe("done"); }); }); describe("Error Handling", () => { it("should handle non-existent object IDs", async () => { const result = await client.callTool("update_issue", { id: "T-nonexistent", priority: "high", }); expect(result.content[0].type).toBe("text"); expect(result.content[0].text).toBe( "Error: Object with ID 'T-nonexistent' not found", ); }); it("should handle invalid priority values", async () => { // Create test task await createObjectFile( testEnv.projectRoot, "task", "T-invalid-priority", createObjectContent({ id: "T-invalid-priority", title: "Invalid Priority Test", }), ); // Attempt update with invalid priority const result = await client.callTool("update_issue", { id: "T-invalid-priority", priority: "critical", // Invalid value }); // The tool accepts any string but may fail on save or return as-is // Check the actual behavior const responseText = result.content[0].text as string; if (responseText.startsWith("Successfully")) { // If it accepts invalid values, verify it's stored const updatedObject = parseUpdateObjectResponse(responseText); expect(updatedObject.priority).toBe("critical"); } else { // If it rejects invalid values expect(responseText).toContain("Error"); } }); it("should handle invalid status values", async () => { // Create test project await createObjectFile( testEnv.projectRoot, "project", "P-invalid-status", createObjectContent({ id: "P-invalid-status", title: "Invalid Status Test", }), ); // Attempt update with invalid status const result = await client.callTool("update_issue", { id: "P-invalid-status", status: "completed", // Invalid value (should be "done") }); const responseText = result.content[0].text as string; if (responseText.startsWith("Successfully")) { // If it accepts invalid values, verify it's stored const updatedObject = parseUpdateObjectResponse(responseText); expect(updatedObject.status).toBe("completed"); } else { // If it rejects invalid values expect(responseText).toContain("Error"); } }); it("should handle malformed object IDs", async () => { const malformedIds = [ "invalid-id", "X-unknown-prefix", "T_wrong_separator", "", "T-", // Missing slug ]; for (const id of malformedIds) { const result = await client.callTool("update_issue", { id, priority: "high", }); expect(result.content[0].type).toBe("text"); expect(result.content[0].text).toContain("Error"); } }); it("should handle empty prerequisites array", async () => { // Create task with prerequisites await createObjectFile( testEnv.projectRoot, "task", "T-clear-prereqs", createObjectContent({ id: "T-clear-prereqs", title: "Clear Prerequisites Test", prerequisites: ["T-dep-1", "T-dep-2"], }), ); // Clear prerequisites const result = await client.callTool("update_issue", { id: "T-clear-prereqs", prerequisites: [], }); expect(result.content[0].text).toContain("Successfully updated object"); const updatedObject = parseUpdateObjectResponse( result.content[0].text as string, ); expect(updatedObject.prerequisites).toEqual([]); // Verify file persistence const file = await readObjectFile( testEnv.projectRoot, "t/open/T-clear-prereqs.md", ); expect(file.yaml.prerequisites).toEqual([]); }); it("should handle very long body content", async () => { // Create test feature await createObjectFile( testEnv.projectRoot, "feature", "F-long-body", createObjectContent({ id: "F-long-body", title: "Long Body Test", }), ); // Create very long body content const longBody = "A".repeat(10000) + "\n\n" + "B".repeat(10000); const result = await client.callTool("update_issue", { id: "F-long-body", body: longBody, }); expect(result.content[0].text).toContain("Successfully updated object"); const updatedObject = parseUpdateObjectResponse( result.content[0].text as string, ); expect(updatedObject.body).toBe(longBody); // Verify file persistence const file = await readObjectFile( testEnv.projectRoot, "f/F-long-body/F-long-body.md", ); expect(file.body).toBe(longBody); }); it("should handle special characters in body content", async () => { // Create test epic await createObjectFile( testEnv.projectRoot, "epic", "E-special-chars", createObjectContent({ id: "E-special-chars", title: "Special Characters Test", parent: "P-parent", }), { projectId: "P-parent" }, ); // Body with special characters const specialBody = `# Special Characters Test ## Code blocks \`\`\`typescript const test = "value with 'quotes' and 'double quotes'"; \`\`\` ## Special symbols - Unicode: 🚀 ✅ ❌ - Math: α β γ δ ε - Arrows: → ← ↑ ↓ - Other: & < > | \\ / * ? : " ' \` ~ ! @ # $ % ^ & * ( ) [ ] { }`; const result = await client.callTool("update_issue", { id: "E-special-chars", body: specialBody, }); expect(result.content[0].text).toContain("Successfully updated object"); const updatedObject = parseUpdateObjectResponse( result.content[0].text as string, ); expect(updatedObject.body).toBe(specialBody); // Verify file persistence const file = await readObjectFile( testEnv.projectRoot, "p/P-parent/e/E-special-chars/E-special-chars.md", ); expect(file.body).toBe(specialBody); }); }); describe("Complex Hierarchy Updates", () => { it("should update objects in deep hierarchy", async () => { // Create complete hierarchy const projectId = "P-hierarchy"; const epicId = "E-hierarchy"; const featureId = "F-hierarchy"; const taskId = "T-hierarchy"; // Create project await createObjectFile( testEnv.projectRoot, "project", projectId, createObjectContent({ id: projectId, title: "Hierarchy Project", childrenIds: [epicId], }), ); // Create epic await createObjectFile( testEnv.projectRoot, "epic", epicId, createObjectContent({ id: epicId, title: "Hierarchy Epic", parent: projectId, childrenIds: [featureId], }), { projectId }, ); // Create feature await createObjectFile( testEnv.projectRoot, "feature", featureId, createObjectContent({ id: featureId, title: "Hierarchy Feature", parent: epicId, childrenIds: [taskId], }), { projectId, epicId }, ); // Create task await createObjectFile( testEnv.projectRoot, "task", taskId, createObjectContent({ id: taskId, title: "Hierarchy Task", parent: featureId, status: "open", }), { projectId, epicId, featureId, status: "open" }, ); // Update task in deep hierarchy const result = await client.callTool("update_issue", { id: taskId, priority: "high", status: "done", body: "Updated task in deep hierarchy", }); expect(result.content[0].text).toContain("Successfully updated object"); const updatedObject = parseUpdateObjectResponse( result.content[0].text as string, ); expect(updatedObject.priority).toBe("high"); expect(updatedObject.status).toBe("done"); expect(updatedObject.parent).toBe(featureId); // Verify file moved to closed folder const file = await readObjectFile( testEnv.projectRoot, `p/${projectId}/e/${epicId}/f/${featureId}/t/closed/${taskId}.md`, ); expect(file.yaml.status).toBe("done"); expect(file.yaml.priority).toBe("high"); expect(file.body).toBe("Updated task in deep hierarchy"); }); it("should handle updates with cross-hierarchy prerequisites", async () => { // Create objects in different hierarchies await createObjectFile( testEnv.projectRoot, "project", "P-proj-1", createObjectContent({ id: "P-proj-1", title: "Project 1", status: "done", }), ); await createObjectFile( testEnv.projectRoot, "feature", "F-feat-1", createObjectContent({ id: "F-feat-1", title: "Feature 1", status: "done", }), ); await createObjectFile( testEnv.projectRoot, "task", "T-cross-deps", createObjectContent({ id: "T-cross-deps", title: "Cross Dependencies Task", status: "open", prerequisites: [], }), ); // Update with cross-hierarchy prerequisites const result = await client.callTool("update_issue", { id: "T-cross-deps", prerequisites: ["P-proj-1", "F-feat-1", "E-external"], status: "in-progress", }); expect(result.content[0].text).toContain("Successfully updated object"); const updatedObject = parseUpdateObjectResponse( result.content[0].text as string, ); expect(updatedObject.prerequisites).toEqual([ "P-proj-1", "F-feat-1", "E-external", ]); expect(updatedObject.status).toBe("in-progress"); }); }); describe("Hierarchical Status Updates", () => { it("should update parent hierarchy to in-progress when task changes to in-progress", async () => { // Create complete hierarchy: Project -> Epic -> Feature -> Task const projectId = "P-hierarchy-status"; const epicId = "E-hierarchy-status"; const featureId = "F-hierarchy-status"; const taskId = "T-hierarchy-status"; // Create project (open status) await createObjectFile( testEnv.projectRoot, "project", projectId, createObjectContent({ id: projectId, title: "Hierarchy Status Project", status: "open", childrenIds: [epicId], }), ); // Create epic (open status) await createObjectFile( testEnv.projectRoot, "epic", epicId, createObjectContent({ id: epicId, title: "Hierarchy Status Epic", parent: projectId, status: "open", childrenIds: [featureId], }), { projectId }, ); // Create feature (open status) await createObjectFile( testEnv.projectRoot, "feature", featureId, createObjectContent({ id: featureId, title: "Hierarchy Status Feature", parent: epicId, status: "open", childrenIds: [taskId], }), { projectId, epicId }, ); // Create task (open status) await createObjectFile( testEnv.projectRoot, "task", taskId, createObjectContent({ id: taskId, title: "Hierarchy Status Task", parent: featureId, status: "open", }), { projectId, epicId, featureId, status: "open" }, ); // Update task to in-progress const result = await client.callTool("update_issue", { id: taskId, status: "in-progress", }); expect(result.content[0].text).toContain("Successfully updated object"); const updatedTask = parseUpdateObjectResponse( result.content[0].text as string, ); expect(updatedTask.status).toBe("in-progress"); // Verify task file remains in open folder (in-progress is still "open") const taskFile = await readObjectFile( testEnv.projectRoot, `p/${projectId}/e/${epicId}/f/${featureId}/t/open/${taskId}.md`, ); expect(taskFile.yaml.status).toBe("in-progress"); // Verify feature was updated to in-progress const featureFile = await readObjectFile( testEnv.projectRoot, `p/${projectId}/e/${epicId}/f/${featureId}/${featureId}.md`, ); expect(featureFile.yaml.status).toBe("in-progress"); // Verify epic was updated to in-progress const epicFile = await readObjectFile( testEnv.projectRoot, `p/${projectId}/e/${epicId}/${epicId}.md`, ); expect(epicFile.yaml.status).toBe("in-progress"); // Verify project was updated to in-progress const projectFile = await readObjectFile( testEnv.projectRoot, `p/${projectId}/${projectId}.md`, ); expect(projectFile.yaml.status).toBe("in-progress"); }); it("should update partial hierarchy when intermediate parent is missing", async () => { // Create partial hierarchy: Feature -> Task (no parent for feature) const featureId = "F-partial-hierarchy"; const taskId = "T-partial-hierarchy"; // Create feature (open status) await createObjectFile( testEnv.projectRoot, "feature", featureId, createObjectContent({ id: featureId, title: "Partial Hierarchy Feature", status: "open", childrenIds: [taskId], }), ); // Create task (open status) await createObjectFile( testEnv.projectRoot, "task", taskId, createObjectContent({ id: taskId, title: "Partial Hierarchy Task", parent: featureId, status: "open", }), { featureId, status: "open" }, ); // Update task to in-progress const result = await client.callTool("update_issue", { id: taskId, status: "in-progress", }); expect(result.content[0].text).toContain("Successfully updated object"); const updatedTask = parseUpdateObjectResponse( result.content[0].text as string, ); expect(updatedTask.status).toBe("in-progress"); // Verify task file remains in open folder (in-progress is still "open") const taskFile = await readObjectFile( testEnv.projectRoot, `f/${featureId}/t/open/${taskId}.md`, ); expect(taskFile.yaml.status).toBe("in-progress"); // Verify feature was updated to in-progress const featureFile = await readObjectFile( testEnv.projectRoot, `f/${featureId}/${featureId}.md`, ); expect(featureFile.yaml.status).toBe("in-progress"); }); it("should not update parent hierarchy if parent is already in-progress", async () => { // Create hierarchy where parent is already in-progress const projectId = "P-already-in-progress-parent"; const epicId = "E-already-in-progress"; const featureId = "F-child-of-in-progress"; const taskId = "T-child-of-in-progress"; // Create project first await createObjectFile( testEnv.projectRoot, "project", projectId, createObjectContent({ id: projectId, title: "Already In Progress Parent Project", status: "open", childrenIds: [epicId], }), ); // Create epic (already in-progress) await createObjectFile( testEnv.projectRoot, "epic", epicId, createObjectContent({ id: epicId, title: "Already In Progress Epic", parent: projectId, status: "in-progress", childrenIds: [featureId], }), { projectId }, ); // Create feature (open status) await createObjectFile( testEnv.projectRoot, "feature", featureId, createObjectContent({ id: featureId, title: "Child Of In Progress Feature", parent: epicId, status: "open", childrenIds: [taskId], }), { projectId, epicId }, ); // Create task (open status) await createObjectFile( testEnv.projectRoot, "task", taskId, createObjectContent({ id: taskId, title: "Child Of In Progress Task", parent: featureId, status: "open", }), { projectId, epicId, featureId, status: "open" }, ); // Update task to in-progress const result = await client.callTool("update_issue", { id: taskId, status: "in-progress", }); expect(result.content[0].text).toContain("Successfully updated object"); // Verify task is in-progress (remains in open folder) const taskFile = await readObjectFile( testEnv.projectRoot, `p/${projectId}/e/${epicId}/f/${featureId}/t/open/${taskId}.md`, ); expect(taskFile.yaml.status).toBe("in-progress"); // Verify feature was updated to in-progress const featureFile = await readObjectFile( testEnv.projectRoot, `p/${projectId}/e/${epicId}/f/${featureId}/${featureId}.md`, ); expect(featureFile.yaml.status).toBe("in-progress"); // Verify epic remains in-progress (not changed) const epicFile = await readObjectFile( testEnv.projectRoot, `p/${projectId}/e/${epicId}/${epicId}.md`, ); expect(epicFile.yaml.status).toBe("in-progress"); }); it("should only update hierarchy when status changes to in-progress from non-in-progress", async () => { // Create simple hierarchy const featureId = "F-status-change-test"; const taskId = "T-status-change-test"; // Create feature (open status) await createObjectFile( testEnv.projectRoot, "feature", featureId, createObjectContent({ id: featureId, title: "Status Change Test Feature", status: "open", childrenIds: [taskId], }), ); // Create task (already in-progress) await createObjectFile( testEnv.projectRoot, "task", taskId, createObjectContent({ id: taskId, title: "Status Change Test Task", parent: featureId, status: "in-progress", }), { featureId, status: "open" }, ); // Update task priority but keep status as in-progress (no status change) const result = await client.callTool("update_issue", { id: taskId, priority: "high", status: "in-progress", // Same status }); expect(result.content[0].text).toContain("Successfully updated object"); // Verify feature status is unchanged (should still be open) const featureFile = await readObjectFile( testEnv.projectRoot, `f/${featureId}/${featureId}.md`, ); expect(featureFile.yaml.status).toBe("open"); }); }); describe("Auto-Complete Parent Hierarchy", () => { it("should auto-complete feature when all tasks are done via update", async () => { // Create complete hierarchy: Project -> Epic -> Feature -> Tasks const projectId = "P-auto-complete-done"; const epicId = "E-auto-complete-done"; const featureId = "F-auto-complete-done"; const task1Id = "T-auto-complete-1"; const task2Id = "T-auto-complete-2"; // Create project (open status) await createObjectFile( testEnv.projectRoot, "project", projectId, createObjectContent({ id: projectId, title: "Auto Complete Done Project", status: "open", childrenIds: [epicId], }), ); // Create epic (open status) await createObjectFile( testEnv.projectRoot, "epic", epicId, createObjectContent({ id: epicId, title: "Auto Complete Done Epic", parent: projectId, status: "open", childrenIds: [featureId], }), { projectId }, ); // Create feature (open status) await createObjectFile( testEnv.projectRoot, "feature", featureId, createObjectContent({ id: featureId, title: "Auto Complete Done Feature", parent: epicId, status: "open", childrenIds: [task1Id, task2Id], }), { projectId, epicId }, ); // Create first task (already done) await createObjectFile( testEnv.projectRoot, "task", task1Id, createObjectContent({ id: task1Id, title: "Auto Complete Task 1", parent: featureId, status: "done", }), { projectId, epicId, featureId, status: "closed" }, ); // Create second task (in-progress) await createObjectFile( testEnv.projectRoot, "task", task2Id, createObjectContent({ id: task2Id, title: "Auto Complete Task 2", parent: featureId, status: "in-progress", }), { projectId, epicId, featureId, status: "open" }, ); // Update second task to done - this should trigger auto-complete const result = await client.callTool("update_issue", { id: task2Id, status: "done", }); expect(result.content[0].text).toContain("Successfully updated object"); const updatedTask = parseUpdateObjectResponse( result.content[0].text as string, ); expect(updatedTask.status).toBe("done"); // Verify task2 file moved to closed folder const task2File = await readObjectFile( testEnv.projectRoot, `p/${projectId}/e/${epicId}/f/${featureId}/t/closed/${task2Id}.md`, ); expect(task2File.yaml.status).toBe("done"); // Verify feature was auto-completed const featureFile = await readObjectFile( testEnv.projectRoot, `p/${projectId}/e/${epicId}/f/${featureId}/${featureId}.md`, ); expect(featureFile.yaml.status).toBe("done"); // Verify epic was auto-completed (only one feature) const epicFile = await readObjectFile( testEnv.projectRoot, `p/${projectId}/e/${epicId}/${epicId}.md`, ); expect(epicFile.yaml.status).toBe("done"); // Verify project was auto-completed (only one epic) const projectFile = await readObjectFile( testEnv.projectRoot, `p/${projectId}/${projectId}.md`, ); expect(projectFile.yaml.status).toBe("done"); }); it("should auto-complete feature when mixing done and wont-do tasks via update", async () => { // Create hierarchy: Feature -> Tasks (mixed done/wont-do completion) const featureId = "F-auto-complete-mixed"; const task1Id = "T-mixed-done"; const task2Id = "T-mixed-wontdo"; const task3Id = "T-mixed-pending"; // Create feature (open status) await createObjectFile( testEnv.projectRoot, "feature", featureId, createObjectContent({ id: featureId, title: "Auto Complete Mixed Feature", status: "open", childrenIds: [task1Id, task2Id, task3Id], }), ); // Create first task (done) await createObjectFile( testEnv.projectRoot, "task", task1Id, createObjectContent({ id: task1Id, title: "Mixed Done Task", parent: featureId, status: "done", }), { featureId, status: "closed" }, ); // Create second task (wont-do) await createObjectFile( testEnv.projectRoot, "task", task2Id, createObjectContent({ id: task2Id, title: "Mixed Wont-Do Task", parent: featureId, status: "wont-do", }), { featureId, status: "closed" }, ); // Create third task (in-progress) await createObjectFile( testEnv.projectRoot, "task", task3Id, createObjectContent({ id: task3Id, title: "Mixed Pending Task", parent: featureId, status: "in-progress", }), { featureId, status: "open" }, ); // Update third task to wont-do - this should trigger auto-complete const result = await client.callTool("update_issue", { id: task3Id, status: "wont-do", }); expect(result.content[0].text).toContain("Successfully updated object"); const updatedTask = parseUpdateObjectResponse( result.content[0].text as string, ); expect(updatedTask.status).toBe("wont-do"); // Verify task3 file moved to closed folder const task3File = await readObjectFile( testEnv.projectRoot, `f/${featureId}/t/closed/${task3Id}.md`, ); expect(task3File.yaml.status).toBe("wont-do"); // Verify feature was auto-completed const featureFile = await readObjectFile( testEnv.projectRoot, `f/${featureId}/${featureId}.md`, ); expect(featureFile.yaml.status).toBe("done"); }); it("should not auto-complete when some children are still open", async () => { // Create hierarchy with one complete feature and one incomplete feature const projectId = "P-not-auto-complete"; const epicId = "E-not-auto-complete"; const feature1Id = "F-complete"; const feature2Id = "F-incomplete"; // Create project first await createObjectFile( testEnv.projectRoot, "project", projectId, createObjectContent({ id: projectId, title: "Not Auto Complete Project", status: "open", childrenIds: [epicId], }), ); // Create epic (open status) await createObjectFile( testEnv.projectRoot, "epic", epicId, createObjectContent({ id: epicId, title: "Not Auto Complete Epic", parent: projectId, status: "open", childrenIds: [feature1Id, feature2Id], }), { projectId }, ); // Create first feature (done) await createObjectFile( testEnv.projectRoot, "feature", feature1Id, createObjectContent({ id: feature1Id, title: "Complete Feature", parent: epicId, status: "done", childrenIds: [], }), { projectId, epicId }, ); // Create second feature (open - incomplete) await createObjectFile( testEnv.projectRoot, "feature", feature2Id, createObjectContent({ id: feature2Id, title: "Incomplete Feature", parent: epicId, status: "open", childrenIds: [], }), { projectId, epicId }, ); // Update first feature to done (already done, no status change) // This should NOT auto-complete the epic because feature2 is still open const result = await client.callTool("update_issue", { id: feature1Id, priority: "high", // Just update priority, keep status as done status: "done", }); expect(result.content[0].text).toContain("Successfully updated object"); // Verify epic status remains open (not auto-completed because feature2 is still open) const epicFile = await readObjectFile( testEnv.projectRoot, `p/${projectId}/e/${epicId}/${epicId}.md`, ); expect(epicFile.yaml.status).toBe("open"); }); it("should not trigger auto-complete when status doesn't change", async () => { // Create simple hierarchy const featureId = "F-no-status-change"; const taskId = "T-already-done"; // Create feature (open) await createObjectFile( testEnv.projectRoot, "feature", featureId, createObjectContent({ id: featureId, title: "No Status Change Feature", status: "open", childrenIds: [taskId], }), ); // Create task (already done) await createObjectFile( testEnv.projectRoot, "task", taskId, createObjectContent({ id: taskId, title: "Already Done Task", parent: featureId, status: "done", }), { featureId, status: "closed" }, ); // Update task priority but keep status as done (no status change) const result = await client.callTool("update_issue", { id: taskId, priority: "high", status: "done", // Same status }); expect(result.content[0].text).toContain("Successfully updated object"); // Verify feature status remains open (auto-complete should not trigger) const featureFile = await readObjectFile( testEnv.projectRoot, `f/${featureId}/${featureId}.md`, ); expect(featureFile.yaml.status).toBe("open"); }); it("should handle auto-complete when parent object is missing", async () => { // Create task with parent that doesn't exist const taskId = "T-orphaned-task"; const missingParentId = "F-missing-parent"; // Create task with non-existent parent await createObjectFile( testEnv.projectRoot, "task", taskId, createObjectContent({ id: taskId, title: "Orphaned Task", parent: missingParentId, status: "in-progress", }), ); // Update task to done (should fail due to missing parent validation) const result = await client.callTool("update_issue", { id: taskId, status: "done", }); // The system validates parent exists, so this should fail expect(result.content[0].text).toContain( "Parent object with ID 'F-missing-parent' not found", ); }); it("should auto-complete up the entire hierarchy", async () => { // Create deep hierarchy: Project -> Epic -> Feature -> Task const projectId = "P-deep-auto-complete"; const epicId = "E-deep-auto-complete"; const featureId = "F-deep-auto-complete"; const taskId = "T-deep-auto-complete"; // Create project await createObjectFile( testEnv.projectRoot, "project", projectId, createObjectContent({ id: projectId, title: "Deep Auto Complete Project", status: "open", childrenIds: [epicId], }), ); // Create epic await createObjectFile( testEnv.projectRoot, "epic", epicId, createObjectContent({ id: epicId, title: "Deep Auto Complete Epic", parent: projectId, status: "open", childrenIds: [featureId], }), { projectId }, ); // Create feature await createObjectFile( testEnv.projectRoot, "feature", featureId, createObjectContent({ id: featureId, title: "Deep Auto Complete Feature", parent: epicId, status: "open", childrenIds: [taskId], }), { projectId, epicId }, ); // Create task await createObjectFile( testEnv.projectRoot, "task", taskId, createObjectContent({ id: taskId, title: "Deep Auto Complete Task", parent: featureId, status: "open", }), { projectId, epicId, featureId, status: "open" }, ); // Update task to done - should trigger auto-complete all the way up const result = await client.callTool("update_issue", { id: taskId, status: "done", }); expect(result.content[0].text).toContain("Successfully updated object"); // Verify task is done const taskFile = await readObjectFile( testEnv.projectRoot, `p/${projectId}/e/${epicId}/f/${featureId}/t/closed/${taskId}.md`, ); expect(taskFile.yaml.status).toBe("done"); // Verify feature was auto-completed const featureFile = await readObjectFile( testEnv.projectRoot, `p/${projectId}/e/${epicId}/f/${featureId}/${featureId}.md`, ); expect(featureFile.yaml.status).toBe("done"); // Verify epic was auto-completed const epicFile = await readObjectFile( testEnv.projectRoot, `p/${projectId}/e/${epicId}/${epicId}.md`, ); expect(epicFile.yaml.status).toBe("done"); // Verify project was auto-completed const projectFile = await readObjectFile( testEnv.projectRoot, `p/${projectId}/${projectId}.md`, ); expect(projectFile.yaml.status).toBe("done"); }); it("should not auto-complete parent if it's already done", async () => { // Create hierarchy where parent is already done const featureId = "F-already-done-parent"; const taskId = "T-child-of-done"; // Create feature (already done) await createObjectFile( testEnv.projectRoot, "feature", featureId, createObjectContent({ id: featureId, title: "Already Done Parent Feature", status: "done", childrenIds: [taskId], }), ); // Create task (open) await createObjectFile( testEnv.projectRoot, "task", taskId, createObjectContent({ id: taskId, title: "Child of Done Task", parent: featureId, status: "open", }), { featureId, status: "open" }, ); // Update task to done const result = await client.callTool("update_issue", { id: taskId, status: "done", }); expect(result.content[0].text).toContain("Successfully updated object"); // Verify feature status remains done (unchanged) const featureFile = await readObjectFile( testEnv.projectRoot, `f/${featureId}/${featureId}.md`, ); expect(featureFile.yaml.status).toBe("done"); // Verify task is done const taskFile = await readObjectFile( testEnv.projectRoot, `f/${featureId}/t/closed/${taskId}.md`, ); expect(taskFile.yaml.status).toBe("done"); }); }); describe("Concurrent Updates", () => { it("should handle sequential updates to same object", async () => { // Create initial task await createObjectFile( testEnv.projectRoot, "task", "T-sequential", createObjectContent({ id: "T-sequential", title: "Sequential Updates Task", priority: "low", status: "draft", body: "Initial", }), ); // First update const result1 = await client.callTool("update_issue", { id: "T-sequential", priority: "medium", }); expect(result1.content[0].text).toContain("Successfully updated object"); // Second update const result2 = await client.callTool("update_issue", { id: "T-sequential", status: "open", }); expect(result2.content[0].text).toContain("Successfully updated object"); // Third update const result3 = await client.callTool("update_issue", { id: "T-sequential", body: "Final content after multiple updates", }); expect(result3.content[0].text).toContain("Successfully updated object"); // Verify final state const finalObject = parseUpdateObjectResponse( result3.content[0].text as string, ); expect(finalObject.priority).toBe("medium"); expect(finalObject.status).toBe("open"); expect(finalObject.body).toBe("Final content after multiple updates"); // Verify file persistence const file = await readObjectFile( testEnv.projectRoot, "t/open/T-sequential.md", ); expect(file.yaml.priority).toBe("medium"); expect(file.yaml.status).toBe("open"); expect(file.body).toBe("Final content after multiple updates"); }); }); });

MCP directory API

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

curl -X GET 'https://glama.ai/api/mcp/v1/servers/langadventurellc/task-trellis-mcp'

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