import { describe, expect, it } from 'vitest'; import type { FindingRecord } from '../../src/domain/finding'; import type { DynamicFacet } from '../../src/features/search-filter/dynamic-facets'; import type { TimeslotComparisonReport } from '../../src/comparison/timeslot-comparison-report'; import { changesWithWorkRecords, comparisonWorkFacetIndex, COMPARISON_CHANGE_FACET_ID, withComparisonFacets } from '../../src/features/comparison/comparison-work-facets'; import type { PublishedComparison } from '../../src/features/comparison/use-timeslot-comparison'; // The Work `Change` facet maps the comparison onto Work records of timeslot B. Synthetic records only. const gpoRecordId = (identity: string) => `gpo-work-setting:${encodeURIComponent(JSON.stringify(['dummy-project', 'aggregation-b', 'computer', identity]))}`; const record = (id: string, findingOccurrenceId?: string) => ({ id, fields: [], ...(findingOccurrenceId ? { findingOccurrenceId } : {}) }) as unknown as FindingRecord; // A rolled-up GPO setting whose Work record ID does not carry the setting identity: matched by its Raw Data fields. const gpoFieldRecord = (scope: string, identity: string) => ({ id: 'record-synthetic', fields: [ { label: 'Category', value: 'GPOs' }, { label: 'Scope', value: scope }, { label: 'Technical Identity', value: identity }] }) as unknown as FindingRecord; const report = { changes: [ { sourceId: 'cis-cat', change: 'resolved', side: 'baseline', recordIds: ['a-1'], counterpartIds: ['b-1'], reasons: [] }, { sourceId: 'cis-cat', change: 'unverified', side: 'baseline', recordIds: ['a-2'], counterpartIds: [], reasons: [] }, { sourceId: 'cis-cat', change: 'regressed', side: 'target', recordIds: ['b-3'], counterpartIds: [], reasons: [] }, { sourceId: 'group-policy', change: 'value-changed', side: 'baseline', recordIds: ['ga-1'], counterpartIds: ['gb-1'], identityKey: JSON.stringify(['computer', 'MinimumPasswordLength']), reasons: [] }, { sourceId: 'group-policy', change: 'new', side: 'target', recordIds: ['gb-2'], counterpartIds: [], identityKey: JSON.stringify(['computer', 'SeEnableDelegationPrivilege']), reasons: [] } ] } as unknown as TimeslotComparisonReport; const timeslot = (id: string, date: string) => ({ id, date, assessmentIds: [id], scanRunIds: [] }); const published: PublishedComparison = { projectId: 'dummy-project', targetAssessmentIds: ['b'], baseline: timeslot('a', '2026-01-10'), target: timeslot('b', '2026-02-10'), report }; describe('Comparison Engine Work Change facet', () => { it('assigns each timeslot B record its change and leaves unrelated records without one', () => { const index = comparisonWorkFacetIndex(published); expect(index.changeOf(record('work-b-1', 'b-1'))).toBe('resolved'); expect(index.changeOf(record('work-b-3', 'b-3'))).toBe('regressed'); expect(index.changeOf(record(gpoRecordId('MinimumPasswordLength')))).toBe('value-changed'); expect(index.changeOf(record(gpoRecordId('SeEnableDelegationPrivilege')))).toBe('new'); expect(index.changeOf(gpoFieldRecord('computer', 'MinimumPasswordLength'))).toBe('value-changed'); expect(index.changeOf(gpoFieldRecord('user', 'MinimumPasswordLength'))).toBeUndefined(); expect(index.changeOf(record('work-other', 'b-9'))).toBeUndefined(); expect(index.facets[0].valuesFor?.(record('work-b-1', 'b-1'))).toEqual(['resolved']); expect(index.facets[0]).toMatchObject({ id: COMPARISON_CHANGE_FACET_ID, label: 'Change' }); }); it('offers only changes with a Work record in timeslot B', () => { expect([...changesWithWorkRecords(report)].sort()).toEqual(['new', 'regressed', 'resolved', 'value-changed']); }); it('places the facet directly before the permanent final Source: Scan control', () => { const facet = (id: string) => ({ id, label: id, source: 'derived', kind: 'category', values: [] }) as unknown as DynamicFacet; const facets = withComparisonFacets([facet('status'), facet('provenance:source-scan')], comparisonWorkFacetIndex(published)); expect(facets.map(item => item.id)).toEqual(['status', COMPARISON_CHANGE_FACET_ID, 'provenance:source-scan']); expect(withComparisonFacets([facet('status')], undefined).map(item => item.id)).toEqual(['status']); }); });