// @vitest-environment jsdom import { act, type ReactNode } from 'react'; import { createRoot, type Root } from 'react-dom/client'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { AssessorApiClient } from '../../src/api/assessor-api-client'; import { AppProvider, useApp } from '../../src/app/AppProvider'; import { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences'; import type { RegulatoryControlOverviewResponse } from '../../src/compliance/regulatory-mapping'; import { BSI_PRACTICE_FACET_ID, BSI_REQUIREMENT_FACET_ID, complianceWorkFacetIndex, ISO_CONTROL_FACET_ID, ISO_MAPPING_FACET_ID, ISO_MAPPING_FAILED, ISO_MAPPING_NO_BSI, ISO_THEME_FACET_ID, resetComplianceWorkFacetsForTests, useComplianceWorkFacets } from '../../src/compliance/ui/compliance-work-facets'; import { markRegulatoryDecisionSaved, markRegulatoryMappingsChanged } from '../../src/compliance/ui/decision-revision'; import { filterAndSortRecords } from '../../src/features/search-filter/filter-records'; import { selectWorkFacets } from '../../src/features/search-filter/work-facets'; import { makeSheet } from '../../src/importers/sheet-factory'; import { genericProfile } from '../../src/profiles/generic/generic-profile'; // Work `ISO control` and `ISO theme` (Compliance Engine): a second reading of accepted controls that // filters Work like the Context Cluster facet, sits before the final `Source: Scan`, and is absent while // the engine is off. const gpoKey = `gpo-work-setting:${encodeURIComponent(JSON.stringify(['dummy', 'baseline', 'computer', 'registry:dummy:setting']))}`; const assignments: RegulatoryControlOverviewResponse = { version: 1, projectId: 'dummy-project', scope: { assessmentIds: ['dummy-assessment'], scanRunIds: [] }, findingCount: 3, controls: [ { controlId: '8.20', annexAControlId: 'A.8.20', title: 'Networks security', theme: 'Technological', findings: [ { resultProjectionId: 'scanner-finding:dummy-a', sourceKind: 'scanner-finding', sourceResultId: 'dummy-a' }, { resultProjectionId: 'scanner-finding:dummy-b', sourceKind: 'scanner-finding', sourceResultId: 'dummy-b' }, { resultProjectionId: 'policy-evaluation-comparison:dummy', sourceKind: 'policy-evaluation', sourceResultId: 'dummy-comparison', sourceRuleId: 'registry:dummy:setting', policyScope: 'computer' }] }, { controlId: '5.15', annexAControlId: 'A.5.15', theme: 'Organizational', findings: [ { resultProjectionId: 'scanner-finding:dummy-b', sourceKind: 'scanner-finding', sourceResultId: 'dummy-b' }] } ] }; function workSheet() { const sheet = makeSheet('Dummy', [ ['Title', 'Status', 'Source: Scan'], ['Dummy check A', 'Failed', 'Purple Knight'], ['Dummy check B', 'Passed', 'Purple Knight'], ['Dummy GPO setting', 'Failed', 'GPOs'], ['Dummy unmapped check', 'Failed', 'Purple Knight'] ], '', 'dummy.csv', { isProjectSheet: true }); const [a, b, gpo] = sheet.analysis.records; a.findingOccurrenceId = 'dummy-a'; b.findingOccurrenceId = 'dummy-b'; (gpo as { id: string }).id = gpoKey; return sheet; } const options = (statusFilter = 'all', facetFilters = {}) => ({ dataset: null, activeProfile: genericProfile, reviews: {}, search: '', actionOnly: false, statusFilter, facetFilters, complianceFacets: complianceWorkFacetIndex(assignments) }); describe('Work ISO control and ISO theme facets', () => { it('offers accepted controls and themes before the final Source: Scan control', () => { const facets = selectWorkFacets(workSheet(), options()); const ids = facets.map(facet => facet.id); expect(ids.at(-1)).toBe('provenance:source-scan'); // Nothing reaches BSI in this fixture, so ISO mapping offers `ISO control, no BSI requirement` after ISO theme. expect(ids.slice(-4, -1)).toEqual([ISO_CONTROL_FACET_ID, ISO_THEME_FACET_ID, ISO_MAPPING_FACET_ID]); const control = facets.find(facet => facet.id === ISO_CONTROL_FACET_ID)!; expect(control.label).toBe('ISO control'); expect(control.values.map(value => [value.label, value.count])).toEqual([['A.5.15', 1], ['A.8.20 · Networks security', 3]]); const theme = facets.find(facet => facet.id === ISO_THEME_FACET_ID)!; expect(theme.values.map(value => [value.value, value.label, value.count])).toEqual([['organizational', 'Organizational', 1], ['technological', 'Technological', 3]]); }); it('offers ISO mapping: Mapping failed only while a mapping failed, and filters to exactly those records', () => { const sheet = workSheet(); sheet.analysis.records[3].findingOccurrenceId = 'dummy-unmapped'; const failed = { ...assignments, failedFindings: [ { resultProjectionId: 'scanner-finding:dummy-unmapped', sourceKind: 'scanner-finding' as const, sourceResultId: 'dummy-unmapped' }] }; const withFailed = { ...options(), complianceFacets: complianceWorkFacetIndex(failed) }; const ids = selectWorkFacets(sheet, withFailed).map(facet => facet.id); expect(ids.slice(-4)).toEqual([ISO_CONTROL_FACET_ID, ISO_THEME_FACET_ID, ISO_MAPPING_FACET_ID, 'provenance:source-scan']); const facet = selectWorkFacets(sheet, withFailed).find(item => item.id === ISO_MAPPING_FACET_ID)!; expect(facet.label).toBe('ISO mapping'); expect(facet.values.map(value => [value.value, value.label, value.count])).toEqual([[ISO_MAPPING_FAILED, 'Mapping failed', 1], [ISO_MAPPING_NO_BSI, 'ISO control, no BSI requirement', 3]]); const titles = filterAndSortRecords(sheet, { ...withFailed, statusFilter: 'deviation', facetFilters: { [ISO_MAPPING_FACET_ID]: { values: [ISO_MAPPING_FAILED] } }, sortColumn: '', sortDirection: 'asc' }).map(record => record.title); expect(titles).toEqual(['Dummy unmapped check']); // Without failed mappings and without a BSI derivation gap there is no such filter. const allReachBsi = { ...assignments, bsi: { catalogVersion: '2026-01-01', crosswalkVersion: '0.1-synthetic', license: 'Synthetic.', practices: [{ id: 'KONF', title: 'Konfiguration' }], missingTargets: [], requirements: [ { requirementId: 'KONF.1.1', title: 'Synthetisch', practice: 'KONF', statement: 'Synthetisch.', via: [{ isoControlId: '8.20', relationship: 'subset-of' as const }, { isoControlId: '5.15', relationship: 'subset-of' as const }] }] } }; expect(selectWorkFacets(sheet, { ...options(), complianceFacets: complianceWorkFacetIndex(allReachBsi) }).some(item => item.id === ISO_MAPPING_FACET_ID)).toBe(false); }); it('offers ISO mapping: ISO control, no BSI requirement for records whose accepted controls reach no BSI requirement', () => { const sheet = workSheet(); // Only 8.20 reaches a BSI requirement; Dummy check B also has 5.15 and therefore one. const partial = { ...assignments, bsi: { catalogVersion: '2026-01-01', crosswalkVersion: '0.1-synthetic', license: 'Synthetic.', practices: [{ id: 'KONF', title: 'Konfiguration' }], missingTargets: [], requirements: [ { requirementId: 'KONF.1.1', title: 'Synthetisch', practice: 'KONF', statement: 'Synthetisch.', via: [{ isoControlId: '5.15', relationship: 'subset-of' as const }] }] } }; const withPartial = { ...options(), complianceFacets: complianceWorkFacetIndex(partial) }; const facet = selectWorkFacets(sheet, withPartial).find(item => item.id === ISO_MAPPING_FACET_ID)!; expect(facet.values.map(value => [value.value, value.label, value.count])).toEqual([[ISO_MAPPING_NO_BSI, 'ISO control, no BSI requirement', 2]]); const titles = filterAndSortRecords(sheet, { ...withPartial, facetFilters: { [ISO_MAPPING_FACET_ID]: { values: [ISO_MAPPING_NO_BSI] } }, sortColumn: '', sortDirection: 'asc' }).map(record => record.title); expect(titles).toEqual(['Dummy check A', 'Dummy GPO setting']); }); it('filters Work records by control and theme, matching scanner occurrences and GPO settings', () => { const sheet = workSheet(); const titles = (filters: object, status = 'all') => filterAndSortRecords(sheet, { ...options(status, filters), sortColumn: '', sortDirection: 'asc' }) .map(record => record.title); expect(titles({ [ISO_CONTROL_FACET_ID]: { values: ['5.15'] } })).toEqual(['Dummy check B']); expect(titles({ [ISO_THEME_FACET_ID]: { values: ['technological'] } })).toEqual(['Dummy check A', 'Dummy check B', 'Dummy GPO setting']); // Other filters still apply: the status filter narrows the ISO selection. expect(titles({ [ISO_CONTROL_FACET_ID]: { values: ['8.20'] } }, 'deviation')).toEqual(['Dummy check A', 'Dummy GPO setting']); }); it('counts each control against the other active filters', () => { const facets = selectWorkFacets(workSheet(), options('deviation')); expect(facets.find(facet => facet.id === ISO_CONTROL_FACET_ID)!.values.map(value => [value.value, value.count])).toEqual([['8.20', 2]]); }); it('adds no ISO facet without loaded assignments', () => { const facets = selectWorkFacets(workSheet(), { ...options(), complianceFacets: undefined }); expect(facets.some(facet => facet.id.startsWith('compliance:'))).toBe(false); }); }); // Work `BSI requirement` and `BSI practice` (Phase 13F, stage 1): requirements derived from the accepted ISO // controls through the BSI mapping, after the ISO facets and before the final `Source: Scan`. Synthetic ids. describe('Work BSI requirement and BSI practice facets', () => { const withBsi: RegulatoryControlOverviewResponse = { ...assignments, bsi: { catalogVersion: '2026-01-01', crosswalkVersion: '0.1-synthetic', license: 'Synthetic attribution.', practices: [{ id: 'BER', title: 'Berechtigung' }, { id: 'ARCH', title: 'Architektur' }, { id: 'DET', title: 'Detektion' }], requirements: [ { requirementId: 'ARCH.1.1', title: 'Synthetische Architektur', practice: 'ARCH', statement: 'Synthetisch.', via: [{ isoControlId: '8.20', relationship: 'subset-of' }] }, { requirementId: 'BER.1.1', title: 'Synthetische Berechtigung', practice: 'BER', statement: 'Synthetisch.', via: [{ isoControlId: '5.15', relationship: 'equivalent-to' }] } ], missingTargets: [] } }; const bsiOptions = (statusFilter = 'all', facetFilters = {}) => ({ ...options(statusFilter, facetFilters), complianceFacets: complianceWorkFacetIndex(withBsi) }); it('offers derived requirements and practices after the ISO facets, before the final Source: Scan control', () => { const facets = selectWorkFacets(workSheet(), bsiOptions()); expect(facets.map(facet => facet.id).slice(-5)).toEqual([ISO_CONTROL_FACET_ID, ISO_THEME_FACET_ID, BSI_REQUIREMENT_FACET_ID, BSI_PRACTICE_FACET_ID, 'provenance:source-scan']); const requirement = facets.find(facet => facet.id === BSI_REQUIREMENT_FACET_ID)!; expect(requirement.label).toBe('BSI requirement'); expect(requirement.values.map(value => [value.value, value.label, value.count])).toEqual([ ['arch.1.1', 'ARCH.1.1 · Synthetische Architektur', 3], ['ber.1.1', 'BER.1.1 · Synthetische Berechtigung', 1]]); // Catalog order, only practices with a derived requirement. expect(facets.find(facet => facet.id === BSI_PRACTICE_FACET_ID)!.values.map(value => [value.value, value.label, value.count])) .toEqual([['ber', 'BER · Berechtigung', 1], ['arch', 'ARCH · Architektur', 3]]); }); it('filters Work records by requirement and practice through their ISO controls, with the other filters still applying', () => { const sheet = workSheet(); const titles = (filters: object, status = 'all') => filterAndSortRecords(sheet, { ...bsiOptions(status, filters), sortColumn: '', sortDirection: 'asc' }) .map(record => record.title); expect(titles({ [BSI_REQUIREMENT_FACET_ID]: { values: ['ber.1.1'] } })).toEqual(['Dummy check B']); expect(titles({ [BSI_PRACTICE_FACET_ID]: { values: ['arch'] } })).toEqual(['Dummy check A', 'Dummy check B', 'Dummy GPO setting']); expect(titles({ [BSI_PRACTICE_FACET_ID]: { values: ['arch'] } }, 'deviation')).toEqual(['Dummy check A', 'Dummy GPO setting']); }); it('adds no BSI facet when no requirement is derived', () => { expect(selectWorkFacets(workSheet(), options()).some(facet => facet.id.startsWith('compliance:bsi-'))).toBe(false); }); }); vi.mock('../../src/features/project-import/load-canonical-project-workspace', () => ({ loadCanonicalProjectWorkspace: vi.fn() })); let host: HTMLDivElement; let root: Root; beforeEach(() => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); localStorage.clear(); resetEnginePreferencesForTests(); resetComplianceWorkFacetsForTests(); vi.spyOn(AssessorApiClient.prototype, 'health').mockResolvedValue({ status: 'ok' } as never); host = document.createElement('div'); document.body.append(host); root = createRoot(host); }); afterEach(async () => { await act(async () => { root.unmount(); }); host.remove(); vi.restoreAllMocks(); vi.unstubAllGlobals(); resetEnginePreferencesForTests(); }); function Consumer(): ReactNode { const index = useComplianceWorkFacets(); return {index ? index.facets.map(facet => facet.values.length).join(',') : 'none'}; } function Harness({ withScope }: { withScope: boolean }): ReactNode { const app = useApp(); return <> {JSON.stringify(app.facetFilters)} {withScope && <>} ; } describe('Work ISO facet loading and the engine switch', () => { it('shares one request for every Work consumer and reloads after a control decision', async () => { const { loadCanonicalProjectWorkspace } = await import('../../src/features/project-import/load-canonical-project-workspace'); vi.mocked(loadCanonicalProjectWorkspace).mockResolvedValue({ assessmentIds: ['dummy-assessment'], models: [], dataset: { fileName: 'Dummy', scanTool: 'Generic', sheets: [workSheet()] } } as never); vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext').mockResolvedValue({} as never); const request = vi.spyOn(AssessorApiClient.prototype, 'getRegulatoryControlAssignments').mockResolvedValue(assignments); await act(async () => root.render()); await act(async () => { host.querySelector('button')!.click(); await new Promise(resolve => setTimeout(resolve, 0)); }); await act(async () => { await new Promise(resolve => setTimeout(resolve, 0)); }); expect(request).toHaveBeenCalledTimes(1); expect([...host.querySelectorAll('[data-iso-facets]')].map(node => node.textContent)).toEqual(['2,2,1', '2,2,1']); await act(async () => { markRegulatoryDecisionSaved(); await new Promise(resolve => setTimeout(resolve, 0)); }); expect(request).toHaveBeenCalledTimes(2); }); // Regression 2026-10-09: `Mapping failed` in the Compliance tab opened Work without filtering when the mapping failed // after Work had loaded its assignments, because a failed mapping is no decision and Work kept the outdated state. it('reloads the assignments after a mapping change and offers no outdated state meanwhile, so Mapping failed stays selected', async () => { const { loadCanonicalProjectWorkspace } = await import('../../src/features/project-import/load-canonical-project-workspace'); vi.mocked(loadCanonicalProjectWorkspace).mockResolvedValue({ assessmentIds: ['dummy-assessment'], models: [], dataset: { fileName: 'Dummy', scanTool: 'Generic', sheets: [workSheet()] } } as never); vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext').mockResolvedValue({} as never); let resolveFresh!: (value: RegulatoryControlOverviewResponse) => void; const withFailure: RegulatoryControlOverviewResponse = { ...assignments, failedFindings: [ { resultProjectionId: 'scanner-finding:dummy-a', sourceKind: 'scanner-finding', sourceResultId: 'dummy-a' }] }; const request = vi.spyOn(AssessorApiClient.prototype, 'getRegulatoryControlAssignments') .mockResolvedValueOnce(assignments) .mockImplementationOnce(() => new Promise(resolve => { resolveFresh = resolve; })); await act(async () => root.render()); await act(async () => { host.querySelector('button')!.click(); await new Promise(resolve => setTimeout(resolve, 0)); }); await act(async () => { await new Promise(resolve => setTimeout(resolve, 0)); }); expect(host.querySelector('[data-iso-facets]')?.textContent).toBe('2,2,1'); // The Compliance tab saw a mapping fail: Work drops the outdated state while it loads the fresh one ... await act(async () => { markRegulatoryMappingsChanged(); await new Promise(resolve => setTimeout(resolve, 0)); }); expect(request).toHaveBeenCalledTimes(2); expect(host.querySelector('[data-iso-facets]')?.textContent).toBe('none'); // ... and then offers ISO mapping with Mapping failed, matching exactly that finding. await act(async () => { resolveFresh(withFailure); await new Promise(resolve => setTimeout(resolve, 0)); }); expect(host.querySelector('[data-iso-facets]')?.textContent).toBe('2,2,2'); const index = complianceWorkFacetIndex(withFailure); const mapping = index.facets.find(facet => facet.id === ISO_MAPPING_FACET_ID)!; expect(mapping.values.map(value => value.value)).toEqual([ISO_MAPPING_FAILED, ISO_MAPPING_NO_BSI]); expect(filterAndSortRecords(workSheet(), { ...options('all', { [ISO_MAPPING_FACET_ID]: { values: [ISO_MAPPING_FAILED] } }), complianceFacets: index, sortColumn: '', sortDirection: 'asc' }) .map(record => record.findingOccurrenceId)).toEqual(['dummy-a']); }); it('loads nothing and clears an active ISO filter when the Compliance Engine is switched off', async () => { const request = vi.spyOn(AssessorApiClient.prototype, 'getRegulatoryControlAssignments').mockResolvedValue(assignments); await act(async () => root.render()); await act(async () => host.querySelectorAll('button')[1].click()); expect(JSON.parse(host.querySelector('[data-filters]')!.textContent!)).toEqual({ [ISO_CONTROL_FACET_ID]: { values: ['8.20'] } }); await act(async () => { setEngineEnabled('compliance', false); }); expect(JSON.parse(host.querySelector('[data-filters]')!.textContent!)).toEqual({}); await act(async () => root.render()); expect(host.querySelector('[data-iso-facets]')?.textContent).toBe('none'); expect(request).not.toHaveBeenCalled(); }); });