// @vitest-environment jsdom import { act } from 'react'; import { createRoot } from 'react-dom/client'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { AssessorApiClient } from '../../src/api/assessor-api-client'; import { deriveUnifiedResultFacets, queryUnifiedResultItems, type UnifiedResultProjection } from '../../src/domain/unified-result'; import { ResultRow, ResultsBrowser } from '../../src/features/results-browser/ResultsBrowser'; import { discloseResultDbProjectionWithTrace } from '../../src/domain/result-db-disclosure'; import { ResultDbIdentityGuard } from '../../src/domain/result-db-identity-guard'; import { browserDownloadAdapter } from '../../src/platform/browser/browser-download-adapter'; function reviewedFinding(id: string): UnifiedResultProjection { return { resultProjectionId: `scanner-finding:${id}`, sourceResultId: id, sourceKind: 'scanner-finding', sourceTool: 'purple-knight', sourceRuleId: 'SI000055', projectId: 'synthetic', assessmentId: 'slot', collectionRunId: 'collection', scanRunId: 'scan', title: `Synthetic delegation finding ${id}`, severityOriginal: 'high', severityNormalized: 'high', outcomeOriginal: 'fail', outcomeNormalized: 'fail', technicalExecutionState: 'completed', resultType: 'exposure_finding', isIoe: true, isDeviation: true, lifecycleState: 'unknown', mappingConfidence: 1, reviewStatus: 'open', reviewDisposition: 'accepted', reviewPriority: 'critical', reviewOwner: 'Synthetic Owner', reviewDueDate: '2026-10-01', importedAt: '2026-09-28T00:00:00Z', createdAt: '2026-09-28T00:00:00Z', frameworkReferences: [], categories: [] }; } afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); describe('Result DB export disclosure', () => { it('shows no review column or review filter and exports JSONL without review state or environment fingerprints', async () => { const items = [reviewedFinding('a'), { ...reviewedFinding('b'), sourceTool: 'pingcastle', sourceRuleId: 'A-NotEnoughDC', title: 'The number of DCs is too small to provide redundancy: 1 DC', description: 'Last backup 55 days ago; 15 accounts never expire.' }]; vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.spyOn(AssessorApiClient.prototype, 'queryUnifiedResultFacets') .mockImplementation(async query => deriveUnifiedResultFacets(items, query)); vi.spyOn(AssessorApiClient.prototype, 'queryUnifiedResults') .mockImplementation(async query => queryUnifiedResultItems(items, query)); const download = vi.spyOn(browserDownloadAdapter, 'download').mockImplementation(() => undefined); const container = document.createElement('div'); const root = createRoot(container); try { await act(async () => root.render()); expect(container.querySelectorAll('.results-browser-title')).toHaveLength(2); const headings = [...container.querySelectorAll('.results-browser-head span')].map(item => item.textContent); expect(headings).not.toContain('Review'); expect(container.querySelector('[data-label="Review"]')).toBeNull(); const filterHeadings = [...container.querySelectorAll('section.results-filter-group h3')].map(item => item.textContent); expect(filterHeadings.some(heading => /review/i.test(heading ?? ''))).toBe(false); const exportButton = [...container.querySelectorAll('button')] .find(button => button.textContent === 'Export JSONL')!; await act(async () => exportButton.click()); expect(download).toHaveBeenCalledTimes(1); const text = download.mock.calls[0][0].text; const [manifest, ...results] = text.trimEnd().split('\n').map(line => JSON.parse(line)); expect(manifest.handling.classification).toBe('confidential-security-posture'); expect(results).toHaveLength(2); expect(results.map(line => line.result.sourceRuleId).sort()).toEqual(['A-NotEnoughDC', 'SI000055']); expect(text).not.toMatch(/reviewStatus|reviewDisposition|reviewPriority|reviewOwner|reviewDueDate|Synthetic Owner|accepted/); const dc = results.find(line => line.result.sourceRuleId === 'A-NotEnoughDC')!.result; expect(dc.title).toBe('The number of DCs is too small to provide redundancy: [count] DC'); expect(dc.description).toBe('Last backup [duration] ago; [count] accounts never expire.'); expect(manifest).not.toHaveProperty('generatedAt'); expect(download.mock.calls[0][0].fileName).toMatch(/^assessor-result-db-filtered-\d{4}-\d{2}-\d{2}\.jsonl$/); } finally { await act(async () => root.unmount()); } }); it('never reveals that a stored row had client review state, neither in the row detail nor in the JSONL export', async () => { // The backend serves materialized rows that carry their trace, so this is the real export input. const reviewed = discloseResultDbProjectionWithTrace({ ...reviewedFinding('first'), score: 27 }); const unreviewed = discloseResultDbProjectionWithTrace({ ...reviewedFinding('second'), reviewStatus: undefined, reviewDisposition: undefined, reviewPriority: undefined, reviewOwner: undefined, reviewDueDate: undefined, score: 96 }); const items = [reviewed, unreviewed]; expect(reviewed.disclosureTrace?.withheldFields).toEqual(unreviewed.disclosureTrace?.withheldFields); vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.spyOn(AssessorApiClient.prototype, 'queryUnifiedResultFacets') .mockImplementation(async query => deriveUnifiedResultFacets(items, query)); vi.spyOn(AssessorApiClient.prototype, 'queryUnifiedResults') .mockImplementation(async query => queryUnifiedResultItems(items, query)); const download = vi.spyOn(browserDownloadAdapter, 'download').mockImplementation(() => undefined); const container = document.createElement('div'); const root = createRoot(container); try { await act(async () => root.render( undefined} workRecordIds={[]} onOpenInWork={() => undefined}/>)); expect(container.textContent).not.toMatch(/review|Synthetic Owner/i); await act(async () => root.render()); const exportButton = [...container.querySelectorAll('button')] .find(button => button.textContent === 'Export JSONL')!; await act(async () => exportButton.click()); const text = download.mock.calls[0][0].text; const results = text.trimEnd().split('\n').slice(1).map(line => JSON.parse(line).result); expect(results).toHaveLength(2); expect(results.every(result => result.disclosureTrace)).toBe(true); expect(text).not.toMatch(/review|Synthetic Owner/i); expect(results.some(result => 'score' in result)).toBe(false); } finally { await act(async () => root.unmount()); } }); it('shows the stored trace of a row and opens its original in Work without showing it in the Result DB', async () => { const stored = discloseResultDbProjectionWithTrace({ ...reviewedFinding('traced'), affectedAsset: 'SYNTHETIC-HOST-9', description: 'Host SRVDC01 answers for 3 hosts.' }, new ResultDbIdentityGuard(['SRVDC01'])); const opened = vi.fn(); vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const container = document.createElement('div'); const root = createRoot(container); try { await act(async () => root.render( undefined} workRecordIds={['work-record-1']} onOpenInWork={opened}/>)); const detail = container.querySelector('.results-disclosure-trace')?.textContent ?? ''; expect(detail).toContain('Checked content'); expect(detail).toContain('Redacted in Description: count × 1, identity-guard × 1'); expect(detail).toContain('Withheld fields:'); expect(container.textContent).not.toContain('SRVDC01'); const button = [...container.querySelectorAll('button')].find(item => item.textContent === 'Open original in Work')!; await act(async () => button.click()); expect(opened).toHaveBeenCalledWith(['work-record-1']); await act(async () => root.render( undefined} workRecordIds={[]} onOpenInWork={opened}/>)); expect(container.textContent).toContain('The original is not part of the loaded Work scope.'); } finally { await act(async () => root.unmount()); } }); });