/** * v0.34 W3b — code_traversal_cache module tests. * * Hermetic PGLite test suite covering: * - cache hit returns memoized response (after migration v59) * - cache miss triggers compute * - D3: cluster_generation bump invalidates cached rows * - clearTraversalCache: source-scoped clear deletes the right rows * - clearTraversalCache: --all-sources gate requires explicit opt-out * - getCachedOrCompute: try-cache-then-compute happy path */ import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test'; import { PGLiteEngine } from '../../src/core/pglite-engine.ts'; import { resetPgliteState } from '../helpers/reset-pglite.ts'; import { getCachedTraversal, putCachedTraversal, getClusterGeneration, bumpClusterGeneration, clearTraversalCache, getCachedOrCompute, type CacheKey, } from '../../src/core/code-intel/traversal-cache.ts'; let engine: PGLiteEngine; beforeAll(async () => { engine = new PGLiteEngine(); await engine.connect({}); await engine.initSchema(); }); afterAll(async () => { await engine.disconnect(); }); beforeEach(async () => { await resetPgliteState(engine); }); const baseKey = (over: Partial = {}): CacheKey => ({ symbol_qualified: 'src/foo::bar', depth: 5, source_id: 'default', cluster_generation: 0, ...over, }); describe('W3b: getClusterGeneration / bumpClusterGeneration', () => { test('defaults to 0 when never set', async () => { const g = await getClusterGeneration(engine); expect(g).toBe(0); }); test('bump increments by 1 and persists', async () => { const next = await bumpClusterGeneration(engine); expect(next).toBe(1); const read = await getClusterGeneration(engine); expect(read).toBe(1); }); test('multiple bumps are monotonic', async () => { await bumpClusterGeneration(engine); await bumpClusterGeneration(engine); const final = await bumpClusterGeneration(engine); expect(final).toBe(3); }); }); describe('W3b: putCachedTraversal / getCachedTraversal', () => { test('cache miss returns null', async () => { const hit = await getCachedTraversal(engine, baseKey()); expect(hit).toBeNull(); }); test('cache hit returns the response after put', async () => { const key = baseKey(); const payload = { result: 'ok', depth_groups: [{ depth: 1, nodes: [] }] }; await putCachedTraversal(engine, key, payload, new Date().toISOString(), 0); const hit = await getCachedTraversal(engine, key); expect(hit).not.toBeNull(); expect(hit?.response).toEqual(payload); }); test('D3: cluster_generation mismatch returns null (cache miss)', async () => { const key = baseKey({ cluster_generation: 1 }); await putCachedTraversal(engine, key, { v: 'fresh' }, new Date().toISOString(), 0); // Now look up with a stale generation const staleKey = baseKey({ cluster_generation: 0 }); const hit = await getCachedTraversal(engine, staleKey); expect(hit).toBeNull(); }); test('UPSERT on conflict replaces older row', async () => { const key = baseKey(); await putCachedTraversal(engine, key, { v: 1 }, new Date().toISOString(), 0); await putCachedTraversal(engine, key, { v: 2 }, new Date().toISOString(), 0); const hit = await getCachedTraversal<{ v: number }>(engine, key); expect(hit?.response).toEqual({ v: 2 }); }); }); describe('W3b: clearTraversalCache', () => { test('refuses without source_id or all_sources', async () => { await expect(clearTraversalCache(engine, {})).rejects.toThrow(/specify source_id/); }); test('source-scoped clear deletes only that source', async () => { await putCachedTraversal(engine, baseKey({ source_id: 'src-a' }), { v: 1 }, new Date().toISOString(), 0); await putCachedTraversal(engine, baseKey({ source_id: 'src-b' }), { v: 2 }, new Date().toISOString(), 0); const deleted = await clearTraversalCache(engine, { sourceId: 'src-a' }); expect(deleted).toBe(1); const a = await getCachedTraversal(engine, baseKey({ source_id: 'src-a' })); const b = await getCachedTraversal(engine, baseKey({ source_id: 'src-b' })); expect(a).toBeNull(); expect(b).not.toBeNull(); }); test('all_sources clears everything', async () => { await putCachedTraversal(engine, baseKey({ source_id: 'src-a' }), { v: 1 }, new Date().toISOString(), 0); await putCachedTraversal(engine, baseKey({ source_id: 'src-b' }), { v: 2 }, new Date().toISOString(), 0); const deleted = await clearTraversalCache(engine, { allSources: true }); expect(deleted).toBe(2); }); }); describe('W3b: getCachedOrCompute', () => { test('miss: runs compute and caches result', async () => { let computeCalls = 0; const result = await getCachedOrCompute( engine, { symbol_qualified: 'foo', depth: 3, source_id: 'default' }, async () => { computeCalls += 1; return { x: 42 }; }, ); expect(result).toEqual({ x: 42 }); expect(computeCalls).toBe(1); }); test('hit: skips compute on second call', async () => { let computeCalls = 0; const compute = async () => { computeCalls += 1; return { x: 42 }; }; await getCachedOrCompute( engine, { symbol_qualified: 'foo', depth: 3, source_id: 'default' }, compute, ); await getCachedOrCompute( engine, { symbol_qualified: 'foo', depth: 3, source_id: 'default' }, compute, ); expect(computeCalls).toBe(1); }); test('D3: bumping cluster_generation invalidates the cache', async () => { let computeCalls = 0; const compute = async () => { computeCalls += 1; return { x: computeCalls }; }; await getCachedOrCompute( engine, { symbol_qualified: 'foo', depth: 3, source_id: 'default' }, compute, ); expect(computeCalls).toBe(1); // Bump generation — next call should recompute. await bumpClusterGeneration(engine); const second = await getCachedOrCompute( engine, { symbol_qualified: 'foo', depth: 3, source_id: 'default' }, compute, ); expect(computeCalls).toBe(2); expect(second).toEqual({ x: 2 }); }); });