Assessor_2.0/tests/persistence/unified-result.test.ts

538 lines
26 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type {
Assessment,
AssessmentProject,
FindingDefinition,
FindingOccurrence,
ImportFileRecord,
ScanRun
} from '../../src/domain/canonical-model';
import { LocalAssessorRepository } from '../../src/persistence/local-assessor-repository';
const timestamp = '2026-07-22T10:00:00.000Z';
const confidence = { level: 'high' as const, score: 1, reasons: ['synthetic'] };
const provenance = { decisionSource: 'source-native' as const, confidence, mappingVersion: '1.0.0' };
const project: AssessmentProject = {
id: 'project-results',
name: 'Synthetic project',
createdAt: timestamp,
updatedAt: timestamp,
attributes: {}
};
const assessment: Assessment = {
id: 'assessment-results',
projectId: project.id,
name: 'Synthetic assessment',
createdAt: timestamp,
updatedAt: timestamp
};
function run(id: string, sequence: number, toolId: string, toolName = toolId): ScanRun {
return {
id,
assessmentId: assessment.id,
sequence,
label: id,
tool: { id: toolId, name: toolName },
importedAt: timestamp
};
}
function file(scanRun: ScanRun): ImportFileRecord {
return {
id: `file-${scanRun.id}`,
scanRunId: scanRun.id,
fileName: `${scanRun.id}.csv`,
format: 'CSV',
sizeBytes: 10,
importedAt: timestamp,
detectedTool: scanRun.tool,
detection: confidence,
processedWith: {
parserId: 'csv',
parserVersion: '1.0.0',
profileId: scanRun.tool.id,
profileVersion: '1.0.0',
mappingVersion: '1.0.0'
},
sourceArtifact: { kind: 'not-retained', reason: 'synthetic' }
};
}
function definition(id: string, key: string, category: string): FindingDefinition {
return {
id,
canonicalKey: key,
identity: { version: 1, strategy: key.includes('unresolved') ? 'unresolved' : 'tool-rule-id', durable: !key.includes('unresolved'), evidence: ['synthetic'] },
title: 'Same visible title',
category,
frameworkReferences: [],
createdAt: timestamp,
updatedAt: timestamp
};
}
function occurrence(
id: string,
scanRun: ScanRun,
definitionId: string,
status: 'failed' | 'error',
originalStatus: string,
ruleId?: string
): FindingOccurrence {
return {
id,
definitionId,
scanRunId: scanRun.id,
importFileId: `file-${scanRun.id}`,
sourceIdentity: {
tool: scanRun.tool,
originalRuleId: ruleId,
canonicalRuleId: ruleId,
originalTitle: 'Same visible title'
},
sourcePosition: { rowIndex: 1 },
originalFields: [
{ key: 'Status', label: 'Status', value: originalStatus, columnIndex: 0 },
{ key: 'Category', label: 'Category', value: scanRun.tool.id === 'pingcastle' ? 'Privileged Accounts' : 'Account Security', columnIndex: 1 }
],
fieldMappings: [{
targetRole: 'category',
sourceFieldKeys: ['Category'],
normalizedValue: scanRun.tool.id === 'pingcastle' ? 'Privileged Accounts' : 'Account Security',
provenance
}],
severity: { originalValue: 'High', normalizedValue: 'high', provenance },
status: { originalValue: originalStatus, normalizedValue: status, provenance }
};
}
async function commit(
repository: LocalAssessorRepository,
scanRun: ScanRun,
definitions: FindingDefinition[],
occurrences: FindingOccurrence[]
): Promise<void> {
const session = await repository.beginImport({
sessionId: `session-${scanRun.id}`,
projectId: project.id,
assessmentId: assessment.id,
scanRun
});
await session.stage({
importFiles: [file(scanRun)],
findingDefinitions: definitions,
findingOccurrences: occurrences
});
await session.commit();
}
describe('Unified result read model', () => {
it('shows the native public PingCastle rule ID and report title through the normal Result DB query', async () => {
const repository = new LocalAssessorRepository();
await repository.saveProject(project);
await repository.saveAssessment(assessment);
const scanRun = run('run-pingcastle-ntlm', 1, 'pingcastle');
const finding = {
...definition('definition-pingcastle-ntlm', 'pingcastle:S-oldNtlm', 'Anomalies'),
title: 'The LAN Manager Authentication Level allows the use of NTLMv1 or LM.'
};
await commit(repository, scanRun, [finding], [
occurrence('occurrence-pingcastle-ntlm', scanRun, finding.id, 'failed', 'Matched', 'S-oldNtlm')
]);
const result = await repository.queryUnifiedResults({ projectId: project.id, scanRunIds: [scanRun.id] });
expect(result).toMatchObject({
total: 1,
items: [{ title: finding.title, sourceRuleId: 'S-oldNtlm', sourceTool: 'pingcastle' }]
});
await expect(repository.getUnifiedResultById('scanner-finding:occurrence-pingcastle-ntlm'))
.resolves.toMatchObject({ title: finding.title, sourceRuleId: 'S-oldNtlm' });
});
it('keeps Purple Knight AD and Entra ID separately filterable in Result DB', async () => {
const repository = new LocalAssessorRepository();
await repository.saveProject(project);
await repository.saveAssessment(assessment);
const adRun = run('run-purple-ad', 1, 'purple-knight', 'Purple Knight for Active Directory');
const entraRun = run('run-purple-entra', 2, 'purple-knight', 'Purple Knight for Entra ID');
const combinedRun = run('run-purple-combined', 3, 'purple-knight', 'Purple Knight for Active Directory and Entra ID');
const adDefinition = definition('definition-purple-ad', 'purple-knight:ad', 'AD');
const entraDefinition = definition('definition-purple-entra', 'purple-knight:entra', 'Entra');
const combinedDefinition = definition('definition-purple-combined', 'purple-knight:combined', 'Combined');
await commit(repository, adRun, [adDefinition], [occurrence('occurrence-purple-ad', adRun, adDefinition.id, 'failed', 'Failed', 'SI000001')]);
await commit(repository, entraRun, [entraDefinition], [occurrence('occurrence-purple-entra', entraRun, entraDefinition.id, 'failed', 'Failed', 'SI000002')]);
await commit(repository, combinedRun, [combinedDefinition], [occurrence('occurrence-purple-combined', combinedRun, combinedDefinition.id, 'failed', 'Failed', 'SI000003')]);
const facets = await repository.queryUnifiedResultFacets({ deviation: true });
expect(facets.facets.find(facet => facet.id === 'sourceTool')?.values).toEqual([
{ value: 'purple-knight-ad', label: 'Purple Knight for Active Directory', count: 1 },
{ value: 'purple-knight-combined', label: 'Purple Knight for Active Directory and Entra ID', count: 1 },
{ value: 'purple-knight-entra', label: 'Purple Knight for Entra ID', count: 1 }
]);
await expect(repository.queryUnifiedResults({ sourceTool: ['purple-knight-entra'], deviation: true })).resolves.toMatchObject({
total: 1,
items: [expect.objectContaining({ sourceResultId: 'occurrence-purple-entra', sourceTool: 'purple-knight-entra', sourceToolScope: 'entra-id' })]
});
});
it('keeps occurrences separate, splits status dimensions and exposes namespaced categories and reviews', async () => {
const repository = new LocalAssessorRepository();
await repository.saveProject(project);
await repository.saveAssessment(assessment);
const firstRun = run('run-pingcastle', 1, 'pingcastle');
const secondRun = run('run-purple-knight', 2, 'purple-knight');
const stable = definition('definition-stable', 'pingcastle:pc-1', 'Privileged Accounts');
const unresolved = definition('definition-unresolved', 'unresolved:run-purple-knight:1', 'Account Security');
await commit(repository, firstRun, [stable], [
occurrence('occurrence-1', firstRun, stable.id, 'failed', 'FAILED', 'PC-1')
]);
await commit(repository, secondRun, [unresolved], [
occurrence('occurrence-2', secondRun, unresolved.id, 'error', 'Failed to run')
]);
await repository.saveReview({
id: 'review-result-1',
target: { kind: 'occurrence', occurrenceId: 'occurrence-1' },
workflowStatus: 'accepted',
priority: 'high',
includeInActionPlan: false,
owner: 'synthetic-owner',
markers: [],
createdAt: timestamp,
updatedAt: timestamp,
revision: 1
}, 0);
const all = await repository.queryUnifiedResults({ projectId: project.id });
expect(all.total).toBe(2);
expect(all.items.map(item => item.sourceResultId)).toEqual(['occurrence-1', 'occurrence-2']);
const currentTimeslot = await repository.queryUnifiedResults({
projectId: project.id,
scanRunIds: [secondRun.id]
});
expect(currentTimeslot.items.map(item => item.sourceResultId)).toEqual(['occurrence-2']);
const first = await repository.getUnifiedResultById('scanner-finding:occurrence-1');
expect(first).toMatchObject({
sourceKind: 'scanner-finding',
severityNormalized: 'high',
outcomeOriginal: 'fail',
outcomeNormalized: 'fail',
technicalExecutionState: 'completed',
resultType: 'exposure_finding',
isIoe: true,
exposureState: 'detected',
lifecycleState: 'unknown',
reviewStatus: 'resolved',
reviewDisposition: 'accepted',
categories: [{ namespace: 'source-tool', categoryLabel: 'PingCastle' }]
});
expect(first).not.toHaveProperty('reviewOwner');
const executionError = await repository.queryUnifiedResults({
outcome: ['execution_error'],
technicalExecutionState: ['failed']
});
expect(executionError.items.map(item => item.sourceResultId)).toEqual(['occurrence-2']);
expect(executionError.items[0].outcomeOriginal).toBe('execution_error');
// Ein technischer Ausführungsfehler ist per RESULT_CONTRACT nie is_ioe=true,
// auch wenn das Werkzeug ein exposure_finding-Werkzeug ist.
expect(executionError.items[0]).toMatchObject({
resultType: 'exposure_finding',
isIoe: false,
exposureState: 'execution_error'
});
const ioeOnly = await repository.queryUnifiedResults({ isIoe: true });
expect(ioeOnly.items.map(item => item.sourceResultId)).toEqual(['occurrence-1']);
const complianceOnly = await repository.queryUnifiedResults({ resultType: ['compliance_check'] });
expect(complianceOnly.total).toBe(0);
const categories = await repository.listResultCategories();
expect(categories.map(category => [category.namespace, category.categoryLabel])).toEqual([
['source-tool', 'PingCastle'],
['source-tool', 'Purple Knight']
]);
const facets = await repository.queryUnifiedResultFacets({ severity: ['high'] });
expect(facets.total).toBe(2);
expect(facets.facets.find(facet => facet.id === 'outcome')?.values).toEqual([
{ value: 'execution_error', label: 'execution_error', count: 1 },
{ value: 'fail', label: 'fail', count: 1 }
]);
expect(facets.facets.find(facet => facet.id === 'resultType')?.values).toEqual([
{ value: 'exposure_finding', label: 'exposure_finding', count: 2 }
]);
expect(facets.facets.find(facet => facet.id === 'isIoe')?.values).toEqual([
{ value: 'false', label: 'false', count: 1 },
{ value: 'true', label: 'true', count: 1 }
]);
});
it('marks a failed CIS-CAT compliance check as deviation without treating it as IOE', async () => {
const repository = new LocalAssessorRepository();
await repository.saveProject(project);
await repository.saveAssessment(assessment);
const scanRun = run('run-cis-cat', 1, 'cis-cat');
const finding = {
...definition('definition-cis-cat', 'cis-cat:2.2.2', 'CIS'),
title: "2.2.2 Ensure 'Access this computer from the network' is configured"
};
await commit(repository, scanRun, [finding], [
occurrence('occurrence-cis-cat', scanRun, finding.id, 'failed', 'Fail', '2.2.2')
]);
const result = await repository.getUnifiedResultById('scanner-finding:occurrence-cis-cat');
expect(result).toMatchObject({
title: "2.2.2 Ensure 'Access this computer from the network' is configured",
outcomeNormalized: 'fail',
resultType: 'compliance_check',
isIoe: false,
isDeviation: true
});
await expect(repository.queryUnifiedResults({ sourceTool: ['cis-cat'], deviation: true })).resolves.toMatchObject({
total: 1,
items: [expect.objectContaining({ sourceResultId: 'occurrence-cis-cat' })]
});
});
it('exposes only privacy-approved PingCastle environment observations in Result DB', async () => {
const repository = new LocalAssessorRepository();
await repository.saveProject(project);
await repository.saveAssessment(assessment);
const scanRun = run('run-pingcastle-environment', 1, 'pingcastle');
const importFile: ImportFileRecord = {
...file(scanRun),
format: 'HTML',
fileName: 'synthetic-pingcastle.html',
processedWith: {
parserId: 'html-report', parserVersion: '1.1.0', profileId: 'pingcastle', profileVersion: '1.6.0',
mappingVersion: '1.2.0', normalizationVersion: '1.1.0', presentationVersion: '1.2.0',
compatibilityKey: 'pingcastle-canonical-v1.1'
}
};
const session = await repository.beginImport({
sessionId: 'session-pingcastle-environment', projectId: project.id, assessmentId: assessment.id, scanRun
});
await session.stage({
importFiles: [importFile],
environmentObservations: [{
version: 1, id: 'environment-dfl', scanRunId: scanRun.id, importFileId: importFile.id, tool: scanRun.tool,
subject: { type: 'domain-information', identity: 'synthetic.example' },
property: 'domain.functional_level', label: 'Domain functional level',
originalValue: 'Windows Server 2016', normalizedValue: 2016, valueType: 'number', state: 'observed',
sectionKey: 'sectionDomainInformation', source: { objectPath: 'html/sections/domain/table/0' },
mappingVersion: '1.2.0+context-1.0.0', attributes: { extractionMethod: 'synthetic-fixture' }
}, {
version: 1, id: 'environment-domain-name', scanRunId: scanRun.id, importFileId: importFile.id, tool: scanRun.tool,
subject: { type: 'domain-information', identity: 'private.synthetic.example' },
property: 'domain.fqdn', label: 'Domain', originalValue: 'private.synthetic.example',
normalizedValue: 'private.synthetic.example', valueType: 'string', state: 'observed',
sectionKey: 'sectionDomainInformation', source: { objectPath: 'html/sections/domain/table/1' },
mappingVersion: '1.2.0+context-1.0.0', attributes: { customerMarker: 'private-synthetic' }
}]
});
await session.commit();
const results = await repository.queryUnifiedResults({
projectId: project.id,
sourceKind: ['environment-observation'],
environmentProperty: ['domain.functional_level'],
environmentSubjectType: ['active-directory-environment'],
environmentState: ['observed'],
search: '2016'
});
expect(results).toMatchObject({
total: 1,
items: [expect.objectContaining({
resultProjectionId: 'environment-observation:environment-dfl',
sourceKind: 'environment-observation',
sourceTool: 'pingcastle',
sourceProfileVersion: '1.6.0',
mappingRuleVersion: '1.2.0+context-1.0.0',
processingStatus: 'current',
environmentProperty: 'domain.functional_level',
environmentSubjectType: 'active-directory-environment',
environmentNormalizedValue: 2016,
environmentValueType: 'number',
environmentState: 'observed',
supplementalDisclosurePolicyVersion: '1.0.0',
supplementalDisclosureClass: 'technical-metadata',
resultType: 'informational',
isIoe: false,
isDeviation: false
})]
});
await expect(repository.getUnifiedResultById('environment-observation:environment-dfl'))
.resolves.toMatchObject({ sourceResultId: 'environment-dfl', environmentNormalizedValue: 2016 });
await expect(repository.getUnifiedResultById('environment-observation:environment-domain-name'))
.resolves.toBeNull();
expect(JSON.stringify(results)).not.toContain('synthetic.example');
expect(JSON.stringify(results)).not.toContain('private-synthetic');
expect(results.items[0]).not.toHaveProperty('sourceLocator');
expect(results.items[0]).not.toHaveProperty('environmentOriginalValue');
expect(results.items[0]).not.toHaveProperty('environmentSubjectIdentity');
expect(results.items[0]).not.toHaveProperty('environmentAttributes');
await expect(repository.queryUnifiedResults({ sourceKind: ['environment-observation'], deviation: true }))
.resolves.toMatchObject({ total: 0 });
const facets = await repository.queryUnifiedResultFacets({ projectId: project.id });
expect(facets.facets.find(facet => facet.id === 'sourceKind')?.values)
.toContainEqual(expect.objectContaining({ value: 'environment-observation', count: 1 }));
expect(facets.facets.find(facet => facet.id === 'environmentProperty')?.values)
.toContainEqual(expect.objectContaining({ value: 'domain.functional_level', count: 1 }));
expect(facets.facets.find(facet => facet.id === 'environmentState')?.values)
.toContainEqual(expect.objectContaining({ value: 'observed', count: 1 }));
expect(facets.facets.find(facet => facet.id === 'environmentSubjectType')?.values)
.toContainEqual(expect.objectContaining({ value: 'active-directory-environment', count: 1 }));
const categories = await repository.listResultCategories({ sourceKind: ['environment-observation'] });
expect(categories).toEqual(expect.arrayContaining([
expect.objectContaining({ namespace: 'environment-subject', categoryCode: 'active-directory-environment' }),
expect.objectContaining({ namespace: 'environment-property', categoryCode: 'domain' })
]));
});
it('does not merge equal titles without a durable identity', async () => {
const repository = new LocalAssessorRepository();
await repository.saveProject(project);
await repository.saveAssessment(assessment);
const scanRun = run('run-unresolved', 1, 'generic');
const left = definition('definition-left', 'unresolved:left', 'Synthetic');
const right = definition('definition-right', 'unresolved:right', 'Synthetic');
await commit(repository, scanRun, [left, right], [
occurrence('occurrence-left', scanRun, left.id, 'failed', 'Fail'),
occurrence('occurrence-right', scanRun, right.id, 'failed', 'Fail')
]);
const results = await repository.queryUnifiedResults({ search: 'Security assessment finding' });
expect(results.total).toBe(2);
expect(new Set(results.items.map(item => item.sourceResultId))).toEqual(
new Set(['occurrence-left', 'occurrence-right'])
);
expect(JSON.stringify(results)).not.toContain('Same visible title');
expect(results.items.every(item => item.canonicalDefinitionId === undefined)).toBe(true);
});
it('filters and facets by mapping rule version and source profile version', async () => {
const repository = new LocalAssessorRepository();
await repository.saveProject(project);
await repository.saveAssessment(assessment);
const firstRun = run('run-mapping-a', 1, 'pingcastle');
const secondRun = run('run-mapping-b', 2, 'purple-knight');
const definitionA = definition('definition-mapping-a', 'pingcastle:mapping-a', 'Privileged Accounts');
const definitionB = definition('definition-mapping-b', 'purple-knight:mapping-b', 'Account Security');
const fileA: ImportFileRecord = { ...file(firstRun), processedWith: { ...file(firstRun).processedWith, profileVersion: '1.0.0', mappingVersion: '1.0.0' } };
const fileB: ImportFileRecord = { ...file(secondRun), processedWith: { ...file(secondRun).processedWith, profileVersion: '2.0.0', mappingVersion: '2.1.0' } };
async function commitWithFile(scanRun: ScanRun, importFile: ImportFileRecord, defs: FindingDefinition[], occurrences: FindingOccurrence[]): Promise<void> {
const session = await repository.beginImport({ sessionId: `session-${scanRun.id}`, projectId: project.id, assessmentId: assessment.id, scanRun });
await session.stage({ importFiles: [importFile], findingDefinitions: defs, findingOccurrences: occurrences });
await session.commit();
}
await commitWithFile(firstRun, fileA, [definitionA], [occurrence('occurrence-mapping-a', firstRun, definitionA.id, 'failed', 'FAILED', 'PC-M1')]);
await commitWithFile(secondRun, fileB, [definitionB], [occurrence('occurrence-mapping-b', secondRun, definitionB.id, 'failed', 'FAILED', 'PK-M1')]);
const onlyNewMapping = await repository.queryUnifiedResults({ mappingRuleVersion: ['2.1.0'] });
expect(onlyNewMapping.items.map(item => item.sourceResultId)).toEqual(['occurrence-mapping-b']);
const onlyOldProfile = await repository.queryUnifiedResults({ sourceProfileVersion: ['1.0.0'] });
expect(onlyOldProfile.items.map(item => item.sourceResultId)).toEqual(['occurrence-mapping-a']);
const facets = await repository.queryUnifiedResultFacets({});
expect(facets.facets.find(facet => facet.id === 'mappingRuleVersion')?.values).toEqual(
expect.arrayContaining([
expect.objectContaining({ value: '1.0.0', count: 1 }),
expect.objectContaining({ value: '2.1.0', count: 1 })
])
);
expect(facets.facets.find(facet => facet.id === 'sourceProfileVersion')?.values).toEqual(
expect.arrayContaining([
expect.objectContaining({ value: '1.0.0', count: 1 }),
expect.objectContaining({ value: '2.0.0', count: 1 })
])
);
});
it('keeps all options of a facet visible even when filtering by that same facet', async () => {
const repository = new LocalAssessorRepository();
await repository.saveProject(project);
await repository.saveAssessment(assessment);
const scanRun = run('run-severity-visibility', 1, 'pingcastle');
const definitionCritical = definition('definition-severity-critical', 'pingcastle:severity-critical', 'Privileged Accounts');
const definitionHigh = definition('definition-severity-high', 'pingcastle:severity-high', 'Privileged Accounts');
const criticalOccurrence: FindingOccurrence = {
...occurrence('occurrence-severity-critical', scanRun, definitionCritical.id, 'failed', 'FAILED', 'SEV-C'),
severity: { originalValue: 'Critical', normalizedValue: 'critical', provenance }
};
const highOccurrence = occurrence('occurrence-severity-high', scanRun, definitionHigh.id, 'failed', 'FAILED', 'SEV-H');
await commit(repository, scanRun, [definitionCritical, definitionHigh], [criticalOccurrence, highOccurrence]);
const facetsFilteredToCritical = await repository.queryUnifiedResultFacets({ severity: ['critical'] });
expect(facetsFilteredToCritical.total).toBe(1);
const severityFacet = facetsFilteredToCritical.facets.find(facet => facet.id === 'severity');
expect(severityFacet?.values.map(value => value.value).sort()).toEqual(['critical', 'high']);
expect(severityFacet?.values.find(value => value.value === 'critical')?.count).toBe(1);
expect(severityFacet?.values.find(value => value.value === 'high')?.count).toBe(1);
});
// Ohne diese Reihenfolge muesste je Werkzeug nach einer eigenen Punktespalte
// sortiert werden (Purple Knight: Weight, PingCastle: Points). Ueber die
// kanonische Severity steht die Kritikalitaet fuer alle Berichte gleich oben.
it('returns the most critical findings first, across tools', async () => {
const repository = new LocalAssessorRepository();
await repository.saveProject(project);
await repository.saveAssessment(assessment);
const scanRun = run('run-severity-order', 1, 'purple-knight');
const severities: Array<[string, 'critical' | 'low' | 'high' | 'medium']> = [
['low', 'low'], ['critical', 'critical'], ['medium', 'medium'], ['high', 'high']
];
const definitions = severities.map(([name]) =>
definition(`definition-order-${name}`, `purple-knight:order-${name}`, 'Account Security'));
const occurrences = severities.map(([name, severity], index) => ({
...occurrence(`occurrence-order-${name}`, scanRun, definitions[index].id, 'failed', 'IOEs found', `ORD-${name}`),
severity: { originalValue: severity, normalizedValue: severity, provenance }
}));
await commit(repository, scanRun, definitions, occurrences);
const results = await repository.queryUnifiedResults({ projectId: project.id });
expect(results.items.map(item => item.severityNormalized)).toEqual(['critical', 'high', 'medium', 'low']);
});
it('removes unavailable options of other facets after applying active filters', async () => {
const repository = new LocalAssessorRepository();
await repository.saveProject(project);
await repository.saveAssessment(assessment);
const scanRun = run('run-cross-facet-visibility', 1, 'pingcastle');
const definitionCriticalFail = definition('definition-cross-critical-fail', 'pingcastle:cross-critical-fail', 'Privileged Accounts');
const definitionHighError = definition('definition-cross-high-error', 'pingcastle:cross-high-error', 'Privileged Accounts');
const criticalFailOccurrence: FindingOccurrence = {
...occurrence('occurrence-cross-critical-fail', scanRun, definitionCriticalFail.id, 'failed', 'FAILED', 'CROSS-1'),
severity: { originalValue: 'Critical', normalizedValue: 'critical', provenance }
};
const highErrorOccurrence = occurrence('occurrence-cross-high-error', scanRun, definitionHighError.id, 'error', 'Failed to run', 'CROSS-2');
await commit(repository, scanRun, [definitionCriticalFail, definitionHighError], [criticalFailOccurrence, highErrorOccurrence]);
// Nur nach severity=critical gefiltert; die zweite Occurrence (severity high,
// outcome execution_error) ist damit auch keine erreichbare Filteroption.
const facets = await repository.queryUnifiedResultFacets({ severity: ['critical'] });
expect(facets.total).toBe(1);
const outcomeFacet = facets.facets.find(facet => facet.id === 'outcome');
expect(outcomeFacet?.values.map(value => value.value)).toEqual(['fail']);
expect(outcomeFacet?.values.find(value => value.value === 'fail')?.count).toBe(1);
expect(outcomeFacet?.values.find(value => value.value === 'execution_error')).toBeUndefined();
});
});