// @vitest-environment jsdom import { describe, expect, it } from 'vitest'; import summaryFixture from '../../fixtures/purple-knight/assessment-summary.synthetic.csv?raw'; import { assertCanonicalAssessmentModel } from '../../src/domain/canonical-validation'; import { confirmEntryMappings, previewEntryFromDataset } from '../../src/features/import-workflow/import-workflow'; import { createCanonicalImportBatch } from '../../src/importers/canonical-import'; import { importFile } from '../../src/importers/importer'; import { importProfiles } from '../../src/profiles/profile-catalog'; const FILE_NAME = 'Security_Assessment_Report_synthetic.example_(Assessment_summary).csv'; async function importSummary(content = summaryFixture, fileName = FILE_NAME) { return importFile(new File([content], fileName, { type: 'text/csv' }), { profiles: importProfiles }); } function observationValue( observations: NonNullable>['extraction']>['environmentObservations'], property: string ) { return observations?.find(item => item.property === property); } describe('Purple Knight assessment summary source extraction', () => { it('turns the summary metrics into typed environment observations without creating findings', async () => { const imported = await importSummary(); expect(imported.extraction?.extractor.id).toBe('purple-knight-summary-source-extractor'); expect(imported.extraction?.candidates).toHaveLength(0); const observations = imported.extraction?.environmentObservations; expect(observationValue(observations, 'purple_knight.ad.security_posture_score')).toMatchObject({ normalizedValue: 71, valueType: 'number', state: 'observed' }); expect(observationValue(observations, 'purple_knight.ad.ioes_found')?.normalizedValue).toBe(17); expect(observationValue(observations, 'purple_knight.ad.evaluated')?.normalizedValue).toBe(117); expect(observationValue(observations, 'purple_knight.ad.passed')?.normalizedValue).toBe(100); expect(observationValue(observations, 'purple_knight.ad.not_relevant')?.normalizedValue).toBe(2); expect(observationValue(observations, 'purple_knight.tool_edition')?.originalValue).toBe('Community'); expect(observationValue(observations, 'purple_knight.ad.scan_duration')?.originalValue).toBe('00:00:21'); }); it('maps tool-neutral facts to the shared environment properties and keeps category scores separate', async () => { const observations = (await importSummary()).extraction?.environmentObservations; expect(observationValue(observations, 'forest.name')).toMatchObject({ originalValue: 'synthetic.example', subject: { type: 'active-directory-environment' } }); expect(observationValue(observations, 'domain.count')?.normalizedValue).toBe(2); expect(observationValue(observations, 'report.generated_at')).toMatchObject({ originalValue: '2026-01-15 09:30', valueType: 'datetime' }); expect(observationValue(observations, 'purple_knight.ad.category_score.kerberos_security')).toMatchObject({ label: 'Kerberos Security', normalizedValue: 95, subject: { type: 'security-domain', identity: 'kerberos_security' } }); expect(observations?.filter(item => item.property.startsWith('purple_knight.ad.category_score.'))).toHaveLength(6); }); it('recognizes the summary as a Purple Knight source and persists it as context without findings', async () => { const imported = await importSummary(); expect(imported.profileId).toBe('purple-knight'); expect(imported.recognition?.source.selected.toolId).toBe('purple-knight'); expect(imported.recognition?.source.requiresConfirmation).toBe(false); const preview = previewEntryFromDataset(imported, { sha256: FILE_NAME.padEnd(64, '0').slice(0, 64), fileName: FILE_NAME, sizeBytes: summaryFixture.length, lastModified: 0 }); const model = createCanonicalImportBatch('summary', [confirmEntryMappings(preview, importProfiles)], '2026-08-25T08:00:00.000Z').model; assertCanonicalAssessmentModel(model); expect(model.findingOccurrences).toHaveLength(0); expect(model.environmentObservations?.every(item => item.tool.id === 'purple-knight')).toBe(true); expect(model.environmentObservations?.find(item => item.property === 'purple_knight.ad.security_posture_score')?.normalizedValue).toBe(71); expect(model.reportSections?.map(section => section.sectionKey)).toContain('purple-knight-assessment-summary'); }); it('leaves an unrelated two-column table to the generic extractor', async () => { const imported = await importFile( new File(['Metric,Value\nUptime,12\nOwner,Team A\n'], 'inventory.csv', { type: 'text/csv' }), { profiles: importProfiles } ); expect(imported.extraction?.extractor.id).not.toBe('purple-knight-summary-source-extractor'); }); });