import { applyTemporalDecayToHits, applyTemporalDecayToScore, calculateTemporalDecayMultiplier, DEFAULT_TEMPORAL_DECAY_CONFIG, isEvergreenMemoryPath, parseMemoryDateFromPath, } from '../temporalDecay'; const DAY_MS = 24 * 60 * 60 * 1000; describe('calculateTemporalDecayMultiplier', () => { it('is 1 at age=0', () => { expect( calculateTemporalDecayMultiplier({ ageInDays: 0, halfLifeDays: 30 }) ).toBe(1); }); it('is exactly 0.5 at half-life', () => { expect( calculateTemporalDecayMultiplier({ ageInDays: 30, halfLifeDays: 30 }) ).toBeCloseTo(0.5); }); it('returns 1 for non-positive half life (no decay)', () => { expect( calculateTemporalDecayMultiplier({ ageInDays: 100, halfLifeDays: 0 }) ).toBe(1); expect( calculateTemporalDecayMultiplier({ ageInDays: 100, halfLifeDays: -1 }) ).toBe(1); }); it('clamps negative age to 0', () => { expect( calculateTemporalDecayMultiplier({ ageInDays: -5, halfLifeDays: 30 }) ).toBe(1); }); }); describe('applyTemporalDecayToScore', () => { it('halves score at half-life', () => { expect( applyTemporalDecayToScore({ score: 0.8, ageInDays: 30, halfLifeDays: 30 }) ).toBeCloseTo(0.4); }); }); describe('parseMemoryDateFromPath', () => { it('parses dated memory files', () => { const d = parseMemoryDateFromPath('memory/2026-04-13.md'); expect(d).not.toBeNull(); expect(d!.getUTCFullYear()).toBe(2026); expect(d!.getUTCMonth()).toBe(3); expect(d!.getUTCDate()).toBe(13); }); it('rejects invalid dates', () => { expect(parseMemoryDateFromPath('memory/2026-02-30.md')).toBeNull(); expect(parseMemoryDateFromPath('memory/notes.md')).toBeNull(); expect(parseMemoryDateFromPath('MEMORY.md')).toBeNull(); }); it('handles windows-style separators', () => { expect(parseMemoryDateFromPath('memory\\2026-04-13.md')).not.toBeNull(); }); }); describe('isEvergreenMemoryPath', () => { it('treats MEMORY.md as evergreen', () => { expect(isEvergreenMemoryPath('MEMORY.md')).toBe(true); expect(isEvergreenMemoryPath('memory.md')).toBe(true); }); it('treats dated files as non-evergreen', () => { expect(isEvergreenMemoryPath('memory/2026-04-13.md')).toBe(false); }); it('treats topic files as evergreen', () => { expect(isEvergreenMemoryPath('memory/topics.md')).toBe(true); expect(isEvergreenMemoryPath('memory/architecture.md')).toBe(true); }); it('treats non-memory paths as non-evergreen', () => { expect(isEvergreenMemoryPath('notes.md')).toBe(false); }); }); describe('applyTemporalDecayToHits', () => { const nowMs = Date.UTC(2026, 3, 13); // 2026-04-13 it('is a no-op when disabled', () => { const hits = [ { path: 'memory/2026-03-14.md', score: 0.9, createdAt: new Date(nowMs) }, ]; expect(applyTemporalDecayToHits(hits, { enabled: false }, nowMs)).toEqual( hits ); }); it('decays dated files by date in path', () => { const hits = [ { path: 'memory/2026-03-14.md', score: 1.0, createdAt: new Date(nowMs) }, // 30 days old ]; const out = applyTemporalDecayToHits( hits, { enabled: true, halfLifeDays: 30 }, nowMs ); expect(out[0].score).toBeCloseTo(0.5, 2); }); it('does NOT decay evergreen files', () => { const hits = [ { path: 'memory/architecture.md', score: 0.9, createdAt: new Date(nowMs - 100 * DAY_MS), }, ]; const out = applyTemporalDecayToHits( hits, { enabled: true, halfLifeDays: 30 }, nowMs ); expect(out[0].score).toBe(0.9); }); it('falls back to createdAt for non-dated non-evergreen paths', () => { const hits = [ { path: 'notes.md', score: 1.0, createdAt: new Date(nowMs - 30 * DAY_MS), }, ]; const out = applyTemporalDecayToHits( hits, { enabled: true, halfLifeDays: 30 }, nowMs ); expect(out[0].score).toBeCloseTo(0.5, 2); }); it('default config is disabled with 30 day half-life', () => { expect(DEFAULT_TEMPORAL_DECAY_CONFIG).toEqual({ enabled: false, halfLifeDays: 30, }); }); });