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>
This commit is contained in:
76
apps/server/review/diff-cache.test.ts
Normal file
76
apps/server/review/diff-cache.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user