@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
JavaScript
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);
});
});
});