@iservu-inc/adf-cli
Version:
CLI tool for AgentDevFramework - AI-assisted development framework with multi-provider AI support
340 lines (255 loc) • 11 kB
JavaScript
const {
calculateDecay,
getDecayRate,
renewPattern,
applyDecayToStoredPatterns,
renewStoredPattern,
getDefaultDecayConfig
} = require('../lib/learning/pattern-detector');
describe('Pattern Decay Algorithm', () => {
const createPattern = (overrides = {}) => ({
id: 'pattern_001',
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
});
const config = getDefaultDecayConfig();
describe('getDecayRate', () => {
test('high confidence (≥90) decays at half rate', () => {
const highRate = getDecayRate(95, 0.15);
expect(highRate).toBe(0.075); // 7.5%
});
test('medium confidence (75-89) decays at normal rate', () => {
const mediumRate = getDecayRate(80, 0.15);
expect(mediumRate).toBe(0.15); // 15%
});
test('low confidence (<75) decays at 1.5x rate', () => {
const lowRate = getDecayRate(65, 0.15);
expect(lowRate).toBe(0.225); // 22.5%
});
});
describe('calculateDecay', () => {
test('decays confidence over time', () => {
const pattern = createPattern({
confidence: 90,
lastSeen: new Date('2025-08-01').toISOString()
});
// Mock current date to 2 months later
const mockDate = new Date('2025-10-01');
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const decayed = calculateDecay(pattern, config);
// Should decay over 2 months
expect(decayed.confidence).toBeLessThan(90);
expect(decayed.lastDecayCalculation).toBe(mockDate.toISOString());
jest.restoreAllMocks();
});
test('high confidence patterns decay slower than low confidence', () => {
const highPattern = createPattern({
confidence: 95,
lastSeen: new Date('2025-08-01').toISOString()
});
const lowPattern = createPattern({
confidence: 65,
lastSeen: new Date('2025-08-01').toISOString()
});
const mockDate = new Date('2025-10-01');
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const highDecayed = calculateDecay(highPattern, config);
const lowDecayed = calculateDecay(lowPattern, config);
// Calculate decay percentages
const highDecayPct = (highPattern.confidence - highDecayed.confidence) / highPattern.confidence;
const lowDecayPct = (lowPattern.confidence - lowDecayed.confidence) / lowPattern.confidence;
expect(highDecayPct).toBeLessThan(lowDecayPct);
jest.restoreAllMocks();
});
test('skips decay if disabled', () => {
const pattern = createPattern();
const disabledConfig = { ...config, enabled: false };
const result = calculateDecay(pattern, disabledConfig);
expect(result.confidence).toBe(pattern.confidence);
});
test('skips decay for user-approved patterns when protected', () => {
const approvedPattern = createPattern({
userApproved: true,
lastSeen: new Date('2025-06-01').toISOString()
});
const protectConfig = { ...config, protectApproved: true };
const mockDate = new Date('2025-10-01');
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const decayed = calculateDecay(approvedPattern, protectConfig);
const normalDecayed = calculateDecay(createPattern({ userApproved: false, lastSeen: new Date('2025-06-01').toISOString() }), config);
// User-approved should decay slower
expect(decayed.confidence).toBeGreaterThan(normalDecayed.confidence);
jest.restoreAllMocks();
});
test('skips decay if recently seen (< 1 week)', () => {
const recentPattern = createPattern({
lastSeen: new Date('2025-09-28').toISOString()
});
const mockDate = new Date('2025-10-01');
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const result = calculateDecay(recentPattern, config);
expect(result.confidence).toBe(recentPattern.confidence);
jest.restoreAllMocks();
});
test('confidence never goes below 0', () => {
const lowPattern = createPattern({
confidence: 5,
lastSeen: new Date('2024-01-01').toISOString()
});
const mockDate = new Date('2025-10-01');
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const decayed = calculateDecay(lowPattern, config);
expect(decayed.confidence).toBeGreaterThanOrEqual(0);
jest.restoreAllMocks();
});
});
describe('renewPattern', () => {
test('increases confidence by renewal boost', () => {
const pattern = createPattern({ confidence: 60 });
const mockDate = new Date('2025-10-01');
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const renewed = renewPattern(pattern, config);
expect(renewed.confidence).toBe(70); // 60 + 10 (renewalBoost)
expect(renewed.timesRenewed).toBe(1);
expect(renewed.lastSeen).toBe(mockDate.toISOString());
jest.restoreAllMocks();
});
test('caps at initialConfidence + 5', () => {
const pattern = createPattern({
confidence: 93,
initialConfidence: 90
});
const renewed = renewPattern(pattern, config);
expect(renewed.confidence).toBe(95); // Max is 90 + 5
});
test('never exceeds 100', () => {
const pattern = createPattern({
confidence: 96,
initialConfidence: 98
});
const renewed = renewPattern(pattern, config);
expect(renewed.confidence).toBe(100); // Capped at 100
});
test('updates lastSeen and prevents immediate decay', () => {
const pattern = createPattern();
const mockDate = new Date('2025-10-01');
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const renewed = renewPattern(pattern, config);
expect(renewed.lastSeen).toBe(mockDate.toISOString());
expect(renewed.lastDecayCalculation).toBe(mockDate.toISOString());
jest.restoreAllMocks();
});
});
describe('applyDecayToStoredPatterns', () => {
test('applies decay to all patterns', () => {
const patterns = [
createPattern({ id: 'p1', confidence: 90, lastSeen: new Date('2025-08-01').toISOString() }),
createPattern({ id: 'p2', confidence: 80, lastSeen: new Date('2025-08-01').toISOString() }),
createPattern({ id: 'p3', confidence: 70, lastSeen: new Date('2025-08-01').toISOString() })
];
const mockDate = new Date('2025-10-01');
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const decayed = applyDecayToStoredPatterns(patterns, config);
expect(decayed.length).toBe(3);
expect(decayed[0].confidence).toBeLessThan(90);
expect(decayed[1].confidence).toBeLessThan(80);
expect(decayed[2].confidence).toBeLessThan(70);
jest.restoreAllMocks();
});
test('handles empty pattern array', () => {
const patterns = [];
const decayed = applyDecayToStoredPatterns(patterns, config);
expect(decayed).toEqual([]);
});
});
describe('renewStoredPattern', () => {
test('renews a single pattern', () => {
const pattern = createPattern({ confidence: 65 });
const mockDate = new Date('2025-10-01');
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const renewed = renewStoredPattern(pattern, config);
expect(renewed.confidence).toBe(75);
expect(renewed.timesRenewed).toBe(1);
jest.restoreAllMocks();
});
});
describe('Edge Cases', () => {
test('handles very old patterns (6+ months)', () => {
const oldPattern = createPattern({
confidence: 80,
lastSeen: new Date('2024-12-01').toISOString()
});
const mockDate = new Date('2025-10-01');
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const decayed = calculateDecay(oldPattern, config);
// Should have significantly decayed after 10 months
expect(decayed.confidence).toBeLessThan(40);
jest.restoreAllMocks();
});
test('handles pattern oscillation (multiple renewals)', () => {
let pattern = createPattern({ confidence: 70, timesRenewed: 0 });
// Simulate multiple renewals
for (let i = 0; i < 3; i++) {
pattern = renewPattern(pattern, config);
}
expect(pattern.timesRenewed).toBe(3);
expect(pattern.confidence).toBeGreaterThan(70);
});
test('handles missing timestamp fields gracefully', () => {
const incompletePattern = {
id: 'pattern_incomplete',
confidence: 80,
type: 'consistent_skip'
// Missing lastSeen, createdAt, etc.
};
// Should not throw error
expect(() => {
calculateDecay(incompletePattern, config);
}).not.toThrow();
});
test('handles zero confidence gracefully', () => {
const zeroPattern = createPattern({ confidence: 0 });
const decayed = calculateDecay(zeroPattern, config);
expect(decayed.confidence).toBe(0);
});
test('handles 100% confidence', () => {
const maxPattern = createPattern({
confidence: 100,
lastSeen: new Date('2025-08-01').toISOString()
});
const mockDate = new Date('2025-10-01');
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const decayed = calculateDecay(maxPattern, config);
expect(decayed.confidence).toBeLessThan(100);
expect(decayed.confidence).toBeGreaterThan(0);
jest.restoreAllMocks();
});
});
describe('getDefaultDecayConfig', () => {
test('returns valid default configuration', () => {
const defaultConfig = getDefaultDecayConfig();
expect(defaultConfig).toHaveProperty('enabled');
expect(defaultConfig).toHaveProperty('baseDecayRate');
expect(defaultConfig).toHaveProperty('minConfidenceThreshold');
expect(defaultConfig).toHaveProperty('removeBelow');
expect(defaultConfig).toHaveProperty('renewalBoost');
expect(defaultConfig).toHaveProperty('protectApproved');
expect(defaultConfig).toHaveProperty('maxInactiveMonths');
expect(defaultConfig.enabled).toBe(true);
expect(defaultConfig.baseDecayRate).toBe(0.15);
expect(defaultConfig.minConfidenceThreshold).toBe(50);
expect(defaultConfig.removeBelow).toBe(40);
});
});
});