Skip to main content
Glama
query-service.test.ts17.5 kB
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; import { QueryService } from '../../src/domain/services/query-service.js'; import { PlanService } from '../../src/domain/services/plan-service.js'; import { RequirementService } from '../../src/domain/services/requirement-service.js'; import { SolutionService } from '../../src/domain/services/solution-service.js'; import { PhaseService } from '../../src/domain/services/phase-service.js'; import { ArtifactService } from '../../src/domain/services/artifact-service.js'; import { LinkingService } from '../../src/domain/services/linking-service.js'; import { FileStorage } from '../../src/infrastructure/file-storage.js'; import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; describe('QueryService', () => { let queryService: QueryService; let planService: PlanService; let requirementService: RequirementService; let solutionService: SolutionService; let phaseService: PhaseService; let artifactService: ArtifactService; let linkingService: LinkingService; let storage: FileStorage; let testDir: string; let planId: string; beforeEach(async () => { testDir = path.join(os.tmpdir(), `mcp-query-test-${Date.now()}`); storage = new FileStorage(testDir); await storage.initialize(); planService = new PlanService(storage); requirementService = new RequirementService(storage, planService); solutionService = new SolutionService(storage, planService); phaseService = new PhaseService(storage, planService); artifactService = new ArtifactService(storage, planService); linkingService = new LinkingService(storage); queryService = new QueryService(storage, planService, linkingService); const plan = await planService.createPlan({ name: 'Test Plan', description: 'For testing queries', }); planId = plan.planId; }); afterEach(async () => { await fs.rm(testDir, { recursive: true, force: true }); }); describe('search_entities', () => { beforeEach(async () => { await requirementService.addRequirement({ planId, requirement: { title: 'User Authentication', description: 'Users must be able to log in securely', source: { type: 'user-request' }, priority: 'critical', category: 'functional', acceptanceCriteria: ['Login works', 'Session management'], }, }); await requirementService.addRequirement({ planId, requirement: { title: 'Data Export', description: 'Export data to CSV format', source: { type: 'user-request' }, priority: 'high', category: 'functional', acceptanceCriteria: ['CSV download works'], }, }); await solutionService.proposeSolution({ planId, solution: { title: 'OAuth Integration', description: 'Use OAuth 2.0 for authentication', approach: 'Integrate with OAuth providers', addressing: [], tradeoffs: [], evaluation: { effortEstimate: { value: 2, unit: 'days', confidence: 'medium' }, technicalFeasibility: 'high', riskAssessment: 'Low risk', }, }, }); }); it('should search entities by query', async () => { const result = await queryService.searchEntities({ planId, query: 'authentication', }); expect(result.results.length).toBeGreaterThan(0); expect(result.results.some((r) => r.entity.type === 'requirement')).toBe(true); }); it('should filter by entity type', async () => { const result = await queryService.searchEntities({ planId, query: 'authentication', entityTypes: ['solution'], }); expect(result.results.every((r) => r.entityType === 'solution')).toBe(true); }); it('should paginate results', async () => { const result = await queryService.searchEntities({ planId, query: 'a', // Broad query to match many limit: 1, offset: 0, }); expect(result.results).toHaveLength(1); expect(result.hasMore).toBe(true); }); it('should return relevance scores', async () => { const result = await queryService.searchEntities({ planId, query: 'authentication', }); for (const r of result.results) { expect(r.relevanceScore).toBeGreaterThan(0); expect(r.matchedFields.length).toBeGreaterThan(0); } }); it('should return empty results for no matches', async () => { const result = await queryService.searchEntities({ planId, query: 'nonexistent_xyz_12345', }); expect(result.results).toHaveLength(0); expect(result.total).toBe(0); }); }); describe('trace_requirement', () => { let reqId: string; let solId: string; let phaseId: string; beforeEach(async () => { const req = await requirementService.addRequirement({ planId, requirement: { title: 'User Auth', description: 'Authentication requirement', source: { type: 'user-request' }, priority: 'critical', category: 'functional', acceptanceCriteria: ['Login works'], }, }); reqId = req.requirementId; const sol = await solutionService.proposeSolution({ planId, solution: { title: 'OAuth Solution', description: 'OAuth implementation', approach: 'Use OAuth', addressing: [reqId], tradeoffs: [], evaluation: { effortEstimate: { value: 4, unit: 'hours', confidence: 'high' }, technicalFeasibility: 'high', riskAssessment: 'Low risk', }, }, }); solId = sol.solutionId; // Create link await linkingService.linkEntities({ planId, sourceId: solId, targetId: reqId, relationType: 'implements', }); const phase = await phaseService.addPhase({ planId, phase: { title: 'Auth Phase', description: 'Implement auth', objectives: ['Build auth'], deliverables: ['Auth system'], successCriteria: ['Tests pass'], }, }); phaseId = phase.phaseId; // Link phase to requirement await linkingService.linkEntities({ planId, sourceId: phaseId, targetId: reqId, relationType: 'addresses', }); }); it('should trace requirement to solutions', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, }); expect(result.requirement.id).toBe(reqId); expect(result.trace.proposedSolutions).toHaveLength(1); expect(result.trace.proposedSolutions[0].id).toBe(solId); }); it('should trace requirement to phases', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, }); expect(result.trace.implementingPhases).toHaveLength(1); expect(result.trace.implementingPhases[0].id).toBe(phaseId); }); it('should calculate completion status', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, }); expect(result.trace.completionStatus.isAddressed).toBe(true); expect(result.trace.completionStatus.isImplemented).toBe(false); expect(result.trace.completionStatus.completionPercentage).toBe(0); }); it('should throw for non-existent requirement', async () => { await expect( queryService.traceRequirement({ planId, requirementId: 'non-existent-id', }) ).rejects.toThrow('Requirement not found'); }); }); describe('validate_plan', () => { it('should detect uncovered requirements', async () => { await requirementService.addRequirement({ planId, requirement: { title: 'Uncovered Req', description: 'No solution addresses this', source: { type: 'user-request' }, priority: 'critical', category: 'functional', acceptanceCriteria: [], }, }); const result = await queryService.validatePlan({ planId }); expect(result.isValid).toBe(false); expect(result.issues.some((i) => i.type === 'uncovered_requirement')).toBe(true); }); it('should detect orphan solutions', async () => { await solutionService.proposeSolution({ planId, solution: { title: 'Orphan Solution', description: 'Not linked to anything', approach: 'Unknown', addressing: [], tradeoffs: [], evaluation: { effortEstimate: { value: 1, unit: 'hours', confidence: 'low' }, technicalFeasibility: 'medium', riskAssessment: 'Unknown', }, }, }); const result = await queryService.validatePlan({ planId }); expect(result.issues.some((i) => i.type === 'orphan_solution')).toBe(true); }); it('should return valid for clean plan', async () => { // Empty plan is valid const result = await queryService.validatePlan({ planId }); expect(result.isValid).toBe(true); expect(result.summary.errors).toBe(0); }); it('should track performed checks', async () => { const result = await queryService.validatePlan({ planId }); expect(result.checksPerformed).toContain('uncovered_requirements'); expect(result.checksPerformed).toContain('orphan_solutions'); expect(result.checksPerformed).toContain('missing_decisions'); expect(result.checksPerformed).toContain('broken_links'); }); }); describe('export_plan', () => { beforeEach(async () => { await requirementService.addRequirement({ planId, requirement: { title: 'Test Requirement', description: 'For export testing', source: { type: 'user-request' }, priority: 'high', category: 'functional', acceptanceCriteria: ['Works correctly'], }, }); await phaseService.addPhase({ planId, phase: { title: 'Phase 1', description: 'First phase', objectives: ['Build something'], deliverables: ['Working code'], successCriteria: ['Tests pass'], }, }); }); it('should export to JSON format', async () => { const result = await queryService.exportPlan({ planId, format: 'json', }); expect(result.format).toBe('json'); expect(result.sizeBytes).toBeGreaterThan(0); const parsed = JSON.parse(result.content); expect(parsed.manifest).toBeDefined(); expect(parsed.entities).toBeDefined(); }); it('should export to markdown format', async () => { const result = await queryService.exportPlan({ planId, format: 'markdown', }); expect(result.format).toBe('markdown'); expect(result.content).toContain('# Test Plan'); expect(result.content).toContain('## Requirements'); expect(result.content).toContain('Test Requirement'); }); it('should save export file', async () => { const result = await queryService.exportPlan({ planId, format: 'markdown', }); expect(result.filePath).toBeDefined(); // File should exist const content = await fs.readFile(result.filePath!, 'utf-8'); expect(content).toBe(result.content); }); it('should include phases in markdown', async () => { const result = await queryService.exportPlan({ planId, format: 'markdown', }); expect(result.content).toContain('## Phases'); expect(result.content).toContain('Phase 1'); }); it('should handle solutions without tradeoffs field (undefined)', async () => { // Simulate solution created without tradeoffs field (as happens via MCP tool) const solutions = await storage.loadEntities(planId, 'solutions'); // eslint-disable-next-line @typescript-eslint/no-explicit-any solutions.push({ id: 'solution-without-tradeoffs', type: 'solution', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), version: 1, metadata: { createdBy: 'test', tags: [], annotations: [], }, title: 'Solution Without Tradeoffs', description: 'This solution has no tradeoffs field', approach: 'Some approach', addressing: [], evaluation: { effortEstimate: { value: 1, unit: 'hours', confidence: 'high' }, technicalFeasibility: 'high', riskAssessment: 'Low', }, status: 'proposed', // NOTE: tradeoffs field is intentionally missing (undefined) } as any); await storage.saveEntities(planId, 'solutions', solutions); // This should NOT throw "Cannot read properties of undefined (reading 'length')" const result = await queryService.exportPlan({ planId, format: 'markdown', }); expect(result.format).toBe('markdown'); expect(result.content).toContain('## Solutions'); expect(result.content).toContain('Solution Without Tradeoffs'); }); it('should include artifacts in markdown', async () => { await artifactService.addArtifact({ planId, artifact: { title: 'User Service Implementation', description: 'Service for user management', artifactType: 'code', content: { language: 'typescript', sourceCode: 'export class UserService { }', filename: 'user-service.ts', }, fileTable: [ { path: 'src/services/user-service.ts', action: 'create', description: 'Main service file' }, ], }, }); const result = await queryService.exportPlan({ planId, format: 'markdown', }); expect(result.content).toContain('## Artifacts'); expect(result.content).toContain('User Service Implementation'); expect(result.content).toContain('typescript'); expect(result.content).toContain('user-service.ts'); }); it('should include artifact file table in markdown', async () => { await artifactService.addArtifact({ 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', description: 'User model' }, { path: 'src/types.ts', action: 'modify', description: 'Add user type' }, ], }, }); const result = await queryService.exportPlan({ planId, format: 'markdown', }); expect(result.content).toContain('## Artifacts'); expect(result.content).toContain('Database Migration'); expect(result.content).toContain('**Files**:'); expect(result.content).toContain('migrations/001_users.sql'); expect(result.content).toContain('[create]'); expect(result.content).toContain('[modify]'); }); }); describe('search_entities with artifacts', () => { it('should search in artifact content', async () => { await artifactService.addArtifact({ planId, artifact: { title: 'Authentication Handler', description: 'JWT authentication', artifactType: 'code', content: { language: 'typescript', sourceCode: 'function verifyJwtToken(token: string) { }', filename: 'auth-handler.ts', }, }, }); const result = await queryService.searchEntities({ planId, query: 'verifyJwtToken', }); expect(result.results.length).toBeGreaterThan(0); expect(result.results.some((r) => r.entity.type === 'artifact')).toBe(true); }); it('should filter search by artifact type', async () => { await artifactService.addArtifact({ planId, artifact: { title: 'Config File', description: 'App configuration', artifactType: 'config', content: { language: 'yaml', sourceCode: 'database: localhost', }, }, }); await requirementService.addRequirement({ planId, requirement: { title: 'Config Requirement', description: 'Need config support', source: { type: 'user-request' }, priority: 'high', category: 'functional', acceptanceCriteria: [], }, }); const result = await queryService.searchEntities({ planId, query: 'config', entityTypes: ['artifact'], }); expect(result.results.every((r) => r.entityType === 'artifact')).toBe(true); }); }); });

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