/** * Cache wrapper tests — P2 with prompt-version + truncation in the key * (Codex fix). Hits the real PGLite engine end-to-end. */ 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 { buildCacheKey, hashContent, JudgeCache, } from '../src/core/eval-contradictions/cache.ts'; import { PROMPT_VERSION, TRUNCATION_POLICY } from '../src/core/eval-contradictions/types.ts'; import type { JudgeVerdict } from '../src/core/eval-contradictions/types.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 verdictHit: JudgeVerdict = { verdict: 'contradiction', severity: 'medium', axis: 'MRR vs ARR', confidence: 0.85, resolution_kind: 'dream_synthesize', }; describe('hashContent', () => { test('produces stable 64-char hex sha256', () => { const h = hashContent('hello'); expect(h.length).toBe(64); expect(h).toMatch(/^[0-9a-f]+$/); }); test('different inputs hash differently', () => { expect(hashContent('a')).not.toBe(hashContent('b')); }); test('same input stable across calls', () => { expect(hashContent('test')).toBe(hashContent('test')); }); }); describe('buildCacheKey', () => { test('sorts hashes lex so (a, b) === (b, a)', () => { const k1 = buildCacheKey({ textA: 'first', textB: 'second', modelId: 'haiku' }); const k2 = buildCacheKey({ textA: 'second', textB: 'first', modelId: 'haiku' }); expect(k1).toEqual(k2); }); test('includes the prompt_version + truncation_policy constants', () => { const k = buildCacheKey({ textA: 'x', textB: 'y', modelId: 'haiku' }); expect(k.prompt_version).toBe(PROMPT_VERSION); expect(k.truncation_policy).toBe(TRUNCATION_POLICY); }); test('model_id pass-through', () => { const k = buildCacheKey({ textA: 'x', textB: 'y', modelId: 'sonnet' }); expect(k.model_id).toBe('sonnet'); }); }); describe('JudgeCache wrapper', () => { test('miss returns null and increments misses', async () => { const cache = new JudgeCache({ engine, modelId: 'haiku-test' }); const hit = await cache.lookup('text-a', 'text-b'); expect(hit).toBeNull(); expect(cache.stats().misses).toBe(1); expect(cache.stats().hits).toBe(0); }); test('store then lookup returns the verdict', async () => { const cache = new JudgeCache({ engine, modelId: 'haiku-test' }); await cache.store('text-a', 'text-b', verdictHit); const hit = await cache.lookup('text-a', 'text-b'); expect(hit).not.toBeNull(); expect(hit?.verdict).toBe('contradiction'); expect(hit?.severity).toBe('medium'); expect(cache.stats().hits).toBe(1); }); test('order-independence: (a,b) and (b,a) both hit', async () => { const cache = new JudgeCache({ engine, modelId: 'haiku-test' }); await cache.store('first', 'second', verdictHit); const hit = await cache.lookup('second', 'first'); expect(hit).not.toBeNull(); expect(hit?.verdict).toBe('contradiction'); }); test('different model_id is a separate key', async () => { const cache1 = new JudgeCache({ engine, modelId: 'haiku-test' }); const cache2 = new JudgeCache({ engine, modelId: 'sonnet-test' }); await cache1.store('a', 'b', verdictHit); const hit = await cache2.lookup('a', 'b'); expect(hit).toBeNull(); }); test('disabled cache always misses, never stores', async () => { const cache = new JudgeCache({ engine, modelId: 'haiku-test', disabled: true }); await cache.store('a', 'b', verdictHit); const hit = await cache.lookup('a', 'b'); expect(hit).toBeNull(); // And a fresh non-disabled cache should also see nothing (store was a no-op). const fresh = new JudgeCache({ engine, modelId: 'haiku-test' }); expect(await fresh.lookup('a', 'b')).toBeNull(); }); test('hit_rate computed correctly', async () => { const cache = new JudgeCache({ engine, modelId: 'haiku-test' }); await cache.store('x', 'y', verdictHit); await cache.lookup('x', 'y'); // hit await cache.lookup('m', 'n'); // miss await cache.lookup('p', 'q'); // miss const s = cache.stats(); expect(s.hits).toBe(1); expect(s.misses).toBe(2); expect(s.hit_rate).toBeCloseTo(1 / 3, 5); }); test('lookup defends against corrupt cache rows (shape validation)', async () => { // Inject a row directly that doesn't match JudgeVerdict shape. const key = buildCacheKey({ textA: 'corrupt-a', textB: 'corrupt-b', modelId: 'haiku-test' }); await engine.putContradictionCacheEntry({ ...key, verdict: { unrelated_field: 'whatever' }, }); const cache = new JudgeCache({ engine, modelId: 'haiku-test' }); const hit = await cache.lookup('corrupt-a', 'corrupt-b'); expect(hit).toBeNull(); expect(cache.stats().misses).toBe(1); }); });