UNPKG

@iservu-inc/adf-cli

Version:

CLI tool for AgentDevFramework - AI-assisted development framework with multi-provider AI support

395 lines (318 loc) 12.7 kB
const fs = require('fs-extra'); const path = require('path'); const DecayManager = require('../lib/learning/decay-manager'); const storage = require('../lib/learning/storage'); describe('DecayManager Integration Tests', () => { const testProjectPath = path.join(__dirname, 'test-project-decay'); let decayManager; beforeEach(async () => { // Clean up test directory await fs.remove(testProjectPath); await fs.ensureDir(testProjectPath); decayManager = new DecayManager(testProjectPath); }); afterEach(async () => { // Clean up await fs.remove(testProjectPath); }); const createTestPattern = (overrides = {}) => ({ id: `pattern_${Date.now()}`, type: 'consistent_skip', questionId: 'q_deployment', confidence: 90, initialConfidence: 90, sessionsAnalyzed: 10, skipCount: 9, status: 'active', userApproved: false, createdAt: new Date('2025-01-01').toISOString(), lastSeen: new Date('2025-09-01').toISOString(), lastDecayCalculation: new Date('2025-09-01').toISOString(), timesRenewed: 0, ...overrides }); describe('loadPatternsWithDecay', () => { test('loads patterns and applies decay', async () => { // Create patterns with old lastSeen dates const patterns = [ createTestPattern({ id: 'p1', confidence: 90, lastSeen: new Date('2025-07-01').toISOString() }), createTestPattern({ id: 'p2', confidence: 80, lastSeen: new Date('2025-07-01').toISOString() }) ]; await storage.savePatterns(testProjectPath, { version: '1.0', patterns }); const loadedPatterns = await decayManager.loadPatternsWithDecay(); expect(loadedPatterns.patterns.length).toBe(2); expect(loadedPatterns.patterns[0].confidence).toBeLessThan(90); expect(loadedPatterns.patterns[1].confidence).toBeLessThan(80); }); test('removes stale patterns automatically', async () => { // Create patterns - one stale, one active const patterns = [ createTestPattern({ id: 'stale', confidence: 35, // Below removeBelow threshold (40) lastSeen: new Date('2025-09-01').toISOString() }), createTestPattern({ id: 'active', confidence: 80, lastSeen: new Date('2025-09-01').toISOString() }) ]; await storage.savePatterns(testProjectPath, { version: '1.0', patterns }); const loadedPatterns = await decayManager.loadPatternsWithDecay(); // Should only have active pattern expect(loadedPatterns.patterns.length).toBe(1); expect(loadedPatterns.patterns[0].id).toBe('active'); }); test('skips decay if disabled in config', async () => { // Set decay disabled const config = await storage.getLearningConfig(testProjectPath); config.decay.enabled = false; await storage.saveLearningConfig(testProjectPath, config); const patterns = [ createTestPattern({ confidence: 90, lastSeen: new Date('2025-06-01').toISOString() }) ]; await storage.savePatterns(testProjectPath, { version: '1.0', patterns }); const loadedPatterns = await decayManager.loadPatternsWithDecay(); // Confidence should remain unchanged expect(loadedPatterns.patterns[0].confidence).toBe(90); }); test('handles empty pattern list', async () => { await storage.savePatterns(testProjectPath, { version: '1.0', patterns: [] }); const loadedPatterns = await decayManager.loadPatternsWithDecay(); expect(loadedPatterns.patterns).toEqual([]); }); }); describe('checkForPatternRenewal', () => { test('renews matching pattern on skip event', async () => { const pattern = createTestPattern({ id: 'renewable', questionId: 'q_deployment', confidence: 60 }); await storage.savePatterns(testProjectPath, { version: '1.0', patterns: [pattern] }); const skipEvent = { questionId: 'q_deployment', action: 'skipped', reason: 'manual' }; const renewedIds = await decayManager.checkForPatternRenewal(skipEvent); expect(renewedIds.length).toBe(1); // Verify pattern was renewed const updatedPatterns = await storage.getPatterns(testProjectPath); expect(updatedPatterns.patterns[0].confidence).toBe(70); // 60 + 10 expect(updatedPatterns.patterns[0].timesRenewed).toBe(1); }); test('renews category pattern on matching skip', async () => { const pattern = createTestPattern({ id: 'category_pattern', type: 'category_skip', category: 'deployment', confidence: 65 }); await storage.savePatterns(testProjectPath, { version: '1.0', patterns: [pattern] }); const skipEvent = { questionId: 'q_any', category: 'deployment', action: 'skipped', reason: 'manual' }; const renewedIds = await decayManager.checkForPatternRenewal(skipEvent); expect(renewedIds.length).toBe(1); }); test('limits renewals per day per pattern', async () => { const pattern = createTestPattern({ id: 'limited', questionId: 'q_test', confidence: 60 }); await storage.savePatterns(testProjectPath, { version: '1.0', patterns: [pattern] }); const skipEvent = { questionId: 'q_test', action: 'skipped', reason: 'manual' }; // First renewal should work let renewedIds = await decayManager.checkForPatternRenewal(skipEvent); expect(renewedIds.length).toBe(1); // Second renewal on same day should be blocked (maxRenewalsPerDay = 1) renewedIds = await decayManager.checkForPatternRenewal(skipEvent); expect(renewedIds.length).toBe(0); }); test('does not renew non-matching patterns', async () => { const pattern = createTestPattern({ id: 'non_matching', questionId: 'q_deployment', confidence: 60 }); await storage.savePatterns(testProjectPath, { version: '1.0', patterns: [pattern] }); const skipEvent = { questionId: 'q_other_question', action: 'skipped', reason: 'manual' }; const renewedIds = await decayManager.checkForPatternRenewal(skipEvent); expect(renewedIds.length).toBe(0); }); test('skips renewal if decay disabled', async () => { // Disable decay const config = await storage.getLearningConfig(testProjectPath); config.decay.enabled = false; await storage.saveLearningConfig(testProjectPath, config); const pattern = createTestPattern({ questionId: 'q_test', confidence: 60 }); await storage.savePatterns(testProjectPath, { version: '1.0', patterns: [pattern] }); const skipEvent = { questionId: 'q_test', action: 'skipped' }; const renewedIds = await decayManager.checkForPatternRenewal(skipEvent); expect(renewedIds.length).toBe(0); }); }); describe('cleanupStalePatterns', () => { test('removes patterns below confidence threshold', () => { const patterns = [ createTestPattern({ id: 'low', confidence: 30 }), // Below 40 createTestPattern({ id: 'ok', confidence: 50 }) ]; const { activePatterns, removedPatterns } = decayManager.cleanupStalePatterns(patterns); expect(activePatterns.length).toBe(1); expect(activePatterns[0].id).toBe('ok'); expect(removedPatterns.length).toBe(1); expect(removedPatterns[0].id).toBe('low'); expect(removedPatterns[0].removalReason).toBe('confidence_too_low'); }); test('removes patterns inactive too long', () => { const patterns = [ createTestPattern({ id: 'ancient', confidence: 80, lastSeen: new Date('2024-12-01').toISOString() // 10 months ago }), createTestPattern({ id: 'recent', confidence: 80, lastSeen: new Date('2025-09-01').toISOString() }) ]; const { activePatterns, removedPatterns } = decayManager.cleanupStalePatterns(patterns); expect(activePatterns.length).toBe(1); expect(activePatterns[0].id).toBe('recent'); expect(removedPatterns.length).toBe(1); expect(removedPatterns[0].id).toBe('ancient'); expect(removedPatterns[0].removalReason).toBe('inactive_too_long'); }); test('keeps all patterns if none meet removal criteria', () => { const patterns = [ createTestPattern({ confidence: 80, lastSeen: new Date('2025-09-01').toISOString() }), createTestPattern({ confidence: 70, lastSeen: new Date('2025-09-01').toISOString() }) ]; const { activePatterns, removedPatterns } = decayManager.cleanupStalePatterns(patterns); expect(activePatterns.length).toBe(2); expect(removedPatterns.length).toBe(0); }); }); describe('triggerDecayCalculation', () => { test('manually triggers decay and returns stats', async () => { const patterns = [ createTestPattern({ confidence: 90, lastSeen: new Date('2025-07-01').toISOString() }), createTestPattern({ confidence: 35, lastSeen: new Date('2025-07-01').toISOString() }) // Will be removed ]; await storage.savePatterns(testProjectPath, { version: '1.0', patterns }); const results = await decayManager.triggerDecayCalculation(); expect(results.totalPatterns).toBe(2); expect(results.activePatterns).toBe(1); expect(results.removedPatterns).toBe(1); expect(results.removed.length).toBe(1); }); test('handles empty pattern list', async () => { await storage.savePatterns(testProjectPath, { version: '1.0', patterns: [] }); const results = await decayManager.triggerDecayCalculation(); expect(results.totalPatterns).toBe(0); expect(results.activePatterns).toBe(0); expect(results.removedPatterns).toBe(0); }); }); describe('getDecayStats', () => { test('calculates decay statistics', async () => { const patterns = [ createTestPattern({ confidence: 85, createdAt: new Date('2025-08-01').toISOString() }), // High createTestPattern({ confidence: 70, createdAt: new Date('2025-08-01').toISOString() }), // Medium createTestPattern({ confidence: 55, createdAt: new Date('2025-08-01').toISOString() }) // Low ]; await storage.savePatterns(testProjectPath, { version: '1.0', patterns }); const stats = await decayManager.getDecayStats(); expect(stats.totalPatterns).toBe(3); expect(stats.highConfidence).toBe(1); expect(stats.mediumConfidence).toBe(1); expect(stats.lowConfidence).toBe(1); expect(stats.avgConfidence).toBeGreaterThan(0); expect(stats.avgAge).toBeGreaterThan(0); }); test('handles empty patterns', async () => { await storage.savePatterns(testProjectPath, { version: '1.0', patterns: [] }); const stats = await decayManager.getDecayStats(); expect(stats.totalPatterns).toBe(0); expect(stats.highConfidence).toBe(0); expect(stats.avgConfidence).toBe(0); }); }); describe('saveRemovedPatternsHistory', () => { test('saves removed patterns to history file', async () => { const removedPatterns = [ createTestPattern({ id: 'removed1', confidence: 30 }), createTestPattern({ id: 'removed2', confidence: 35 }) ]; await decayManager.saveRemovedPatternsHistory(removedPatterns); const historyFile = path.join(testProjectPath, '.adf', 'learning', 'removed-patterns.json'); const exists = await fs.pathExists(historyFile); expect(exists).toBe(true); const history = await fs.readJSON(historyFile); expect(history.removals).toHaveLength(1); expect(history.removals[0].removedCount).toBe(2); expect(history.removals[0].patterns).toHaveLength(2); }); }); });