import { describe, expect, it, vi } from 'vitest'; import { clientInformationFixture } from '../client-information-fixture'; import { clientInformationReadPortFrom } from '../../src/persistence/client-information-read-port'; import { selectClientInformation } from '../../src/domain/client-information'; import { LocalAssessorRepository } from '../../src/persistence/local-assessor-repository'; import type { ScanRun } from '../../src/domain/canonical-model'; describe('published Client Information read port', () => { it('filters exact properties, subjects, areas and sources, preserving conflicting original values and provenance', async () => { const snapshot = clientInformationFixture(); const original = structuredClone(snapshot); const reader = clientInformationReadPortFrom({ getEnvironmentSnapshot: async () => snapshot }, async () => 17); const result = await reader.queryClientInformation({ projectId: 'client-1', assessmentId: 'slot-1', scanRunIds: ['run-1'], properties: ['domain.fqdn'], subjectIds: ['object-1'], areas: ['directory'], toolIds: ['synthetic'] }); expect(result).toMatchObject({ contractVersion: '1.1.0', purpose: 'internal-analysis', repositoryRevision: 17, totalObservations: 2 }); expect(result.observations.map(item => item.originalValue).sort()).toEqual(['other.invalid', 'synthetic.invalid']); result.observations[0].originalValue = 'consumer mutation'; expect(snapshot).toEqual(original); expect(Object.keys(reader)).toEqual(['queryClientInformation']); expect(Object.isFrozen(reader)).toBe(true); }); it('keeps false, zero, restrictions and full original tables; paginates without changing totals', () => { const snapshot = clientInformationFixture(); const all = selectClientInformation(snapshot, { projectId: 'client-1' }, 1); expect(all.observations.map(item => item.originalValue)).toEqual(expect.arrayContaining([false, 0])); const restricted = selectClientInformation(snapshot, { projectId: 'client-1', states: ['access-denied'] }, 1); expect(restricted.totalObservations).toBe(1); expect(restricted.coverage.restricted).toBe(1); const first = selectClientInformation(snapshot, { projectId: 'client-1', limit: 2 }, 1); const next = selectClientInformation(snapshot, { projectId: 'client-1', limit: 2, offset: 2 }, 1); expect(first.totalObservations).toBe(6); expect(first.observations).toHaveLength(2); expect(next.observations.some(item => first.observations.some(previous => previous.id === item.id))).toBe(false); expect(all.sections).toEqual(snapshot.sections); expect(selectClientInformation(snapshot, { projectId: 'client-1', properties: ['missing'] }, 1).totalObservations).toBe(0); }); it('retries changed revisions, rejects cross-scope data and fails after bounded unstable reads', async () => { const snapshot = clientInformationFixture(); const revision = vi.fn().mockResolvedValueOnce(1).mockResolvedValueOnce(2).mockResolvedValue(2); const getEnvironmentSnapshot = vi.fn(async () => snapshot); const reader = clientInformationReadPortFrom({ getEnvironmentSnapshot }, revision); expect((await reader.queryClientInformation({ projectId: 'client-1' })).repositoryRevision).toBe(2); expect(getEnvironmentSnapshot).toHaveBeenCalledTimes(2); await expect(reader.queryClientInformation({ projectId: 'foreign' })).rejects.toThrow('different scope'); await expect(reader.queryClientInformation({ projectId: 'client-1', scanRunIds: ['foreign'] })).rejects.toThrow('different scope'); let counter = 0; await expect(clientInformationReadPortFrom({ getEnvironmentSnapshot }, async () => counter++).queryClientInformation({ projectId: 'client-1' })) .rejects.toThrow('changed during loading'); for (const query of [{ areas: ['typo'] }, { limit: 201 }, { offset: -1 }, { states: ['passed'] }]) { await expect(reader.queryClientInformation({ projectId: 'client-1', ...query } as never)).rejects.toThrow(); } }); it('preserves exact nested source links beyond a section page without guessing duplicate or foreign-file targets', () => { const snapshot = clientInformationFixture(); const parent = snapshot.sections[0]; parent.detailLinks = [{ targetId: 'child', label: 'Native details' }, { targetId: 'ambiguous', label: 'Ambiguous' }, { targetId: 'foreign', label: 'Foreign' }]; snapshot.sections.push( { ...parent, id: 'child-id', sectionKey: 'child', ordinal: 2, detailLinks: [{ targetId: 'nested', label: 'Nested' }] }, { ...parent, id: 'nested-id', sectionKey: 'nested', ordinal: 3, detailLinks: [] }, { ...parent, id: 'ambiguous-1', sectionKey: 'ambiguous', ordinal: 4, detailLinks: [] }, { ...parent, id: 'ambiguous-2', sectionKey: 'ambiguous', ordinal: 5, detailLinks: [] }, { ...parent, id: 'foreign-id', sectionKey: 'foreign', importFileId: 'file-2', ordinal: 6, detailLinks: [] }); const result = selectClientInformation(snapshot, { projectId: 'client-1', sectionLimit: 1 }, 1); expect(result.totalSections).toBe(6); expect(result.sections.map(item => item.id)).toEqual(['section-1', 'child-id', 'nested-id']); expect(result.linkedSectionIds).toEqual(['child-id', 'nested-id']); }); it('queries real repository history explicitly and rejects another client or assessment without writes', async () => { const repository = new LocalAssessorRepository(); const stamp = '2026-10-01T10:00:00Z'; await repository.saveProject({ id: 'client-1', name: 'Synthetic', createdAt: stamp, updatedAt: stamp, attributes: {} }); await repository.saveAssessment({ id: 'slot-1', projectId: 'client-1', name: 'Synthetic slot', createdAt: stamp, updatedAt: stamp }); const fixture = clientInformationFixture(); for (const [index, id] of ['old', 'run-1'].entries()) { const run: ScanRun = { id, assessmentId: 'slot-1', sequence: index + 1, label: id, importedAt: index ? '2026-10-02T10:00:00Z' : stamp, tool: { id: 'synthetic', name: 'Synthetic' } }; const session = await repository.beginImport({ sessionId: id, projectId: 'client-1', assessmentId: 'slot-1', scanRun: run }); await session.stage({ importFiles: [{ id: `file-${id}`, scanRunId: id, fileName: 'synthetic.csv', format: 'CSV', sizeBytes: 1, sha256: 'a'.repeat(64), importedAt: stamp, detectedTool: run.tool, detection: { level: 'high', score: 1, reasons: ['synthetic'] }, processedWith: { parserId: 'csv', parserVersion: '1', profileId: 'generic', profileVersion: '1', mappingVersion: '1' }, sourceArtifact: { kind: 'not-retained', reason: 'synthetic' } }], environmentObservations: [{ ...fixture.observations[0], id: `obs-${id}`, scanRunId: id, importFileId: `file-${id}`, sectionKey: undefined }] }); await session.commit(); } await repository.adoptScanRunReplacement('client-1', 'old', 'run-1', stamp); const revision = repository.revision; const reader = clientInformationReadPortFrom(repository, async () => repository.revision); expect((await reader.queryClientInformation({ projectId: 'client-1' })).scanRunIds).toEqual(['run-1']); expect((await reader.queryClientInformation({ projectId: 'client-1', scanRunIds: ['old'], includeSuperseded: true })).observations[0].id).toBe('obs-old'); await expect(reader.queryClientInformation({ projectId: 'client-1', scanRunIds: ['old'] })).rejects.toThrow(); await expect(reader.queryClientInformation({ projectId: 'client-1', assessmentId: 'foreign' })).rejects.toThrow(); await expect(reader.queryClientInformation({ projectId: 'foreign' })).rejects.toThrow(); expect(repository.revision).toBe(revision); }); });