Skip to main content
Glama
query-service.test.ts103 kB
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; import { QueryService } from '../../src/domain/services/query-service.js'; import type { ValidatePlanInput } 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 { RepositoryFactory } from '../../src/infrastructure/factory/repository-factory.js'; import { FileLockManager } from '../../src/infrastructure/repositories/file/file-lock-manager.js'; import type { Requirement, Solution, Phase, Entity, EntityType } from '../../src/domain/entities/types.js'; // Helper functions for loading/saving entities via repository async function loadEntities<T extends Entity>( repositoryFactory: RepositoryFactory, planId: string, entityType: 'requirements' | 'solutions' | 'phases' | 'artifacts' ): Promise<T[]> { const typeMap: Record<string, EntityType> = { requirements: 'requirement', solutions: 'solution', phases: 'phase', artifacts: 'artifact' }; const repo = repositoryFactory.createRepository<T>(typeMap[entityType], planId); return repo.findAll(); } async function saveEntities( repositoryFactory: RepositoryFactory, planId: string, entityType: 'requirements' | 'solutions' | 'phases' | 'artifacts', entities: Entity[] ): Promise<void> { const typeMap: Record<string, EntityType> = { requirements: 'requirement', solutions: 'solution', phases: 'phase', artifacts: 'artifact' }; const repo = repositoryFactory.createRepository<Entity>(typeMap[entityType], planId); for (const entity of entities) { await repo.update(entity.id, entity); } } 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 repositoryFactory: RepositoryFactory; let lockManager: FileLockManager; let testDir: string; let planId: string; beforeEach(async () => { testDir = path.join(os.tmpdir(), `mcp-query-test-${String(Date.now())}`); lockManager = new FileLockManager(testDir); await lockManager.initialize(); repositoryFactory = new RepositoryFactory({ type: 'file', baseDir: testDir, lockManager, cacheOptions: { enabled: true, ttl: 5000, maxSize: 1000 } }); const planRepo = repositoryFactory.createPlanRepository(); await planRepo.initialize(); planService = new PlanService(repositoryFactory); linkingService = new LinkingService(repositoryFactory); // BUG-046 FIX: Pass linkingService to services for proper cascading deletion requirementService = new RequirementService(repositoryFactory, planService, undefined, linkingService); solutionService = new SolutionService(repositoryFactory, planService, undefined, undefined, linkingService); phaseService = new PhaseService(repositoryFactory, planService, undefined, linkingService); artifactService = new ArtifactService(repositoryFactory, planService, undefined, linkingService); queryService = new QueryService(repositoryFactory, planService, linkingService); const plan = await planService.createPlan({ name: 'Test Plan', description: 'For testing queries', }); planId = plan.planId; }); afterEach(async () => { await repositoryFactory.dispose(); await lockManager.dispose(); 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); }); // RED: BUG #15 - searchEntities crashes when entity has undefined description/approach/context it('should handle entities with undefined searchable fields', async () => { // Create entities with missing description/approach/context fields const repo = repositoryFactory.createRepository('solution', planId); const solutionWithMissingFields = { id: 'sol-missing-desc', type: 'solution' as const, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), version: 1, metadata: { createdBy: 'test', tags: [], annotations: [] }, title: 'Solution Without Description', // description: undefined - intentionally missing // approach: undefined - intentionally missing addressing: [], tradeoffs: [], evaluation: { effortEstimate: { value: 1, unit: 'hours' as const, confidence: 'high' as const }, technicalFeasibility: 'high' as const, riskAssessment: 'Low', }, status: 'proposed' as const, }; await repo.create(solutionWithMissingFields); // This should NOT crash - it should handle undefined fields gracefully const result = await queryService.searchEntities({ planId, query: 'solution', }); expect(result).toBeDefined(); expect(result.results.length).toBeGreaterThan(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/i); }); }); 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, validationLevel: 'strict' }); 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'); }); // RED TEST 1: Phase status logic - child completed but parent planned it('should detect child.status=completed but parent.status=planned', async () => { const { phaseId: parentId } = await phaseService.addPhase({ planId, phase: { title: 'Parent Phase', description: 'Parent', objectives: [], deliverables: [], successCriteria: [], }, }); const { phaseId: childId } = await phaseService.addPhase({ planId, phase: { title: 'Child Phase', description: 'Child', parentId, objectives: [], deliverables: [], successCriteria: [], }, }); // Set child to completed await phaseService.updatePhaseStatus({ planId, phaseId: childId, status: 'completed', progress: 100 }); const result = await queryService.validatePlan({ planId }); const issue = result.issues.find(i => i.type === 'invalid_phase_status' && i.entityId === childId); expect(issue).toBeDefined(); if (issue === undefined) throw new Error('issue is required'); expect(issue.severity).toBe('error'); expect(issue.message).toContain('Child Phase'); expect(issue.message).toContain('completed'); expect(issue.message).toContain('Parent Phase'); expect(issue.message).toContain('planned'); }); // RED TEST 2: Phase status logic - child in_progress but parent planned it('should detect child.status=in_progress but parent.status=planned', async () => { const { phaseId: parentId } = await phaseService.addPhase({ planId, phase: { title: 'Parent Phase', description: 'Parent', objectives: [], deliverables: [], successCriteria: [], }, }); const { phaseId: childId } = await phaseService.addPhase({ planId, phase: { title: 'Child Phase', description: 'Child', parentId, objectives: [], deliverables: [], successCriteria: [], }, }); // Set child to in_progress await phaseService.updatePhaseStatus({ planId, phaseId: childId, status: 'in_progress', progress: 50 }); const result = await queryService.validatePlan({ planId, validationLevel: 'strict' }); const issue = result.issues.find(i => i.type === 'invalid_phase_status' && i.entityId === childId); expect(issue).toBeDefined(); if (issue === undefined) throw new Error('issue is required'); expect(issue.severity).toBe('warning'); expect(issue.message).toContain('in progress'); expect(issue.message).toContain('planned'); }); // GREEN TEST 3: Phase status logic - child completed and parent completed (valid) it('should pass validation for child.status=completed and parent.status=completed', async () => { const { phaseId: parentId } = await phaseService.addPhase({ planId, phase: { title: 'Parent Phase', description: 'Parent', objectives: [], deliverables: [], successCriteria: [], }, }); const { phaseId: childId } = await phaseService.addPhase({ planId, phase: { title: 'Child Phase', description: 'Child', parentId, objectives: [], deliverables: [], successCriteria: [], }, }); // Set both to completed await phaseService.updatePhaseStatus({ planId, phaseId: childId, status: 'completed', progress: 100 }); await phaseService.updatePhaseStatus({ planId, phaseId: parentId, status: 'completed', progress: 100 }); const result = await queryService.validatePlan({ planId }); const statusIssues = result.issues.filter(i => i.type === 'invalid_phase_status' && i.entityId === childId); expect(statusIssues.length).toBe(0); }); // GREEN TEST 4: Phase status logic - child in_progress and parent in_progress (valid) it('should pass validation for child.status=in_progress and parent.status=in_progress', async () => { const { phaseId: parentId } = await phaseService.addPhase({ planId, phase: { title: 'Parent Phase', description: 'Parent', objectives: [], deliverables: [], successCriteria: [], }, }); const { phaseId: childId } = await phaseService.addPhase({ planId, phase: { title: 'Child Phase', description: 'Child', parentId, objectives: [], deliverables: [], successCriteria: [], }, }); // Set both to in_progress await phaseService.updatePhaseStatus({ planId, phaseId: parentId, status: 'in_progress', progress: 50 }); await phaseService.updatePhaseStatus({ planId, phaseId: childId, status: 'in_progress', progress: 30 }); const result = await queryService.validatePlan({ planId }); const statusIssues = result.issues.filter(i => i.type === 'invalid_phase_status' && i.entityId === childId); expect(statusIssues.length).toBe(0); }); // RED TEST 5: All children completed but parent not completed it('should detect all children completed but parent.status!=completed', async () => { const { phaseId: parentId } = await phaseService.addPhase({ planId, phase: { title: 'Parent Phase', description: 'Parent', objectives: [], deliverables: [], successCriteria: [], }, }); // Create 3 child phases const childIds = []; for (let i = 1; i <= 3; i++) { const { phaseId } = await phaseService.addPhase({ planId, phase: { title: `Child Phase ${String(i)}`, description: `Child ${String(i)}`, parentId, objectives: [], deliverables: [], successCriteria: [], }, }); childIds.push(phaseId); } // Set all children to completed for (const childId of childIds) { await phaseService.updatePhaseStatus({ planId, phaseId: childId, status: 'completed', progress: 100 }); } // Parent remains in_progress await phaseService.updatePhaseStatus({ planId, phaseId: parentId, status: 'in_progress', progress: 80 }); const result = await queryService.validatePlan({ planId, validationLevel: 'strict' }); const issue = result.issues.find(i => i.type === 'parent_should_complete' && i.entityId === parentId); expect(issue).toBeDefined(); if (issue === undefined) throw new Error('issue is required'); expect(issue.severity).toBe('info'); expect(issue.message).toContain('all children completed'); expect(issue.message).toContain('Parent Phase'); }); // RED TEST 6: File existence - detect missing file in artifact.targets it('should detect missing file in artifact.targets', async () => { const artifactService = new (await import('../../src/domain/services/artifact-service.js')).ArtifactService(repositoryFactory, planService); await artifactService.addArtifact({ planId, artifact: { title: 'Code Artifact', description: 'Implementation', artifactType: 'code', targets: [ { path: 'src/nonexistent.ts', action: 'modify', description: 'Nonexistent file' }, ], }, }); const result = await queryService.validatePlan({ planId, validationLevel: 'strict' }); const issue = result.issues.find(i => i.type === 'missing_file'); expect(issue).toBeDefined(); if (issue === undefined) throw new Error('issue is required'); expect(issue.severity).toBe('warning'); expect(issue.filePath).toBe('src/nonexistent.ts'); expect(issue.message).toContain('src/nonexistent.ts'); expect(issue.message).toContain('modify'); }); // GREEN TEST 7: File existence - skip files with action='create' it('should NOT check files with action=create (will be created)', async () => { const artifactService = new (await import('../../src/domain/services/artifact-service.js')).ArtifactService(repositoryFactory, planService); await artifactService.addArtifact({ planId, artifact: { title: 'New Code', description: 'New implementation', artifactType: 'code', targets: [ { path: 'src/new-file.ts', action: 'create', description: 'Will be created' }, ], }, }); const result = await queryService.validatePlan({ planId }); const missingFileIssues = result.issues.filter(i => i.type === 'missing_file'); expect(missingFileIssues.length).toBe(0); }); // RED TEST 8: File existence - check files with action='modify' exist it('should check that files with action=modify exist', async () => { const artifactService = new (await import('../../src/domain/services/artifact-service.js')).ArtifactService(repositoryFactory, planService); const fs = await import('fs/promises'); const path = await import('path'); // Create a file in a relative path location const relativePath = 'test-data/existing.ts'; const tmpDir = path.join(process.cwd(), 'test-data'); await fs.mkdir(tmpDir, { recursive: true }); const tmpFile = path.join(tmpDir, 'existing.ts'); await fs.writeFile(tmpFile, 'content'); try { await artifactService.addArtifact({ planId, artifact: { title: 'Modify Existing', description: 'Modify existing file', artifactType: 'code', targets: [ { path: relativePath, action: 'modify', description: 'Existing file' }, ], }, }); const result = await queryService.validatePlan({ planId }); const missingFileIssue = result.issues.find(i => i.type === 'missing_file' && i.filePath === relativePath); expect(missingFileIssue).toBeUndefined(); } finally { await fs.rm(tmpDir, { recursive: true, force: true }); } }); // GREEN TEST 9: File existence - skip check if artifact.targets is undefined it('should skip file check if artifact.targets is undefined', async () => { const artifactService = new (await import('../../src/domain/services/artifact-service.js')).ArtifactService(repositoryFactory, planService); await artifactService.addArtifact({ planId, artifact: { title: 'No File Table', description: 'Artifact without targets', artifactType: 'documentation', }, }); const result = await queryService.validatePlan({ planId }); const missingFileIssues = result.issues.filter(i => i.type === 'missing_file'); expect(missingFileIssues.length).toBe(0); }); // RED TEST 11: Requirement coverage - solution.status='selected' without implementing phases it('should detect solution.status=selected without implementing phases', async () => { const { requirementId } = await requirementService.addRequirement({ planId, requirement: { title: 'Test Requirement', description: 'Req', source: { type: 'user-request' }, priority: 'high', category: 'functional', acceptanceCriteria: [], }, }); const { solutionId } = await solutionService.proposeSolution({ planId, solution: { title: 'Selected Solution', description: 'Sol', addressing: [requirementId], approach: 'Approach', tradeoffs: [], evaluation: { effortEstimate: { value: 1, unit: 'hours', confidence: 'medium' }, technicalFeasibility: 'high', riskAssessment: 'Low risk', }, }, }); // Select the solution await solutionService.selectSolution({ planId, solutionId, reason: 'Best approach' }); // Create link const linkingService = new (await import('../../src/domain/services/linking-service.js')).LinkingService(repositoryFactory); await linkingService.linkEntities({ planId, sourceId: solutionId, targetId: requirementId, relationType: 'implements', }); // NO phases created -> issue expected const result = await queryService.validatePlan({ planId, validationLevel: 'strict' }); const issue = result.issues.find(i => i.type === 'unimplemented_solution' && i.entityId === solutionId); expect(issue).toBeDefined(); if (issue === undefined) throw new Error('issue is required'); expect(issue.severity).toBe('warning'); expect(issue.message).toContain('Selected Solution'); expect(issue.message).toContain('no implementing phases'); }); // GREEN TEST 12: Requirement coverage - solution with implementing phase passes it('should pass validation for solution with implementing phase', async () => { const { requirementId } = await requirementService.addRequirement({ planId, requirement: { title: 'Test Requirement', description: 'Req', source: { type: 'user-request' }, priority: 'high', category: 'functional', acceptanceCriteria: [], }, }); const { solutionId } = await solutionService.proposeSolution({ planId, solution: { title: 'Selected Solution', description: 'Sol', addressing: [requirementId], approach: 'Approach', tradeoffs: [], evaluation: { effortEstimate: { value: 1, unit: 'hours', confidence: 'medium' }, technicalFeasibility: 'high', riskAssessment: 'Low risk', }, }, }); await solutionService.selectSolution({ planId, solutionId, reason: 'Best' }); // Create phase const { phaseId } = await phaseService.addPhase({ planId, phase: { title: 'Implementation Phase', description: 'Implements requirement', objectives: [], deliverables: [], successCriteria: [], }, }); // Link phase to requirement const linkingService = new (await import('../../src/domain/services/linking-service.js')).LinkingService(repositoryFactory); await linkingService.linkEntities({ planId, sourceId: phaseId, targetId: requirementId, relationType: 'addresses', }); const result = await queryService.validatePlan({ planId }); const unimplementedIssues = result.issues.filter(i => i.type === 'unimplemented_solution' && i.entityId === solutionId); expect(unimplementedIssues.length).toBe(0); }); // GREEN TEST 13: Requirement coverage - ignore solution.status='proposed' (not selected) it('should ignore solution.status=proposed (not selected)', async () => { const { requirementId } = await requirementService.addRequirement({ planId, requirement: { title: 'Test Requirement', description: 'Req', source: { type: 'user-request' }, priority: 'high', category: 'functional', acceptanceCriteria: [], }, }); await solutionService.proposeSolution({ planId, solution: { title: 'Proposed Solution', description: 'Not selected', addressing: [requirementId], approach: 'Approach', tradeoffs: [], evaluation: { effortEstimate: { value: 1, unit: 'hours', confidence: 'medium' }, technicalFeasibility: 'medium', riskAssessment: 'Low risk', }, }, }); // Solution remains 'proposed', not selected -> no issue expected const result = await queryService.validatePlan({ planId }); const unimplementedIssues = result.issues.filter(i => i.type === 'unimplemented_solution'); expect(unimplementedIssues.length).toBe(0); }); // RED TEST 14: Validation level - ValidatePlanInput accepts validationLevel it('should accept validationLevel parameter in ValidatePlanInput', () => { // This test verifies TypeScript compilation const input: ValidatePlanInput = { planId, validationLevel: 'strict', }; expect(input.validationLevel).toBe('strict'); }); // RED TEST 15: Validation level - basic returns only severity='error' it('should return only severity=error when validationLevel=basic', async () => { // Create issues with different severities // Error: uncovered requirement await requirementService.addRequirement({ planId, requirement: { title: 'Uncovered Req', description: 'No solution', source: { type: 'user-request' }, priority: 'high', category: 'functional', acceptanceCriteria: [], }, }); // Warning: child in_progress, parent planned const { phaseId: parentId } = await phaseService.addPhase({ planId, phase: { title: 'Parent', description: 'Parent', objectives: [], deliverables: [], successCriteria: [], }, }); const { phaseId: childId } = await phaseService.addPhase({ planId, phase: { title: 'Child', description: 'Child', parentId, objectives: [], deliverables: [], successCriteria: [], }, }); await phaseService.updatePhaseStatus({ planId, phaseId: childId, status: 'in_progress', progress: 50 }); const result = await queryService.validatePlan({ planId, validationLevel: 'basic' }); expect(result.issues.every(i => i.severity === 'error')).toBe(true); expect(result.summary.errors).toBeGreaterThan(0); expect(result.summary.warnings).toBe(0); expect(result.summary.infos).toBe(0); }); // RED TEST 16: Validation level - strict returns ALL issues it('should return ALL issues when validationLevel=strict', async () => { // Create issues with different severities // Error: uncovered requirement await requirementService.addRequirement({ planId, requirement: { title: 'Uncovered Req', description: 'No solution', source: { type: 'user-request' }, priority: 'high', category: 'functional', acceptanceCriteria: [], }, }); // Warning: child in_progress, parent planned const { phaseId: parentId } = await phaseService.addPhase({ planId, phase: { title: 'Parent', description: 'Parent', objectives: [], deliverables: [], successCriteria: [], }, }); const { phaseId: childId } = await phaseService.addPhase({ planId, phase: { title: 'Child', description: 'Child', parentId, objectives: [], deliverables: [], successCriteria: [], }, }); await phaseService.updatePhaseStatus({ planId, phaseId: childId, status: 'in_progress', progress: 50 }); const result = await queryService.validatePlan({ planId, validationLevel: 'strict' }); expect(result.summary.errors).toBeGreaterThan(0); expect(result.summary.warnings).toBeGreaterThan(0); }); // GREEN TEST 17: Validation level - default is 'basic' it('should use validationLevel=basic by default', async () => { // Create a warning issue: child in_progress, parent planned const { phaseId: parentId } = await phaseService.addPhase({ planId, phase: { title: 'Parent', description: 'Parent', objectives: [], deliverables: [], successCriteria: [], }, }); const { phaseId: childId } = await phaseService.addPhase({ planId, phase: { title: 'Child', description: 'Child', parentId, objectives: [], deliverables: [], successCriteria: [], }, }); await phaseService.updatePhaseStatus({ planId, phaseId: childId, status: 'in_progress', progress: 50 }); // Call without validationLevel parameter const result = await queryService.validatePlan({ planId }); // Should filter out warnings (basic mode) const warningIssues = result.issues.filter(i => i.severity === 'warning'); expect(warningIssues.length).toBe(0); }); }); 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 if (result.filePath === undefined) throw new Error('filePath is required'); 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) // Use RepositoryFactory directly to bypass validation const repo = repositoryFactory.createRepository<Solution>('solution', planId); await repo.create({ 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 unknown as Solution); // 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', }, targets: [ { 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 targets 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);', }, targets: [ { 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('getSearchableText - null-safety (RED phase)', () => { beforeEach(async () => { // Create base entities for null-safety testing await requirementService.addRequirement({ planId, requirement: { title: 'Test Requirement', description: 'For null-safety testing', source: { type: 'user-request' }, priority: 'high', category: 'functional', acceptanceCriteria: ['AC1', 'AC2'], }, }); await solutionService.proposeSolution({ planId, solution: { title: 'Test Solution', description: 'For null-safety testing', approach: 'Test approach', addressing: [], tradeoffs: [ { aspect: 'Performance', pros: ['Fast'], cons: ['Memory usage'], }, ], evaluation: { effortEstimate: { value: 1, unit: 'days', confidence: 'high' }, technicalFeasibility: 'high', riskAssessment: 'Low', }, }, }); await phaseService.addPhase({ planId, phase: { title: 'Test Phase', description: 'For null-safety testing', objectives: ['Obj1', 'Obj2'], deliverables: ['Del1', 'Del2'], successCriteria: ['SC1'], }, }); }); // REQUIREMENT ENTITY it('RED: should handle requirement with undefined acceptanceCriteria', async () => { // Use RepositoryFactory instead of FileStorage const repo = repositoryFactory.createRepository<Requirement>('requirement', planId); const requirements = await repo.findAll(); const requirement = requirements[0] as Partial<Requirement>; delete requirement.acceptanceCriteria; // Set to undefined await repo.update(requirement.id as string, requirement as Requirement); const result = await queryService.searchEntities({ planId, query: 'requirement', }); expect(result).toBeDefined(); // Should not crash }); it('RED: should handle requirement with empty acceptanceCriteria', async () => { await requirementService.addRequirement({ planId, requirement: { title: 'Empty AC Requirement', description: 'Test empty acceptance criteria', source: { type: 'user-request' }, priority: 'high', category: 'functional', acceptanceCriteria: [], // Empty array }, }); const result = await queryService.searchEntities({ planId, query: 'Empty', }); expect(result.results.length).toBeGreaterThan(0); }); it('RED: should handle requirement with null rationale', async () => { // Use RepositoryFactory instead of FileStorage const repo = repositoryFactory.createRepository<Requirement>('requirement', planId); const requirements = await repo.findAll(); const requirement = requirements[0]; requirement.rationale = null as unknown as string; await repo.update(requirement.id, requirement); const result = await queryService.searchEntities({ planId, query: 'test' }); expect(result).toBeDefined(); }); // SOLUTION ENTITY it('RED: should handle solution with undefined tradeoffs', async () => { const solutions = await loadEntities<Solution>(repositoryFactory, planId, 'solutions'); if (solutions.length > 0) { solutions[0].tradeoffs = undefined as unknown as never; await saveEntities(repositoryFactory, planId, 'solutions', solutions); } const result = await queryService.exportPlan({ planId, format: 'markdown' }); expect(result.content).toContain('Solution'); }); it('RED: should handle tradeoff with undefined pros', async () => { const solutions = await loadEntities<Solution>(repositoryFactory, planId, 'solutions'); if (solutions.length > 0) { solutions[0].tradeoffs = [ { aspect: 'Performance', pros: undefined as unknown as string[], cons: ['Slower'], }, ]; await saveEntities(repositoryFactory, planId, 'solutions', solutions); } const result = await queryService.exportPlan({ planId, format: 'markdown' }); expect(result.content).toBeDefined(); // Should not crash }); it('RED: should handle tradeoff with undefined cons', async () => { const solutions = await loadEntities<Solution>(repositoryFactory, planId, 'solutions'); if (solutions.length > 0) { solutions[0].tradeoffs = [ { aspect: 'Test', pros: ['Fast'], cons: undefined as unknown as string[], }, ]; await saveEntities(repositoryFactory, planId, 'solutions', solutions); } const result = await queryService.exportPlan({ planId, format: 'markdown' }); expect(result).toBeDefined(); }); // PHASE ENTITY it('RED: should handle phase with undefined objectives', async () => { const phases = await loadEntities<Phase>(repositoryFactory, planId, 'phases'); if (phases.length > 0) { phases[0].objectives = undefined as unknown as never; await saveEntities(repositoryFactory, planId, 'phases', phases); } const result = await queryService.searchEntities({ planId, query: 'phase' }); expect(result).toBeDefined(); }); it('RED: should handle phase with undefined deliverables', async () => { const phases = await loadEntities<Phase>(repositoryFactory, planId, 'phases'); if (phases.length > 0) { phases[0].deliverables = undefined as unknown as never; await saveEntities(repositoryFactory, planId, 'phases', phases); } const result = await queryService.searchEntities({ planId, query: 'phase' }); expect(result).toBeDefined(); }); it('RED: should handle phase with empty arrays', async () => { await phaseService.addPhase({ planId, phase: { title: 'Empty Phase', description: 'Test empty arrays', objectives: [], deliverables: [], successCriteria: [], }, }); const result = await queryService.searchEntities({ planId, query: 'Empty' }); expect(result.results.length).toBeGreaterThan(0); }); // ARTIFACT ENTITY it('RED: should handle artifact with undefined sourceCode', async () => { await artifactService.addArtifact({ planId, artifact: { title: 'Test Artifact No Code', description: 'Test undefined sourceCode', artifactType: 'code', content: { sourceCode: undefined as unknown as string, }, }, }); const result = await queryService.searchEntities({ planId, query: 'Artifact' }); expect(result).toBeDefined(); }); it('RED: should handle artifact with undefined filename', async () => { await artifactService.addArtifact({ planId, artifact: { title: 'No Filename', description: 'Test undefined filename', artifactType: 'code', content: { sourceCode: 'code', filename: undefined, }, }, }); const result = await queryService.searchEntities({ planId, query: 'Filename' }); expect(result).toBeDefined(); }); // INTEGRATION TESTS it('RED: should search phase without objectives/deliverables', async () => { const phases = await loadEntities<Phase>(repositoryFactory, planId, 'phases'); if (phases.length > 0) { const phaseTitle = phases[0].title; phases[0].objectives = undefined as unknown as never; phases[0].deliverables = undefined as unknown as never; await saveEntities(repositoryFactory, planId, 'phases', phases); const result = await queryService.searchEntities({ planId, query: phaseTitle, }); expect(result.results.length).toBeGreaterThan(0); expect(result.results[0].entityType).toBe('phase'); } else { expect(true).toBe(true); // Skip if no phases } }); it('RED: should export plan with mixed null fields', async () => { // Create entities with various undefined fields const phases = await loadEntities<Phase>(repositoryFactory, planId, 'phases'); if (phases.length > 0) { phases[0].objectives = undefined as unknown as never; await saveEntities(repositoryFactory, planId, 'phases', phases); } const requirements = await loadEntities<Requirement>(repositoryFactory, planId, 'requirements'); if (requirements.length > 0) { requirements[0].acceptanceCriteria = undefined as unknown as never; await saveEntities(repositoryFactory, planId, 'requirements', requirements); } const result = await queryService.exportPlan({ planId, format: 'markdown' }); expect(result.format).toBe('markdown'); expect(result.content).toBeDefined(); expect(result.content.length).toBeGreaterThan(0); }); it('RED: should export markdown with undefined acceptanceCriteria', async () => { const requirements = await loadEntities<Requirement>(repositoryFactory, planId, 'requirements'); if (requirements.length > 0) { requirements[0].acceptanceCriteria = undefined as unknown as never; await saveEntities(repositoryFactory, planId, 'requirements', requirements); } const result = await queryService.exportPlan({ planId, format: 'markdown' }); expect(result.content).toContain('## Requirements'); // Should not crash on acceptanceCriteria.length check }); }); 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); }); }); // ============================================================================ // Sprint 8: Paginated Trace Results - RED Phase (58 tests) // ============================================================================ describe('trace_requirement with pagination (Sprint 8 - RED Phase)', () => { let reqId: string; let sol1Id: string; let sol2Id: string; let sol3Id: string; let phase1Id: string; let phase2Id: string; let phase3Id: string; beforeEach(async () => { // Create complex trace structure for testing const req = await requirementService.addRequirement({ planId, requirement: { title: 'Complex Requirement', description: 'Requirement with multiple solutions and phases', source: { type: 'user-request' }, priority: 'critical', category: 'functional', acceptanceCriteria: ['AC1', 'AC2'], }, }); reqId = req.requirementId; // Create 3 solutions const sol1 = await solutionService.proposeSolution({ planId, solution: { title: 'Solution 1', description: 'First solution', approach: 'Approach 1', addressing: [reqId], tradeoffs: [], evaluation: { effortEstimate: { value: 4, unit: 'hours', confidence: 'high' }, technicalFeasibility: 'high', riskAssessment: 'Low', }, }, }); sol1Id = sol1.solutionId; const sol2 = await solutionService.proposeSolution({ planId, solution: { title: 'Solution 2', description: 'Second solution', approach: 'Approach 2', addressing: [reqId], tradeoffs: [], evaluation: { effortEstimate: { value: 2, unit: 'days', confidence: 'medium' }, technicalFeasibility: 'medium', riskAssessment: 'Medium', }, }, }); sol2Id = sol2.solutionId; const sol3 = await solutionService.proposeSolution({ planId, solution: { title: 'Solution 3', description: 'Third solution', approach: 'Approach 3', addressing: [reqId], tradeoffs: [], evaluation: { effortEstimate: { value: 1, unit: 'weeks', confidence: 'low' }, technicalFeasibility: 'low', riskAssessment: 'High', }, }, }); sol3Id = sol3.solutionId; // Create links await linkingService.linkEntities({ planId, sourceId: sol1Id, targetId: reqId, relationType: 'implements' }); await linkingService.linkEntities({ planId, sourceId: sol2Id, targetId: reqId, relationType: 'implements' }); await linkingService.linkEntities({ planId, sourceId: sol3Id, targetId: reqId, relationType: 'implements' }); // Create 3 phases const p1 = await phaseService.addPhase({ planId, phase: { title: 'Phase 1', description: 'First phase', objectives: ['Obj1'], deliverables: ['Del1'], successCriteria: ['SC1'], }, }); phase1Id = p1.phaseId; const p2 = await phaseService.addPhase({ planId, phase: { title: 'Phase 2', description: 'Second phase', objectives: ['Obj2'], deliverables: ['Del2'], successCriteria: ['SC2'], }, }); phase2Id = p2.phaseId; const p3 = await phaseService.addPhase({ planId, phase: { title: 'Phase 3', description: 'Third phase', objectives: ['Obj3'], deliverables: ['Del3'], successCriteria: ['SC3'], }, }); phase3Id = p3.phaseId; // Link phases to requirement await linkingService.linkEntities({ planId, sourceId: phase1Id, targetId: reqId, relationType: 'addresses' }); await linkingService.linkEntities({ planId, sourceId: phase2Id, targetId: reqId, relationType: 'addresses' }); await linkingService.linkEntities({ planId, sourceId: phase3Id, targetId: reqId, relationType: 'addresses' }); // Create 2 artifacts await artifactService.addArtifact({ planId, artifact: { title: 'Artifact 1', description: 'First artifact', artifactType: 'code', relatedPhaseId: phase1Id, relatedRequirementIds: [reqId], }, }); await artifactService.addArtifact({ planId, artifact: { title: 'Artifact 2', description: 'Second artifact', artifactType: 'documentation', relatedPhaseId: phase2Id, relatedRequirementIds: [reqId], }, }); }); // ======================================================================== // 1. DEPTH PARAMETER TESTS (10 tests) // ======================================================================== describe('depth parameter', () => { it('GREEN 1.1: should accept depth=1 parameter and return only solutions', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, depth: 1, } as never); // depth=1: only solutions, no phases, no artifacts expect(result.trace.proposedSolutions).toBeDefined(); expect(result.trace.proposedSolutions.length).toBe(3); expect(result.trace.implementingPhases).toBeUndefined(); expect(result.trace.artifacts).toBeUndefined(); }); it('GREEN 1.2: should accept depth=2 parameter and return solutions + phases', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, depth: 2, } as never); // depth=2: solutions + phases, but no artifacts expect(result.trace.proposedSolutions).toBeDefined(); expect(result.trace.proposedSolutions.length).toBe(3); expect(result.trace.implementingPhases).toBeDefined(); if (result.trace.implementingPhases === undefined) throw new Error('implementingPhases is required'); expect(result.trace.implementingPhases.length).toBe(3); expect(result.trace.artifacts).toBeUndefined(); }); it('GREEN 1.3: should accept depth=3 parameter and return all (solutions + phases + artifacts)', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, depth: 3, } as never); // depth=3: full trace with all levels expect(result.trace.proposedSolutions).toBeDefined(); expect(result.trace.proposedSolutions.length).toBe(3); expect(result.trace.implementingPhases).toBeDefined(); if (result.trace.implementingPhases === undefined) throw new Error('implementingPhases is required'); expect(result.trace.implementingPhases.length).toBe(3); expect(result.trace.artifacts).toBeDefined(); if (result.trace.artifacts === undefined) throw new Error('artifacts is required'); expect(result.trace.artifacts.length).toBe(2); }); it('RED 1.4: should reject depth=0 (invalid)', async () => { await expect( queryService.traceRequirement({ planId, requirementId: reqId, depth: 0, } as never) ).rejects.toThrow('depth must be between 1 and 3'); }); it('RED 1.5: should reject depth=4 (out of bounds)', async () => { await expect( queryService.traceRequirement({ planId, requirementId: reqId, depth: 4, } as never) ).rejects.toThrow('depth must be between 1 and 3'); }); it('RED 1.6: should reject depth=-1 (negative)', async () => { await expect( queryService.traceRequirement({ planId, requirementId: reqId, depth: -1, } as never) ).rejects.toThrow('depth must be between 1 and 3'); }); it('RED 1.7: should reject depth="2" (string instead of number)', async () => { await expect( queryService.traceRequirement({ planId, requirementId: reqId, depth: '2' as unknown as number, } as never) ).rejects.toThrow('depth must be a number'); }); it('RED 1.8: depth=1 should return only direct solutions (level 1)', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, depth: 1, } as never); // Depth 1: requirement -> solutions only expect(result.trace.proposedSolutions).toHaveLength(3); expect(result.trace.implementingPhases).toBeUndefined(); // Should be excluded at depth=1 }); it('RED 1.9: depth=2 should return solutions + phases (2 levels)', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, depth: 2, } as never); // Depth 2: requirement -> solutions -> phases expect(result.trace.proposedSolutions).toHaveLength(3); expect(result.trace.implementingPhases).toHaveLength(3); }); it('RED 1.10: depth=3 should return all levels (solutions + phases + artifacts)', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, depth: 3, } as never); // Depth 3: requirement -> solutions -> phases -> artifacts expect(result.trace.proposedSolutions).toHaveLength(3); expect(result.trace.implementingPhases).toHaveLength(3); expect(result.trace.artifacts).toHaveLength(2); // New field }); }); // ======================================================================== // 2. INCLUDE FLAGS TESTS (12 tests) // ======================================================================== describe('includePhases and includeArtifacts flags', () => { it('RED 2.1: should accept includePhases=true', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, includePhases: true, } as never); expect(result.trace.implementingPhases).toHaveLength(3); }); it('RED 2.2: should accept includePhases=false and exclude phases', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, includePhases: false, } as never); expect(result.trace.implementingPhases).toBeUndefined(); // Should not be returned }); it('RED (BUGFIX): completion status should be calculated even when includePhases=false', async () => { /** * BUG: When includePhases=false, the implementingPhases array remains empty * (initialized but never populated). However, calculateAverageProgress() is still * called with this empty array to compute completion status. This causes: * - isImplemented to always be false (even when phases exist and are completed) * - completionPercentage to always be 0 * * The phases used for completion calculation should be determined independently * of whether they're included in the response. * * Current code (query-service.ts:334-340): * if (depth >= TRACE_DEPTH.PHASES && includePhases) { * // rawPhases is populated here * implementingPhases = this.applyEntityFilters(rawPhases, {...}); * } * // Later: calculateAverageProgress(implementingPhases) uses empty array! * * Expected: Completion should be calculated from ALL phases, regardless of includePhases */ // First, mark all 3 phases as completed await phaseService.updatePhaseStatus({ planId, phaseId: phase1Id, status: 'completed', progress: 100 }); await phaseService.updatePhaseStatus({ planId, phaseId: phase2Id, status: 'completed', progress: 100 }); await phaseService.updatePhaseStatus({ planId, phaseId: phase3Id, status: 'completed', progress: 100 }); // Call with includePhases=false (phases should NOT be in output) const result = await queryService.traceRequirement({ planId, requirementId: reqId, includePhases: false, } as never); // Phases should not be in output expect(result.trace.implementingPhases).toBeUndefined(); // BUT completion status SHOULD reflect the actual phase completion expect(result.trace.completionStatus.isImplemented).toBe(true); expect(result.trace.completionStatus.completionPercentage).toBe(100); // Reset phases back to planned for other tests await phaseService.updatePhaseStatus({ planId, phaseId: phase1Id, status: 'planned', progress: 0 }); await phaseService.updatePhaseStatus({ planId, phaseId: phase2Id, status: 'planned', progress: 0 }); await phaseService.updatePhaseStatus({ planId, phaseId: phase3Id, status: 'planned', progress: 0 }); }); it('RED 2.3: should accept includeArtifacts=true', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, includeArtifacts: true, } as never); expect(result.trace.artifacts).toHaveLength(2); }); it('RED 2.4: should accept includeArtifacts=false and exclude artifacts', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, includeArtifacts: false, } as never); expect(result.trace.artifacts).toBeUndefined(); }); it('RED 2.5: includePhases=false + includeArtifacts=false should return only solutions', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, includePhases: false, includeArtifacts: false, } as never); expect(result.trace.proposedSolutions).toHaveLength(3); expect(result.trace.implementingPhases).toBeUndefined(); expect(result.trace.artifacts).toBeUndefined(); }); it('RED 2.6: includePhases=true + includeArtifacts=true should return all', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, includePhases: true, includeArtifacts: true, } as never); expect(result.trace.proposedSolutions).toHaveLength(3); expect(result.trace.implementingPhases).toHaveLength(3); expect(result.trace.artifacts).toHaveLength(2); }); it('RED 2.7: default behavior (no flags) should include phases and artifacts', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, } as never); // Default: include everything (backward compatible) expect(result.trace.implementingPhases).toHaveLength(3); }); it('RED 2.8: includePhases="true" (string) should throw ValidationError', async () => { await expect( queryService.traceRequirement({ planId, requirementId: reqId, includePhases: 'true' as unknown as boolean, } as never) ).rejects.toThrow('includePhases must be a boolean'); }); it('RED 2.9: includeArtifacts=null should default to true', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, includeArtifacts: null as unknown as boolean, } as never); // null -> default true expect(result.trace.artifacts).toBeDefined(); }); it('RED 2.10: includePhases=undefined should default to true', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, includePhases: undefined, } as never); expect(result.trace.implementingPhases).toBeDefined(); }); it('RED 2.11: depth=1 + includePhases=false should work together', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, depth: 1, includePhases: false, } as never); expect(result.trace.proposedSolutions).toHaveLength(3); expect(result.trace.implementingPhases).toBeUndefined(); }); it('RED 2.12: depth=2 + includeArtifacts=false should exclude artifacts', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, depth: 2, includeArtifacts: false, } as never); expect(result.trace.proposedSolutions).toHaveLength(3); expect(result.trace.implementingPhases).toHaveLength(3); expect(result.trace.artifacts).toBeUndefined(); }); /** * BUG FIX TEST: Artifact discovery broken when phases excluded from trace output * * The allPhaseIds set used for artifact discovery is only populated when both * depth >= PHASES AND includePhases is true. However, artifacts should be * discoverable through phases even when includePhases=false. * * When depth=3, includePhases=false, includeArtifacts=true, the allPhaseIds * remains empty, preventing artifacts linked only to phases from being found. * The phase IDs should be computed independently of whether phases are included * in the response, since they're needed for artifact discovery. */ it('RED 2.13 (BUGFIX): includePhases=false + includeArtifacts=true should still find phase-linked artifacts', async () => { // Create an artifact linked ONLY to a phase (no relatedRequirementIds) const phaseOnlyArtifact = await artifactService.addArtifact({ planId, artifact: { title: 'Phase-Only Artifact', description: 'Artifact linked only to phase, not directly to requirement', artifactType: 'code', relatedPhaseId: phase3Id, // NOTE: No relatedRequirementIds - can ONLY be found via phase relationship }, }); const result = await queryService.traceRequirement({ planId, requirementId: reqId, depth: 3, includePhases: false, includeArtifacts: true, } as never); // Phases should NOT be in the output (includePhases=false) expect(result.trace.implementingPhases).toBeUndefined(); // But artifacts should include the phase-linked artifact // because artifact discovery should work independently of includePhases flag expect(result.trace.artifacts).toBeDefined(); if (result.trace.artifacts === undefined) throw new Error('artifacts is required'); // Should find: 2 artifacts with relatedRequirementIds + 1 phase-only artifact = 3 total expect(result.trace.artifacts.length).toBe(3); // Verify the phase-only artifact is included const foundPhaseOnlyArtifact = result.trace.artifacts.find( (a: { id: string }) => a.id === phaseOnlyArtifact.artifactId ); expect(foundPhaseOnlyArtifact).toBeDefined(); if (foundPhaseOnlyArtifact === undefined) throw new Error('foundPhaseOnlyArtifact is required'); expect(foundPhaseOnlyArtifact.title).toBe('Phase-Only Artifact'); }); }); // ======================================================================== // 3. LIMIT PARAMETER TESTS (8 tests) // ======================================================================== describe('limit parameter', () => { it('RED 3.1: should accept limit parameter', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, limit: 2, } as never); expect(result).toBeDefined(); }); it('RED 3.2: limit=1 should return max 1 solution', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, limit: 1, } as never); expect(result.trace.proposedSolutions).toHaveLength(1); }); it('RED 3.3: limit=2 should return max 2 solutions', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, limit: 2, } as never); expect(result.trace.proposedSolutions.length).toBeLessThanOrEqual(2); }); it('RED 3.4: limit=10 (more than available) should return all 3 solutions', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, limit: 10, } as never); expect(result.trace.proposedSolutions).toHaveLength(3); }); it('RED 3.5: limit=0 should throw ValidationError', async () => { await expect( queryService.traceRequirement({ planId, requirementId: reqId, limit: 0, } as never) ).rejects.toThrow('limit must be greater than 0'); }); it('RED 3.6: limit=-1 (negative) should throw ValidationError', async () => { await expect( queryService.traceRequirement({ planId, requirementId: reqId, limit: -1, } as never) ).rejects.toThrow('limit must be greater than 0'); }); it('RED 3.7: limit="2" (string) should throw ValidationError', async () => { await expect( queryService.traceRequirement({ planId, requirementId: reqId, limit: '2' as unknown as number, } as never) ).rejects.toThrow('limit must be a number'); }); it('RED 3.8: limit should apply per entity type (solutions, phases, artifacts)', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, limit: 2, } as never); // Limit=2 applies to each entity type independently expect(result.trace.proposedSolutions.length).toBeLessThanOrEqual(2); expect(result.trace.implementingPhases).toBeDefined(); if (result.trace.implementingPhases === undefined) throw new Error('implementingPhases is required'); expect(result.trace.implementingPhases.length).toBeLessThanOrEqual(2); if (result.trace.artifacts) { expect(result.trace.artifacts.length).toBeLessThanOrEqual(2); } }); }); // ======================================================================== // 4. COMBINATIONS: depth + fields + exclude (10 tests) // ======================================================================== describe('combinations with fields and exclude parameters', () => { it('RED 4.1: depth=1 + fields=["id","title"] should work', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, depth: 1, fields: ['id', 'title'], } as never); // Solutions should have only id and title const solution = result.trace.proposedSolutions[0]; expect(solution.id).toBeDefined(); expect(solution.title).toBeDefined(); expect(solution.description).toBeUndefined(); }); it('RED 4.2: depth=2 + excludeMetadata=true should work', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, depth: 2, excludeMetadata: true, } as never); const solution = result.trace.proposedSolutions[0]; expect(solution.metadata).toBeUndefined(); expect(solution.createdAt).toBeUndefined(); }); it('RED (BUGFIX): excludeMetadata=true should remove type field per API documentation', async () => { /** * BUG: The API documentation in tool-definitions.ts states that excludeMetadata * should remove: "createdAt, updatedAt, version, metadata, type" * * Current implementation (query-service.ts:953) only removes: * const { metadata, createdAt, updatedAt, version, ...rest } = entity as any; * * Missing: type field is not being removed despite documentation */ const result = await queryService.traceRequirement({ planId, requirementId: reqId, depth: 2, excludeMetadata: true, } as never); const solution = result.trace.proposedSolutions[0]; // Per API docs, excludeMetadata should remove: createdAt, updatedAt, version, metadata, type expect(solution.metadata).toBeUndefined(); expect(solution.createdAt).toBeUndefined(); expect(solution.updatedAt).toBeUndefined(); expect(solution.version).toBeUndefined(); expect((solution as unknown as Record<string, unknown>).type).toBeUndefined(); // BUGFIX: type should also be removed }); it('RED 4.3: includePhases=true + fields=["id","title","status"] for phases', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, includePhases: true, phaseFields: ['id', 'title', 'status'], } as never); expect(result.trace.implementingPhases).toBeDefined(); if (result.trace.implementingPhases === undefined) throw new Error('implementingPhases is required'); const phase = result.trace.implementingPhases[0]; expect(phase.id).toBeDefined(); expect(phase.title).toBeDefined(); expect(phase.status).toBeDefined(); expect(phase.description).toBeUndefined(); }); it('RED 4.4: depth=1 + limit=2 + fields=["id"] should combine all 3 params', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, depth: 1, limit: 2, fields: ['id'], } as never); expect(result.trace.proposedSolutions).toHaveLength(2); // limit const sol = result.trace.proposedSolutions[0]; expect(sol.id).toBeDefined(); expect(sol.title).toBeUndefined(); // fields filter }); it('RED 4.5: includeArtifacts=true + excludeMetadata=true for artifacts', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, includeArtifacts: true, excludeMetadata: true, } as never); if (result.trace.artifacts !== undefined && result.trace.artifacts.length > 0) { const artifact = result.trace.artifacts[0]; expect(artifact.metadata).toBeUndefined(); } }); it('RED 4.6: depth=3 + includePhases=false + includeArtifacts=true should respect flags', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, depth: 3, includePhases: false, includeArtifacts: true, } as never); expect(result.trace.implementingPhases).toBeUndefined(); expect(result.trace.artifacts).toBeDefined(); }); it('RED 4.7: limit=1 + excludeMetadata=true + fields=["id","title"]', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, limit: 1, excludeMetadata: true, fields: ['id', 'title'], } as never); expect(result.trace.proposedSolutions).toHaveLength(1); const sol = result.trace.proposedSolutions[0]; expect(sol.id).toBeDefined(); expect(sol.title).toBeDefined(); expect(sol.metadata).toBeUndefined(); expect(sol.description).toBeUndefined(); }); it('RED 4.8: depth=2 + limit=2 + includePhases=true should apply limit to phases', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, depth: 2, limit: 2, includePhases: true, } as never); expect(result.trace.implementingPhases).toBeDefined(); if (result.trace.implementingPhases === undefined) throw new Error('implementingPhases is required'); expect(result.trace.implementingPhases.length).toBeLessThanOrEqual(2); }); it('RED 4.9: all parameters combined: depth + limit + include flags + fields + exclude', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, depth: 2, limit: 2, includePhases: true, includeArtifacts: false, fields: ['id', 'title'], excludeMetadata: true, } as never); expect(result.trace.proposedSolutions.length).toBeLessThanOrEqual(2); expect(result.trace.implementingPhases).toBeDefined(); if (result.trace.implementingPhases === undefined) throw new Error('implementingPhases is required'); expect(result.trace.implementingPhases.length).toBeLessThanOrEqual(2); expect(result.trace.artifacts).toBeUndefined(); const sol = result.trace.proposedSolutions[0]; expect(sol.id).toBeDefined(); expect(sol.title).toBeDefined(); expect(sol.metadata).toBeUndefined(); expect(sol.description).toBeUndefined(); }); it('RED 4.10: fields parameter should support different fields for different entity types', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, solutionFields: ['id', 'title', 'approach'], phaseFields: ['id', 'title', 'status'], } as never); const sol = result.trace.proposedSolutions[0]; expect(sol.id).toBeDefined(); expect(sol.title).toBeDefined(); expect(sol.approach).toBeDefined(); expect(sol.description).toBeUndefined(); expect(result.trace.implementingPhases).toBeDefined(); if (result.trace.implementingPhases === undefined) throw new Error('implementingPhases is required'); const phase = result.trace.implementingPhases[0]; expect(phase.id).toBeDefined(); expect(phase.title).toBeDefined(); expect(phase.status).toBeDefined(); expect(phase.description).toBeUndefined(); }); }); // ======================================================================== // 5. PERFORMANCE TESTS (8 tests) // ======================================================================== describe('performance and payload size', () => { it('RED 5.1: depth=1 + minimal fields should reduce payload to ~5KB (from ~32KB)', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, depth: 1, fields: ['id', 'title'], } as never); const payloadSize = JSON.stringify(result).length; expect(payloadSize).toBeLessThan(6000); // ~5KB with buffer }); it('RED 5.2: full trace (no params) should be ~32KB', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, }); const payloadSize = JSON.stringify(result).length; // With 3 solutions, 3 phases, 2 artifacts - expect large payload expect(payloadSize).toBeGreaterThan(1000); // At least 1KB }); it('RED 5.3: verify 6x payload reduction: full vs minimal', async () => { const fullResult = await queryService.traceRequirement({ planId, requirementId: reqId, }); const fullSize = JSON.stringify(fullResult).length; const minimalResult = await queryService.traceRequirement({ planId, requirementId: reqId, depth: 1, fields: ['id', 'title'], excludeMetadata: true, } as never); const minimalSize = JSON.stringify(minimalResult).length; const ratio = fullSize / minimalSize; expect(ratio).toBeGreaterThanOrEqual(5.9); // At least ~6x reduction (allowing for JSON overhead variance) }); it('RED 5.4: includePhases=false should reduce payload size', async () => { const withPhases = await queryService.traceRequirement({ planId, requirementId: reqId, includePhases: true, } as never); const withPhasesSize = JSON.stringify(withPhases).length; const withoutPhases = await queryService.traceRequirement({ planId, requirementId: reqId, includePhases: false, } as never); const withoutPhasesSize = JSON.stringify(withoutPhases).length; expect(withoutPhasesSize).toBeLessThan(withPhasesSize); }); it('RED 5.5: includeArtifacts=false should reduce payload', async () => { const withArtifacts = await queryService.traceRequirement({ planId, requirementId: reqId, includeArtifacts: true, } as never); const withArtifactsSize = JSON.stringify(withArtifacts).length; const withoutArtifacts = await queryService.traceRequirement({ planId, requirementId: reqId, includeArtifacts: false, } as never); const withoutArtifactsSize = JSON.stringify(withoutArtifacts).length; expect(withoutArtifactsSize).toBeLessThan(withArtifactsSize); }); it('RED 5.6: limit parameter should reduce payload', async () => { const unlimited = await queryService.traceRequirement({ planId, requirementId: reqId, }); const unlimitedSize = JSON.stringify(unlimited).length; const limited = await queryService.traceRequirement({ planId, requirementId: reqId, limit: 1, } as never); const limitedSize = JSON.stringify(limited).length; expect(limitedSize).toBeLessThan(unlimitedSize); }); it('RED 5.7: excludeMetadata should save ~162 bytes per entity', async () => { const withMetadata = await queryService.traceRequirement({ planId, requirementId: reqId, excludeMetadata: false, } as never); const withSize = JSON.stringify(withMetadata).length; const withoutMetadata = await queryService.traceRequirement({ planId, requirementId: reqId, excludeMetadata: true, } as never); const withoutSize = JSON.stringify(withoutMetadata).length; // With 3 solutions + 3 phases = 6 entities * 162 bytes = ~972 bytes saved const saved = withSize - withoutSize; expect(saved).toBeGreaterThan(500); // At least 500 bytes saved }); it('RED 5.8: measure time performance (should be fast <100ms)', async () => { const start = Date.now(); await queryService.traceRequirement({ planId, requirementId: reqId, depth: 1, fields: ['id', 'title'], } as never); const duration = Date.now() - start; expect(duration).toBeLessThan(100); // Fast response }); }); // ======================================================================== // 6. EDGE CASES AND VALIDATION (10 tests) // ======================================================================== describe('edge cases and validation', () => { it('RED 6.1: empty trace (no solutions, phases, artifacts) should work', async () => { const emptyReq = await requirementService.addRequirement({ planId, requirement: { title: 'Empty Req', description: 'No trace', source: { type: 'user-request' }, priority: 'low', category: 'functional', acceptanceCriteria: [], }, }); const result = await queryService.traceRequirement({ planId, requirementId: emptyReq.requirementId, }); expect(result.trace.proposedSolutions).toHaveLength(0); expect(result.trace.implementingPhases).toHaveLength(0); }); it('RED 6.2: depth + limit + fields with empty results should not crash', async () => { const emptyReq = await requirementService.addRequirement({ planId, requirement: { title: 'Empty', description: 'Empty', source: { type: 'user-request' }, priority: 'low', category: 'functional', acceptanceCriteria: [], }, }); const result = await queryService.traceRequirement({ planId, requirementId: emptyReq.requirementId, depth: 2, limit: 5, fields: ['id', 'title'], } as never); expect(result.trace.proposedSolutions).toHaveLength(0); }); it('RED 6.3: invalid requirementId with pagination params should throw', async () => { await expect( queryService.traceRequirement({ planId, requirementId: 'invalid-id', depth: 1, } as never) ).rejects.toThrow(/requirement.*not found/i); }); it('RED 6.4: depth=1.5 (float) should throw ValidationError', async () => { await expect( queryService.traceRequirement({ planId, requirementId: reqId, depth: 1.5, } as never) ).rejects.toThrow('depth must be an integer'); }); it('RED 6.5: limit=2.5 (float) should throw ValidationError', async () => { await expect( queryService.traceRequirement({ planId, requirementId: reqId, limit: 2.5, } as never) ).rejects.toThrow('limit must be an integer'); }); it('RED 6.6: fields=[] (empty array) should default to all fields', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, fields: [], } as never); const sol = result.trace.proposedSolutions[0]; expect(sol.id).toBeDefined(); expect(sol.title).toBeDefined(); expect(sol.description).toBeDefined(); }); it('RED 6.7: fields=["nonexistent"] should ignore invalid fields', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, fields: ['nonexistent', 'id', 'title'], } as never); const sol = result.trace.proposedSolutions[0]; expect(sol.id).toBeDefined(); expect(sol.title).toBeDefined(); // nonexistent field is ignored }); it('RED 6.8: null planId should throw error', async () => { await expect( queryService.traceRequirement({ planId: null as unknown as string, requirementId: reqId, }) ).rejects.toThrow(); }); it('RED 6.9: large limit (1000) should work without crashing', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, limit: 1000, } as never); expect(result.trace.proposedSolutions).toHaveLength(3); // Only 3 available }); it('RED 6.10: backward compatibility - existing calls without new params should work', async () => { const result = await queryService.traceRequirement({ planId, requirementId: reqId, }); // Old behavior: return everything expect(result.requirement.id).toBe(reqId); expect(result.trace.proposedSolutions).toHaveLength(3); expect(result.trace.implementingPhases).toHaveLength(3); }); it('BUG FIX: selectedSolution should be found even when beyond limit', async () => { // Select the 3rd solution (sol3Id is the last one created) await solutionService.selectSolution({ planId, solutionId: sol3Id, reason: 'Best approach', }); // Apply limit=1 - only first solution will be in proposedSolutions const result = await queryService.traceRequirement({ planId, requirementId: reqId, limit: 1, } as never); // BUG: selectedSolution should NOT be null even though it's beyond limit // The selectedSolution is a semantic singleton and should always be found // from the full set of solutions, regardless of pagination expect(result.trace.selectedSolution).not.toBeNull(); expect(result.trace.selectedSolution?.id).toBe(sol3Id); // proposedSolutions = selectedSolution + limited alternativeSolutions // With limit=1: selectedSolution (sol3) + 1 alternative (sol1) = 2 total expect(result.trace.proposedSolutions.length).toBe(2); expect(result.trace.alternativeSolutions.length).toBe(1); // limit applies to alternatives }); it('BUG FIX: artifacts should be found from ALL phases, not just limited ones', async () => { // Create a new artifact linked ONLY to phase3 (no relatedRequirementIds) // This artifact should still be discoverable even if phase3 is excluded by limit const phaseOnlyArtifact = await artifactService.addArtifact({ planId, artifact: { title: 'Phase-Only Artifact', description: 'Artifact linked only to phase3, no direct requirement link', artifactType: 'test', relatedPhaseId: phase3Id, // NO relatedRequirementIds - only linked via phase }, }); // First verify: without limit, we find ALL 3 artifacts const resultNoLimit = await queryService.traceRequirement({ planId, requirementId: reqId, depth: 3, } as never); if (resultNoLimit.trace.artifacts === undefined) throw new Error('artifacts is required'); expect(resultNoLimit.trace.artifacts.length).toBe(3); // BUG TEST: The key issue is that artifact DISCOVERY should use all phases, // not just limited phases. The limit applies to the RESPONSE, not discovery. // // With buggy code: limit=1 on phases -> only phase1 in response AND discovery // -> artifacts linked only to phase2/phase3 are NEVER found // With fixed code: limit=1 on phases -> only phase1 in response, BUT discovery // uses ALL phases -> artifacts from all phases are in the pool // Test with limit=1: phases limited to 1, but artifacts should still be // discovered from ALL phases (then limited separately) const limitedResult = await queryService.traceRequirement({ planId, requirementId: reqId, depth: 3, limit: 1, // Limits each entity type to 1 in response } as never); // Phases ARE limited to 1 in response if (limitedResult.trace.implementingPhases === undefined) throw new Error('implementingPhases is required'); expect(limitedResult.trace.implementingPhases.length).toBe(1); // Artifacts are also limited to 1 in response (limit applies per entity type) if (limitedResult.trace.artifacts === undefined) throw new Error('artifacts is required'); expect(limitedResult.trace.artifacts.length).toBe(1); // KEY TEST: Use high limit to verify artifact DISCOVERY uses all phases // With buggy code: even with high limit, artifacts from excluded phases // wouldn't be discovered because discovery used limited phases // With fixed code: all artifacts ARE discovered, then limited separately const resultHighLimit = await queryService.traceRequirement({ planId, requirementId: reqId, depth: 3, limit: 100, // High limit to see full discovery pool } as never); // With high limit, all 3 artifacts should be found // This proves artifact DISCOVERY uses all phases (allPhaseIds), not limited ones if (resultHighLimit.trace.artifacts === undefined) throw new Error('artifacts is required'); expect(resultHighLimit.trace.artifacts.length).toBe(3); // Verify the phase-only artifact (linked only to phase3) IS discoverable const hasPhaseOnlyArtifact = resultHighLimit.trace.artifacts.some( (a: { id: string }) => a.id === phaseOnlyArtifact.artifactId ); expect(hasPhaseOnlyArtifact).toBe(true); }); it('BUGFIX: proposedSolutions should include selected solution (backward compatibility)', async () => { /** * API CONTRACT BUG: proposedSolutions field currently excludes selected solution * * Current code (query-service.ts:405): * proposedSolutions: alternativeSolutions, // ❌ excludes selected! * * This breaks the API contract and backward compatibility: * - proposedSolutions should contain ALL solutions (selected + alternatives) * - This field represents "all proposed solutions for this requirement" * - Separate fields exist for selectedSolution and alternativeSolutions * * Expected behavior: * - proposedSolutions: [sol1, sol2, sol3] - ALL solutions * - selectedSolution: sol2 - only the selected one * - alternativeSolutions: [sol1, sol3] - only alternatives */ // Select sol2 as the chosen solution await solutionService.selectSolution({ planId, solutionId: sol2Id, reason: 'Best approach', }); const result = await queryService.traceRequirement({ planId, requirementId: reqId, } as never); // BUGFIX TEST: proposedSolutions MUST include selected solution expect(result.trace.proposedSolutions).toHaveLength(3); // ALL solutions expect(result.trace.selectedSolution).not.toBeNull(); if (result.trace.selectedSolution === null) throw new Error('selectedSolution is required'); expect(result.trace.selectedSolution.id).toBe(sol2Id); expect(result.trace.alternativeSolutions).toHaveLength(2); // only alternatives // Verify selected solution IS in proposedSolutions const selectedInProposed = result.trace.proposedSolutions.some( (s: { id: string }) => s.id === sol2Id ); expect(selectedInProposed).toBe(true); // MUST be true for backward compatibility // Verify proposedSolutions contains all 3 solution IDs const solutionIds = result.trace.proposedSolutions.map((s: { id: string }) => s.id); expect(solutionIds).toContain(sol1Id); expect(solutionIds).toContain(sol2Id); // selected expect(solutionIds).toContain(sol3Id); }); }); }); describe('BUG-046: Validate must not show deleted links as broken', () => { it('GREEN: After deleting entity with links, validate should return isValid: true', async () => { // Test Case from QA-REGRESSION-REPORT-2025-12-14.md // 1. Create Phase A const phaseA = await phaseService.addPhase({ planId, phase: { title: 'Phase A for link test', objectives: ['Test link deletion'], }, }); // 2. Create Phase B const phaseB = await phaseService.addPhase({ planId, phase: { title: 'Phase B for link test', objectives: ['Be target'], }, }); // 3. Create Link (A depends_on B) await linkingService.linkEntities({ planId, sourceId: phaseA.phaseId, targetId: phaseB.phaseId, relationType: 'depends_on', }); // 4. Delete Phase A (should cascade delete link) await phaseService.deletePhase({ planId, phaseId: phaseA.phaseId, }); // 5. Verify Link was deleted via get const linkResult = await linkingService.getEntityLinks({ planId, entityId: phaseA.phaseId, direction: 'both', }); expect(linkResult.links).toHaveLength(0); // Link correctly deleted ✅ // 6. Run Validate - should return isValid: true (NO broken_link errors) const validateResult = await queryService.validatePlan({ planId }); // CRITICAL ASSERTION: Should not show broken_link for deleted link expect(validateResult.isValid).toBe(true); expect(validateResult.issues).toEqual([]); // Additional check: no broken_link errors at all const brokenLinks = validateResult.issues.filter((i) => i.type === 'broken_link'); expect(brokenLinks).toHaveLength(0); }); it('GREEN: Multiple deleted links should not accumulate in validate errors', async () => { // Test Case #2 from QA report - verify links don't accumulate // Create Phase X and Y const phaseX = await phaseService.addPhase({ planId, phase: { title: 'Phase X', objectives: ['Test'] }, }); const phaseY = await phaseService.addPhase({ planId, phase: { title: 'Phase Y', objectives: ['Test'] }, }); // Create link X → Y await linkingService.linkEntities({ planId, sourceId: phaseX.phaseId, targetId: phaseY.phaseId, relationType: 'depends_on', }); // Delete Phase X await phaseService.deletePhase({ planId, phaseId: phaseX.phaseId, }); // Run validate - should not show broken links const validateResult = await queryService.validatePlan({ planId }); const brokenLinks = validateResult.issues.filter((i) => i.type === 'broken_link'); expect(brokenLinks).toHaveLength(0); // If this fails, it means deleted links are accumulating from previous test runs expect(validateResult.isValid).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