Skip to main content
Glama
version-history-service.test.ts78.5 kB
/** * Version History Service Tests * Sprint 7: Version History & Diff * * Test coverage: * - enableHistory and maxHistoryDepth in plan creation * - get_history action for all entity types * - diff action for version comparison * - Automatic history rotation * - Edge cases and backwards compatibility * * Total test cases: 72 */ import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; import { RepositoryFactory } from '../../src/infrastructure/factory/repository-factory.js'; import { FileLockManager } from '../../src/infrastructure/repositories/file/file-lock-manager.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 { DecisionService } from '../../src/domain/services/decision-service.js'; import { ArtifactService } from '../../src/domain/services/artifact-service.js'; import { VersionHistoryService } from '../../src/domain/services/version-history-service.js'; import type { PlanManifest } from '../../src/domain/entities/types.js'; import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; // Helper function to load manifest via repository async function loadManifest(repositoryFactory: RepositoryFactory, planId: string): Promise<PlanManifest> { const planRepo = repositoryFactory.createPlanRepository(); return planRepo.loadManifest(planId); } describe('Version History Service (Sprint 7)', () => { let repositoryFactory: RepositoryFactory; let lockManager: FileLockManager; let planService: PlanService; let versionHistoryService: VersionHistoryService; let requirementService: RequirementService; let solutionService: SolutionService; let phaseService: PhaseService; let decisionService: DecisionService; let artifactService: ArtifactService; let testDir: string; beforeEach(async () => { testDir = path.join(os.tmpdir(), `mcp-version-history-test-${Date.now().toString()}`); 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); versionHistoryService = new VersionHistoryService(repositoryFactory); requirementService = new RequirementService(repositoryFactory, planService, versionHistoryService); solutionService = new SolutionService(repositoryFactory, planService, versionHistoryService); phaseService = new PhaseService(repositoryFactory, planService, versionHistoryService); decisionService = new DecisionService(repositoryFactory, planService, versionHistoryService); artifactService = new ArtifactService(repositoryFactory, planService, versionHistoryService); }); afterEach(async () => { await repositoryFactory.dispose(); await lockManager.dispose(); await fs.rm(testDir, { recursive: true, force: true }); }); // Helper function to create a valid requirement object const createRequirement = (title: string, additionalFields: Record<string, unknown> = {}): { title: string; description: string; priority: 'high' | 'medium' | 'low' | 'critical'; category: 'functional' | 'non-functional' | 'technical'; source: { type: 'user-request'; context?: string }; acceptanceCriteria: string[]; } & Record<string, unknown> => { const { description, priority, category, source, acceptanceCriteria, ...rest } = additionalFields; return { title, description: (description as string | undefined) ?? `Description for ${title}`, priority: (priority as 'high' | 'medium' | 'low' | 'critical' | undefined) ?? 'high', category: (category as 'functional' | 'non-functional' | 'technical' | undefined) ?? 'functional', source: (source as { type: 'user-request'; context?: string } | undefined) ?? { type: 'user-request' as const, context: 'Test context' }, acceptanceCriteria: (acceptanceCriteria as string[] | undefined) ?? ['Criteria 1', 'Criteria 2'], ...rest }; }; // ============================================================================ // TEST GROUP 1: enableHistory and maxHistoryDepth in plan creation (12 tests) // ============================================================================ describe('Plan Creation with History Settings', () => { // Test 1: Create plan WITHOUT enableHistory (default behavior) it('should create plan with history DISABLED by default (enableHistory not specified)', async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test' }); expect(plan.planId).toBeDefined(); // History should be disabled by default const manifest = await loadManifest(repositoryFactory, plan.planId); expect(manifest.enableHistory).toBeUndefined(); // or false expect(manifest.maxHistoryDepth).toBeUndefined(); // or 0 }); // Test 2: Create plan with enableHistory=false explicitly it('should create plan with history disabled when enableHistory=false', async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test', enableHistory: false }); const manifest = await loadManifest(repositoryFactory, plan.planId); expect(manifest.enableHistory).toBe(false); expect(manifest.maxHistoryDepth).toBe(0); }); // Test 3: Create plan with enableHistory=true and default maxHistoryDepth it('should create plan with history enabled when enableHistory=true', async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test', enableHistory: true }); const manifest = await loadManifest(repositoryFactory, plan.planId); expect(manifest.enableHistory).toBe(true); expect(manifest.maxHistoryDepth).toBe(5); // Default depth when enabled }); // Test 4: Create plan with enableHistory=true and custom maxHistoryDepth it('should create plan with custom maxHistoryDepth when specified', async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test', enableHistory: true, maxHistoryDepth: 3 }); const manifest = await loadManifest(repositoryFactory, plan.planId); expect(manifest.enableHistory).toBe(true); expect(manifest.maxHistoryDepth).toBe(3); }); // Test 5: Validate maxHistoryDepth range (0-10) it('should reject maxHistoryDepth > 10', async () => { await expect(planService.createPlan({ name: 'Test Plan', description: 'Test', enableHistory: true, maxHistoryDepth: 15 })).rejects.toThrow(/maxHistoryDepth must be between 0 and 10/i); }); // Test 6: Validate maxHistoryDepth cannot be negative it('should reject negative maxHistoryDepth', async () => { await expect(planService.createPlan({ name: 'Test Plan', description: 'Test', enableHistory: true, maxHistoryDepth: -1 })).rejects.toThrow(/maxHistoryDepth must be between 0 and 10/i); }); // Test 7: maxHistoryDepth=0 should disable history even if enableHistory=true it('should disable history when maxHistoryDepth=0 even if enableHistory=true', async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test', enableHistory: true, maxHistoryDepth: 0 }); const manifest = await loadManifest(repositoryFactory, plan.planId); // maxHistoryDepth=0 means history is effectively disabled expect(manifest.maxHistoryDepth).toBe(0); }); // Test 8: maxHistoryDepth without enableHistory should enable history it('should enable history when maxHistoryDepth > 0 without explicit enableHistory', async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test', maxHistoryDepth: 5 }); const manifest = await loadManifest(repositoryFactory, plan.planId); expect(manifest.enableHistory).toBe(true); expect(manifest.maxHistoryDepth).toBe(5); }); // Test 9: Validate maxHistoryDepth is integer it('should reject non-integer maxHistoryDepth', async () => { await expect(planService.createPlan({ name: 'Test Plan', description: 'Test', maxHistoryDepth: 5.5 as unknown as number })).rejects.toThrow(/maxHistoryDepth must be an integer/i); }); // Test 10: Boundary test - maxHistoryDepth=1 (minimum valid) it('should accept maxHistoryDepth=1', async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test', maxHistoryDepth: 1 }); const manifest = await loadManifest(repositoryFactory, plan.planId); expect(manifest.maxHistoryDepth).toBe(1); expect(manifest.enableHistory).toBe(true); }); // Test 11: Boundary test - maxHistoryDepth=10 (maximum valid) it('should accept maxHistoryDepth=10', async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test', maxHistoryDepth: 10 }); const manifest = await loadManifest(repositoryFactory, plan.planId); expect(manifest.maxHistoryDepth).toBe(10); }); // Test 12: Backwards compatibility - old plans without history settings it('should handle old plans created without enableHistory field', async () => { const plan = await planService.createPlan({ name: 'Old Plan', description: 'Test' }); // Should work without errors const manifest = await loadManifest(repositoryFactory, plan.planId); expect(manifest).toBeDefined(); }); }); // ============================================================================ // TEST GROUP 2: get_history action for Requirements (10 tests) // ============================================================================ describe('get_history for Requirements', () => { let planId: string; beforeEach(async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test', enableHistory: true, maxHistoryDepth: 5 }); planId = plan.planId; }); // Test 13: get_history returns empty array for new requirement it('should return empty history for new requirement', async () => { const req = await requirementService.addRequirement({ planId, requirement: createRequirement('Test Requirement', { description: 'Test', priority: 'high', category: 'functional' }) }); const history = await requirementService.getHistory({ planId, requirementId: req.requirementId }); expect(history.versions).toEqual([]); expect(history.total).toBe(0); }); // Test 14: get_history returns version after update it('should save version in history after requirement update', async () => { const req = await requirementService.addRequirement({ planId, requirement: createRequirement('Original Title', { description: 'Original Description', priority: 'high', category: 'functional' }) }); // Update requirement await requirementService.updateRequirement({ planId, requirementId: req.requirementId, updates: { title: 'Updated Title' } }); const history = await requirementService.getHistory({ planId, requirementId: req.requirementId }); expect(history.versions).toHaveLength(1); expect(history.versions[0].data.title).toBe('Original Title'); expect(history.versions[0].version).toBe(1); expect(history.versions[0].timestamp).toBeDefined(); }); // Test 15: get_history returns multiple versions it('should save multiple versions after multiple updates', async () => { const req = await requirementService.addRequirement({ planId, requirement: createRequirement('Version 1', { description: 'Test', priority: 'high', category: 'functional' }) }); // Update 1 await requirementService.updateRequirement({ planId, requirementId: req.requirementId, updates: { title: 'Version 2' } }); // Update 2 await requirementService.updateRequirement({ planId, requirementId: req.requirementId, updates: { title: 'Version 3' } }); const history = await requirementService.getHistory({ planId, requirementId: req.requirementId }); expect(history.versions).toHaveLength(2); // Reverse chronological order (newest first) expect(history.versions[0].data.title).toBe('Version 2'); expect(history.versions[1].data.title).toBe('Version 1'); }); // Test 16: get_history should NOT save versions when history disabled it('should NOT save history when plan has history disabled', async () => { // Create plan WITHOUT history const noHistoryPlan = await planService.createPlan({ name: 'No History Plan', description: 'Test', enableHistory: false }); const req = await requirementService.addRequirement({ planId: noHistoryPlan.planId, requirement: createRequirement('Original', { description: 'Test', priority: 'high', category: 'functional' }) }); await requirementService.updateRequirement({ planId: noHistoryPlan.planId, requirementId: req.requirementId, updates: { title: 'Updated' } }); // Should return empty history (no versions saved) const history = await requirementService.getHistory({ planId: noHistoryPlan.planId, requirementId: req.requirementId }); expect(history.versions).toHaveLength(0); expect(history.total).toBe(0); }); // Test 17: get_history with pagination (limit) it('should support pagination with limit parameter', async () => { const req = await requirementService.addRequirement({ planId, requirement: createRequirement('V1', { description: 'Test', priority: 'high', category: 'functional' }) }); // Create 5 updates for (let i = 2; i <= 6; i++) { await requirementService.updateRequirement({ planId, requirementId: req.requirementId, updates: { title: `V${i.toString()}` } }); } const history = await requirementService.getHistory({ planId, requirementId: req.requirementId, limit: 3 }); expect(history.versions).toHaveLength(3); expect(history.total).toBe(5); expect(history.hasMore).toBe(true); }); // Test 18: get_history with offset it('should support pagination with offset parameter', async () => { const req = await requirementService.addRequirement({ planId, requirement: createRequirement('V1', { description: 'Test', priority: 'high', category: 'functional' }) }); // Create 3 updates (saves V1, V2, V3 to history) for (let i = 2; i <= 4; i++) { await requirementService.updateRequirement({ planId, requirementId: req.requirementId, updates: { title: `V${i.toString()}` } }); } const history = await requirementService.getHistory({ planId, requirementId: req.requirementId, offset: 1, limit: 2 }); expect(history.versions).toHaveLength(2); // History has [V1, V2, V3], reversed to [V3, V2, V1] // offset=1, limit=2 gives [V2, V1] expect(history.versions[0].data.title).toBe('V2'); expect(history.versions[1].data.title).toBe('V1'); }); // Test 19: get_history saves author if provided it('should save author in version history', async () => { const req = await requirementService.addRequirement({ planId, requirement: createRequirement('Original', { description: 'Test', priority: 'high', category: 'functional' }) }); await requirementService.updateRequirement({ planId, requirementId: req.requirementId, updates: { title: 'Updated' } }); const history = await requirementService.getHistory({ planId, requirementId: req.requirementId }); // Auto-saved with 'claude-code' by system expect(history.versions[0].author).toBeDefined(); expect(history.versions[0].author).toBe('claude-code'); }); // Test 20: get_history returns versions in reverse chronological order (newest first) it('should return versions in reverse chronological order', async () => { const req = await requirementService.addRequirement({ planId, requirement: createRequirement('V1', { description: 'Test', priority: 'high', category: 'functional' }) }); await new Promise(resolve => setTimeout(resolve, 10)); // Small delay await requirementService.updateRequirement({ planId, requirementId: req.requirementId, updates: { title: 'V2' } }); await new Promise(resolve => setTimeout(resolve, 10)); await requirementService.updateRequirement({ planId, requirementId: req.requirementId, updates: { title: 'V3' } }); const history = await requirementService.getHistory({ planId, requirementId: req.requirementId }); // Newest first expect(history.versions[0].data.title).toBe('V2'); // Second newest expect(history.versions[1].data.title).toBe('V1'); // Oldest }); // Test 21: get_history includes timestamp for each version it('should include timestamp for each version', async () => { const req = await requirementService.addRequirement({ planId, requirement: createRequirement('Original', { description: 'Test', priority: 'high', category: 'functional' }) }); await requirementService.updateRequirement({ planId, requirementId: req.requirementId, updates: { title: 'Updated' } }); const history = await requirementService.getHistory({ planId, requirementId: req.requirementId }); expect(history.versions[0].timestamp).toBeDefined(); expect(new Date(history.versions[0].timestamp).getTime()).toBeLessThanOrEqual(Date.now()); }); // Test 21.5: Sprint 7 Fix - currentVersion field updates correctly it('should update currentVersion field when saving versions (Sprint 7 fix)', async () => { const req = await requirementService.addRequirement({ planId, requirement: createRequirement('V1', { description: 'Test', priority: 'high', category: 'functional' }) }); // Update to create version 2 await requirementService.updateRequirement({ planId, requirementId: req.requirementId, updates: { title: 'V2' } }); // Update to create version 3 await requirementService.updateRequirement({ planId, requirementId: req.requirementId, updates: { title: 'V3' } }); const history = await requirementService.getHistory({ planId, requirementId: req.requirementId }); // currentVersion should be updated to the latest version saved // Note: After 3 updates (V1->V2->V3), currentVersion should be 3 expect(history.currentVersion).toBe(3); }); // Test 22: get_history for non-existent requirement it('should return empty history for non-existent requirement', async () => { const history = await requirementService.getHistory({ planId, requirementId: 'non-existent-id' }); // Should return empty history expect(history.versions).toHaveLength(0); expect(history.total).toBe(0); }); }); // ============================================================================ // TEST GROUP 3: Automatic History Rotation (10 tests) // ============================================================================ describe('Automatic History Rotation', () => { // Test 23: Rotation when exceeding maxHistoryDepth it('should automatically delete oldest version when exceeding maxHistoryDepth', async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test', maxHistoryDepth: 3 // Keep only 3 versions }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('V1', { description: 'Test', priority: 'high', category: 'functional' }) }); // Create 5 updates (should keep only last 3) for (let i = 2; i <= 6; i++) { await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: `V${i.toString()}` } }); } const history = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); expect(history.versions).toHaveLength(3); // Should have V3, V4, V5 (oldest V1 and V2 deleted) expect(history.versions[2].data.title).toBe('V3'); expect(history.versions[1].data.title).toBe('V4'); expect(history.versions[0].data.title).toBe('V5'); }); // Test 24: No rotation when under maxHistoryDepth it('should NOT delete versions when under maxHistoryDepth', async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test', maxHistoryDepth: 10 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('V1', { description: 'Test', priority: 'high', category: 'functional' }) }); // Create 5 updates (under limit of 10) for (let i = 2; i <= 6; i++) { await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: `V${i.toString()}` } }); } const history = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); expect(history.versions).toHaveLength(5); }); // Test 25: Rotation with maxHistoryDepth=1 it('should keep only 1 version with maxHistoryDepth=1', async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test', maxHistoryDepth: 1 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('V1', { description: 'Test', priority: 'high', category: 'functional' }) }); await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'V2' } }); await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'V3' } }); const history = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); expect(history.versions).toHaveLength(1); expect(history.versions[0].data.title).toBe('V2'); // Only most recent old version }); // Test 26: Rotation applies to all entity types it('should rotate history for solutions same as requirements', async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test', maxHistoryDepth: 2 }); const sol = await solutionService.proposeSolution({ planId: plan.planId, solution: { title: 'Solution V1', description: 'Test', approach: 'Test approach', tradeoffs: [], addressing: [], evaluation: { effortEstimate: { value: 1, unit: 'days', confidence: 'high' }, technicalFeasibility: 'high', riskAssessment: 'Low risk' } } }); // Create 4 updates for (let i = 2; i <= 5; i++) { await solutionService.updateSolution({ planId: plan.planId, solutionId: sol.solutionId, updates: { title: `Solution V${i.toString()}` } }); } const history = await solutionService.getHistory({ planId: plan.planId, solutionId: sol.solutionId }); expect(history.versions).toHaveLength(2); }); // Test 27: Rotation applies to phases it('should rotate history for phases', async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test', maxHistoryDepth: 2 }); const phase = await phaseService.addPhase({ planId: plan.planId, phase: { title: 'Phase V1', description: 'Test', objectives: ['Test objective'], deliverables: ['Test deliverable'], successCriteria: ['Test criteria'] } }); // Create 3 updates for (let i = 2; i <= 4; i++) { await phaseService.updatePhase({ planId: plan.planId, phaseId: phase.phaseId, updates: { title: `Phase V${i.toString()}` } }); } const history = await phaseService.getHistory({ planId: plan.planId, phaseId: phase.phaseId }); expect(history.versions).toHaveLength(2); }); // Test 28: Rotation applies to decisions it('should rotate history for decisions', async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test', maxHistoryDepth: 2 }); const decision = await decisionService.recordDecision({ planId: plan.planId, decision: { title: 'Decision V1', question: 'Test question', decision: 'Test decision', context: 'Test context', alternativesConsidered: [] } }); // Create 3 updates for (let i = 2; i <= 4; i++) { await decisionService.updateDecision({ planId: plan.planId, decisionId: decision.decisionId, updates: { title: `Decision V${i.toString()}` } }); } const history = await decisionService.getHistory({ planId: plan.planId, decisionId: decision.decisionId }); expect(history.versions).toHaveLength(2); }); // Test 28.5: Sprint 7 Fix - Rotation applies to artifacts it('should rotate history for artifacts (Sprint 7 fix)', async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test', maxHistoryDepth: 2 }); const artifact = await artifactService.addArtifact({ planId: plan.planId, artifact: { title: 'Artifact V1', description: 'Test artifact', artifactType: 'code', content: { language: 'typescript', sourceCode: 'const x = 1;' } } }); // Create 3 updates for (let i = 2; i <= 4; i++) { await artifactService.updateArtifact({ planId: plan.planId, artifactId: artifact.artifactId, updates: { title: `Artifact V${i.toString()}` } }); } const history = await artifactService.getHistory({ planId: plan.planId, artifactId: artifact.artifactId }); expect(history.versions).toHaveLength(2); }); // Test 29: No history saved when maxHistoryDepth=0 it('should NOT save any history when maxHistoryDepth=0', async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test', maxHistoryDepth: 0 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('Original', { description: 'Test', priority: 'high', category: 'functional' }) }); await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'Updated' } }); // Should return empty history const history = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); expect(history.versions).toHaveLength(0); expect(history.total).toBe(0); }); // Test 30: Rotation happens immediately on update it('should rotate history immediately when limit reached', async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test', maxHistoryDepth: 2 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('V1', { description: 'Test', priority: 'high', category: 'functional' }) }); await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'V2' } }); await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'V3' } }); // After third update, should have only 2 versions await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'V4' } }); const history = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); expect(history.versions).toHaveLength(2); // V1 should be deleted expect(history.versions.every(v => v.data.title !== 'V1')).toBe(true); }); // Test 31: Verify storage space is actually freed after rotation it('should physically delete old versions from storage', async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test', maxHistoryDepth: 1 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('V1', { description: 'Test', priority: 'high', category: 'functional' }) }); await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'V2' } }); await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'V3' } }); // Try to access old version directly - should not exist // This tests physical deletion, not just logical hiding const history = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); expect(history.versions).toHaveLength(1); expect(history.versions[0].data.title).toBe('V2'); }); // Test 31.5: RED - history.total should be updated after rotation (Bug Fix) it('should update history.total correctly after automatic rotation', async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test', maxHistoryDepth: 3 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('V1', { description: 'Test', priority: 'high', category: 'functional' }) }); // Create 5 versions total (should trigger rotation, keeping only 3) for (let i = 2; i <= 5; i++) { await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: `V${i.toString()}` } }); } const history = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); // BUG: history.total should be 3 (after rotation), not 5 (before rotation) expect(history.total).toBe(3); // SHOULD FAIL initially expect(history.versions).toHaveLength(3); // Verify hasMore flag is also correct (should be false since we got all 3) expect(history.hasMore).toBe(false); }); // Test 32: Concurrent updates should not corrupt rotation // Skip on Windows due to file locking issues with concurrent writes (process.platform === 'win32' ? it.skip : it)('should handle concurrent updates without corrupting history rotation', async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test', maxHistoryDepth: 3 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('V1', { description: 'Test', priority: 'high', category: 'functional' }) }); // Simulate concurrent updates const updates = []; for (let i = 2; i <= 10; i++) { updates.push( requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: `V${i.toString()}` } }) ); } await Promise.all(updates); const history = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); // Should still have exactly 3 versions expect(history.versions.length).toBeLessThanOrEqual(3); }); }); // ============================================================================ // TEST GROUP 4: diff action (15 tests) // ============================================================================ describe('diff action - Version Comparison', () => { let planId: string; let reqId: string; beforeEach(async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test', enableHistory: true, maxHistoryDepth: 10 }); planId = plan.planId; const req = await requirementService.addRequirement({ planId, requirement: createRequirement('Original Title', { description: 'Original Description', priority: 'high', category: 'functional' }) }); reqId = req.requirementId; }); // Test 33: diff between version 1 and version 2 it('should show diff between two versions', async () => { await requirementService.updateRequirement({ planId, requirementId: reqId, updates: { title: 'Updated Title' } }); const diff = await requirementService.diff({ planId, requirementId: reqId, version1: 1, version2: 2 }); expect(diff.changes).toBeDefined(); expect(diff.changes.title).toBeDefined(); expect(diff.changes.title.from).toBe('Original Title'); expect(diff.changes.title.to).toBe('Updated Title'); }); // Test 34: diff shows only changed fields it('should only include changed fields in diff', async () => { await requirementService.updateRequirement({ planId, requirementId: reqId, updates: { title: 'New Title' } // Only title changed }); const diff = await requirementService.diff({ planId, requirementId: reqId, version1: 1, version2: 2 }); expect(diff.changes.title).toBeDefined(); expect(diff.changes.description).toBeUndefined(); // Not changed expect(diff.changes.priority).toBeUndefined(); // Not changed }); // Test 35: diff with multiple field changes it('should show all changed fields', async () => { await requirementService.updateRequirement({ planId, requirementId: reqId, updates: { title: 'New Title', description: 'New Description', priority: 'low' } }); const diff = await requirementService.diff({ planId, requirementId: reqId, version1: 1, version2: 2 }); expect(Object.keys(diff.changes)).toHaveLength(3); expect(diff.changes.title.from).toBe('Original Title'); expect(diff.changes.description.from).toBe('Original Description'); expect(diff.changes.priority.from).toBe('high'); }); // Test 36: diff between non-adjacent versions it('should support diff between non-adjacent versions', async () => { // V1 -> V2 await requirementService.updateRequirement({ planId, requirementId: reqId, updates: { title: 'V2 Title' } }); // V2 -> V3 await requirementService.updateRequirement({ planId, requirementId: reqId, updates: { title: 'V3 Title' } }); // Diff between V1 and V3 (skip V2) const diff = await requirementService.diff({ planId, requirementId: reqId, version1: 1, version2: 3 }); expect(diff.changes.title.from).toBe('Original Title'); expect(diff.changes.title.to).toBe('V3 Title'); }); // Test 37: diff with array fields (acceptanceCriteria) it('should handle diff for array fields', async () => { // Add requirement with acceptance criteria const req2 = await requirementService.addRequirement({ planId, requirement: createRequirement('Test', { description: 'Test', priority: 'high', category: 'functional', acceptanceCriteria: ['AC1', 'AC2'] }) }); await requirementService.updateRequirement({ planId, requirementId: req2.requirementId, updates: { acceptanceCriteria: ['AC1', 'AC2', 'AC3'] // Added AC3 } }); const diff = await requirementService.diff({ planId, requirementId: req2.requirementId, version1: 1, version2: 2 }); expect(diff.changes.acceptanceCriteria).toBeDefined(); expect(diff.changes.acceptanceCriteria.changed).toBe(true); }); // Test 38: diff with nested object fields it('should handle diff for nested objects', async () => { const sol = await solutionService.proposeSolution({ planId, solution: { title: 'Solution', description: 'Test', approach: 'Test approach', tradeoffs: [], addressing: [], evaluation: { technicalFeasibility: 'high', effortEstimate: { value: 5, unit: 'days', confidence: 'high' }, riskAssessment: 'Test risk assessment' } } }); await solutionService.updateSolution({ planId, solutionId: sol.solutionId, updates: { evaluation: { technicalFeasibility: 'medium', effortEstimate: { value: 10, unit: 'days', confidence: 'medium' }, riskAssessment: 'Test risk assessment' } } }); const diff = await solutionService.diff({ planId, solutionId: sol.solutionId, version1: 1, version2: 2 }); expect(diff.changes.evaluation).toBeDefined(); expect((diff.changes.evaluation.from as { technicalFeasibility: string }).technicalFeasibility).toBe('high'); expect((diff.changes.evaluation.to as { technicalFeasibility: string }).technicalFeasibility).toBe('medium'); }); // Test 39: diff returns empty when no changes it('should return empty changes when versions are identical', async () => { // Create version 2 identical to version 1 await requirementService.updateRequirement({ planId, requirementId: reqId, updates: {} // No actual changes }); const diff = await requirementService.diff({ planId, requirementId: reqId, version1: 1, version2: 2 }); expect(Object.keys(diff.changes)).toHaveLength(0); }); // Test 39.5: Sprint 7 Fix - diff excludes metadata fields correctly it('should exclude metadata fields from diff (Sprint 7 fix)', async () => { // Update requirement - this changes updatedAt, version automatically await requirementService.updateRequirement({ planId, requirementId: reqId, updates: { title: 'Updated Title' } }); const diff = await requirementService.diff({ planId, requirementId: reqId, version1: 1, version2: 2 }); // Metadata fields should NOT appear in changes // Even though updatedAt, version, and createdAt differ between versions expect(diff.changes.updatedAt).toBeUndefined(); expect(diff.changes.version).toBeUndefined(); expect(diff.changes.createdAt).toBeUndefined(); // Only actual data field changes should appear expect(diff.changes.title).toBeDefined(); expect(diff.changes.title.from).toBe('Original Title'); expect(diff.changes.title.to).toBe('Updated Title'); }); // Test 40: diff with version=current (compare with current state) it('should support diffing with current version', async () => { await requirementService.updateRequirement({ planId, requirementId: reqId, updates: { title: 'V2' } }); await requirementService.updateRequirement({ planId, requirementId: reqId, updates: { title: 'V3 Current' } }); const diff = await requirementService.diff({ planId, requirementId: reqId, version1: 1, version2: 3 // Compare with current version (v3) }); expect(diff.changes.title.from).toBe('Original Title'); expect(diff.changes.title.to).toBe('V3 Current'); }); // Test 41: diff throws error for invalid version numbers it('should throw error for non-existent version numbers', async () => { // Create at least one version in history await requirementService.updateRequirement({ planId, requirementId: reqId, updates: { title: 'Updated' } }); await expect(requirementService.diff({ planId, requirementId: reqId, version1: 1, version2: 999 })).rejects.toThrow(/Version 999 not found/i); }); // Test 42: diff validates version1 < version2 it('should allow version1 > version2 (reverse diff)', async () => { await requirementService.updateRequirement({ planId, requirementId: reqId, updates: { title: 'Updated' } }); // Reverse diff should work const diff = await requirementService.diff({ planId, requirementId: reqId, version1: 2, version2: 1 }); expect(diff.changes.title.from).toBe('Updated'); expect(diff.changes.title.to).toBe('Original Title'); }); // Test 43: diff includes metadata about versions it('should include version metadata in diff result', async () => { await requirementService.updateRequirement({ planId, requirementId: reqId, updates: { title: 'Updated' } }); const diff = await requirementService.diff({ planId, requirementId: reqId, version1: 1, version2: 2 }); expect(diff.version1).toBeDefined(); expect(diff.version2).toBeDefined(); expect(diff.version1.version).toBe(1); expect(diff.version2.version).toBe(2); expect(diff.version1.timestamp).toBeDefined(); expect(diff.version2.timestamp).toBeDefined(); }); // Test 44: diff for phases it('should support diff for phases', async () => { const phase = await phaseService.addPhase({ planId, phase: { title: 'Original Phase', description: 'Test', objectives: ['Test objective'], deliverables: ['Test deliverable'], successCriteria: ['Test criteria'] } }); await phaseService.updatePhase({ planId, phaseId: phase.phaseId, updates: { title: 'Updated Phase' } }); const diff = await phaseService.diff({ planId, phaseId: phase.phaseId, version1: 1, version2: 2 }); expect(diff.changes.title.from).toBe('Original Phase'); expect(diff.changes.title.to).toBe('Updated Phase'); }); // Test 45: diff for decisions it('should support diff for decisions', async () => { const decision = await decisionService.recordDecision({ planId, decision: { title: 'Original Decision', question: 'Test?', decision: 'Original', context: 'Test', alternativesConsidered: [] } }); await decisionService.updateDecision({ planId, decisionId: decision.decisionId, updates: { decision: 'Updated Decision' } }); const diff = await decisionService.diff({ planId, decisionId: decision.decisionId, version1: 1, version2: 2 }); expect(diff.changes.decision.from).toBe('Original'); expect(diff.changes.decision.to).toBe('Updated Decision'); }); // Test 46: diff for solutions it('should support diff for solutions', async () => { const sol = await solutionService.proposeSolution({ planId, solution: { title: 'Original', description: 'Test', approach: 'Test approach', tradeoffs: [], addressing: [], evaluation: { effortEstimate: { value: 1, unit: 'days', confidence: 'high' }, technicalFeasibility: 'high', riskAssessment: 'Low risk' } } }); await solutionService.updateSolution({ planId, solutionId: sol.solutionId, updates: { title: 'Updated' } }); const diff = await solutionService.diff({ planId, solutionId: sol.solutionId, version1: 1, version2: 2 }); expect(diff.changes.title.from).toBe('Original'); expect(diff.changes.title.to).toBe('Updated'); }); // Test 47: diff when history is disabled it('should throw error when trying to diff with no history data', async () => { const noHistoryPlan = await planService.createPlan({ name: 'No History', description: 'Test', enableHistory: false }); const req = await requirementService.addRequirement({ planId: noHistoryPlan.planId, requirement: createRequirement('Test', { description: 'Test', priority: 'high', category: 'functional' }) }); // Since history was never enabled, no versions exist to compare await expect(requirementService.diff({ planId: noHistoryPlan.planId, requirementId: req.requirementId, version1: 1, version2: 2 })).rejects.toThrow(/Version \d+ not found/i); }); }); // ============================================================================ // TEST GROUP 5: Edge Cases and Error Handling (13 tests) // ============================================================================ describe('Edge Cases and Error Handling', () => { // Test 48: History for deleted entities should still be accessible it('should preserve history even after entity deletion', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 5 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('Original', { description: 'Test', priority: 'high', category: 'functional' }) }); await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'Updated' } }); // Delete the requirement await requirementService.deleteRequirement({ planId: plan.planId, requirementId: req.requirementId }); // History should still be accessible const history = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); expect(history.versions).toHaveLength(1); }); // Test 49: get_history with invalid planId it('should throw error for invalid planId', async () => { await expect(requirementService.getHistory({ planId: 'invalid-plan-id', requirementId: 'some-id' })).rejects.toThrow(/Plan not found/i); }); // Test 50: get_history with invalid requirementId it('should return empty history for invalid requirementId', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 5 }); // Should return empty history for non-existent requirement const history = await requirementService.getHistory({ planId: plan.planId, requirementId: 'invalid-req-id' }); expect(history.versions).toHaveLength(0); expect(history.total).toBe(0); }); // Test 51: Backwards compatibility - old plans without enableHistory it('should handle old plans created before history feature', async () => { // Create plan without enableHistory (old behavior) const plan = await planService.createPlan({ name: 'Old Plan', description: 'Test' }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('Test', { description: 'Test', priority: 'high', category: 'functional' }) }); // Should return empty history (no history was saved) const history = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); expect(history.versions).toHaveLength(0); expect(history.total).toBe(0); }); // Test 52: Update plan to enable history after creation it('should allow enabling history on existing plan', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', enableHistory: false }); // Enable history await planService.updatePlan({ planId: plan.planId, updates: { enableHistory: true, maxHistoryDepth: 5 } }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('Original', { description: 'Test', priority: 'high', category: 'functional' }) }); await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'Updated' } }); // History should work now const history = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); expect(history.versions).toHaveLength(1); }); // Test 53: Disable history on existing plan it('should allow disabling history on existing plan', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 5 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('Original', { description: 'Test', priority: 'high', category: 'functional' }) }); await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'V2' } }); // Disable history await planService.updatePlan({ planId: plan.planId, updates: { enableHistory: false, maxHistoryDepth: 0 } }); // New updates should NOT save history await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'V3' } }); const history = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); // Should only have 1 version (before disabling) expect(history.versions).toHaveLength(1); expect(history.versions[0].data.title).toBe('Original'); }); // Test 53.5: Sprint 7 Fix - lockVersion increments on plan update it('should increment lockVersion when updating plan (Sprint 7 fix)', async () => { const plan = await planService.createPlan({ name: 'Test Plan', description: 'Test' }); // Get initial lockVersion let manifest = await loadManifest(repositoryFactory, plan.planId); const initialLockVersion = manifest.lockVersion; expect(initialLockVersion).toBe(1); // Update plan await planService.updatePlan({ planId: plan.planId, updates: { name: 'Updated Plan Name' } }); // lockVersion should increment manifest = await loadManifest(repositoryFactory, plan.planId); expect(manifest.lockVersion).toBe(initialLockVersion + 1); expect(manifest.lockVersion).toBe(2); // Another update await planService.updatePlan({ planId: plan.planId, updates: { description: 'Updated Description' } }); // lockVersion should increment again manifest = await loadManifest(repositoryFactory, plan.planId); expect(manifest.lockVersion).toBe(3); }); // Test 54: Very large diff (many fields changed) it('should handle diff with many changed fields', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 5 }); const phase = await phaseService.addPhase({ planId: plan.planId, phase: { title: 'Original', description: 'Original', objectives: ['Obj1', 'Obj2'], deliverables: ['Del1', 'Del2'], successCriteria: ['SC1', 'SC2'] } }); // Update many fields await phaseService.updatePhase({ planId: plan.planId, phaseId: phase.phaseId, updates: { title: 'New Title', description: 'New Description', objectives: ['Obj1', 'Obj2', 'Obj3'], deliverables: ['Del1'], successCriteria: ['SC1', 'SC2', 'SC3', 'SC4'] } }); const diff = await phaseService.diff({ planId: plan.planId, phaseId: phase.phaseId, version1: 1, version2: 2 }); expect(Object.keys(diff.changes).length).toBeGreaterThan(3); }); // Test 55: Pagination with limit=0 it('should handle get_history with limit=0', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 5 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('Test', { description: 'Test', priority: 'high', category: 'functional' }) }); const history = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId, limit: 0 }); expect(history.versions).toHaveLength(0); expect(history.total).toBe(0); }); // Test 56: Pagination with negative offset it('should reject negative offset', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 5 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('Test', { description: 'Test', priority: 'high', category: 'functional' }) }); await expect(requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId, offset: -1 })).rejects.toThrow(/offset must be non-negative/i); }); // Test 57: Diff with same version (version1 === version2) it('should allow diff with same version (should show no changes)', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 5 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('Test', { description: 'Test', priority: 'high', category: 'functional' }) }); await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'Updated' } }); const diff = await requirementService.diff({ planId: plan.planId, requirementId: req.requirementId, version1: 1, version2: 1 }); expect(Object.keys(diff.changes)).toHaveLength(0); }); // Test 58: Storage corruption recovery it('should handle corrupted history gracefully', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 5 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('Test', { description: 'Test', priority: 'high', category: 'functional' }) }); await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'Updated' } }); // Simulate corruption by manually corrupting history file // (Implementation specific - depends on storage structure) // Should either recover or fail gracefully with clear error const historyPromise = requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); await expect(historyPromise).resolves.toBeDefined(); }); // Test 59: Author field persistence it('should persist author field correctly', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 5 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('Test', { description: 'Test', priority: 'high', category: 'functional' }) }); await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'Updated by Alice' } }); await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'Updated by Bob' } }); const history = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); expect(history.versions[0].author).toBeDefined(); // Auto-saved by system // Most recent expect(history.versions[1].author).toBeDefined(); // Auto-saved by system }); // Test 60: Default author when not specified it('should use default author when not specified', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 5 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('Test', { description: 'Test', priority: 'high', category: 'functional' }) }); await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'Updated' } // No author specified }); const history = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); expect(history.versions[0].author).toBeDefined(); // Auto-saved by system // or 'unknown', or null }); }); // ============================================================================ // TEST GROUP 6: Performance and Storage Tests (12 tests) // ============================================================================ describe('Performance and Storage', () => { // Test 61: Storage size with history vs without it('should measure storage impact of history', async () => { const planWithHistory = await planService.createPlan({ name: 'With History', description: 'Test', maxHistoryDepth: 10 }); const planWithoutHistory = await planService.createPlan({ name: 'Without History', description: 'Test', enableHistory: false }); // Add and update same entity in both plans const req1 = await requirementService.addRequirement({ planId: planWithHistory.planId, requirement: createRequirement('Test', { description: 'Test', priority: 'high', category: 'functional' }) }); const req2 = await requirementService.addRequirement({ planId: planWithoutHistory.planId, requirement: createRequirement('Test', { description: 'Test', priority: 'high', category: 'functional' }) }); // Perform 10 updates for (let i = 1; i <= 10; i++) { await requirementService.updateRequirement({ planId: planWithHistory.planId, requirementId: req1.requirementId, updates: { title: `Update ${i.toString()}` } }); await requirementService.updateRequirement({ planId: planWithoutHistory.planId, requirementId: req2.requirementId, updates: { title: `Update ${i.toString()}` } }); } // Measure storage // (Implementation specific - depends on storage structure) // Should verify that with-history plan uses more storage expect(true).toBe(true); // Placeholder }); // Test 62: get_history performance with large history it('should perform reasonably with maxHistoryDepth=10', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 10 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('V1', { description: 'Test', priority: 'high', category: 'functional' }) }); // Create 20 updates (should keep only 10) for (let i = 2; i <= 21; i++) { await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: `V${i.toString()}` } }); } const start = Date.now(); const history = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); const duration = Date.now() - start; expect(history.versions).toHaveLength(10); expect(duration).toBeLessThan(100); // Should be fast }); // Test 63: diff performance it('should perform diff reasonably fast', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 10 }); const phase = await phaseService.addPhase({ planId: plan.planId, phase: { title: 'Test', description: 'Test', objectives: Array(50).fill('Objective'), deliverables: Array(50).fill('Deliverable'), successCriteria: ['Phase complete'] } }); await phaseService.updatePhase({ planId: plan.planId, phaseId: phase.phaseId, updates: { objectives: Array(50).fill('Updated Objective') } }); const start = Date.now(); const diff = await phaseService.diff({ planId: plan.planId, phaseId: phase.phaseId, version1: 1, version2: 2 }); const duration = Date.now() - start; expect(diff.changes).toBeDefined(); expect(duration).toBeLessThan(50); // Should be very fast }); // Test 64: Concurrent get_history calls it('should handle concurrent get_history calls', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 5 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('Test', { description: 'Test', priority: 'high', category: 'functional' }) }); await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'Updated' } }); // Multiple concurrent get_history calls const promises = Array(10).fill(null).map(() => requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }) ); const results = await Promise.all(promises); results.forEach(result => { expect(result.versions).toHaveLength(1); }); }); // Test 65: Memory usage with maxHistoryDepth=10 it('should not leak memory with continuous updates', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 10 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('Test', { description: 'Test', priority: 'high', category: 'functional' }) }); // Simulate 100 updates (should maintain constant memory with rotation) for (let i = 1; i <= 100; i++) { await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: `Update ${i.toString()}` } }); } const history = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); // Should still have exactly 10 versions expect(history.versions).toHaveLength(10); }); // Test 66: Large entity diff it('should handle diff of large entities', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 5 }); const largeDescription = 'A'.repeat(2000); // Max allowed length (BUG-012 fix) const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('Test', { description: largeDescription, priority: 'high', category: 'functional' }) }); await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { description: 'B'.repeat(2000) } // Max allowed length (BUG-012 fix) }); const start = Date.now(); const diff = await requirementService.diff({ planId: plan.planId, requirementId: req.requirementId, version1: 1, version2: 2 }); const duration = Date.now() - start; expect(diff.changes.description).toBeDefined(); expect(duration).toBeLessThan(200); // Should still be reasonably fast }); // Test 67: Batch updates with history it('should handle history with rapid updates', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 5 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('Test', { description: 'Test', priority: 'high', category: 'functional' }) }); // Rapid sequential updates for (let i = 1; i <= 20; i++) { await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: `Rapid Update ${i.toString()}` } }); } const history = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); expect(history.versions).toHaveLength(5); }); // Test 68: Storage cleanup after maxHistoryDepth change it('should cleanup history when maxHistoryDepth decreased', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 10 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('Test', { description: 'Test', priority: 'high', category: 'functional' }) }); // Create 10 versions for (let i = 1; i <= 10; i++) { await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: `V${i.toString()}` } }); } // Decrease maxHistoryDepth await planService.updatePlan({ planId: plan.planId, updates: { maxHistoryDepth: 3 } }); // Trigger cleanup (may require explicit call or happen on next update) await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'Trigger cleanup' } }); const history = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); expect(history.versions.length).toBeLessThanOrEqual(3); }); // Test 69: Verify version numbers are sequential it('should maintain sequential version numbers', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 10 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('Test', { description: 'Test', priority: 'high', category: 'functional' }) }); for (let i = 1; i <= 5; i++) { await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: `V${i.toString()}` } }); } const history = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); // Version numbers should be sequential const versions = history.versions.map(v => v.version).sort((a, b) => a - b); for (let i = 0; i < versions.length - 1; i++) { expect(versions[i + 1] - versions[i]).toBe(1); } }); // Test 70: History with fields parameter (minimal payload) it('should support fields parameter in get_history for minimal payload', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 5 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('Test', { description: 'Test', priority: 'high', category: 'functional', acceptanceCriteria: ['AC1', 'AC2', 'AC3'] }) }); await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'Updated' } }); const history = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId}); expect(history.versions[0].data.title).toBeDefined(); expect(history.versions[0].data.description).toBeDefined(); // Fields filtering not implemented yet }); // Test 71: History ordering consistency it('should return history in consistent order across calls', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 5 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('Test', { description: 'Test', priority: 'high', category: 'functional' }) }); for (let i = 1; i <= 5; i++) { await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: `V${i.toString()}` } }); } // Call get_history multiple times const history1 = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); const history2 = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); // Order should be identical expect(history1.versions.map(v => v.version)).toEqual( history2.versions.map(v => v.version) ); }); // Test 72: Total count accuracy it('should return accurate total count with pagination', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 10 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('Test', { description: 'Test', priority: 'high', category: 'functional' }) }); // Create 7 versions for (let i = 1; i <= 7; i++) { await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: `V${i.toString()}` } }); } const page1 = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId, limit: 3, offset: 0 }); const page2 = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId, limit: 3, offset: 3 }); expect(page1.total).toBe(7); expect(page2.total).toBe(7); expect(page1.versions).toHaveLength(3); expect(page2.versions).toHaveLength(3); }); }); // REQ-6: Version History Bugs describe('version history bugs (REQ-6)', () => { it('RED: diff should find version 1 within maxHistoryDepth', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 10 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('V1', { description: 'Test', priority: 'high', category: 'functional' }) }); // Create versions 2, 3, 4 await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'V2' } }); await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'V3' } }); await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'V4' } }); // Should be able to diff version 1 and 4 (maxHistoryDepth=10, only 4 versions total) const diff = await requirementService.diff({ planId: plan.planId, requirementId: req.requirementId, version1: 1, version2: 4 }); expect(diff.version1.version).toBe(1); expect(diff.version2.version).toBe(4); expect(diff.changes.title).toBeDefined(); expect(diff.changes.title.from).toBe('V1'); expect(diff.changes.title.to).toBe('V4'); }); it('RED: getHistory should return correct currentVersion', async () => { const plan = await planService.createPlan({ name: 'Test', description: 'Test', maxHistoryDepth: 10 }); const req = await requirementService.addRequirement({ planId: plan.planId, requirement: createRequirement('V1', { description: 'Test', priority: 'high', category: 'functional' }) }); // Create versions 2, 3, 4 await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'V2' } }); await requirementService.updateRequirement({ planId: plan.planId, requirementId: req.requirementId, updates: { title: 'V3' } }); // Get current requirement to verify actual version const current = await requirementService.getRequirement({ planId: plan.planId, requirementId: req.requirementId }); // Get history const history = await requirementService.getHistory({ planId: plan.planId, requirementId: req.requirementId }); // currentVersion in history should match the actual entity version expect(history.currentVersion).toBe(current.requirement.version); expect(history.currentVersion).toBe(3); // Should be 3, not 2 }); }); });

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