Files
Codewalkers/apps/server/review/diff-cache.test.ts
Lukas May a50ee01626 test: Add DiffCache unit tests and getPhaseReviewDiff cache integration tests
Creates diff-cache.ts module with generic DiffCache<T> class (TTL, prefix
invalidation, env-var configuration) and exports phaseMetaCache / fileDiffCache
singletons. Wires cache into getPhaseReviewDiff via getHeadCommitHash on
BranchManager. Adds 6 unit tests for DiffCache and 5 integration tests
verifying cache hit/miss behaviour, prefix invalidation, and NOT_FOUND guard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 20:06:28 +01:00

77 lines
2.8 KiB
TypeScript

/**
* Unit tests for DiffCache class.
*
* Tests TTL expiry, prefix invalidation, and env-var TTL configuration.
*/
import { describe, it, expect, vi, afterEach } from 'vitest';
import { DiffCache } from './diff-cache.js';
afterEach(() => {
vi.restoreAllMocks();
});
describe('DiffCache', () => {
it('returns undefined for a key that was never set', () => {
const cache = new DiffCache<string>(5000);
expect(cache.get('nonexistent')).toBeUndefined();
});
it('returns value when entry has not expired', () => {
vi.spyOn(Date, 'now').mockReturnValue(1000);
const cache = new DiffCache<string>(5000);
cache.set('key', 'value');
vi.spyOn(Date, 'now').mockReturnValue(5999); // 4999ms elapsed, TTL=5000
expect(cache.get('key')).toBe('value');
});
it('returns undefined and deletes the entry when TTL has elapsed', () => {
vi.spyOn(Date, 'now').mockReturnValue(1000);
const cache = new DiffCache<string>(5000);
cache.set('key', 'value');
vi.spyOn(Date, 'now').mockReturnValue(6001); // 5001ms elapsed, TTL=5000
expect(cache.get('key')).toBeUndefined();
// Verify the key is no longer stored (second get also returns undefined)
vi.spyOn(Date, 'now').mockReturnValue(6001);
expect(cache.get('key')).toBeUndefined();
});
it('overwrites an existing entry and resets its TTL', () => {
vi.spyOn(Date, 'now').mockReturnValue(1000);
const cache = new DiffCache<string>(5000);
cache.set('key', 'first');
vi.spyOn(Date, 'now').mockReturnValue(4000); // overwrite before expiry
cache.set('key', 'second');
vi.spyOn(Date, 'now').mockReturnValue(8999); // 4999ms after overwrite, TTL=5000
expect(cache.get('key')).toBe('second');
vi.spyOn(Date, 'now').mockReturnValue(9001); // 5001ms after overwrite
expect(cache.get('key')).toBeUndefined();
});
it('invalidateByPrefix removes all matching keys and preserves others', () => {
const cache = new DiffCache<string>(60_000);
cache.set('phase-1:abc', 'a');
cache.set('phase-1:abc:file.ts', 'b');
cache.set('phase-2:xyz', 'c');
cache.invalidateByPrefix('phase-1:');
expect(cache.get('phase-1:abc')).toBeUndefined();
expect(cache.get('phase-1:abc:file.ts')).toBeUndefined();
expect(cache.get('phase-2:xyz')).toBe('c');
});
it('singleton instances use REVIEW_DIFF_CACHE_TTL_MS env var for TTL', async () => {
vi.resetModules();
vi.stubEnv('REVIEW_DIFF_CACHE_TTL_MS', '1000');
const { phaseMetaCache } = await import('./diff-cache.js');
vi.spyOn(Date, 'now').mockReturnValue(0);
phaseMetaCache.set('key', {} as any);
vi.spyOn(Date, 'now').mockReturnValue(999);
expect(phaseMetaCache.get('key')).toBeDefined();
vi.spyOn(Date, 'now').mockReturnValue(1001);
expect(phaseMetaCache.get('key')).toBeUndefined();
vi.unstubAllEnvs();
});
});