chore: resolve merge conflicts for DiffCache test task

Resolves add/add conflict in diff-cache.ts (kept typed PhaseMetaResponse/
FileDiffResponse interfaces from HEAD over unknown-typed singletons from test
branch) and content conflict in phase.ts (kept both phaseMetaCache and
fileDiffCache imports; removed auto-merged duplicate firstClone/headHash/
cacheKey/cached declarations and unreachable empty-projects guard).

Also cleans auto-merged duplicate getHeadCommitHash in orchestrator.test.ts
and simple-git-branch-manager.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas May
2026-03-06 20:33:41 +01:00
6 changed files with 350 additions and 12 deletions

View 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();
});
});

View File

@@ -1,6 +1,9 @@
/**
* DiffCache — in-memory cache with TTL and prefix-based invalidation.
* Used to avoid re-running expensive git diff subprocesses on repeated requests.
* DiffCache — in-memory TTL cache for git diff results.
*
* Keyed by `phaseId:headHash` (or `phaseId:headHash:filePath` for per-file diffs).
* TTL defaults to 5 minutes, configurable via REVIEW_DIFF_CACHE_TTL_MS env var.
* Prefix-based invalidation clears all entries for a phase when a new commit lands.
*/
import type { FileStatEntry } from '../git/types.js';