Assessor_2.0/tests/remediation/work-package-interaction.te...

730 lines
57 KiB
TypeScript
Raw Permalink 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, useState } from 'react';
import { FloatingResultsWindow } from '../../src/features/linked-results/FloatingResultsWindow';
import { readFileSync } from 'node:fs';
import { createRoot, type Root } from 'react-dom/client';
import { beforeEach, afterEach, expect, it, vi } from 'vitest';
import { useApp } from '../../src/app/AppProvider';
import { AssessorApiClient } from '../../src/api/assessor-api-client';
import { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences';
import { SearchFilterBar } from '../../src/features/search-filter/SearchFilterBar';
import { useFilteredRecords } from '../../src/features/search-filter/use-filtered-records';
import { FindingReader } from '../../src/features/finding-reader/FindingReader';
import { RelatedFindingsProvider } from '../../src/features/related-findings/RelatedFindings';
import { ActionlistControls } from '../../src/features/actionlist/ActionlistControls';
import { SidebarSecurityDomains } from '../../src/features/sidebar/SidebarSecurityDomains';
import { useContextClusterIndex } from '../../src/features/context-layer/use-context-cluster-index';
import { makeSheet } from '../../src/importers/sheet-factory';
import { genericProfile } from '../../src/profiles/generic/generic-profile';
import { buildWorkPackages, scannerFindingKey, UNASSIGNED_WORK_PACKAGE, WORK_PACKAGE_FACET_ID, type WorkPackage } from '../../src/remediation/remediation-engine';
import { resetWorkPackageRequestsForTests } from '../../src/remediation/use-work-packages';
import { workPackageOverview } from '../../src/remediation/work-package-overview';
import { workPackageIndex } from '../../src/remediation/work-package-index';
import { ContextHeatmap } from '../../src/features/context-layer/ContextHeatmap';
import { contextClusterIndex } from '../../src/features/context-layer/context-cluster-facet';
import { emptyHeatmapContext } from '../assessment-context/context-heatmap-fixture';
vi.mock('../../src/app/AppProvider', () => { const useApp = vi.fn(); return { useApp, useOptionalApp: useApp }; });
vi.mock('../../src/features/context-layer/use-context-cluster-index', () => ({ useContextClusterIndex: vi.fn(() => undefined) }));
let host: HTMLDivElement; let root: Root; let app: ReturnType<typeof useApp>; let result: ReturnType<typeof buildWorkPackages>;
it('keeps only the table head sticky when scrolling package row headings', async () => {
const style = document.createElement('style');
style.textContent = ['base.css', 'components.css'].map(file => readFileSync(`src/styles/${file}`, 'utf8')).join('\n');
document.head.append(style);
try {
await act(async () => root.render(<><ActionlistControls/><View/></>));
await act(async () => host.querySelector<HTMLButtonElement>('[data-work-packages-open]')!.click());
const scroll = document.querySelector<HTMLElement>('.work-packages__scroll')!;
scroll.scrollTop = 300;
await act(async () => scroll.dispatchEvent(new Event('scroll')));
expect(getComputedStyle(scroll.querySelector('thead')!).position).toBe('sticky');
for (const heading of scroll.querySelectorAll('tbody th')) {
expect(getComputedStyle(heading).position).toBe('static');
expect(getComputedStyle(heading).zIndex).toBe('auto');
}
} finally { style.remove(); }
});
function View() {
const records = useFilteredRecords();
// Work shows the selected record in the Reader, otherwise the first visible one.
const selected = records.find(record => record.id === useApp().selectedRecordId) ?? records[0];
return <><SearchFilterBar/><output data-record-count>{records.length}</output>{selected && <FindingReader
record={selected} title={selected.title} linkedSheets={[]} resultReferences={[]} toolId="generic" search="" position={0}
recordCount={records.length} onPrevious={() => {}} onNext={() => {}} onReview={() => {}}/>}</>;
}
it.each(['light', 'dark'])('places yellow-outlined Work Packages before CSV and keeps its outline when opened (%s)', async theme => {
document.documentElement.dataset.theme = theme;
const style = document.createElement('style');
// JSDOM does not resolve custom colour properties; resolve this shared token for its CSS cascade.
const accent = readFileSync('src/styles/ui-tokens.css', 'utf8').match(/--button-accent:\s*([^;]+);/)![1];
style.textContent = ['base.css', 'components.css'].map(file => readFileSync(`src/styles/${file}`, 'utf8')).join('\n')
.replaceAll('var(--button-accent)', accent);
const expected = document.createElement('span'); expected.style.borderColor = accent;
document.head.append(style);
try {
await act(async () => root.render(<ActionlistControls/>));
const row = host.querySelector('.header-export-row')!;
expect([...row.children].filter(node => node.tagName === 'BUTTON').map(node => node.textContent))
.toEqual(['Work Packages', 'CSV', 'MD', 'HTML']);
const button = row.querySelector<HTMLButtonElement>('[data-work-packages-open]')!;
expect(getComputedStyle(button).borderColor).toBe(expected.style.borderColor);
await act(async () => button.click());
expect(button.getAttribute('aria-pressed')).toBe('true');
expect(getComputedStyle(button).borderColor).toBe(expected.style.borderColor);
await act(async () => button.click());
expect(document.querySelector('[data-work-packages-window]')).toBeNull();
expect(button.getAttribute('aria-pressed')).toBe('false');
await act(async () => button.click());
expect(document.querySelector('[data-work-packages-window]')).not.toBeNull();
await act(async () => document.querySelector<HTMLButtonElement>('[aria-label="Close Work packages window"]')!.click());
expect(button.getAttribute('aria-pressed')).toBe('false');
expect(getComputedStyle(button).borderColor).toBe(expected.style.borderColor);
} finally { style.remove(); delete document.documentElement.dataset.theme; }
});
beforeEach(() => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); localStorage.clear(); resetEnginePreferencesForTests(); resetWorkPackageRequestsForTests();
setEngineEnabled('context', false);
vi.mocked(useContextClusterIndex).mockReturnValue(undefined);
const sheet = makeSheet('Synthetic', [['Title','Status','Severity','Source Rule ID','Result Message','Source: Scan'],
['Admin sign-in A','Failed','High','MT.1014','Message A','Synthetic'], ['Admin sign-in B','Failed','High','CISA.MS.AAD.3.6','Message B','Synthetic'],
['Azure hierarchy','Failed','High','MT.1056','Message C','Synthetic']], '', 'synthetic.csv', { isProjectSheet: true });
sheet.analysis.records.forEach((r, i) => { r.findingOccurrenceId = `occ-${i}`; });
const fs = sheet.analysis.records.map(r => ({ key: scannerFindingKey(r.findingOccurrenceId!), occurrenceId: r.findingOccurrenceId, tool: 'maester',
ruleId: r.fields[3].value, title: r.title, outcome: 'fail' }));
result = buildWorkPackages({ projectId: 'synthetic', assessmentIds: ['a'], scanRunIds: ['s'], findings: fs,
context: { engine: { version: '1' }, catalog: { version: '1' }, repositoryRevision: 1,
clusters: [{ clusterId: 'cloud-privileged-access-exposure', label: { en: 'Cloud Privileged Access Exposure' } },
{ clusterId: 'cloud-external-access-exposure', label: { en: 'Cloud External Access Exposure' } }],
// The third finding belongs to another cluster, so the package boundary yields one two-finding package.
findings: fs.map((f, i) => ({ occurrenceId: f.occurrenceId!, clusterIds: [i < 2 ? 'cloud-privileged-access-exposure' : 'cloud-external-access-exposure'], conditionIds: [] })), settings: [] } });
app = { activeSheet: sheet, activeProfile: genericProfile, dataset: { fileName: 'Synthetic', sheets: [sheet] }, targetProjectId: 'synthetic',
activeAssessmentIds: ['a'], activeScanRunIds: ['s'], reviews: {}, search: '', statusFilter: 'all', facetFilters: {}, sortColumn: '', sortDirection: 'asc',
pathFindingIds: [], actionOnly: false, selectedRecordId: '', setFacetFilter: (id: string, filter: unknown) => {
app = { ...app, facetFilters: { ...app.facetFilters } }; if (filter) app.facetFilters[id] = filter as never; else delete app.facetFilters[id];
vi.mocked(useApp).mockReturnValue(app); root.render(<><ActionlistControls/><View/></>);
}, setSelectedRecord: (id: string) => { app = { ...app, selectedRecordId: id }; vi.mocked(useApp).mockReturnValue(app); root.render(<><ActionlistControls/><View/></>); },
setStatusFilter: vi.fn(), setActiveSheet: vi.fn(), resetFilters: vi.fn(), setActionOnly: vi.fn() } as unknown as ReturnType<typeof useApp>;
vi.mocked(useApp).mockReturnValue(app);
vi.spyOn(AssessorApiClient.prototype, 'getWorkPackages').mockResolvedValue({ status: 'ready', result });
host = document.createElement('div'); document.body.append(host); root = createRoot(host);
});
it.each(['light', 'dark'])('minimizes below Source: Scan and restores the retained Work Packages table (%s)', async theme => {
document.documentElement.dataset.theme = theme;
const originalWidth = window.innerWidth;
Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1400 });
try {
await act(async () => root.render(<><ActionlistControls/><View/></>));
const anchor = host.querySelector<HTMLElement>('[aria-label="Source: Scan"]')!;
expect(anchor).not.toBeNull();
vi.spyOn(anchor, 'getBoundingClientRect').mockReturnValue({ bottom: 96 } as DOMRect);
await act(async () => host.querySelector<HTMLButtonElement>('[data-work-packages-open]')!.click());
const panel = document.querySelector<HTMLElement>('.floating-data-window')!;
const filter = panel.querySelector<HTMLSelectElement>('[aria-label="Filter Package"]')!;
await act(async () => { filter.value = filter.options[1].value; filter.dispatchEvent(new Event('change', { bubbles: true })); });
await act(async () => panel.querySelector<HTMLButtonElement>('[data-sort-column="findings"]')!.click());
const before = panel.querySelector('tbody')!.innerHTML;
const selectedFilter = filter.value;
const minimize = () => panel.querySelector<HTMLButtonElement>('[aria-label="Minimize Work packages window"]')!.click();
await act(async () => minimize());
expect(panel.classList.contains('is-minimized')).toBe(true);
expect(panel.style.left).toBe('1064px'); expect(panel.style.top).toBe('104px'); expect(panel.style.width).toBe('320px');
expect(panel.querySelector<HTMLElement>('.floating-data-window-body')!.hidden).toBe(true);
expect(panel.querySelector('.results-window-resize')).toBeNull();
expect(panel.querySelector('.floating-data-window-actions')!.lastElementChild!.getAttribute('aria-label')).toBe('Close Work packages window');
await act(async () => panel.querySelector<HTMLButtonElement>('[aria-label="Open Work packages"]')!.click());
expect(panel.classList.contains('is-minimized')).toBe(false);
expect(panel.style.left).toBe('554px'); expect(panel.style.width).toBe('830px');
expect(panel.querySelector<HTMLElement>('.floating-data-window-body')!.hidden).toBe(false);
expect(filter.value).toBe(selectedFilter); expect(panel.querySelector('tbody')!.innerHTML).toBe(before);
await act(async () => panel.querySelector<HTMLButtonElement>('[aria-label="Maximize Work packages window"]')!.click());
await act(async () => minimize());
expect(panel.classList.contains('is-maximized')).toBe(false);
expect(panel.classList.contains('is-minimized')).toBe(true);
await act(async () => panel.querySelector<HTMLButtonElement>('[aria-label="Restore Work packages window"]')!.click());
expect(panel.style.left).toBe('554px'); expect(filter.value).toBe(selectedFilter);
await act(async () => minimize());
await act(async () => panel.querySelector<HTMLButtonElement>('[aria-label="Close Work packages window"]')!.click());
expect(document.querySelector('.floating-data-window')).toBeNull();
} finally { Object.defineProperty(window, 'innerWidth', { configurable: true, value: originalWidth }); delete document.documentElement.dataset.theme; }
});
afterEach(async () => { await act(async () => root.unmount()); host.remove(); vi.restoreAllMocks(); vi.unstubAllGlobals(); resetEnginePreferencesForTests(); });
it.each(['light', 'dark'])('opens Results above Work Packages and brings either clicked window forward while Filters stays above both (%s)', async theme => {
vi.stubGlobal('ResizeObserver', class { observe() {} disconnect() {} });
document.documentElement.dataset.theme = theme;
function Windows() {
const [shown, setShown] = useState(false);
return <><ActionlistControls/><SidebarSecurityDomains sheet={app.activeSheet}/>
<button onClick={() => setShown(true)}>Results</button>
{shown && <FloatingResultsWindow id="synthetic" title="Results – Synthetic" sheets={[]} color="" zIndex={2100}
offset={0} search="" onActivate={() => {}} onClose={() => setShown(false)}/>}</>;
}
await act(async () => root.render(<Windows/>));
await act(async () => host.querySelector<HTMLButtonElement>('[data-work-packages-open]')!.click());
const packages = document.querySelector<HTMLElement>('.floating-data-window')!;
await act(async () => [...host.querySelectorAll('button')].find(b => b.textContent === 'Results')!.click());
const results = host.querySelector<HTMLElement>('.results-window')!;
expect(Number(results.style.zIndex)).toBeGreaterThan(Number(packages.style.zIndex));
const activate = async (element: Element) => act(async () => element.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true })));
for (let index = 0; index < 3; index++) {
await activate(packages);
expect(Number(packages.style.zIndex)).toBeGreaterThan(Number(results.style.zIndex));
await activate(results);
expect(Number(results.style.zIndex)).toBeGreaterThan(Number(packages.style.zIndex));
}
await act(async () => document.querySelector<HTMLButtonElement>('[aria-label="Detach filters"]')!.click());
const filterLayer = Number(readFileSync('src/styles/ui-tokens.css', 'utf8').match(/--z-floating-filters:\s*(\d+)/)![1]);
expect(filterLayer).toBeGreaterThan(Number(results.style.zIndex));
expect(filterLayer).toBeGreaterThan(Number(packages.style.zIndex));
expect(document.querySelector('.work-filter-panel.is-floating')).not.toBeNull();
delete document.documentElement.dataset.theme;
});
it.each(['light', 'dark'])('shows legacy AP snapshots as WP in every package control and preserves exact selection IDs (%s)', async theme => {
document.documentElement.dataset.theme = theme;
result.packages.forEach(p => { p.code = p.code.replace(/^WP-/, 'AP-'); });
const before = JSON.stringify(result);
await act(async () => root.render(<><ActionlistControls/><View/><SidebarSecurityDomains sheet={app.activeSheet}/></>));
expect(host.querySelector('[data-work-package-id]')!.textContent).toContain('WP-01');
const dropdown = host.querySelector<HTMLSelectElement>('[aria-label="Work Package"]')!;
expect(dropdown.textContent).toContain('WP-01');
expect(dropdown.textContent).not.toContain('AP-');
await act(async () => host.querySelector<HTMLButtonElement>('[data-work-packages-open]')!.click());
const first = document.querySelector<HTMLButtonElement>('[data-work-package-select]')!;
expect(first.textContent).toBe('WP-01');
expect(document.querySelector('[data-work-packages-window]')!.textContent).not.toContain('AP-');
await act(async () => first.click());
expect(app.facetFilters[WORK_PACKAGE_FACET_ID]).toEqual({ values: [result.packages[0].id] });
expect(JSON.stringify(result)).toBe(before);
delete document.documentElement.dataset.theme;
});
it.each(['light', 'dark'])('uses the actual heatmap positions in groups, both filters and the Reader without renumbering filtered groups (%s)', async theme => {
document.documentElement.dataset.theme = theme;
setEngineEnabled('context', true);
let context = emptyHeatmapContext();
vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext').mockResolvedValue(context);
context.clusters.forEach(cluster => { cluster.satisfied = 1; });
const sheet = makeSheet('Synthetic heatmap order', [['Title', 'Status', 'Severity', 'Source Rule ID'],
...Array.from({ length: 10 }, (_, i) => [`Synthetic ${i}`, 'Failed', 'High', `order-${i}`])], '', 'order.csv', { isProjectSheet: true });
const findings = sheet.analysis.records.map((record, i) => {
record.findingOccurrenceId = `order-${i}`;
const clusterId = i < 9 ? 'cloud-privileged-access-exposure' : 'credential-relay-exposure';
const conditionId = context.clusters.find(cluster => cluster.clusterId === clusterId)!.conditions[0].conditionId;
context.observations.push({ id: record.id, occurrenceId: record.findingOccurrenceId, resultProjectionId: record.id,
conditionId, tool: 'maester', sourceRuleId: `order-${i}`, ruleText: record.title, outcome: 'fail',
matchedBy: 'rule-id', assertion: 'observed-by-scanner' } as never);
context.findings.push({ occurrenceId: record.findingOccurrenceId, resultProjectionId: record.id, tool: 'maester', sourceRuleId: `order-${i}`,
observationIds: [record.id], conditionIds: [], clusterIds: [clusterId], declaredReferenceIds: [], cisControlAreaIds: [] });
return { key: scannerFindingKey(record.findingOccurrenceId), occurrenceId: record.findingOccurrenceId, tool: 'maester', ruleId: `order-${i}`, title: record.title, outcome: 'fail' };
});
result = buildWorkPackages({ projectId: 'synthetic', assessmentIds: ['a'], scanRunIds: ['s'], findings, context });
vi.mocked(AssessorApiClient.prototype.getWorkPackages).mockResolvedValue({ status: 'ready', result });
vi.mocked(useContextClusterIndex).mockReturnValue(contextClusterIndex(context));
const render = () => root.render(<><ContextHeatmap context={context} onOpenCluster={vi.fn()}/><SidebarSecurityDomains sheet={sheet}/><ActionlistControls/><View/></>);
app = { ...app, activeSheet: sheet, dataset: { ...app.dataset!, fileName: 'Synthetic order', sheets: [sheet] }, setFacetFilter: (id, filter) => {
app = { ...app, facetFilters: { ...app.facetFilters } };
if (filter) app.facetFilters[id] = filter; else delete app.facetFilters[id];
vi.mocked(useApp).mockReturnValue(app); render();
} };
vi.mocked(useApp).mockReturnValue(app);
const original = JSON.stringify({ context, result, records: sheet.analysis.records });
await act(async () => render());
await act(async () => host.querySelector<HTMLButtonElement>('[data-work-packages-open]')!.click());
const heatmapLabels = new Map([...host.querySelectorAll<HTMLElement>('[data-cluster]')].map(cell => [cell.dataset.cluster!, `${cell.querySelector('.context-heatmap__number')!.textContent} · ${cell.querySelector('strong')!.textContent}`]));
expect(heatmapLabels.get('cloud-privileged-access-exposure')).toBe('01 · Cloud Privileged Access Exposure');
expect(heatmapLabels.get('credential-relay-exposure')).toBe('05 · Credential Relay Exposure');
const groups = [...document.querySelectorAll<HTMLElement>('[data-work-package-cluster]')];
expect(groups.map(group => group.textContent)).toEqual(['01 · Cloud Privileged Access Exposure', '05 · Credential Relay Exposure']);
for (const group of groups) expect(group.textContent).toBe(heatmapLabels.get(group.dataset.workPackageCluster!));
expect([...document.querySelectorAll('[data-work-package-select]')].map(button => button.textContent)).toEqual(['WP-01', 'WP-02', 'WP-01']);
expect([...host.querySelectorAll('.work-package-cluster-head > span:first-child')].map(node => node.textContent)).toEqual(groups.map(group => group.textContent));
for (const facetLabel of ['Context Cluster', 'Work Package']) {
const labels = [...host.querySelector<HTMLSelectElement>(`select[aria-label="${facetLabel}"]`)!.options].slice(1).map(option => option.textContent);
expect(labels[0]).toMatch(/^01 · Cloud Privileged Access Exposure/);
expect(labels.some(label => label?.startsWith('05 · Credential Relay Exposure'))).toBe(true);
}
expect(host.querySelector('[data-work-package-id]')!.textContent).toContain('01 · Cloud Privileged Access Exposure → WP-01');
const relay = result.packages.find(p => p.clusterId === 'credential-relay-exposure')!;
await act(async () => document.querySelector<HTMLButtonElement>(`[data-work-package-select="${relay.id}"]`)!.click());
expect(app.facetFilters[WORK_PACKAGE_FACET_ID]).toEqual({ values: [relay.id] });
expect(host.querySelector('[data-record-count]')!.textContent).toBe('1');
expect(host.querySelector('[data-work-package-id]')!.textContent).toContain('05 · Credential Relay Exposure → WP-01');
expect([...document.querySelectorAll('[data-work-package-cluster]')].map(group => group.textContent)).toEqual(groups.map(group => group.textContent));
const tableFilter = document.querySelector<HTMLSelectElement>('[aria-label="Filter Package"]')!;
await act(async () => { tableFilter.value = relay.id; tableFilter.dispatchEvent(new Event('change', { bubbles: true })); });
expect([...document.querySelectorAll('[data-work-package-cluster]')].map(group => group.textContent)).toEqual(['05 · Credential Relay Exposure']);
expect(JSON.stringify({ context, result, records: sheet.analysis.records })).toBe(original);
// A different published tie order must update every visible position, retaining the selected package ID.
context = { ...context, clusters: [...context.clusters].reverse() };
vi.mocked(useContextClusterIndex).mockReturnValue(contextClusterIndex(context));
await act(async () => render());
const relayTile = host.querySelector('[data-cluster="credential-relay-exposure"]')!;
const relayHeatmapLabel = `${relayTile.querySelector('.context-heatmap__number')!.textContent} · ${relayTile.querySelector('strong')!.textContent}`;
expect(relayHeatmapLabel).not.toBe('05 · Credential Relay Exposure');
expect(document.querySelector('[data-work-package-cluster]')!.textContent).toBe(relayHeatmapLabel);
expect(tableFilter.value).toBe(relay.id);
expect(host.querySelector('[data-work-package-id]')!.textContent).toContain(`${relayHeatmapLabel} → WP-01`);
delete document.documentElement.dataset.theme;
});
it.each(['dark', 'light'])('selects the real Work Package filter, displays the flag beside Source ID and removes Result Message in %s', async theme => {
document.documentElement.dataset.theme = theme;
await act(async () => { root.render(<><ActionlistControls/><View/></>); await new Promise(resolve => setTimeout(resolve, 0)); });
expect(host.querySelector('[data-record-count]')?.textContent).toBe('3');
expect(host.querySelector('select[aria-label="Result Message"]')).toBeNull();
const select = host.querySelector<HTMLSelectElement>('select[aria-label="Work Package"]')!;
const selected = result.packages.find(p => p.findingKeys.length === 2)!;
expect(select.options.namedItem(selected.id)).toBeNull();
expect([...select.options].find(o => o.value === selected.id)?.textContent).toContain('· 2');
await act(async () => { select.value = selected.id; select.dispatchEvent(new Event('change', { bubbles: true })); });
expect(host.querySelector('[data-record-count]')?.textContent).toBe('2');
const source = host.querySelector('[data-source-rule-id]')!;
expect(source.closest('.reader-summary')).not.toBeNull();
expect(source.querySelector('h4')?.textContent).toBe('Source Rule ID');
expect(source.querySelector('code.mono')).not.toBeNull();
expect(host.querySelector('.reader-heading [data-source-rule-id]')).toBeNull();
expect(host.querySelector('.finding-badges [data-work-package-id]')).toBeNull();
const link = source.querySelector<HTMLAnchorElement>('a[data-work-package-id]')!;
expect(link.textContent).toBe(`Cloud Privileged Access Exposure → ${selected.code}`);
expect(link.classList.contains('status-badge')).toBe(false);
const badges = host.querySelector('.finding-badges')!;
expect(badges.firstElementChild?.className).toBe('outcome-mark');
expect(badges.querySelector('.outcome-mark')?.getAttribute('data-outcome')).toBe('fail');
expect(badges.querySelector('.severity-badge.severity-high')?.textContent).toBe('High');
await act(async () => app.setFacetFilter(WORK_PACKAGE_FACET_ID, null));
expect(host.querySelector('[data-record-count]')?.textContent).toBe('3');
await act(async () => host.querySelector<HTMLAnchorElement>('a[data-work-package-id]')!.click());
expect(host.querySelector('[data-record-count]')?.textContent).toBe('2');
expect(app.facetFilters[WORK_PACKAGE_FACET_ID]).toEqual({ values: [selected.id] });
expect(AssessorApiClient.prototype.getWorkPackages).toHaveBeenCalledTimes(1);
await act(async () => setEngineEnabled('remediation', false));
expect(host.querySelector('select[aria-label="Work Package"]')).toBeNull();
expect(host.querySelector('[data-work-package-id]')).toBeNull();
expect(app.facetFilters[WORK_PACKAGE_FACET_ID]).toBeUndefined();
expect(host.querySelector('[data-record-count]')?.textContent).toBe('3');
delete document.documentElement.dataset.theme;
});
it('suppresses late output after disabling the engine and makes no new request', async () => {
let resolve!: (value: { status: 'ready'; result: typeof result }) => void;
vi.mocked(AssessorApiClient.prototype.getWorkPackages).mockReturnValue(new Promise(r => { resolve = r; }));
await act(async () => root.render(<View/>));
await act(async () => setEngineEnabled('remediation', false));
await act(async () => resolve({ status: 'ready', result }));
expect(host.querySelector('[data-work-package-id]')).toBeNull();
expect(host.querySelector('[data-record-count]')?.textContent).toBe('3');
expect(AssessorApiClient.prototype.getWorkPackages).toHaveBeenCalledTimes(1);
});
it('offers a shared non-Cloud package and selects every associated finding through the real Work filter', async () => {
const fs = app.activeSheet!.analysis.records.map((r, i) => ({ key: scannerFindingKey(r.findingOccurrenceId!),
occurrenceId: r.findingOccurrenceId, tool: i === 0 ? 'pingcastle' : 'purple-knight', ruleId: `synthetic-${i}`,
title: r.title, outcome: 'fail' }));
result = buildWorkPackages({ projectId: 'synthetic', assessmentIds: ['a'], scanRunIds: ['s'], findings: fs,
context: { engine: { version: '1' }, catalog: { version: '1' }, repositoryRevision: 1,
clusters: [{ clusterId: 'ad-privileged-account-exposure', label: { en: 'AD Privileged Account Exposure' } },
{ clusterId: 'ad-persistence-indicators', label: { en: 'AD Persistence Indicators' } }],
findings: fs.map((f, i) => ({ occurrenceId: f.occurrenceId!, clusterIds: [i < 2 ? 'ad-privileged-account-exposure' : 'ad-persistence-indicators'],
conditionIds: [i < 2 ? 'ad-admins-not-protected-users' : 'schema-admins-not-empty'] })), settings: [] } });
vi.mocked(AssessorApiClient.prototype.getWorkPackages).mockResolvedValue({ status: 'ready', result });
await act(async () => root.render(<View/>));
const select = host.querySelector<HTMLSelectElement>('select[aria-label="Work Package"]')!;
const selected = result.packages.find(p => p.findingKeys.length === 2)!;
expect([...select.options].find(o => o.value === selected.id)?.textContent).toContain('· 2');
await act(async () => { select.value = selected.id; select.dispatchEvent(new Event('change', { bubbles: true })); });
expect(host.querySelector('[data-record-count]')?.textContent).toBe('2');
expect(host.querySelector('[data-work-package-id]')?.textContent).toBe(`AD Privileged Account Exposure → ${selected.code}`);
});
it('keeps a selected package from showing unrelated findings while a changed scope loads', async () => {
await act(async () => root.render(<View/>));
const select = host.querySelector<HTMLSelectElement>('select[aria-label="Work Package"]')!;
const selected = result.packages.find(p => p.findingKeys.length === 2)!;
await act(async () => { select.value = selected.id; select.dispatchEvent(new Event('change', { bubbles: true })); });
expect(host.querySelector('[data-record-count]')?.textContent).toBe('2');
vi.mocked(AssessorApiClient.prototype.getWorkPackages).mockReturnValue(new Promise(() => {}));
app = { ...app, dataset: { ...app.dataset! } };
vi.mocked(useApp).mockReturnValue(app);
await act(async () => root.render(<View/>));
expect(host.querySelector('[data-record-count]')?.textContent).toBe('0');
expect(app.facetFilters[WORK_PACKAGE_FACET_ID]).toBeDefined();
});
it.each(['dark', 'light'])('opens the sortable package list from the Work toolbar, selects a package and shows its redundant task in the Reader in %s', async theme => {
document.documentElement.dataset.theme = theme;
const fs = app.activeSheet!.analysis.records.map((r, i) => ({ key: scannerFindingKey(r.findingOccurrenceId!),
occurrenceId: r.findingOccurrenceId, tool: i === 0 ? 'pingcastle' : 'purple-knight', ruleId: r.fields[3].value,
title: r.title, outcome: 'fail', severity: i === 0 ? 'high' : 'critical' }));
// Both sources report the same specific condition once: one redundant task. The third finding sits in another cluster.
result = buildWorkPackages({ projectId: 'synthetic', assessmentIds: ['a'], scanRunIds: ['s'], findings: fs,
context: { engine: { version: '1' }, catalog: { version: '1' }, repositoryRevision: 1,
clusters: [{ clusterId: 'ad-privileged-account-exposure', label: { en: 'AD Privileged Account Exposure' } },
{ clusterId: 'ad-persistence-indicators', label: { en: 'AD Persistence Indicators' } }],
findings: fs.map((f, i) => ({ occurrenceId: f.occurrenceId!, clusterIds: [i < 2 ? 'ad-privileged-account-exposure' : 'ad-persistence-indicators'],
conditionIds: [i < 2 ? 'ad-admins-not-protected-users' : 'ad-stale-admincount'] })), settings: [] } });
vi.mocked(AssessorApiClient.prototype.getWorkPackages).mockResolvedValue({ status: 'ready', result });
await act(async () => { root.render(<><ActionlistControls/><View/></>); await new Promise(resolve => setTimeout(resolve, 0)); });
const redundant = result.packages.find(p => p.facts.redundantFindings === 2)!;
expect(redundant.tasks).toHaveLength(1);
expect(host.querySelector('[data-work-packages-open]')?.getAttribute('aria-pressed')).toBe('false');
await act(async () => host.querySelector<HTMLButtonElement>('[data-work-packages-open]')!.click());
expect(host.querySelector('[data-work-packages-open]')?.getAttribute('aria-pressed')).toBe('true');
expect(document.querySelector('.floating-data-window-head.is-marked')?.textContent).toContain('Work packages');
await act(async () => document.querySelector<HTMLButtonElement>('[aria-label="Close Work packages window"]')!.click());
expect(host.querySelector('[data-work-packages-open]')?.getAttribute('aria-pressed')).toBe('false');
await act(async () => host.querySelector<HTMLButtonElement>('[data-work-packages-open]')!.click());
const window = document.querySelector('[data-work-packages-window]')!;
expect(window.querySelector('th[aria-sort="ascending"] [data-sort-column="package"]')).not.toBeNull();
expect(window.querySelector(`[data-work-package-select="${redundant.id}"]`)?.textContent).toBe(redundant.code);
expect(window.querySelector(`[data-work-package-select="${redundant.id}"]`)?.getAttribute('aria-label')).toBe(`AD Privileged Account Exposure → ${redundant.code}`);
expect(window.querySelectorAll('[data-work-package-row]')).toHaveLength(2);
expect(window.querySelectorAll('[data-work-package-unassigned]')).toHaveLength(1);
expect(window.querySelector<HTMLButtonElement>('[data-work-package-select-unassigned]')!.disabled).toBe(true);
await act(async () => window.querySelector<HTMLButtonElement>('[data-sort-column="findings"]')!.click());
expect(window.querySelector('th[aria-sort="descending"] [data-sort-column="findings"]')).not.toBeNull();
expect(window.querySelector('[data-work-package-row]')?.getAttribute('data-work-package-row')).toBe(result.packages.find(p => p.clusterId === 'ad-persistence-indicators')!.id);
expect(window.querySelector('[data-sort-column="severity"]')).toBeNull();
await act(async () => window.querySelector<HTMLButtonElement>(`[data-work-package-select="${redundant.id}"]`)!.click());
expect(document.querySelector('[data-work-packages-window]')).not.toBeNull();
expect(app.facetFilters[WORK_PACKAGE_FACET_ID]).toEqual({ values: [redundant.id] });
await act(async () => root.render(<><ActionlistControls/><View/></>));
expect(host.querySelector('[data-work-packages-open]')?.getAttribute('aria-pressed')).toBe('true');
expect(host.querySelector('[data-record-count]')?.textContent).toBe('2');
const detail = host.querySelector('[data-work-package-task]')!;
expect(detail.textContent).toContain('One of 1 task');
expect(detail.textContent).not.toMatch(/Task \d+ of/);
expect(host.querySelector('[data-work-package-redundant]')?.textContent).toBe(' · Same change as Synthetic CISA.MS.AAD.3.6');
expect(host.querySelector('a[data-work-package-id]')?.textContent).toBe(`AD Privileged Account Exposure → ${redundant.code}`);
await act(async () => setEngineEnabled('remediation', false));
expect(host.querySelector('[data-work-packages-open]')).toBeNull();
expect(host.querySelector('[data-work-package-task]')).toBeNull();
delete document.documentElement.dataset.theme;
});
it.each(['dark', 'light'])('offers selectable values without Measure, finding-severity or Critical/High filters (%s)', async theme => {
document.documentElement.dataset.theme = theme;
await act(async () => root.render(<><ActionlistControls/><View/></>));
await act(async () => host.querySelector<HTMLButtonElement>('[data-work-packages-open]')!.click());
const panel = document.querySelector('[data-work-packages-window]')!;
const before = JSON.stringify(result);
const filter = async (label: string, value: string) => act(async () => {
const input = panel.querySelector<HTMLSelectElement>(`select[aria-label="Filter ${label}"]`)!;
value = [...input.options].find(option => option.textContent === value)?.value ?? value;
expect([...input.options].some(option => option.value === value)).toBe(true);
input.value = value;
input.dispatchEvent(new Event('change', { bubbles: true }));
});
expect(panel.querySelector('input')).toBeNull();
expect(panel.querySelector('[aria-label="Filter Measure"]')).toBeNull();
expect(panel.querySelector('[aria-label="Filter Highest finding severity"]')).toBeNull();
expect(panel.querySelector('[aria-label="Filter Critical/High"]')).toBeNull();
expect(panel.querySelector('[data-sort-column="severity"]')).toBeNull();
expect(panel.querySelector('[data-sort-column="criticalHigh"]')).toBeNull();
const inputs = [...panel.querySelectorAll<HTMLSelectElement>('.work-packages__filter')];
expect(inputs).toHaveLength(8);
for (const input of inputs) {
const label = input.getAttribute('aria-label')!.replace('Filter ', '');
await filter(label, input.options[1].value);
expect(panel.querySelector('tbody tr')).not.toBeNull();
expect(host.querySelector('[data-record-count]')?.textContent).toBe('3');
await filter(label, '');
expect(panel.querySelectorAll('[data-work-package-row]')).toHaveLength(2);
}
await filter('Tasks', '2');
expect(panel.querySelectorAll('[data-work-package-row]')).toHaveLength(1);
await filter('Package', 'Cloud External Access Exposure → WP-01');
expect(panel.querySelectorAll('[data-work-package-row]')).toHaveLength(0);
await filter('Tasks', '');
expect(panel.querySelectorAll('[data-work-package-row]')).toHaveLength(1);
expect(panel.querySelector('[data-work-package-cluster]')?.textContent).toContain('Cloud External Access Exposure');
await act(async () => [...panel.querySelectorAll<HTMLButtonElement>('button')].find(b => b.textContent === 'Clear column filters')!.click());
expect(panel.querySelectorAll('[data-work-package-row]')).toHaveLength(2);
expect(panel.querySelector('[data-work-package-unassigned]')).not.toBeNull();
expect(panel.querySelector('[data-work-package-filter-count]')).toBeNull();
expect(JSON.stringify(result)).toBe(before);
expect(app.facetFilters).toEqual({});
delete document.documentElement.dataset.theme;
});
it.each(['dark', 'light'])('keeps numbered groups in published heatmap order through filtering and column sorting (%s)', async theme => {
document.documentElement.dataset.theme = theme;
const clusters = [
['credential-relay-exposure', 'Credential Relay Exposure', '01'],
['cloud-privileged-access-exposure', 'Cloud Privileged Access Exposure', '02'],
['cloud-sign-in-protection-gaps', 'Cloud Sign-in Protection Gaps', '03'],
['malicious-code-execution-exposure', 'Malicious Code Execution Exposure', '12']
];
const sheet = makeSheet('Synthetic order', [['Title', 'Status', 'Severity', 'Source Rule ID'],
...Array.from({ length: 12 }, (_, i) => [`Finding ${i}`, 'Failed', i === 11 ? 'Critical' : 'High', `synthetic-${i}`])], '', 'order.csv', { isProjectSheet: true });
sheet.analysis.records.forEach((r, i) => { r.findingOccurrenceId = `order-${i}`; });
const findings = sheet.analysis.records.map((r, i) => ({ key: scannerFindingKey(r.findingOccurrenceId!), occurrenceId: r.findingOccurrenceId,
tool: 'maester', ruleId: `synthetic-${i}`, title: r.title, outcome: 'fail', severity: i === 11 ? 'critical' : 'high' }));
result = buildWorkPackages({ projectId: 'synthetic', assessmentIds: ['a'], scanRunIds: ['s'], findings,
context: { engine: { version: '1' }, catalog: { version: '1' }, repositoryRevision: 1,
clusters: clusters.map(([clusterId, label]) => ({ clusterId, label: { en: label } })),
findings: findings.map((f, i) => ({ occurrenceId: f.occurrenceId!, clusterIds: [clusters[i < 9 ? 0 : i - 8][0]], conditionIds: [] })), settings: [] } });
app = { ...app, activeSheet: sheet, dataset: { ...app.dataset!, fileName: 'Synthetic order', sheets: [sheet] } };
vi.mocked(useApp).mockReturnValue(app);
vi.mocked(AssessorApiClient.prototype.getWorkPackages).mockResolvedValue({ status: 'ready', result });
vi.mocked(useContextClusterIndex).mockReturnValue({ publication: { clusters: [clusters[1], clusters[3], clusters[0], clusters[2]].map(([id, label], i) => ({ id, label, hasEvidence: true, severity: i < 2 ? 'critical' : 'high' })) },
clusters: [], facet: { id: 'context:cluster', label: 'Context Cluster', kind: 'category', source: 'derived', values: [], valuesFor: () => [] } } as never);
await act(async () => root.render(<ActionlistControls/>));
const open = async () => act(async () => host.querySelector<HTMLButtonElement>('[data-work-packages-open]')!.click());
const labels = () => [...document.querySelectorAll('[data-work-package-select]')].map(el => el.getAttribute('aria-label'));
const expected = [
'01 · Cloud Privileged Access Exposure → WP-01', '02 · Malicious Code Execution Exposure → WP-01',
'03 · Credential Relay Exposure → WP-01', '03 · Credential Relay Exposure → WP-02',
'04 · Cloud Sign-in Protection Gaps → WP-01'
];
await open();
expect(labels()).toEqual(expected);
expect([...document.querySelectorAll('[data-work-package-cluster]')].map(el => el.textContent)).toEqual([
'01 · Cloud Privileged Access Exposure', '02 · Malicious Code Execution Exposure', '03 · Credential Relay Exposure', '04 · Cloud Sign-in Protection Gaps']);
expect([...document.querySelectorAll('[data-work-package-select]')].map(el => el.textContent)).toEqual(['WP-01', 'WP-01', 'WP-01', 'WP-02', 'WP-01']);
const filter = document.querySelector<HTMLSelectElement>('[aria-label="Filter Package"]')!;
await act(async () => { filter.value = [...filter.options].find(option => option.textContent === expected[1])!.value; filter.dispatchEvent(new Event('change', { bubbles: true })); });
expect(labels()).toEqual([expected[1]]);
await act(async () => { filter.value = ''; filter.dispatchEvent(new Event('change', { bubbles: true })); });
expect(labels()).toEqual(expected);
await act(async () => document.querySelector<HTMLButtonElement>('[data-sort-column="contextSeverity"]')!.click());
expect(labels()).toEqual(expected);
await act(async () => document.querySelector<HTMLButtonElement>('[data-sort-column="package"]')!.click());
expect(labels()).toEqual(expected);
expect(document.querySelector('.work-packages__table tbody tr:last-child')?.hasAttribute('data-work-package-unassigned')).toBe(true);
await act(async () => document.querySelector<HTMLButtonElement>('[aria-label="Close Work packages window"]')!.click());
await open();
expect(labels()).toEqual(expected);
delete document.documentElement.dataset.theme;
});
it('updates an open package overview after Context preparation and dataset refresh, suppressing an older late response', async () => {
const contextRequest = vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext').mockResolvedValue({
findings: [], settings: [], clusters: [], coverage: { uncataloguedWithoutRecognition: 0, scannerFindings: 0 },
engine: { version: 'synthetic' }, catalog: { version: 'synthetic' }, repositoryRevision: 2
} as never);
await act(async () => setEngineEnabled('context', true));
const render = () => root.render(<><ActionlistControls/><View/></>);
await act(async () => render());
await act(async () => host.querySelector<HTMLButtonElement>('[data-work-packages-open]')!.click());
expect(document.querySelectorAll('[data-work-package-row]')).toHaveLength(2);
let resolveOld!: (response: { status: 'ready'; result: typeof result }) => void;
let resolveNew!: typeof resolveOld;
vi.mocked(AssessorApiClient.prototype.getWorkPackages)
.mockReturnValueOnce(new Promise(resolve => { resolveOld = resolve; }))
.mockReturnValueOnce(new Promise(resolve => { resolveNew = resolve; }));
app = { ...app, dataset: { ...app.dataset! } }; vi.mocked(useApp).mockReturnValue(app);
await act(async () => render());
expect(document.querySelector('[data-work-packages-window]')).toBeNull();
app = { ...app, dataset: { ...app.dataset! } }; vi.mocked(useApp).mockReturnValue(app);
await act(async () => render());
await act(async () => resolveOld({ status: 'ready', result }));
expect(document.querySelector('[data-work-packages-window]')).toBeNull();
const p = result.packages.find(p => p.findingKeys.length === 1)!;
const next = { ...result, repositoryRevision: 2, packages: [{ ...p, title: 'Refreshed published measure' }],
unassignedFindingKeys: result.packages.filter(other => other.id !== p.id).flatMap(other => other.findingKeys) };
await act(async () => resolveNew({ status: 'ready', result: next }));
expect(document.querySelectorAll('[data-work-package-row]')).toHaveLength(1);
expect(document.querySelector('[data-work-packages-window]')?.textContent).toContain('Refreshed published measure');
expect(document.querySelector('[data-work-package-unassigned-summary]')?.textContent).toBe('2');
expect(contextRequest).toHaveBeenCalledTimes(3);
expect(AssessorApiClient.prototype.getWorkPackages).toHaveBeenCalledTimes(3);
await act(async () => setEngineEnabled('remediation', false));
expect(document.querySelector('[data-work-packages-window]')).toBeNull();
expect(host.querySelector('[data-work-packages-open]')).toBeNull();
});
it.each(['dark', 'light'])('keeps the full published package contents and compact totals across Work search, status, package filters and reopening (%s)', async theme => {
document.documentElement.dataset.theme = theme;
const sheet = makeSheet('Synthetic reconciliation', [['Title', 'Status', 'Severity', 'Source Rule ID', 'Source: Scan'],
...Array.from({ length: 274 }, (_, i) => [i === 0 ? 'Only matched declaration' : i < 258 ? `Assigned ${i}` : `Unassigned ${i - 258}`,
'Failed', 'High', `synthetic-${i}`, 'Synthetic'])], '', 'synthetic.csv', { isProjectSheet: true });
sheet.analysis.records.forEach((record, i) => { record.findingOccurrenceId = `reconcile-${i}`; });
const keys = sheet.analysis.records.map(record => scannerFindingKey(record.findingOccurrenceId!));
let nextFinding = 0;
const packages: WorkPackage[] = Array.from({ length: 49 }, (_, packageIndex) => {
const tasks = Array.from({ length: packageIndex < 7 ? 6 : 5 }, (_, taskIndex) => {
const findingKeys = keys.slice(nextFinding, nextFinding + (packageIndex === 0 ? 2 : 1));
nextFinding += findingKeys.length;
return { id: `task-${packageIndex}-${taskIndex}`, findingKeys, redundant: findingKeys.length > 1 };
});
const findingKeys = tasks.flatMap(task => task.findingKeys);
return { id: `package-${packageIndex}`, code: `WP-${String(packageIndex + 1).padStart(2, '0')}`,
clusterId: 'ad-privileged-account-exposure', clusterLabel: 'Synthetic cluster', platform: 'active-directory',
title: 'Synthetic published change', tasks, findingKeys, measures: [], method: 'individual', reason: 'Synthetic published task identities',
facts: { tasks: tasks.length, findings: findingKeys.length, redundantFindings: packageIndex === 0 ? 12 : 0,
highestSeverity: 'high', criticalOrHighFindings: findingKeys.length, conditionsClosed: 0, conditionsTouched: 0, sources: ['Synthetic'] } };
});
expect(nextFinding).toBe(258);
result = { ...result, packages, unassignedFindingKeys: keys.slice(258) };
app = { ...app, activeSheet: sheet, dataset: { ...app.dataset!, fileName: 'Synthetic reconciliation', sheets: [sheet] }, statusFilter: 'deviation',
facetFilters: {}, search: '', reviews: {} };
vi.mocked(useApp).mockReturnValue(app);
vi.mocked(AssessorApiClient.prototype.getWorkPackages).mockResolvedValue({ status: 'ready', result });
const originalRecords = JSON.stringify(sheet.analysis.records), originalPackages = JSON.stringify(result);
const openOverview = async () => {
await act(async () => { root.render(<><ActionlistControls/><View/></>); await new Promise(resolve => setTimeout(resolve, 0)); });
if (!document.querySelector('[data-work-packages-window]'))
await act(async () => host.querySelector<HTMLButtonElement>('[data-work-packages-open]')!.click());
return document.querySelector('[data-work-packages-window]')!;
};
let window = await openOverview();
expect(host.querySelector('[data-record-count]')!.textContent).toBe('274');
const facts = () => Object.fromEntries([...window.querySelectorAll('.work-packages__facts > div')].map(cell =>
[cell.querySelector('dt')!.textContent, cell.querySelector('dd')!.textContent]));
const expectedFacts = { Findings: '274', Assigned: '258', Unassigned: '16', Packages: '49', Tasks: '252',
'Redundant findings': '12' };
const originalTable = window.querySelector('tbody')!.innerHTML;
const unchanged = () => {
expect(facts()).toEqual(expectedFacts);
expect(window.querySelectorAll('[data-work-package-row]')).toHaveLength(49);
expect(window.querySelector('tbody')!.innerHTML).toBe(originalTable);
};
unchanged();
app = { ...app, search: 'Unassigned' }; vi.mocked(useApp).mockReturnValue(app);
await act(async () => root.render(<><ActionlistControls/><View/></>));
expect(host.querySelector('[data-record-count]')!.textContent).toBe('16');
unchanged();
await act(async () => window.querySelector<HTMLButtonElement>(theme === 'dark' ? '[data-work-package-select-unassigned]' : '[data-work-package-unassigned-summary]')!.click());
expect(app.facetFilters[WORK_PACKAGE_FACET_ID]).toEqual({ values: [UNASSIGNED_WORK_PACKAGE] });
expect(app.statusFilter).toBe('deviation');
expect(app.search).toBe('Unassigned');
expect(document.querySelector('[data-work-packages-window]')).not.toBeNull();
window = await openOverview();
unchanged();
await act(async () => document.querySelector<HTMLButtonElement>('[aria-label="Close Work packages window"]')!.click());
app = { ...app, facetFilters: {}, search: 'Only matched declaration' }; vi.mocked(useApp).mockReturnValue(app);
window = await openOverview();
expect(host.querySelector('[data-record-count]')!.textContent).toBe('1');
unchanged();
app = { ...app, statusFilter: 'passed', search: 'no matching finding' }; vi.mocked(useApp).mockReturnValue(app);
await act(async () => root.render(<><ActionlistControls/><View/></>));
expect(host.querySelector('[data-record-count]')!.textContent).toBe('0');
unchanged();
expect(JSON.stringify(sheet.analysis.records)).toBe(originalRecords);
expect(JSON.stringify(result)).toBe(originalPackages);
delete document.documentElement.dataset.theme;
});
it('keeps records outside the published package snapshot separately visible in the reconciliation', () => {
const records = app.activeSheet!.analysis.records;
const outside = { ...records[0], id: 'outside-snapshot', findingOccurrenceId: 'outside-snapshot' };
const before = JSON.stringify(result);
const overview = workPackageOverview([...records, outside], workPackageIndex(result));
expect(overview.records).toBe(4);
expect(overview.assigned).toBe(3); expect(overview.unassigned).toBe(0); expect(overview.outsideSnapshot).toBe(1);
expect(overview.assigned + overview.unassigned + overview.outsideSnapshot).toBe(overview.records);
expect(JSON.stringify(result)).toBe(before);
});
it('opens the redundant finding of the same task with only the package filter changed, and reports a finding hidden by other Work filters', async () => {
const fs = app.activeSheet!.analysis.records.map((r, i) => ({ key: scannerFindingKey(r.findingOccurrenceId!),
occurrenceId: r.findingOccurrenceId, tool: i === 0 ? 'pingcastle' : 'purple-knight', ruleId: r.fields[3].value, title: r.title, outcome: 'fail' }));
result = buildWorkPackages({ projectId: 'synthetic', assessmentIds: ['a'], scanRunIds: ['s'], findings: fs,
context: { engine: { version: '1' }, catalog: { version: '1' }, repositoryRevision: 1,
clusters: [{ clusterId: 'ad-privileged-account-exposure', label: { en: 'AD Privileged Account Exposure' } },
{ clusterId: 'ad-persistence-indicators', label: { en: 'AD Persistence Indicators' } }],
findings: fs.map((f, i) => ({ occurrenceId: f.occurrenceId!, clusterIds: [i < 2 ? 'ad-privileged-account-exposure' : 'ad-persistence-indicators'],
conditionIds: [i < 2 ? 'ad-admins-not-protected-users' : 'ad-stale-admincount'] })), settings: [] } });
vi.mocked(AssessorApiClient.prototype.getWorkPackages).mockResolvedValue({ status: 'ready', result });
const [first, second] = app.activeSheet!.analysis.records;
const redundant = result.packages.find(p => p.facts.redundantFindings === 2)!;
await act(async () => { root.render(<><ActionlistControls/><View/></>); await new Promise(resolve => setTimeout(resolve, 0)); });
// Visible case: no package filter yet; the click sets it and selects the other finding.
const link = host.querySelector<HTMLAnchorElement>(`[data-work-package-finding="${second.id}"]`)!;
expect(link.textContent).toBe('Synthetic CISA.MS.AAD.3.6');
await act(async () => link.click());
expect(app.facetFilters[WORK_PACKAGE_FACET_ID]).toEqual({ values: [redundant.id] });
expect(app.selectedRecordId).toBe(second.id);
expect(host.querySelector('[data-record-count]')?.textContent).toBe('2');
expect(host.querySelector(`[data-work-package-finding="${first.id}"]`)?.textContent).toBe('Synthetic MT.1014');
// Hidden case: a search keeps only the current finding; the click reports it and changes nothing.
app = { ...app, search: 'Admin sign-in B', facetFilters: {}, selectedRecordId: '' };
vi.mocked(useApp).mockReturnValue(app);
await act(async () => root.render(<View/>));
expect(host.querySelector('[data-record-count]')?.textContent).toBe('1');
await act(async () => host.querySelector<HTMLAnchorElement>(`[data-work-package-finding="${first.id}"]`)!.click());
expect(host.querySelector('[data-work-package-hidden]')?.textContent).toBe(' · Synthetic MT.1014 is hidden by the current Work filters.');
expect(app.facetFilters).toEqual({});
expect(app.search).toBe('Admin sign-in B');
expect(app.selectedRecordId).toBe('');
});
it.each(['dark', 'light'])('matches Context severity between Filters and Packages, including GPO and live updates (%s)', async theme => {
document.documentElement.dataset.theme = theme;
const p = result.packages.find(p => p.findingKeys.length === 2)!;
p.platform = 'windows-host'; p.facts.highestSeverity = undefined;
const cluster = { id: p.clusterId, severity: 'critical', hasEvidence: true };
const index = { publication: { clusters: [cluster] }, clusters: [], facet: {
id: 'context:cluster', label: 'Context Cluster', kind: 'category', source: 'derived', values: [], valuesFor: () => []
} } as unknown as NonNullable<ReturnType<typeof useContextClusterIndex>>;
vi.mocked(useContextClusterIndex).mockReturnValue(index);
const render = () => root.render(<><SidebarSecurityDomains sheet={app.activeSheet}/><ActionlistControls/></>);
await act(async () => render());
await act(async () => host.querySelector<HTMLButtonElement>('[data-work-packages-open]')!.click());
const row = () => document.querySelector('[data-work-package-row="' + p.id + '"]')!;
expect(document.querySelector('[data-work-package-row]')?.getAttribute('data-work-package-row')).toBe(p.id);
expect(row().querySelector('[data-context-severity="critical"]')?.textContent).toBe('Critical');
expect(document.querySelector('.work-package-cluster-colour.severity-critical')).not.toBeNull();
expect(document.querySelector('[data-sort-column="severity"]')).toBeNull();
cluster.severity = 'medium';
vi.mocked(useContextClusterIndex).mockReturnValue({ ...index });
await act(async () => render());
expect(row().querySelector('[data-context-severity="medium"]')?.textContent).toBe('Medium');
expect(document.querySelector('.work-package-cluster-colour.severity-medium')).not.toBeNull();
cluster.hasEvidence = false;
await act(async () => render());
expect(row().querySelector('[data-context-severity="undetermined"]')?.textContent).toBe('Undetermined');
expect(document.querySelector('.work-package-cluster-colour.severity-undetermined')).not.toBeNull();
expect(p.facts.highestSeverity).toBeUndefined();
delete document.documentElement.dataset.theme;
});
it.each(['light', 'dark'])('opens all same-change evidence in Work from the summary and parks Packages with matching cluster squares (%s)', async theme => {
document.documentElement.dataset.theme = theme;
Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1400 });
Object.defineProperty(window, 'innerHeight', { configurable: true, value: 1200 });
const p = result.packages.find(p => p.findingKeys.length === 2)!;
p.tasks = [{ ...p.tasks[0], findingKeys: p.findingKeys, redundant: true }];
p.facts.tasks = 1; p.facts.redundantFindings = 2;
app = { ...app, activeSheetId: app.activeSheet!.id, workScopeId: 'synthetic', search: 'Azure hierarchy', actionOnly: false,
applyViewBookmark: saved => { app = { ...app, ...saved }; vi.mocked(useApp).mockReturnValue(app); } };
vi.mocked(useApp).mockReturnValue(app);
const render = () => root.render(<RelatedFindingsProvider><ActionlistControls/><View/></RelatedFindingsProvider>);
await act(async () => { render(); await new Promise(resolve => setTimeout(resolve, 0)); });
expect(host.querySelector('[data-record-count]')!.textContent).toBe('1');
await act(async () => host.querySelector<HTMLButtonElement>('[data-work-packages-open]')!.click());
const panel = document.querySelector<HTMLElement>('.floating-data-window')!;
expect(panel.style.width).toBe('830px'); expect(panel.style.height).toBe('1080px');
expect(panel.style.left).toBe('554px'); expect(panel.style.top).toBe('60px');
await act(async () => panel.querySelector<HTMLButtonElement>('[aria-label="Maximize Work packages window"]')!.click());
expect(panel.classList.contains('is-maximized')).toBe(true);
Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1600 });
await act(async () => panel.querySelector<HTMLButtonElement>('[aria-label="Restore Work packages window"]')!.click());
expect(panel.style.left).toBe('754px'); expect(panel.style.width).toBe('830px');
await act(async () => panel.querySelector('.floating-data-window-head')!.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })));
expect(panel.classList.contains('is-maximized')).toBe(true);
await act(async () => panel.querySelector('.floating-data-window-head')!.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })));
expect(panel.style.left).toBe('754px');
expect(panel.querySelectorAll('[data-work-package-colour]')).toHaveLength(0);
for (const row of panel.querySelectorAll('[data-work-package-row]'))
expect(row.querySelector('.work-package-cluster-colour')).toBeNull();
expect(panel.querySelectorAll('.work-packages__cluster .work-package-cluster-colour')).toHaveLength(new Set(result.packages.map(p => p.clusterId)).size);
const before = JSON.stringify(result);
const link = panel.querySelector<HTMLButtonElement>('[data-work-package-related-summary]')!;
expect(link.textContent).toBe('2'); expect(link.disabled).toBe(false);
await act(async () => { link.click(); render(); });
expect(document.querySelector('[data-work-packages-window]')).not.toBeNull();
expect(panel.style.left).toBe('754px');
expect(app.pathFindingIds).toHaveLength(2); expect(app.search).toBe(''); expect(app.facetFilters).toEqual({});
expect(app.statusFilter).toBe('all'); expect(app.view).toBe('records'); expect(app.actionOnly).toBe(false);
expect(host.querySelector('[data-record-count]')!.textContent).toBe('2');
expect(JSON.stringify(result)).toBe(before);
await act(async () => panel.querySelector<HTMLButtonElement>('[aria-label="Close Work packages window"]')!.click());
expect(document.querySelector('[data-work-packages-window]')).toBeNull();
await act(async () => host.querySelector<HTMLButtonElement>('[data-work-packages-open]')!.click());
expect(document.querySelector<HTMLElement>('.floating-data-window')!.style.left).toBe('754px');
delete document.documentElement.dataset.theme;
});