Skip to main content
Glama
tool-handlers.test.ts38 kB
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; import { handleToolCall } from '../../src/server/tool-handlers.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(); expect(parsed.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.plan.name).toBe('Updated Name'); }); it('plan set_active and get_active should work', async () => { const workspacePath = '/test-workspace-' + 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.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.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(); expect(parsed.solution.tradeoffs).toHaveLength(2); expect(parsed.solution.tradeoffs[0].aspect).toBe('Performance'); expect(parsed.solution.tradeoffs[0].pros).toEqual(['Fast', 'Efficient']); expect(parsed.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(); expect(parsed.solution.evaluation.effortEstimate.value).toBe(8); expect(parsed.solution.evaluation.effortEstimate.unit).toBe('hours'); expect(parsed.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(); expect(parsed.decision.alternativesConsidered).toHaveLength(2); expect(parsed.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.newDecision).toBeDefined(); expect(parsed.supersededDecision.status).toBe('superseded'); }); }); 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(); expect(parsed.phase.schedule.estimatedEffort.value).toBe(4); expect(parsed.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.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.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(); }); }); describe('Linking Tools', () => { it('link create should create link', async () => { const result = await handleToolCall( 'link', { action: 'create', planId: ctx.planId, sourceId: 'entity-1', targetId: 'entity-2', 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: 'entity-1', targetId: 'entity-2', relationType: 'implements', }, ctx.services ); const result = await handleToolCall( 'link', { action: 'get', planId: ctx.planId, entityId: 'entity-1' }, 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: 'entity-1', targetId: 'entity-2', 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(); expect(parsed.artifact.title).toBe('User Service'); expect(parsed.artifact.artifactType).toBe('code'); expect(parsed.artifact.status).toBe('draft'); }); it('artifact add should accept valid fileTable', 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);', }, fileTable: [ { 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(); expect(parsed.artifact.fileTable).toHaveLength(2); expect(parsed.artifact.fileTable[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 fileTable action', async () => { await expect( handleToolCall( 'artifact', { action: 'add', planId: ctx.planId, artifact: { title: 'Test Artifact', description: 'Test', artifactType: 'code', content: { language: 'ts', sourceCode: '' }, fileTable: [{ 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.artifact.title).toBe('Updated Title'); expect(parsed.artifact.status).toBe('reviewed'); expect(parsed.artifact.version).toBe(2); }); it('artifact update should reject invalid fileTable', async () => { const artifact = await createTestArtifact(ctx); await expect( handleToolCall( 'artifact', { action: 'update', planId: ctx.planId, artifactId: artifact.artifactId, updates: { fileTable: [{ 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(); }); }); });

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