// @vitest-environment jsdom
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
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 { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences';
import { useApp } from '../../src/app/AppProvider';
import type { RegulatoryCorpusStatus } from '../../src/compliance/regulatory-corpus-management';
import type { RegulatoryMappingResponse } from '../../src/compliance/regulatory-mapping';
import { IsoCorpusLibrary } from '../../src/compliance/ui/IsoCorpusLibrary';
import { useRegulatoryMappingBatch } from '../../src/compliance/ui/RegulatoryMappingBatchDialog';
import { useAiOverview } from '../../src/compliance/ui/AiOverviewPanel';
import { FindingReader } from '../../src/features/finding-reader/FindingReader';
import { makeSheet } from '../../src/importers/sheet-factory';
vi.mock('../../src/app/AppProvider', () => {
const useApp = vi.fn();
return { useApp, useOptionalApp: useApp };
});
// Compliance Engine switch (AGENTS.md, "Core Engine and engines"): switched off, the Finding Reader,
// Result DB, Client Info AI overview and Data Hub show none of its controls and send none of its requests; switched on again,
// the Finding Reader ISO panel starts closed.
let host: HTMLDivElement;
let root: Root;
function scannerRecord() {
const record = makeSheet('Dummy', [['Title', 'Status', 'Source: Scan'], ['Dummy SMB signing check', 'Fail', 'Purple Knight']],
'', 'dummy.csv', { isProjectSheet: true }).analysis.records[0];
return { ...record, findingOccurrenceId: 'dummy-occurrence' };
}
const mappingResponse: RegulatoryMappingResponse = {
version: 1, projectId: 'dummy-project', resultProjectionId: 'scanner-finding:dummy-occurrence',
generationEnabled: true, state: 'empty', decisions: []
};
async function renderReader() {
await act(async () => {
root.render();
await new Promise(resolve => setTimeout(resolve, 0));
});
}
const isoButton = () => [...host.querySelectorAll('button')].find(button => button.textContent?.startsWith('Map ISO controls'));
function BatchProbe(): ReactNode {
const batch = useRegulatoryMappingBatch('dummy-project', { assessmentIds: ['dummy-assessment'], scanRunIds: [], filters: {} });
return ;
}
function AiOverviewProbe(): ReactNode {
const overview = useAiOverview('dummy-project', ['dummy-assessment'], []);
return ;
}
beforeEach(() => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
localStorage.clear();
resetEnginePreferencesForTests();
// The Context Engine is not under test here.
setEngineEnabled('context', false);
vi.mocked(useApp).mockReturnValue({ targetProjectId: 'dummy-project', activeAssessmentIds: [], dataset: null, setFacetFilter: vi.fn() } as unknown as ReturnType);
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();
});
describe('Compliance Engine switch', () => {
it('removes the Finding Reader ISO window, section and requests while off and reopens closed', async () => {
const request = vi.spyOn(AssessorApiClient.prototype, 'getRegulatoryMappings').mockResolvedValue(mappingResponse);
const isoWindow = () => document.querySelector('[role="dialog"].regulatory-mapping-dialog');
setEngineEnabled('compliance', false);
await renderReader();
expect(isoButton()).toBeUndefined();
expect(request).not.toHaveBeenCalled();
expect(host.querySelector('[data-iso-controls]')).toBeNull();
// Switched on, the mapping loads with the finding; the button opens it as a window like Accepted Risk.
await act(async () => { setEngineEnabled('compliance', true); await new Promise(resolve => setTimeout(resolve, 0)); });
expect(request).toHaveBeenCalledTimes(1);
expect(isoWindow()).toBeNull();
await act(async () => { isoButton()!.click(); await new Promise(resolve => setTimeout(resolve, 0)); });
expect(isoWindow()?.querySelector('.regulatory-mapping-panel')).not.toBeNull();
expect(host.querySelector('.regulatory-mapping-panel')).toBeNull();
await act(async () => isoWindow()!.querySelector('[aria-label="Close window"]')!.click());
expect(isoWindow()).toBeNull();
await act(async () => { isoButton()!.click(); });
await act(async () => { setEngineEnabled('compliance', false); });
expect(isoButton()).toBeUndefined();
expect(isoWindow()).toBeNull();
await act(async () => { setEngineEnabled('compliance', true); });
expect(isoButton()?.getAttribute('aria-expanded')).toBe('false');
expect(isoWindow()).toBeNull();
expect(request).toHaveBeenCalledTimes(1);
});
it('lists the controls accepted for the finding as a reader section like Description', async () => {
const decision = (id: string, controlId: string, value: 'accepted' | 'rejected', createdAt: string) => ({
version: 1 as const, id, ruleKey: 'purple-knight:dummy', sourceTool: 'purple-knight', mappingId: 'dummy-mapping', controlId, decision: value, createdAt });
vi.spyOn(AssessorApiClient.prototype, 'getRegulatoryMappings').mockResolvedValue({ ...mappingResponse, state: 'succeeded',
run: { candidates: [{ controlId: '8.20', annexAControlId: 'A.8.20', title: 'Networks security', score: 0.9, evidence: [] },
{ controlId: '5.1', annexAControlId: 'A.5.1', title: 'Policies for information security', score: 0.8, evidence: [] }] } as never,
decisions: [decision('d3', '5.1', 'rejected', '2026-10-05T10:02:00Z'), decision('d2', '8.20', 'accepted', '2026-10-05T10:01:00Z'),
decision('d1', '5.1', 'accepted', '2026-10-05T10:00:00Z'), decision('d0', '7.4', 'accepted', '2026-10-05T09:00:00Z')] });
await renderReader();
const section = host.querySelector('[data-iso-controls]')!;
expect(section.querySelector('h4')?.textContent).toBe('ISO controls');
// A later rejection replaces an earlier acceptance; a control without a retrieved title keeps its Annex A id.
expect([...section.querySelectorAll('li')].map(item => item.textContent)).toEqual(['A.7.4', 'A.8.20Networks security']);
expect(isoButton()?.textContent).toBe('Map ISO controls (2)');
});
it('requests no Result DB batch status while off and hides the batch action', async () => {
const request = vi.spyOn(AssessorApiClient.prototype, 'getRegulatoryMappingBatch').mockResolvedValue({
version: 1, projectId: 'dummy-project', scope: { assessmentIds: ['dummy-assessment'], scanRunIds: [], filters: {} },
generationEnabled: true, state: 'empty'
} as never);
setEngineEnabled('compliance', false);
await act(async () => root.render());
expect(host.textContent).toBe('off');
expect(request).not.toHaveBeenCalled();
await act(async () => { setEngineEnabled('compliance', true); });
expect(host.textContent).toBe('available');
expect(request).toHaveBeenCalledTimes(1);
const source = readFileSync(join(process.cwd(), 'src/features/results-browser/ResultsBrowser.tsx'), 'utf8');
expect(source).toContain('{regulatoryBatch.available &&