Assessor_2.0/tests/features/scanner-first-read.test.tsx

342 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @vitest-environment jsdom
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { renderToStaticMarkup } from 'react-dom/server';
import { describe, expect, it, vi } from 'vitest';
import type { DatasetSheet } from '../../src/domain/dataset';
import type { FindingRecord } from '../../src/domain/finding';
import { FindingReader } from '../../src/features/finding-reader/FindingReader';
import { makeSheet } from '../../src/importers/sheet-factory';
import { toolIdForRecord } from '../../src/importers/status-normalization';
import { powerShellDisplayLines } from '../../src/features/finding-reader/first-read';
import { cisSectionHeading } from '../../src/profiles/cis-cat/cis-benchmark-section-titles';
/** Synthetic scanner finding with the given retained fields. */
it.each(['\n', '<br>'])('renders a Maester description table with a cell break (%s)', (breakValue) => {
const description = 'Synthetic introduction. | Application permission | Path type | Known shortest path | |---|---|---| '
+ '| [Synthetic.Read.All](https://example.invalid/read) | Indirect | First explanation.' + breakValue
+ 'Note: retain this complete explanation. | | Synthetic.Write.All | Direct | Last explanation. |';
const item = record([['Title', 'Synthetic permissions check'], ['Status', 'Failed'], ['Description', description]]);
const { container } = render(item, 'maester');
const table = container.querySelector('.rich-text-table-wrap table')!;
expect(table).not.toBeNull();
expect([...table.querySelectorAll('th')].map(cell => cell.textContent)).toEqual(['Application permission', 'Path type', 'Known shortest path']);
expect(table.querySelectorAll('tbody tr')).toHaveLength(2);
expect(table.querySelectorAll('tbody tr')[0].textContent).toContain('Note: retain this complete explanation.');
expect(table.querySelector('a')?.getAttribute('href')).toBe('https://example.invalid/read');
expect(item.fields.find(field => field.label === 'Description')?.value).toBe(description);
});
function record(fields: Array<[string, string]>): FindingRecord {
const values = fields.map(([label, value], index) => ({ label, value, index }));
return { id: 'synthetic', index: 0, title: 'Synthetic finding', fields: values, filled: values, meta: [] };
}
function sheet(rows: string[][]): DatasetSheet {
return makeSheet('Synthetic results', rows, '', 'synthetic.html');
}
function render(item: FindingRecord, toolId: string, linkedSheets: DatasetSheet[] = []) {
const container = document.createElement('div');
container.innerHTML = renderToStaticMarkup(<FindingReader record={item} title={item.title} linkedSheets={linkedSheets} resultReferences={[]}
search="" position={0} recordCount={1} toolId={toolId} onPrevious={() => {}} onNext={() => {}} onReview={() => {}}/>);
const values = (selector: string) => Object.fromEntries([...container.querySelectorAll(`${selector} .policy-problem__values > div`)]
.map(row => [row.querySelector('dt')!.textContent, row.querySelector('dd')!.textContent]));
return { container, values };
}
const headings = (container: HTMLElement) => [...container.querySelectorAll('.reader-summary > div > h4')].map(item => item.textContent);
describe('scanner first read', () => {
it('PingCastle: evidence and result rows as the problem, the advised solution as the next step, background below', () => {
const item = record([['Rule ID', 'S-Synthetic'], ['Title', 'Synthetic finding'], ['Status', 'Matched'], ['Evidence', 'Synthetic accounts found: 3'],
['Description', 'Synthetic description.'], ['Technical explanation', 'Synthetic explanation.'],
['Advised solution', 'Change the synthetic setting.\n\n$Synthetic = Get-Synthetic'], ['Documentation', 'Synthetic documentation']]);
const { container, values } = render(item, 'pingcastle', [sheet([['Account'], ['synthetic-a'], ['synthetic-b'], ['synthetic-c']])]);
const problem = container.querySelector('.finding-first-read.policy-problem')!;
expect(problem.getAttribute('data-outcome')).toBe(container.querySelector('.outcome-mark')?.getAttribute('data-outcome'));
expect(values('.finding-first-read')).toEqual({ Result: 'Synthetic accounts found: 3', Results: '3 rows in 1 table' });
const next = problem.nextElementSibling!;
expect(next.matches('section.policy-next-step')).toBe(true);
expect(next.textContent).toContain('Change the synthetic setting.');
// The remaining remediation text is complete, only behind a disclosure.
expect(next.querySelector('details summary')?.textContent).toBe('Full remediation');
expect(next.querySelector('details')?.textContent).toContain('$Synthetic = Get-Synthetic');
expect(headings(container)).toEqual(['Description', 'Technical explanation', 'Documentation']);
});
it('keeps a PingCastle PowerShell command complete and readable outside the prose', () => {
const command = 'Get-ADComputer -Filter {(Enabled -eq $true) -and (OperatingSystem -Like "*Windows 7*")} -Property Name,OperatingSystem | format-table Name,OperatingSystem -Wrap -Auto';
const advice = `Upgrade the workstations. You can check the OS with the following PowerShell command: ${command}`;
const item = record([['Advised solution', `${advice}\n\nConfirm the resulting inventory.`]]);
const { container } = render(item, 'pingcastle');
const step = container.querySelector('.finding-first-read.policy-next-step')!;
expect(step.querySelector('p')?.textContent).toBe('Upgrade the workstations. You can check the OS with the following PowerShell command:');
expect(step.querySelector('pre.finding-first-read__command code')?.textContent).toBe(
'Get-ADComputer -Filter {(Enabled -eq $true) -and (OperatingSystem -Like "*Windows 7*")} -Property Name,OperatingSystem |\n'
+ ' format-table Name,OperatingSystem -Wrap -Auto');
expect(step.querySelector('details')?.textContent).toContain('Confirm the resulting inventory.');
expect(item.fields[0].value).toBe(`${advice}\n\nConfirm the resulting inventory.`);
});
// Regression 2026-09-26: a script introduced by "the following script to ...:" and starting with a
// variable assignment stayed inside the prose instead of its own command block.
it('separates an introduced PowerShell script that starts with a variable assignment', () => {
const script = '$SyntheticSid = New-Object System.Security.Principal.SecurityIdentifier ([System.Security.Principal.WellKnownSidType]::AccountDomainUsersSid,(Get-ADDomain).DomainSID) '
+ 'Get-ADUser -Filter * -Properties PrimaryGroup | Where-Object { $_.PrimaryGroup -ne (Get-ADGroup -Filter {SID -eq $SyntheticSid}).DistinguishedName } | Select-Object UserPrincipalName,PrimaryGroup';
const lead = 'Restore the synthetic default. You can use the following script to list users with a synthetic primary group:';
const item = record([['Advised solution', `${lead} ${script}`]]);
const { container } = render(item, 'pingcastle');
const step = container.querySelector('.finding-first-read.policy-next-step')!;
expect(step.querySelector('p')?.textContent).toBe(lead);
const shown = step.querySelector('pre.finding-first-read__command code')?.textContent ?? '';
expect(shown.split('\n')).toEqual([
'$SyntheticSid = New-Object System.Security.Principal.SecurityIdentifier ([System.Security.Principal.WellKnownSidType]::AccountDomainUsersSid,(Get-ADDomain).DomainSID)',
'Get-ADUser -Filter * -Properties PrimaryGroup |',
' Where-Object { $_.PrimaryGroup -ne (Get-ADGroup -Filter {SID -eq $SyntheticSid}).DistinguishedName } |',
' Select-Object UserPrincipalName,PrimaryGroup']);
// Only whitespace changes; every other character is retained.
expect(shown.replace(/\s+/g, ' ')).toBe(script);
expect(item.fields[0].value).toBe(`${lead} ${script}`);
});
it('lays out a flattened command only at unambiguous structure boundaries', () => {
expect(powerShellDisplayLines('Get-Help Get-ADUser -Full')).toEqual(['Get-Help Get-ADUser -Full']);
expect(powerShellDisplayLines('Write-Output "a | b" | Out-Null')).toEqual(['Write-Output "a | b" |', ' Out-Null']);
expect(powerShellDisplayLines('Test-Path $a || Write-Output x')).toEqual(['Test-Path $a || Write-Output x']);
expect(powerShellDisplayLines('Get-A |\n Get-B')).toEqual(['Get-A |', ' Get-B']);
});
it('keeps prose with a colon but no command in the text', () => {
const advice = 'Review the script policy: re-run only signed scripts.';
const { container } = render(record([['Advised solution', advice]]), 'pingcastle');
expect(container.querySelector('pre.finding-first-read__command')).toBeNull();
expect(container.querySelector('.policy-next-step')?.textContent).toContain(advice);
});
it('Purple Knight: result message, affected count and likelihood as the problem, remediation as the next step', () => {
const item = record([['Name', 'Synthetic finding'], ['Status', 'IOE Found'], ['Description', 'Synthetic description.'],
['Likelihood of compromise', 'Synthetic likelihood.'], ['Result message', 'Found 2 synthetic objects.'], ['Number of results', '2'],
['Remediation', 'Remove the synthetic permission.']]);
const { container, values } = render(item, 'purple-knight');
expect(values('.finding-first-read')).toEqual({ Result: 'Found 2 synthetic objects.', Affected: '2', Impact: 'Synthetic likelihood.' });
expect(container.querySelector('.policy-next-step')?.textContent).toContain('Remove the synthetic permission.');
expect(container.querySelector('.policy-next-step details')).toBeNull();
expect(headings(container)).toEqual(['Description']);
});
it('CIS-CAT: the report criterion and evaluated row as expected and found, colored by the finding outcome', () => {
const item = record([['Benchmark Item', 'Synthetic finding'], ['Result', 'Fail'], ['Description', 'Synthetic description.'],
['Rationale', 'Synthetic rationale.'], ['Impact', 'Synthetic impact.'], ['Remediation', 'Set the synthetic value to 1.']]);
const evidence = sheet([['Item', 'Value'], ['Criterion: synthetic value', 'equals 1'], ['Synthetic value', '0']]);
evidence.analysis.records[1].highlighted = true;
const { container, values } = render(item, 'cis-cat', [evidence]);
expect(values('.finding-first-read')).toEqual({ Expected: 'Criterion: synthetic value · equals 1', Found: 'Synthetic value · 0', Impact: 'Synthetic impact.' });
expect(container.querySelector('.finding-first-read [data-state="deviating"] .policy-value-mark.is-deviating')?.textContent).toBe('Synthetic value · 0');
expect(headings(container)).toEqual(['Description', 'Rationale']);
});
it('Maester: the test result lead as the problem, source-marked row counts and the https documentation link', () => {
const item = record([['ID', 'MT.9001'], ['Title', 'Synthetic finding'], ['Result', 'Failed'], ['ResultDetail', 'Synthetic native test result'],
['Description', 'Synthetic description.'], ['HelpUrl', 'https://maester.dev/docs/tests/MT.9001']]);
const results = sheet([['Policy', 'Status'], ['Synthetic A', '❌ Disabled'], ['Synthetic B', '✅ Enabled'], ['Synthetic C', 'Report-only']]);
const { container, values } = render(item, 'maester', [results]);
expect(values('.finding-first-read')).toEqual({ Result: 'Synthetic native test result', Results: '1 of 3 rows marked failed · 1 marked passed' });
const link = container.querySelector<HTMLAnchorElement>('.policy-next-step a')!;
expect(link.getAttribute('href')).toBe('https://maester.dev/docs/tests/MT.9001');
expect(link.getAttribute('rel')).toBe('noopener noreferrer');
});
// Regression 2026-09-28 (user decision): the Maester test result statement was missing from the reader;
// only its first prose paragraph is shown, verbatim, while headings and the impacted-resources table stay in Results.
it('Maester: opening a finding shows the first paragraph of the native test result as the problem', () => {
const detail = '## Test result\n\nYou have 3 of 16 users with administrative roles that aren’t registered and protected with MFA.\n\n'
+ '#### Impacted resources\n\n| Status | Name | First detected |\n| :--- | :--- | :--- |\n| ❌ Fail | synthetic-a@example.test | 04/17/2025 |';
const item = record([['ID', 'MT.1024.adminMFAV2'], ['Title', 'Require multifactor authentication for administrative roles.'],
['Result', 'Failed'], ['ResultDetail', detail], ['Description', 'Synthetic description.\n\n#### Remediation action:\n\n1. Enable MFA.']]);
const { container, values } = render(item, 'maester');
expect(values('.finding-first-read')).toEqual({
Result: 'You have 3 of 16 users with administrative roles that aren’t registered and protected with MFA.'
});
const reader = container.querySelector('.reader-summary')?.textContent ?? '';
expect(reader).not.toContain('synthetic-a@example.test');
expect(reader).not.toContain('Impacted resources');
expect(item.fields.find(field => field.label === 'ResultDetail')?.value).toBe(detail);
});
// Regression 2026-09-28: the Maester Excel export keeps the test result in `Result` next to a separate
// `Status` column, with its line breaks flattened; the statement must still become the problem.
it.each([
['CIS.M365.1.1.1', ' Your tenant has 1 or more hybrid Global Administrators: | Display Name | Cloud Only | | --- | --- | | Synthetic A | ❌ Fail |',
'Your tenant has 1 or more hybrid Global Administrators:'],
['MT.1024.adminMFAV2', 'You have 3 of 16 users with administrative roles that aren’t registered and protected with MFA. #### Impacted resources | Status | Name |',
'You have 3 of 16 users with administrative roles that aren’t registered and protected with MFA.']
])('Maester export %s: the Result statement next to a Status column is the problem', (id, result, expected) => {
const item = record([['ID', id], ['Title', 'Synthetic finding'], ['Severity', 'High'], ['Status', 'Failed'],
['Category', 'CIS'], ['Description', 'Synthetic description.'], ['Result', result]]);
const { values } = render(item, 'maester');
expect(values('.finding-first-read')).toEqual({ Result: expected });
expect(item.fields.find(field => field.label === 'Result')?.value).toBe(result);
});
// Regression 2026-10-13: a list following the result statement (one item per line, or flattened behind the
// introducing colon) appeared completely in the Problem block; it belongs to Results like tables and headings.
it.each([
['flattened export (Result next to Status)', [['Status', 'Failed'], ['Result',
'Synthetic emergency access * user: Synthetic A (00000000-0000-0000-0000-000000000001) These synthetic policies lack the exclusion: '
+ '- [Synthetic policy - one](https://entra.microsoft.com/#synthetic-1) - [Synthetic policy two](https://entra.microsoft.com/#synthetic-2)']]],
['native ResultDetail with line breaks', [['Result', 'Failed'], ['ResultDetail',
'## Test result\n\nSynthetic emergency access * user: Synthetic A (00000000-0000-0000-0000-000000000001) These synthetic policies lack the exclusion:\n'
+ ' - [Synthetic policy - one](https://entra.microsoft.com/#synthetic-1)\n - [Synthetic policy two](https://entra.microsoft.com/#synthetic-2)']]]
] as Array<[string, Array<[string, string]>]>)('Maester %s: the Problem ends before the listed items', (_variant, fields) => {
const item = record([['ID', 'MT.9002'], ['Title', 'Synthetic break glass finding'], ['Description', 'Synthetic description.'], ...fields]);
const { container, values } = render(item, 'maester');
expect(values('.finding-first-read')).toEqual({ Result:
'Synthetic emergency access * user: Synthetic A (00000000-0000-0000-0000-000000000001) These synthetic policies lack the exclusion:' });
const problem = container.querySelector('.finding-first-read')?.textContent ?? '';
expect(problem).not.toContain('Synthetic policy');
expect(container.querySelector('.finding-first-read li, .finding-first-read a')).toBeNull();
});
it('Maester: a test result that opens with a list states no problem paragraph from later text', () => {
const item = record([['ID', 'MT.9003'], ['Title', 'Synthetic finding'], ['Result', 'Failed'],
['ResultDetail', '- [Synthetic one](https://entra.microsoft.com/#1)\n- [Synthetic two](https://entra.microsoft.com/#2)\n\nSynthetic trailing note.']]);
expect(render(item, 'maester').values('.finding-first-read').Result).toBeUndefined();
});
it('Maester export without a Status column keeps Result as the status, not as a problem statement', () => {
const item = record([['ID', 'MT.9002'], ['Title', 'Synthetic finding'], ['Result', 'Failed'], ['Description', 'Synthetic description.']]);
expect(render(item, 'maester').container.querySelector('.finding-first-read.policy-problem')).toBeNull();
});
// Regression 2026-09-26: the mixed Work sheet carries the generic profile and names each record's scan in
// `Source: Scan`. Purple Knight runs are named by scope, which did not resolve to the profile, so the
// report showed the fields as plain sections instead of Problem and Next step.
it.each(['Purple Knight for Active Directory', 'Purple Knight for Entra ID', 'Purple Knight for Active Directory and Entra ID'])(
'Purple Knight in mixed Work (%s): opening the report shows Problem and Next step', runName => {
const item = record([['Name', 'Synthetic finding'], ['Status', 'IOE Found'], ['Description', 'Synthetic description.'],
['Likelihood of compromise', 'Synthetic likelihood.'], ['Result message', 'Found 2 synthetic objects.'], ['Number of results', '2'],
['Remediation', 'Remove the synthetic permission.'], ['Source: Scan', `${runName} · 2026-01-01T00:00:00.000Z`]]);
expect(toolIdForRecord(item, 'generic')).toBe('purple-knight');
const { container, values } = render(item, 'generic');
expect(values('.finding-first-read')).toEqual({ Result: 'Found 2 synthetic objects.', Affected: '2', Impact: 'Synthetic likelihood.' });
expect(container.querySelector('.policy-next-step')?.textContent).toContain('Remove the synthetic permission.');
expect(headings(container)).not.toContain('Remediation');
expect(headings(container)).not.toContain('Result message');
});
// Regression 2026-09-26: Purple Knight exports append the reference titles to the likelihood text and lose
// the paragraph breaks, so the Impact row ended in ">ReferencesNIST.SP.800-63-3...".
it('Purple Knight: impact ends before the appended references, which move to the references position', () => {
const likelihood = 'Synthetic weak passwords are easier to crack.The risk is higher for synthetic privileged accounts.'
+ '>ReferencesNIST.SP.800-63-3Synthetic weak password policy | ANSSI';
const item = record([['Name', 'Synthetic finding'], ['Status', 'IOE Found'], ['Description', 'Synthetic description.'],
['Likelihood of compromise', likelihood], ['Result message', 'Found 5 synthetic users.'], ['Number of results', '5'],
['Remediation', 'Apply the synthetic policy.'], ['Source: Scan', 'Purple Knight for Active Directory · 2026-01-01T00:00:00.000Z']]);
const { container } = render(item, 'generic');
const impact = [...container.querySelectorAll('.finding-first-read .policy-problem__values > div')]
.find(row => row.querySelector('dt')?.textContent === 'Impact')!.querySelector('dd')!;
expect(impact.textContent).not.toContain('References');
// The reader paragraph keeps line breaks (white-space: pre-wrap), so the restored break shows as a new paragraph.
expect(impact.textContent!.split('\n\n')).toEqual([
'Synthetic weak passwords are easier to crack.', 'The risk is higher for synthetic privileged accounts.']);
expect(headings(container)).toEqual(['Description', 'References']);
expect(container.querySelector('.reader-summary')!.textContent).toContain('NIST.SP.800-63-3Synthetic weak password policy | ANSSI');
expect(container.querySelector('.reader-summary')!.textContent!.split('Synthetic weak password policy').length).toBe(2);
expect(item.fields.find(field => field.label === 'Likelihood of compromise')?.value).toBe(likelihood);
});
it.each([
['a spaced heading', 'Synthetic impact text.References Synthetic reference title', 'Synthetic impact text.', 'Synthetic reference title'],
['a heading glued to the last word', 'Synthetic impact is resolvedReferencesSynthetic reference title', 'Synthetic impact is resolved', 'Synthetic reference title'],
['an ordinary mention', 'Open the References tab in the synthetic console.', 'Open the References tab in the synthetic console.', undefined]
])('Purple Knight impact with %s', (_case, likelihood, impact, references) => {
const item = record([['Name', 'Synthetic finding'], ['Likelihood of compromise', likelihood]]);
const { container } = render(item, 'purple-knight');
const row = [...container.querySelectorAll('.finding-first-read .policy-problem__values > div')].find(item => item.querySelector('dt')?.textContent === 'Impact');
expect(row?.querySelector('dd')?.textContent).toBe(impact);
expect(headings(container).includes('References')).toBe(Boolean(references));
if (references) expect(container.querySelector('.reader-summary')!.textContent).toContain(references);
});
it('Maester without a remediation field: the native Remediation action section becomes the next step, shown once', () => {
const description = 'Synthetic background paragraph.\n\n#### Remediation action:\n\nOpen the synthetic portal.\n\n#### Related links\n\n* Synthetic link';
const item = record([['ID', 'MT.9003'], ['Title', 'Synthetic finding'], ['Status', 'Failed'], ['Description', description],
['Source: Scan', 'Maester · 2026-01-01T00:00:00.000Z']]);
const { container } = render(item, 'generic');
const step = container.querySelector('.finding-first-read.policy-next-step')!;
expect(step.querySelector('p')?.textContent).toBe('Open the synthetic portal.');
expect(step.querySelector('details')?.textContent).toContain('Synthetic link');
const summary = container.querySelector('.reader-summary')!.textContent!;
expect(summary).toContain('Synthetic background paragraph.');
expect(summary.split('Open the synthetic portal.').length).toBe(2);
expect(summary).not.toContain('Remediation action');
expect(item.fields.find(field => field.label === 'Description')?.value).toBe(description);
});
it('Maester export with flattened line breaks still separates the native section at its heading', () => {
const item = record([['ID', 'MT.9004'], ['Title', 'Synthetic finding'], ['Status', 'Failed'],
['Description', 'Synthetic background. #### Remediation action: 1. Open the synthetic portal. 2. Save.']]);
const { container } = render(item, 'maester');
const step = container.querySelector('.policy-next-step')!.textContent!;
expect(step).toContain('Open the synthetic portal.');
expect(step).toContain('Save.');
const summary = container.querySelector('.reader-summary')!.textContent!;
expect(summary).toContain('Synthetic background.');
expect(summary.split('Open the synthetic portal.').length).toBe(2);
});
it('Maester without a native remediation section keeps the full description and shows no next step', () => {
const item = record([['ID', 'MT.9005'], ['Title', 'Synthetic finding'], ['Status', 'Failed'], ['Description', 'Only synthetic background.']]);
const { container } = render(item, 'maester');
expect(container.querySelector('.policy-next-step')).toBeNull();
expect(container.querySelector('.reader-summary')!.textContent).toContain('Only synthetic background.');
});
it('CIS-CAT report: each number badge names its report section on hover', () => {
const item: FindingRecord = { ...record([['Rule ID', '2.2.2'], ['Benchmark Item', '2.2.2 Ensure a synthetic user right'], ['Result', 'Fail']]),
benchmarkSectionTitles: new Map([['2', 'Synthetic Local Policies'], ['2.2', 'Synthetic User Rights Assignment']]) };
const badges = [...render(item, 'cis-cat').container.querySelectorAll('[data-derived-category="cis"]')];
expect(badges.map(badge => badge.textContent)).toEqual(['CIS 2', 'CIS 2.2']);
expect(badges.map(badge => badge.getAttribute('title'))).toEqual(['Synthetic Local Policies', 'Synthetic User Rights Assignment']);
});
it.each([
['2.2.2', '2.2.2 Ensure a synthetic user right', 'Synthetic User Rights Assignment: Ensure a synthetic user right'],
['9.9.9', '9.9.9 Ensure a synthetic unknown section', '9.9.9 Ensure a synthetic unknown section'],
['2.2.2', 'Ensure a heading without number', 'Ensure a heading without number']
])('CIS heading for rule %s: %s', (ruleId, heading, expected) => {
const titles = { benchmarkSectionTitles: new Map([['2.2', 'Synthetic User Rights Assignment']]) };
expect(cisSectionHeading(titles, ruleId, heading)).toBe(expected);
expect(cisSectionHeading({}, ruleId, heading)).toBe(heading);
});
it('never offers a non-https documentation link from imported content', () => {
const item = record([['ID', 'MT.9002'], ['Title', 'Synthetic finding'], ['Result', 'Failed'], ['HelpUrl', 'javascript:alert(1)']]);
expect(render(item, 'maester').container.querySelector('.policy-next-step')).toBeNull();
});
});
it('Maester opens with its native title and collapsed complete test code, and discloses absent evidence', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const script='$EntraPremiumRecommendations = @(\n"insiderRiskPolicy"\n)';
const item=record([['Source Rule ID','MT.1024'],['Title','Synthetic recommendation check'],['Result','Failed'],['TestScript',script],['HelpUrl','https://maester.dev/docs/tests/MT.1024/']]);
const container=document.createElement('div');document.body.append(container);const root=createRoot(container);
const show=(value:FindingRecord)=><FindingReader record={value} title={value.title} linkedSheets={[]} resultReferences={[]} search="" position={0} recordCount={1} toolId="maester" onPrevious={()=>{}} onNext={()=>{}} onReview={()=>{}}/>;
try {
await act(async()=>root.render(show(item)));
expect(container.querySelector('.reader-summary > h3')?.textContent).toBe('Synthetic recommendation check');
expect(container.querySelector('.maester-missing-result')).not.toBeNull();
expect(container.querySelector('.policy-problem')).toBeNull();
const details=[...container.querySelectorAll<HTMLDetailsElement>('details')].find(d=>d.querySelector('summary')?.textContent==='Test details')!;
expect(details.open).toBe(false);expect(details.querySelector('code')?.textContent).toBe(script);
await act(async()=>details.querySelector('summary')!.click());expect(details.open).toBe(true);
await act(async()=>details.querySelector('summary')!.click());expect(details.open).toBe(false);
const complete=record([...item.fields.map(f=>[f.label,f.value] as [string,string]),['ResultDetail','Synthetic native result statement.']]);
await act(async()=>root.render(show(complete)));
expect(container.querySelector('.maester-missing-result')).toBeNull();
expect(container.querySelector('.policy-problem')?.textContent).toContain('Synthetic native result statement.');
expect(item.fields.find(f=>f.label==='TestScript')?.value).toBe(script);
} finally {await act(async()=>root.unmount());container.remove();vi.unstubAllGlobals();}
});