import { describe, expect, it, vi } from 'vitest'; import type { Pool } from 'pg'; import { RevisionReadCache } from '../../server/postgres/revision-read-cache'; import { PostgresAssessorRepository } from '../../server/postgres/postgres-assessor-repository'; import { queryUnifiedResultItems, type UnifiedResultProjection } from '../../src/domain/unified-result'; describe('revision-checked read reuse', () => { it('shares concurrent reads and validates both sides of every cache hit', async () => { const cache = new RevisionReadCache(); const revision = vi.fn(async () => 7); const load = vi.fn(async () => ['complete', 'original']); expect(await Promise.all([cache.read('scope', revision, load), cache.read('scope', revision, load)])) .toEqual([['complete', 'original'], ['complete', 'original']]); await cache.read('scope', revision, load); expect(load).toHaveBeenCalledTimes(1); expect(revision).toHaveBeenCalledTimes(6); }); it('invalidates after commits and discards a read spanning two revisions', async () => { const cache = new RevisionReadCache(); let revision = 1; const load = vi.fn(async () => { const value = revision; if (revision === 1) revision = 2; return value; }); expect(await cache.read('scope', async () => revision, load)).toBe(2); revision = 3; expect(await cache.read('scope', async () => revision, load)).toBe(3); expect(load).toHaveBeenCalledTimes(3); }); it('refuses unverified or repeatedly changing data and retries failed loads', async () => { const cache = new RevisionReadCache(); await expect(cache.read('scope', async () => -1, async () => 1)).rejects.toThrow('Cannot verify'); let revision = 1; await expect(cache.read('scope', async () => revision, async () => ++revision)).rejects.toThrow('changed repeatedly'); const load = vi.fn().mockRejectedValueOnce(new Error('read failed')).mockResolvedValue(42); await expect(cache.read('scope', async () => revision, load)).rejects.toThrow('read failed'); expect(await cache.read('scope', async () => revision, load)).toBe(42); }); it('bounds entry count and retained bytes without truncating oversized results', async () => { const cache = new RevisionReadCache(1, 20); const revision = async () => 1; const load = vi.fn(async () => 'full original value larger than capacity'); expect(await cache.read('large', revision, load)).toBe(await cache.read('large', revision, load)); expect(load).toHaveBeenCalledTimes(2); const small = vi.fn(async () => 'small'); await cache.read('a', revision, small); await cache.read('b', revision, small); await cache.read('a', revision, small); expect(small).toHaveBeenCalledTimes(3); }); }); describe('PostgreSQL paginated client reads', () => { it('preserves every field, ordering and count across pages; refreshes same-count edits and separates scopes', async () => { let revision = 7; const pool = { query: vi.fn(async (sql: string) => ({ rows: sql.startsWith('SHOW') ? [{ default_transaction_read_only: 'on' }] : [{ revision: String(revision) }] })) } as unknown as Pool; const repository = await PostgresAssessorRepository.openReadOnly(pool); const rows = Array.from({ length: 2501 }, (_, index) => ({ resultProjectionId: `finding-${String(index).padStart(5, '0')}`, projectId: 'synthetic', assessmentId: 'a', scanRunId: 'scan', sourceKind: 'scanner-finding', severityNormalized: index % 2 ? 'high' : 'low', importedAt: '2026-10-02T00:00:00Z', outcomeNormalized: 'fail', title: `Original ${index}`, description: 'Complete source text', categories: [{ namespace: 'source', categoryCode: 'native', categoryLabel: 'Native' }], policyValues: [{ original: 'retained evidence' }] } as unknown as UnifiedResultProjection)); const load = vi.spyOn(repository as unknown as { loadInternalUnifiedResults(): Promise }, 'loadInternalUnifiedResults').mockImplementation(async () => structuredClone(rows)); const query = { audience: 'internal-analysis' as const, projectId: 'synthetic', assessmentIds: ['a'], scanRunIds: ['scan'] }; const actual: UnifiedResultProjection[] = []; for (let offset = 0; offset < rows.length; offset += 1000) { const page = await repository.queryUnifiedResults({ ...query, offset, limit: 1000 }); expect(page).toEqual(queryUnifiedResultItems(rows, { ...query, offset, limit: 1000 })); actual.push(...page.items); } expect(actual).toHaveLength(2501); expect(new Set(actual.map(row => row.resultProjectionId)).size).toBe(2501); expect(load).toHaveBeenCalledTimes(1); // Previously: full source reads for each of three pages. actual[0].categories[0].categoryLabel = 'consumer mutation'; expect((await repository.queryUnifiedResults(query)).items[0].categories[0].categoryLabel).toBe('Native'); rows[0].title = 'Updated source'; revision += 1; // A worker commit, independent of repository.cachedRevision. expect(await repository.queryUnifiedResults(query)).toEqual(queryUnifiedResultItems(rows, query)); expect(load).toHaveBeenCalledTimes(2); await repository.queryUnifiedResults({ ...query, scanRunIds: ['different'] }); await repository.queryUnifiedResults({ ...query, assessmentIds: ['different'] }); await repository.queryUnifiedResults({ ...query, projectId: 'different' }); expect(load).toHaveBeenCalledTimes(5); const disclosed = vi.spyOn(repository as unknown as { storedResultDbRows(): Promise }, 'storedResultDbRows').mockResolvedValue([]); expect((await repository.queryUnifiedResults({ projectId: 'synthetic' })).items).toEqual([]); expect(disclosed).toHaveBeenCalledTimes(1); expect(load).toHaveBeenCalledTimes(5); // Internal evidence never supplies Result DB disclosure. }); });