476 lines
21 KiB
TypeScript
476 lines
21 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): ScanRun {
|
|
return {
|
|
id,
|
|
assessmentId: assessment.id,
|
|
sequence,
|
|
label: id,
|
|
tool: { id: toolId, name: toolId },
|
|
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('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: 'FAILED',
|
|
outcomeNormalized: 'fail',
|
|
technicalExecutionState: 'completed',
|
|
resultType: 'exposure_finding',
|
|
isIoe: true,
|
|
exposureState: 'detected',
|
|
lifecycleState: 'unknown',
|
|
reviewStatus: 'resolved',
|
|
reviewDisposition: 'accepted',
|
|
reviewOwner: 'synthetic-owner',
|
|
categories: [{ namespace: 'pingcastle', categoryLabel: 'Privileged Accounts' }]
|
|
});
|
|
|
|
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('Failed to run');
|
|
// 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([
|
|
['pingcastle', 'Privileged Accounts'],
|
|
['purple-knight', 'Account Security']
|
|
]);
|
|
|
|
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: finding.title,
|
|
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 PingCastle environment observations as searchable, provenance-preserving results', 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.0.0', profileId: 'pingcastle', profileVersion: '1.4.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' }
|
|
}]
|
|
});
|
|
await session.commit();
|
|
|
|
const results = await repository.queryUnifiedResults({
|
|
projectId: project.id,
|
|
sourceKind: ['environment-observation'],
|
|
environmentProperty: ['domain.functional_level'],
|
|
environmentSubjectType: ['domain-information'],
|
|
environmentState: ['observed'],
|
|
search: '2016'
|
|
});
|
|
expect(results).toMatchObject({
|
|
total: 1,
|
|
items: [expect.objectContaining({
|
|
resultProjectionId: 'environment-observation:environment-dfl',
|
|
sourceKind: 'environment-observation',
|
|
sourceTool: 'pingcastle',
|
|
sourceProfileVersion: '1.4.0',
|
|
mappingRuleVersion: '1.2.0+context-1.0.0',
|
|
processingStatus: 'current',
|
|
importArtifactId: importFile.id,
|
|
sourceLocator: { objectPath: 'html/sections/domain/table/0' },
|
|
environmentProperty: 'domain.functional_level',
|
|
environmentSubjectType: 'domain-information',
|
|
environmentSubjectIdentity: 'synthetic.example',
|
|
environmentOriginalValue: 'Windows Server 2016',
|
|
environmentNormalizedValue: 2016,
|
|
environmentValueType: 'number',
|
|
environmentState: 'observed',
|
|
environmentSectionKey: 'sectionDomainInformation',
|
|
resultType: 'informational',
|
|
isIoe: false,
|
|
isDeviation: false
|
|
})]
|
|
});
|
|
await expect(repository.getUnifiedResultById('environment-observation:environment-dfl'))
|
|
.resolves.toMatchObject({ sourceResultId: 'environment-dfl', environmentNormalizedValue: 2016 });
|
|
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: 'domain-information', count: 1 }));
|
|
|
|
const categories = await repository.listResultCategories({ sourceKind: ['environment-observation'] });
|
|
expect(categories).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ namespace: 'environment-subject', categoryCode: 'domain-information' }),
|
|
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: 'Same visible title' });
|
|
expect(results.total).toBe(2);
|
|
expect(new Set(results.items.map(item => item.canonicalDefinitionId))).toEqual(
|
|
new Set(['definition-left', 'definition-right'])
|
|
);
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|