Skip to main content
Glama
tool-handlers.test.ts70.3 kB
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; import { handleToolCall } from '../../src/server/handlers/index.js'; import { createTestContext, cleanupTestContext, createTestRequirement, createTestSolution, createTestPhase, createTestDecision, createTestArtifact, type TestContext, } from '../helpers/test-utils.js'; /** * Integration tests for tool handlers. * * NOTE: These tests call handleToolCall() directly, bypassing MCP transport. * For E2E tests through the MCP protocol, see tests/e2e/mcp-all-tools.test.ts */ describe('Tool Handlers Integration', () => { let ctx: TestContext; beforeEach(async () => { ctx = await createTestContext('tool-handler-test'); }); afterEach(async () => { await cleanupTestContext(ctx); }); describe('Plan Management Tools', () => { it('plan create should create a new plan', async () => { const result = await handleToolCall( 'plan', { action: 'create', name: 'New Plan', description: 'Test' }, ctx.services ); expect(result.content[0].type).toBe('text'); const parsed = JSON.parse(result.content[0].text); expect(parsed.planId).toBeDefined(); // Verify via get const getResult = await handleToolCall( 'plan', { action: 'get', planId: parsed.planId }, ctx.services ); const getParsed = JSON.parse(getResult.content[0].text); expect(getParsed.plan.manifest.name).toBe('New Plan'); }); it('plan list should return plans', async () => { const result = await handleToolCall('plan', { action: 'list' }, ctx.services); const parsed = JSON.parse(result.content[0].text); expect(parsed.plans.length).toBeGreaterThan(0); }); it('plan get should return plan details', async () => { const result = await handleToolCall( 'plan', { action: 'get', planId: ctx.planId, includeEntities: true }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.plan.manifest.id).toBe(ctx.planId); expect(parsed.plan.entities).toBeDefined(); }); it('plan update should update plan', async () => { const result = await handleToolCall( 'plan', { action: 'update', planId: ctx.planId, updates: { name: 'Updated Name' } }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.success).toBe(true); // Verify via get const getResult = await handleToolCall( 'plan', { action: 'get', planId: ctx.planId }, ctx.services ); const getParsed = JSON.parse(getResult.content[0].text); expect(getParsed.plan.manifest.name).toBe('Updated Name'); }); it('plan set_active and get_active should work', async () => { const workspacePath = '/test-workspace-' + String(Date.now()); await handleToolCall( 'plan', { action: 'set_active', planId: ctx.planId, workspacePath }, ctx.services ); const result = await handleToolCall( 'plan', { action: 'get_active', workspacePath }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.activePlan).toBeDefined(); expect(parsed.activePlan.planId).toBe(ctx.planId); }); it('plan archive should archive plan', async () => { const result = await handleToolCall( 'plan', { action: 'archive', planId: ctx.planId, reason: 'Test' }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.success).toBe(true); }); }); describe('Requirement Tools', () => { it('requirement add should add requirement', async () => { const result = await handleToolCall( 'requirement', { action: 'add', planId: ctx.planId, requirement: { title: 'Test Req', description: 'Desc', source: { type: 'user-request' }, acceptanceCriteria: ['AC1'], priority: 'high', category: 'functional', }, }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.requirementId).toBeDefined(); }); it('requirement get should return requirement', async () => { const req = await createTestRequirement(ctx); const result = await handleToolCall( 'requirement', { action: 'get', planId: ctx.planId, requirementId: req.requirementId }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.requirement.id).toBe(req.requirementId); }); it('requirement list should return requirements', async () => { await createTestRequirement(ctx); const result = await handleToolCall( 'requirement', { action: 'list', planId: ctx.planId }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.requirements.length).toBeGreaterThan(0); }); it('requirement update should update', async () => { const req = await createTestRequirement(ctx); const result = await handleToolCall( 'requirement', { action: 'update', planId: ctx.planId, requirementId: req.requirementId, updates: { title: 'Updated' }, }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.success).toBe(true); // Verify via get const getResult = await handleToolCall( 'requirement', { action: 'get', planId: ctx.planId, requirementId: req.requirementId }, ctx.services ); const getParsed = JSON.parse(getResult.content[0].text); expect(getParsed.requirement.title).toBe('Updated'); }); it('requirement delete should delete', async () => { const req = await createTestRequirement(ctx); const result = await handleToolCall( 'requirement', { action: 'delete', planId: ctx.planId, requirementId: req.requirementId }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.success).toBe(true); }); it('requirement add should accept valid tags format', async () => { const result = await handleToolCall( 'requirement', { action: 'add', planId: ctx.planId, requirement: { title: 'Req with tags', description: 'Testing tags', source: { type: 'user-request' }, acceptanceCriteria: ['AC1'], priority: 'high', category: 'functional', tags: [ { key: 'priority', value: 'p1' }, { key: 'team', value: 'backend' }, ], }, }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.requirementId).toBeDefined(); }); it('requirement add should reject invalid tags format', async () => { await expect( handleToolCall( 'requirement', { action: 'add', planId: ctx.planId, requirement: { title: 'Req with invalid tags', description: 'Should fail', source: { type: 'user-request' }, acceptanceCriteria: ['AC1'], priority: 'high', category: 'functional', tags: [ { name: 'tag1', label: 'Label' }, // Invalid format! ], }, }, ctx.services ) ).rejects.toThrow(/tag|key|value/i); }); }); describe('Solution Tools', () => { it('solution propose should create solution', async () => { const result = await handleToolCall( 'solution', { action: 'propose', planId: ctx.planId, solution: { title: 'Solution', description: 'Desc', approach: 'Approach', addressing: [], tradeoffs: [], evaluation: { effortEstimate: { value: 1, unit: 'days', confidence: 'medium' }, technicalFeasibility: 'high', riskAssessment: 'Low', }, }, }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.solutionId).toBeDefined(); }); it('solution get should return solution', async () => { const sol = await createTestSolution(ctx); const result = await handleToolCall( 'solution', { action: 'get', planId: ctx.planId, solutionId: sol.solutionId }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.solution.id).toBe(sol.solutionId); }); it('solution compare should compare', async () => { const sol1 = await createTestSolution(ctx, [], { title: 'Solution 1' }); const sol2 = await createTestSolution(ctx, [], { title: 'Solution 2' }); const result = await handleToolCall( 'solution', { action: 'compare', planId: ctx.planId, solutionIds: [sol1.solutionId, sol2.solutionId] }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.comparison.solutions).toHaveLength(2); }); it('solution select should select', async () => { const sol = await createTestSolution(ctx); const result = await handleToolCall( 'solution', { action: 'select', planId: ctx.planId, solutionId: sol.solutionId, reason: 'Best fit' }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.success).toBe(true); // Verify via get const getResult = await handleToolCall( 'solution', { action: 'get', planId: ctx.planId, solutionId: sol.solutionId }, ctx.services ); const getParsed = JSON.parse(getResult.content[0].text); expect(getParsed.solution.status).toBe('selected'); }); it('solution delete should delete', async () => { const sol = await createTestSolution(ctx); const result = await handleToolCall( 'solution', { action: 'delete', planId: ctx.planId, solutionId: sol.solutionId }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.success).toBe(true); }); it('solution propose should accept valid tradeoffs format', async () => { const result = await handleToolCall( 'solution', { action: 'propose', planId: ctx.planId, solution: { title: 'Solution with tradeoffs', description: 'Testing tradeoffs validation', approach: 'Standard approach', addressing: [], tradeoffs: [ { aspect: 'Performance', pros: ['Fast', 'Efficient'], cons: ['Memory usage'], score: 8 }, { aspect: 'Maintainability', pros: ['Clean code'], cons: ['Learning curve'], score: 7 }, ], evaluation: { effortEstimate: { value: 1, unit: 'days', confidence: 'medium' }, technicalFeasibility: 'high', riskAssessment: 'Low', }, }, }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.solutionId).toBeDefined(); // Verify via get const getResult = await handleToolCall( 'solution', { action: 'get', planId: ctx.planId, solutionId: parsed.solutionId }, ctx.services ); const getParsed = JSON.parse(getResult.content[0].text); expect(getParsed.solution.tradeoffs).toHaveLength(2); expect(getParsed.solution.tradeoffs[0].aspect).toBe('Performance'); expect(getParsed.solution.tradeoffs[0].pros).toEqual(['Fast', 'Efficient']); expect(getParsed.solution.tradeoffs[0].cons).toEqual(['Memory usage']); }); it('solution propose should reject invalid tradeoffs format { pro, con }', async () => { await expect( handleToolCall( 'solution', { action: 'propose', planId: ctx.planId, solution: { title: 'Solution with invalid tradeoffs', description: 'Should fail', approach: 'Approach', addressing: [], tradeoffs: [ { pro: 'Some benefit', con: 'Some drawback' }, // Invalid format! ], evaluation: { effortEstimate: { value: 1, unit: 'days', confidence: 'medium' }, technicalFeasibility: 'high', riskAssessment: 'Low', }, }, }, ctx.services ) ).rejects.toThrow(/tradeoff|aspect|pros|cons/i); }); it('solution propose should accept valid effortEstimate format', async () => { const result = await handleToolCall( 'solution', { action: 'propose', planId: ctx.planId, solution: { title: 'Solution with valid effortEstimate', description: 'Testing effortEstimate validation', approach: 'Standard approach', addressing: [], tradeoffs: [], evaluation: { effortEstimate: { value: 8, unit: 'hours', confidence: 'high' }, technicalFeasibility: 'high', riskAssessment: 'Low', }, }, }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.solutionId).toBeDefined(); // Verify via get const getResult = await handleToolCall( 'solution', { action: 'get', planId: ctx.planId, solutionId: parsed.solutionId }, ctx.services ); const getParsed = JSON.parse(getResult.content[0].text); expect(getParsed.solution.evaluation.effortEstimate.value).toBe(8); expect(getParsed.solution.evaluation.effortEstimate.unit).toBe('hours'); expect(getParsed.solution.evaluation.effortEstimate.confidence).toBe('high'); }); it('solution propose should reject invalid effortEstimate format', async () => { await expect( handleToolCall( 'solution', { action: 'propose', planId: ctx.planId, solution: { title: 'Solution with invalid effortEstimate', description: 'Should fail', approach: 'Approach', addressing: [], tradeoffs: [], evaluation: { effortEstimate: { hours: 8, complexity: 'medium' }, // Invalid format! technicalFeasibility: 'high', riskAssessment: 'Low', }, }, }, ctx.services ) ).rejects.toThrow(/effortEstimate|value|unit|confidence/i); }); it('solution update should reject invalid effortEstimate format', async () => { // First create a valid solution const createResult = await handleToolCall( 'solution', { action: 'propose', planId: ctx.planId, solution: { title: 'Solution for update test', description: 'Valid solution', approach: 'Approach', addressing: [], tradeoffs: [], evaluation: { effortEstimate: { value: 8, unit: 'hours', confidence: 'high' }, technicalFeasibility: 'high', riskAssessment: 'Low', }, }, }, ctx.services ); const { solutionId } = JSON.parse(createResult.content[0].text); // Then try to update with invalid effortEstimate await expect( handleToolCall( 'solution', { action: 'update', planId: ctx.planId, solutionId, updates: { evaluation: { effortEstimate: { hours: 16, complexity: 'low' }, // Invalid format! technicalFeasibility: 'medium', riskAssessment: 'Medium', }, }, }, ctx.services ) ).rejects.toThrow(/effortEstimate|value|unit|confidence/i); }); it('solution update should reject invalid tags format', async () => { // First create a valid solution const createResult = await handleToolCall( 'solution', { action: 'propose', planId: ctx.planId, solution: { title: 'Solution for tags update test', description: 'Valid solution', approach: 'Approach', addressing: [], tradeoffs: [], evaluation: { effortEstimate: { value: 4, unit: 'hours', confidence: 'medium' }, technicalFeasibility: 'high', riskAssessment: 'Low', }, }, }, ctx.services ); const { solutionId } = JSON.parse(createResult.content[0].text); // Then try to update with invalid tags await expect( handleToolCall( 'solution', { action: 'update', planId: ctx.planId, solutionId, updates: { tags: [{ name: 'tag1', label: 'Label' }], // Invalid format! }, }, ctx.services ) ).rejects.toThrow(/tag|key|value/i); }); it('solution update should reject invalid tradeoffs format', async () => { // First create a valid solution const createResult = await handleToolCall( 'solution', { action: 'propose', planId: ctx.planId, solution: { title: 'Solution for tradeoffs update test', description: 'Valid solution', approach: 'Approach', addressing: [], tradeoffs: [], evaluation: { effortEstimate: { value: 2, unit: 'days', confidence: 'low' }, technicalFeasibility: 'high', riskAssessment: 'Low', }, }, }, ctx.services ); const { solutionId } = JSON.parse(createResult.content[0].text); // Then try to update with invalid tradeoffs await expect( handleToolCall( 'solution', { action: 'update', planId: ctx.planId, solutionId, updates: { tradeoffs: [{ pro: 'Good', con: 'Bad' }], // Invalid format! }, }, ctx.services ) ).rejects.toThrow(/tradeoff|aspect|pros|cons/i); }); }); describe('Decision Tools', () => { it('decision record should create decision', async () => { const result = await handleToolCall( 'decision', { action: 'record', planId: ctx.planId, decision: { title: 'Decision', question: 'What?', context: 'Context', decision: 'Answer', alternativesConsidered: [], }, }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.decisionId).toBeDefined(); }); it('decision record should accept valid alternativesConsidered format', async () => { const result = await handleToolCall( 'decision', { action: 'record', planId: ctx.planId, decision: { title: 'Decision with alternatives', question: 'Which approach?', context: 'Context', decision: 'Option A', alternativesConsidered: [ { option: 'Option B', reasoning: 'Simpler', whyNotChosen: 'Less flexible' }, { option: 'Option C', reasoning: 'Faster' }, ], }, }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.decisionId).toBeDefined(); // Verify via get const getResult = await handleToolCall( 'decision', { action: 'get', planId: ctx.planId, decisionId: parsed.decisionId }, ctx.services ); const getParsed = JSON.parse(getResult.content[0].text); expect(getParsed.decision.alternativesConsidered).toHaveLength(2); expect(getParsed.decision.alternativesConsidered[0].option).toBe('Option B'); }); it('decision record should reject invalid alternativesConsidered format', async () => { await expect( handleToolCall( 'decision', { action: 'record', planId: ctx.planId, decision: { title: 'Decision with invalid alternatives', question: 'What?', context: 'Context', decision: 'Answer', alternativesConsidered: [ { name: 'Option A', desc: 'Description' }, // Invalid format! ], }, }, ctx.services ) ).rejects.toThrow(/alternativesConsidered|option|reasoning/i); }); it('decision get should return decision', async () => { const dec = await createTestDecision(ctx); const result = await handleToolCall( 'decision', { action: 'get', planId: ctx.planId, decisionId: dec.decisionId }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.decision.id).toBe(dec.decisionId); }); it('decision list should return decisions', async () => { await createTestDecision(ctx); const result = await handleToolCall( 'decision', { action: 'list', planId: ctx.planId }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.decisions.length).toBeGreaterThan(0); }); it('decision supersede should create new decision', async () => { const dec = await createTestDecision(ctx); const result = await handleToolCall( 'decision', { action: 'supersede', planId: ctx.planId, decisionId: dec.decisionId, newDecision: { decision: 'New answer' }, reason: 'Changed requirements', }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.success).toBe(true); expect(parsed.newDecisionId).toBeDefined(); expect(parsed.supersededDecisionId).toBe(dec.decisionId); // Verify old decision is superseded const getOld = await handleToolCall( 'decision', { action: 'get', planId: ctx.planId, decisionId: dec.decisionId }, ctx.services ); const oldParsed = JSON.parse(getOld.content[0].text); expect(oldParsed.decision.status).toBe('superseded'); // Verify new decision exists const getNew = await handleToolCall( 'decision', { action: 'get', planId: ctx.planId, decisionId: parsed.newDecisionId }, ctx.services ); const newParsed = JSON.parse(getNew.content[0].text); expect(newParsed.decision.decision).toBe('New answer'); }); }); describe('Phase Tools', () => { it('phase add should create phase', async () => { const result = await handleToolCall( 'phase', { action: 'add', planId: ctx.planId, phase: { title: 'Phase 1', description: 'Desc', objectives: ['Obj1'], deliverables: ['Del1'], successCriteria: ['Crit1'], }, }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.phaseId).toBeDefined(); }); it('phase add should accept valid estimatedEffort format', async () => { const result = await handleToolCall( 'phase', { action: 'add', planId: ctx.planId, phase: { title: 'Phase with effort', description: 'Testing estimatedEffort', objectives: ['Obj1'], deliverables: ['Del1'], successCriteria: ['Crit1'], estimatedEffort: { value: 4, unit: 'days', confidence: 'medium' }, }, }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.phaseId).toBeDefined(); // Verify via get (use fields=['*'] to get schedule) const getResult = await handleToolCall( 'phase', { action: 'get', planId: ctx.planId, phaseId: parsed.phaseId, fields: ['*'] }, ctx.services ); const getParsed = JSON.parse(getResult.content[0].text); expect(getParsed.phase.schedule.estimatedEffort.value).toBe(4); expect(getParsed.phase.schedule.estimatedEffort.unit).toBe('days'); }); it('phase add should reject invalid estimatedEffort format', async () => { await expect( handleToolCall( 'phase', { action: 'add', planId: ctx.planId, phase: { title: 'Phase with invalid effort', description: 'Should fail', objectives: ['Obj1'], deliverables: ['Del1'], successCriteria: ['Crit1'], estimatedEffort: { hours: 8 }, // Invalid format! }, }, ctx.services ) ).rejects.toThrow(/effortEstimate|estimatedEffort|value|unit|confidence/i); }); it('phase get_tree should return tree', async () => { await createTestPhase(ctx); const result = await handleToolCall( 'phase', { action: 'get_tree', planId: ctx.planId }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.tree.length).toBeGreaterThan(0); }); it('phase update_status should update status', async () => { const phase = await createTestPhase(ctx); const result = await handleToolCall( 'phase', { action: 'update_status', planId: ctx.planId, phaseId: phase.phaseId, status: 'in_progress' }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.success).toBe(true); // Verify via get const getResult = await handleToolCall( 'phase', { action: 'get', planId: ctx.planId, phaseId: phase.phaseId }, ctx.services ); const getParsed = JSON.parse(getResult.content[0].text); expect(getParsed.phase.status).toBe('in_progress'); }); it('phase move should move phase', async () => { const parent = await createTestPhase(ctx, { title: 'Parent' }); const child = await createTestPhase(ctx, { title: 'Child' }); const result = await handleToolCall( 'phase', { action: 'move', planId: ctx.planId, phaseId: child.phaseId, newParentId: parent.phaseId }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.success).toBe(true); // Verify via get const getResult = await handleToolCall( 'phase', { action: 'get', planId: ctx.planId, phaseId: child.phaseId }, ctx.services ); const getParsed = JSON.parse(getResult.content[0].text); expect(getParsed.phase.parentId).toBe(parent.phaseId); }); it('phase delete should delete phase', async () => { const phase = await createTestPhase(ctx); const result = await handleToolCall( 'phase', { action: 'delete', planId: ctx.planId, phaseId: phase.phaseId }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.success).toBe(true); }); it('phase get_next_actions should return actions', async () => { await createTestPhase(ctx); const result = await handleToolCall( 'phase', { action: 'get_next_actions', planId: ctx.planId }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.actions).toBeDefined(); }); // Sprint 5 - Order/Path Bug Fix Integration Tests describe('order/path persistence (Sprint 5 - Bug Fix)', () => { it('should generate correct order after adding multiple phases', async () => { // Add 3 phases sequentially const phase1 = await handleToolCall( 'phase', { action: 'add', planId: ctx.planId, phase: { title: 'Phase 1', description: 'First' } }, ctx.services ); const phase2 = await handleToolCall( 'phase', { action: 'add', planId: ctx.planId, phase: { title: 'Phase 2', description: 'Second' } }, ctx.services ); const phase3 = await handleToolCall( 'phase', { action: 'add', planId: ctx.planId, phase: { title: 'Phase 3', description: 'Third' } }, ctx.services ); const p1 = JSON.parse(phase1.content[0].text); const p2 = JSON.parse(phase2.content[0].text); const p3 = JSON.parse(phase3.content[0].text); // Verify via get const get1 = await handleToolCall('phase', { action: 'get', planId: ctx.planId, phaseId: p1.phaseId }, ctx.services); const get2 = await handleToolCall('phase', { action: 'get', planId: ctx.planId, phaseId: p2.phaseId }, ctx.services); const get3 = await handleToolCall('phase', { action: 'get', planId: ctx.planId, phaseId: p3.phaseId }, ctx.services); const getParsed1 = JSON.parse(get1.content[0].text); const getParsed2 = JSON.parse(get2.content[0].text); const getParsed3 = JSON.parse(get3.content[0].text); expect(getParsed1.phase.order).toBe(1); expect(getParsed2.phase.order).toBe(2); expect(getParsed3.phase.order).toBe(3); expect(getParsed1.phase.path).toBe('1'); expect(getParsed2.phase.path).toBe('2'); expect(getParsed3.phase.path).toBe('3'); }); it('should calculate order based on max existing order after delete', async () => { // Add 3 phases await handleToolCall( 'phase', { action: 'add', planId: ctx.planId, phase: { title: 'Phase 1', description: 'First' } }, ctx.services ); const phase2 = await handleToolCall( 'phase', { action: 'add', planId: ctx.planId, phase: { title: 'Phase 2', description: 'Second' } }, ctx.services ); const phase3 = await handleToolCall( 'phase', { action: 'add', planId: ctx.planId, phase: { title: 'Phase 3', description: 'Third' } }, ctx.services ); const p2 = JSON.parse(phase2.content[0].text); JSON.parse(phase3.content[0].text); // Delete phase 2 await handleToolCall( 'phase', { action: 'delete', planId: ctx.planId, phaseId: p2.phaseId }, ctx.services ); // Add new phase - should get order 4 (max=3, +1), not 3 (count=2, +1) const phase4 = await handleToolCall( 'phase', { action: 'add', planId: ctx.planId, phase: { title: 'Phase 4', description: 'Fourth' } }, ctx.services ); const p4 = JSON.parse(phase4.content[0].text); // Verify via get const get4 = await handleToolCall('phase', { action: 'get', planId: ctx.planId, phaseId: p4.phaseId }, ctx.services); const getParsed4 = JSON.parse(get4.content[0].text); expect(getParsed4.phase.order).toBe(4); expect(getParsed4.phase.path).toBe('4'); }); it('should maintain unique paths for all phases in tree', async () => { // Add parent phases const parent1 = await handleToolCall( 'phase', { action: 'add', planId: ctx.planId, phase: { title: 'Parent 1', description: 'P1' } }, ctx.services ); const parent2 = await handleToolCall( 'phase', { action: 'add', planId: ctx.planId, phase: { title: 'Parent 2', description: 'P2' } }, ctx.services ); const p1 = JSON.parse(parent1.content[0].text); const p2 = JSON.parse(parent2.content[0].text); // Add children to parent1 const child1 = await handleToolCall( 'phase', { action: 'add', planId: ctx.planId, phase: { title: 'Child 1.1', description: 'C1', parentId: p1.phaseId }, }, ctx.services ); const child2 = await handleToolCall( 'phase', { action: 'add', planId: ctx.planId, phase: { title: 'Child 1.2', description: 'C2', parentId: p1.phaseId }, }, ctx.services ); const c1 = JSON.parse(child1.content[0].text); const c2 = JSON.parse(child2.content[0].text); // Verify paths via get const get1 = await handleToolCall('phase', { action: 'get', planId: ctx.planId, phaseId: p1.phaseId }, ctx.services); const get2 = await handleToolCall('phase', { action: 'get', planId: ctx.planId, phaseId: p2.phaseId }, ctx.services); const getC1 = await handleToolCall('phase', { action: 'get', planId: ctx.planId, phaseId: c1.phaseId }, ctx.services); const getC2 = await handleToolCall('phase', { action: 'get', planId: ctx.planId, phaseId: c2.phaseId }, ctx.services); const getParsed1 = JSON.parse(get1.content[0].text); const getParsed2 = JSON.parse(get2.content[0].text); const getParsedC1 = JSON.parse(getC1.content[0].text); const getParsedC2 = JSON.parse(getC2.content[0].text); expect(getParsed1.phase.path).toBe('1'); expect(getParsed2.phase.path).toBe('2'); expect(getParsedC1.phase.path).toBe('1.1'); expect(getParsedC2.phase.path).toBe('1.2'); // Get tree and verify all paths const tree = await handleToolCall( 'phase', { action: 'get_tree', planId: ctx.planId }, ctx.services ); const treeData = JSON.parse(tree.content[0].text); const allPaths = new Set<string>(); const collectPaths = ( nodes: { phase: { path: string }; children?: { phase: { path: string } }[] }[] ): void => { for (const node of nodes) { expect(allPaths.has(node.phase.path)).toBe(false); // Should not have duplicates allPaths.add(node.phase.path); if (node.children !== undefined && node.children.length > 0) { collectPaths( node.children as { phase: { path: string }; children?: { phase: { path: string } }[]; }[] ); } } }; // eslint-disable-next-line @typescript-eslint/no-unsafe-argument collectPaths(treeData.tree); expect(allPaths.size).toBe(4); // 2 parents + 2 children }); it('should handle order gaps when adding child phases after delete', async () => { // Add parent const parent = await handleToolCall( 'phase', { action: 'add', planId: ctx.planId, phase: { title: 'Parent', description: 'P' } }, ctx.services ); const p = JSON.parse(parent.content[0].text); // Add 3 children const child1 = await handleToolCall( 'phase', { action: 'add', planId: ctx.planId, phase: { title: 'Child 1', description: 'C1', parentId: p.phaseId }, }, ctx.services ); const child2 = await handleToolCall( 'phase', { action: 'add', planId: ctx.planId, phase: { title: 'Child 2', description: 'C2', parentId: p.phaseId }, }, ctx.services ); const child3 = await handleToolCall( 'phase', { action: 'add', planId: ctx.planId, phase: { title: 'Child 3', description: 'C3', parentId: p.phaseId }, }, ctx.services ); JSON.parse(child1.content[0].text); const c2 = JSON.parse(child2.content[0].text); JSON.parse(child3.content[0].text); // Delete child 2 (creates gap: 1, _, 3) await handleToolCall( 'phase', { action: 'delete', planId: ctx.planId, phaseId: c2.phaseId }, ctx.services ); // Add new child - should get order 4 (max=3, +1), not 3 (count=2, +1) const child4 = await handleToolCall( 'phase', { action: 'add', planId: ctx.planId, phase: { title: 'Child 4', description: 'C4', parentId: p.phaseId }, }, ctx.services ); const c4 = JSON.parse(child4.content[0].text); // Verify via get const get4 = await handleToolCall('phase', { action: 'get', planId: ctx.planId, phaseId: c4.phaseId }, ctx.services); const getParsed4 = JSON.parse(get4.content[0].text); expect(getParsed4.phase.order).toBe(4); expect(getParsed4.phase.path).toBe('1.4'); }); it('should persist order correctly when using explicit order', async () => { // Add phase with explicit order=10 const phase1 = await handleToolCall( 'phase', { action: 'add', planId: ctx.planId, phase: { title: 'Phase 10', description: 'Explicit order', order: 10 }, }, ctx.services ); const p1 = JSON.parse(phase1.content[0].text); // Verify via get const get1 = await handleToolCall('phase', { action: 'get', planId: ctx.planId, phaseId: p1.phaseId }, ctx.services); const getParsed1 = JSON.parse(get1.content[0].text); expect(getParsed1.phase.order).toBe(10); expect(getParsed1.phase.path).toBe('10'); // Add auto-ordered phase - should get order 11 (max=10, +1) const phase2 = await handleToolCall( 'phase', { action: 'add', planId: ctx.planId, phase: { title: 'Phase Auto', description: 'Auto order' } }, ctx.services ); const p2 = JSON.parse(phase2.content[0].text); // Verify via get const get2 = await handleToolCall('phase', { action: 'get', planId: ctx.planId, phaseId: p2.phaseId }, ctx.services); const getParsed2 = JSON.parse(get2.content[0].text); expect(getParsed2.phase.order).toBe(11); expect(getParsed2.phase.path).toBe('11'); }); }); }); describe('Linking Tools', () => { let solutionId: string; let requirementId: string; beforeEach(async () => { // Create real entities for link tests const req = await createTestRequirement(ctx, { title: 'Test Requirement' }); requirementId = req.requirementId; const sol = await createTestSolution(ctx, [], { title: 'Test Solution' }); solutionId = sol.solutionId; }); it('link create should create link', async () => { const result = await handleToolCall( 'link', { action: 'create', planId: ctx.planId, sourceId: solutionId, targetId: requirementId, relationType: 'implements', }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.linkId).toBeDefined(); }); it('link get should return links', async () => { await handleToolCall( 'link', { action: 'create', planId: ctx.planId, sourceId: solutionId, targetId: requirementId, relationType: 'implements', }, ctx.services ); const result = await handleToolCall( 'link', { action: 'get', planId: ctx.planId, entityId: solutionId }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.links.length).toBeGreaterThan(0); }); it('link delete should remove link', async () => { const link = await handleToolCall( 'link', { action: 'create', planId: ctx.planId, sourceId: solutionId, targetId: requirementId, relationType: 'implements', }, ctx.services ); const linkId = JSON.parse(link.content[0].text).linkId; const result = await handleToolCall( 'link', { action: 'delete', planId: ctx.planId, linkId }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.success).toBe(true); }); }); describe('Query Tools', () => { it('query search should search', async () => { await createTestRequirement(ctx, { title: 'Authentication' }); const result = await handleToolCall( 'query', { action: 'search', planId: ctx.planId, query: 'auth' }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.results.length).toBeGreaterThan(0); }); it('query trace should trace', async () => { const req = await createTestRequirement(ctx); const result = await handleToolCall( 'query', { action: 'trace', planId: ctx.planId, requirementId: req.requirementId }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.requirement.id).toBe(req.requirementId); }); it('query validate should validate', async () => { const result = await handleToolCall( 'query', { action: 'validate', planId: ctx.planId }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.checksPerformed).toBeDefined(); }); it('query export should export to markdown', async () => { const result = await handleToolCall( 'query', { action: 'export', planId: ctx.planId, format: 'markdown' }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.format).toBe('markdown'); expect(parsed.content).toContain('# Test Plan'); }); it('query export should export to json', async () => { const result = await handleToolCall( 'query', { action: 'export', planId: ctx.planId, format: 'json' }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.format).toBe('json'); }); it('query export markdown should include tradeoffs correctly', async () => { // Create solution with tradeoffs await handleToolCall( 'solution', { action: 'propose', planId: ctx.planId, solution: { title: 'Solution for export test', description: 'Testing markdown export', approach: 'Standard approach', addressing: [], tradeoffs: [ { aspect: 'Performance', pros: ['Fast', 'Scalable'], cons: ['Memory heavy'], score: 8 }, ], evaluation: { effortEstimate: { value: 1, unit: 'days', confidence: 'medium' }, technicalFeasibility: 'high', riskAssessment: 'Low', }, }, }, ctx.services ); const result = await handleToolCall( 'query', { action: 'export', planId: ctx.planId, format: 'markdown' }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.format).toBe('markdown'); expect(parsed.content).toContain('Trade-offs'); expect(parsed.content).toContain('Performance'); expect(parsed.content).toContain('Fast'); expect(parsed.content).toContain('Scalable'); expect(parsed.content).toContain('Memory heavy'); }); it('query health should return status', async () => { const result = await handleToolCall('query', { action: 'health' }, ctx.services); const parsed = JSON.parse(result.content[0].text); expect(parsed.status).toBe('healthy'); expect(parsed.version).toBe('1.0.0'); }); }); describe('Artifact Tools', () => { it('artifact add should create artifact', async () => { const result = await handleToolCall( 'artifact', { action: 'add', planId: ctx.planId, artifact: { title: 'User Service', description: 'Service for user management', artifactType: 'code', content: { language: 'typescript', sourceCode: 'export class UserService { }', filename: 'user-service.ts', }, }, }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.artifactId).toBeDefined(); // Verify via get const getResult = await handleToolCall( 'artifact', { action: 'get', planId: ctx.planId, artifactId: parsed.artifactId }, ctx.services ); const getParsed = JSON.parse(getResult.content[0].text); expect(getParsed.artifact.title).toBe('User Service'); expect(getParsed.artifact.artifactType).toBe('code'); expect(getParsed.artifact.status).toBe('draft'); }); it('artifact add should accept valid targets', async () => { const result = await handleToolCall( 'artifact', { action: 'add', planId: ctx.planId, artifact: { title: 'Database Migration', description: 'Add users table', artifactType: 'migration', content: { language: 'sql', sourceCode: 'CREATE TABLE users (id INT PRIMARY KEY);', }, targets: [ { path: 'migrations/001_users.sql', action: 'create', description: 'User table migration' }, { path: 'src/models/user.ts', action: 'create' }, ], }, }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.artifactId).toBeDefined(); // Verify via get const getResult = await handleToolCall( 'artifact', { action: 'get', planId: ctx.planId, artifactId: parsed.artifactId }, ctx.services ); const getParsed = JSON.parse(getResult.content[0].text); expect(getParsed.artifact.targets).toHaveLength(2); expect(getParsed.artifact.targets[0].action).toBe('create'); }); it('artifact add should reject invalid artifactType', async () => { await expect( handleToolCall( 'artifact', { action: 'add', planId: ctx.planId, artifact: { title: 'Invalid Artifact', description: 'Should fail', artifactType: 'invalid-type', content: { language: 'ts', sourceCode: '' }, }, }, ctx.services ) ).rejects.toThrow(/artifactType/i); }); it('artifact add should reject invalid targets action', async () => { await expect( handleToolCall( 'artifact', { action: 'add', planId: ctx.planId, artifact: { title: 'Test Artifact', description: 'Test', artifactType: 'code', content: { language: 'ts', sourceCode: '' }, targets: [{ path: 'file.ts', action: 'invalid-action' }], }, }, ctx.services ) ).rejects.toThrow(/action/i); }); it('artifact get should return artifact', async () => { const artifact = await createTestArtifact(ctx); const result = await handleToolCall( 'artifact', { action: 'get', planId: ctx.planId, artifactId: artifact.artifactId }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.artifact.id).toBe(artifact.artifactId); }); it('artifact update should update artifact', async () => { const artifact = await createTestArtifact(ctx); const result = await handleToolCall( 'artifact', { action: 'update', planId: ctx.planId, artifactId: artifact.artifactId, updates: { title: 'Updated Title', status: 'reviewed', }, }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.success).toBe(true); // Verify via get const getResult = await handleToolCall( 'artifact', { action: 'get', planId: ctx.planId, artifactId: artifact.artifactId }, ctx.services ); const getParsed = JSON.parse(getResult.content[0].text); expect(getParsed.artifact.title).toBe('Updated Title'); expect(getParsed.artifact.status).toBe('reviewed'); expect(getParsed.artifact.version).toBe(2); }); it('artifact update should reject invalid targets', async () => { const artifact = await createTestArtifact(ctx); await expect( handleToolCall( 'artifact', { action: 'update', planId: ctx.planId, artifactId: artifact.artifactId, updates: { targets: [{ path: '', action: 'create' }], // Empty path }, }, ctx.services ) ).rejects.toThrow(/path/i); }); it('artifact list should return artifacts', async () => { await createTestArtifact(ctx, { title: 'Artifact 1', artifactType: 'code' }); await createTestArtifact(ctx, { title: 'Artifact 2', artifactType: 'config' }); const result = await handleToolCall( 'artifact', { action: 'list', planId: ctx.planId }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.artifacts).toHaveLength(2); }); it('artifact list should filter by artifactType', async () => { await createTestArtifact(ctx, { title: 'Code Artifact', artifactType: 'code' }); await createTestArtifact(ctx, { title: 'Config Artifact', artifactType: 'config' }); const result = await handleToolCall( 'artifact', { action: 'list', planId: ctx.planId, filters: { artifactType: 'code' } }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.artifacts).toHaveLength(1); expect(parsed.artifacts[0].title).toBe('Code Artifact'); }); it('artifact delete should delete artifact', async () => { const artifact = await createTestArtifact(ctx); const result = await handleToolCall( 'artifact', { action: 'delete', planId: ctx.planId, artifactId: artifact.artifactId }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.success).toBe(true); // Verify deletion const list = await handleToolCall( 'artifact', { action: 'list', planId: ctx.planId }, ctx.services ); const listParsed = JSON.parse(list.content[0].text); expect(listParsed.artifacts).toHaveLength(0); }); it('artifact get should throw for non-existent plan', async () => { await expect( handleToolCall( 'artifact', { action: 'get', planId: 'non-existent-plan', artifactId: 'any' }, ctx.services ) ).rejects.toThrow(/Plan not found/i); }); it('artifact get should throw for non-existent artifact', async () => { await expect( handleToolCall( 'artifact', { action: 'get', planId: ctx.planId, artifactId: 'non-existent-artifact' }, ctx.services ) ).rejects.toThrow(/artifact.*not found/i); }); }); describe('Error Handling', () => { it('should throw McpError for unknown tool', async () => { await expect( handleToolCall('unknown_tool', {}, ctx.services) ).rejects.toThrow('Unknown tool'); }); it('should throw McpError for unknown action', async () => { await expect( handleToolCall('plan', { action: 'unknown_action' }, ctx.services) ).rejects.toThrow('Unknown action'); }); it('should throw McpError for missing requirement', async () => { await expect( handleToolCall( 'requirement', { action: 'get', planId: ctx.planId, requirementId: 'non-existent' }, ctx.services ) ).rejects.toThrow(); }); }); /** * Sprint 6 - Minimal Return Values Integration Tests * * These tests verify that CREATE/UPDATE operations return only * minimal data (IDs and success flags) instead of full objects * to reduce context pollution. */ describe('Minimal Return Values (Sprint 6)', () => { describe('CREATE operations should return only ID', () => { it('plan create should not return manifest or full plan object', async () => { const result = await handleToolCall( 'plan', { action: 'create', name: 'Minimal Test', description: 'Test' }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.planId).toBeDefined(); expect(parsed).not.toHaveProperty('manifest'); expect(parsed).not.toHaveProperty('plan'); expect(parsed).not.toHaveProperty('createdAt'); }); it('requirement add should not return full requirement object', async () => { const result = await handleToolCall( 'requirement', { action: 'add', planId: ctx.planId, requirement: { title: 'Minimal Req', description: 'Test', source: { type: 'user-request' }, acceptanceCriteria: ['AC1'], priority: 'high', category: 'functional', }, }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.requirementId).toBeDefined(); expect(parsed).not.toHaveProperty('requirement'); }); it('solution propose should not return full solution object', async () => { const result = await handleToolCall( 'solution', { action: 'propose', planId: ctx.planId, solution: { title: 'Minimal Solution', description: 'Test', approach: 'Approach', addressing: [], tradeoffs: [], evaluation: { effortEstimate: { value: 1, unit: 'days', confidence: 'medium' }, technicalFeasibility: 'high', riskAssessment: 'Low', }, }, }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.solutionId).toBeDefined(); expect(parsed).not.toHaveProperty('solution'); }); it('decision record should not return full decision object', async () => { const result = await handleToolCall( 'decision', { action: 'record', planId: ctx.planId, decision: { title: 'Minimal Decision', question: 'What?', context: 'Context', decision: 'Answer', alternativesConsidered: [], }, }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.decisionId).toBeDefined(); expect(parsed).not.toHaveProperty('decision'); }); it('phase add should not return full phase object', async () => { const result = await handleToolCall( 'phase', { action: 'add', planId: ctx.planId, phase: { title: 'Minimal Phase', description: 'Test', objectives: ['Obj1'], deliverables: ['Del1'], successCriteria: ['Crit1'], }, }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.phaseId).toBeDefined(); expect(parsed).not.toHaveProperty('phase'); }); it('artifact add should not return full artifact object', async () => { const result = await handleToolCall( 'artifact', { action: 'add', planId: ctx.planId, artifact: { title: 'Minimal Artifact', description: 'Test', artifactType: 'code', content: { language: 'typescript', sourceCode: 'const x = 1;', }, }, }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.artifactId).toBeDefined(); expect(parsed).not.toHaveProperty('artifact'); }); it('link create should not return full link object', async () => { // Create real entities for link test const req = await createTestRequirement(ctx, { title: 'Link Test Requirement' }); const sol = await createTestSolution(ctx, [], { title: 'Link Test Solution' }); const result = await handleToolCall( 'link', { action: 'create', planId: ctx.planId, sourceId: sol.solutionId, targetId: req.requirementId, relationType: 'implements', }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.linkId).toBeDefined(); expect(parsed).not.toHaveProperty('link'); }); }); describe('UPDATE operations should return only success and ID', () => { it('plan update should not return full plan object', async () => { const result = await handleToolCall( 'plan', { action: 'update', planId: ctx.planId, updates: { name: 'Updated' } }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.success).toBe(true); expect(parsed).not.toHaveProperty('plan'); expect(parsed).not.toHaveProperty('updatedAt'); }); it('requirement update should not return full requirement object', async () => { const req = await createTestRequirement(ctx); const result = await handleToolCall( 'requirement', { action: 'update', planId: ctx.planId, requirementId: req.requirementId, updates: { title: 'Updated' }, }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.success).toBe(true); expect(parsed).not.toHaveProperty('requirement'); }); it('solution update should not return full solution object', async () => { const sol = await createTestSolution(ctx); const result = await handleToolCall( 'solution', { action: 'update', planId: ctx.planId, solutionId: sol.solutionId, updates: { title: 'Updated' }, }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.success).toBe(true); expect(parsed).not.toHaveProperty('solution'); }); it('solution select should not return full solution objects', async () => { const sol = await createTestSolution(ctx); const result = await handleToolCall( 'solution', { action: 'select', planId: ctx.planId, solutionId: sol.solutionId, reason: 'Best' }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.success).toBe(true); expect(parsed).not.toHaveProperty('solution'); expect(parsed).not.toHaveProperty('deselected'); }); it('phase update should not return full phase object', async () => { const phase = await createTestPhase(ctx); const result = await handleToolCall( 'phase', { action: 'update', planId: ctx.planId, phaseId: phase.phaseId, updates: { title: 'Updated' }, }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.success).toBe(true); expect(parsed).not.toHaveProperty('phase'); }); it('phase update_status should not return full phase object', async () => { const phase = await createTestPhase(ctx); const result = await handleToolCall( 'phase', { action: 'update_status', planId: ctx.planId, phaseId: phase.phaseId, status: 'in_progress' }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.success).toBe(true); expect(parsed).not.toHaveProperty('phase'); }); it('artifact update should not return full artifact object', async () => { const artifact = await createTestArtifact(ctx); const result = await handleToolCall( 'artifact', { action: 'update', planId: ctx.planId, artifactId: artifact.artifactId, updates: { title: 'Updated' }, }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.success).toBe(true); expect(parsed).not.toHaveProperty('artifact'); }); }); describe('SPECIAL operations should return only success and IDs', () => { it('decision supersede should not return full decision objects', async () => { const dec = await createTestDecision(ctx); const result = await handleToolCall( 'decision', { action: 'supersede', planId: ctx.planId, decisionId: dec.decisionId, newDecision: { decision: 'New answer' }, reason: 'Changed requirements', }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.success).toBe(true); expect(parsed).not.toHaveProperty('newDecision'); expect(parsed).not.toHaveProperty('supersededDecision'); }); it('phase move should not return full phase object', async () => { const parent = await createTestPhase(ctx, { title: 'Parent' }); const child = await createTestPhase(ctx, { title: 'Child' }); const result = await handleToolCall( 'phase', { action: 'move', planId: ctx.planId, phaseId: child.phaseId, newParentId: parent.phaseId }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.success).toBe(true); expect(parsed).not.toHaveProperty('phase'); }); }); }); describe('Batch Tools', () => { /** * BUG-026 E2E Test: Empty operations validation * RED → GREEN → REFACTOR → VERIFY * * E2E test through MCP tool handler */ it('BUG-026 E2E: batch with empty operations should throw ValidationError', async () => { await expect( handleToolCall( 'batch', { planId: ctx.planId, operations: [] }, ctx.services ) ).rejects.toThrow('operations array cannot be empty'); }); /** * BUG-001 E2E Test: Temp ID resolution in addressing * Verify temp IDs resolve correctly through full stack */ it('BUG-001 E2E: batch should resolve temp IDs in solution addressing', async () => { const result = await handleToolCall( 'batch', { planId: ctx.planId, operations: [ { entityType: 'requirement', payload: { tempId: '$0', title: 'Test Req', description: 'Test requirement', source: { type: 'user-request' }, acceptanceCriteria: [], priority: 'high', category: 'functional' } }, { entityType: 'solution', payload: { title: 'Test Solution', description: 'Addresses requirement', approach: 'Use library X', addressing: ['$0'] } } ] }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.results).toHaveLength(2); expect(parsed.results[0].success).toBe(true); expect(parsed.results[1].success).toBe(true); // Verify tempIdMapping populated expect(parsed.tempIdMapping).toHaveProperty('$0'); expect(parsed.tempIdMapping.$0).toMatch(/^[a-f0-9-]{36}$/i); // Verify solution addressing contains resolved ID const solutionResult = await handleToolCall( 'solution', { action: 'get', planId: ctx.planId, solutionId: parsed.results[1].id }, ctx.services ); const solution = JSON.parse(solutionResult.content[0].text); expect(solution.solution.addressing).toContain(parsed.tempIdMapping.$0); expect(solution.solution.addressing).not.toContain('$0'); }); /** * BUG-008 E2E Test: tempIdMapping populated * Verify tempIdMapping returned through full stack */ it('BUG-008 E2E: batch should return tempIdMapping with all temp IDs', async () => { const result = await handleToolCall( 'batch', { planId: ctx.planId, operations: [ { entityType: 'requirement', payload: { tempId: '$0', title: 'Req with tempId', description: 'Test', source: { type: 'user-request' }, acceptanceCriteria: [], priority: 'high', category: 'functional' } }, { entityType: 'phase', payload: { tempId: '$1', title: 'Phase with tempId', description: 'Test phase' } } ] }, ctx.services ); const parsed = JSON.parse(result.content[0].text); // Verify tempIdMapping is not empty expect(parsed.tempIdMapping).not.toEqual({}); expect(parsed.tempIdMapping).toHaveProperty('$0'); expect(parsed.tempIdMapping).toHaveProperty('$1'); expect(parsed.tempIdMapping.$0).toMatch(/^[a-f0-9-]{36}$/i); expect(parsed.tempIdMapping.$1).toMatch(/^[a-f0-9-]{36}$/i); }); /** * BUG-001 REAL ISSUE E2E Test: Nested payload format (requirement handler style) * Verify batch supports nested format with action + entity object * This is the format users expect based on individual tool handlers */ it('BUG-001 REAL: batch should support nested payload format with action', async () => { const result = await handleToolCall( 'batch', { planId: ctx.planId, operations: [ { entityType: 'requirement', payload: { action: 'add', requirement: { tempId: '$0', title: 'Test Requirement', description: 'Test requirement description', source: { type: 'user-request' }, acceptanceCriteria: [], priority: 'high', category: 'functional' } } }, { entityType: 'solution', payload: { action: 'propose', solution: { tempId: '$1', title: 'Test Solution', description: 'Addresses requirement', approach: 'Use library X', addressing: ['$0'] } } } ] }, ctx.services ); const parsed = JSON.parse(result.content[0].text); expect(parsed.results).toHaveLength(2); expect(parsed.results[0].success).toBe(true); expect(parsed.results[1].success).toBe(true); // Verify tempIdMapping populated expect(parsed.tempIdMapping).toHaveProperty('$0'); expect(parsed.tempIdMapping).toHaveProperty('$1'); expect(parsed.tempIdMapping.$0).toMatch(/^[a-f0-9-]{36}$/i); expect(parsed.tempIdMapping.$1).toMatch(/^[a-f0-9-]{36}$/i); // Verify temp ID resolution in addressing const solutionResult = await handleToolCall( 'solution', { action: 'get', planId: ctx.planId, solutionId: parsed.results[1].id }, ctx.services ); const solution = JSON.parse(solutionResult.content[0].text); expect(solution.solution.addressing).toContain(parsed.tempIdMapping.$0); expect(solution.solution.addressing).not.toContain('$0'); }); }); });

Latest Blog Posts

MCP directory API

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

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cppmyjob/cpp-mcp-planner'

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