Files
breakpilot-compliance/ai-compliance-sdk/internal/iace/risk_trajectory_test.go
T
Benjamin Admin 6d2616cad7 feat(iace): Sprint 4A — Residual Risk Modeling (Suppression Engine)
RiskReduction Struct + automatische Risk Trajectory:
- RiskReduction{SeverityDelta, ExposureDelta, ProbabilityDelta} auf ProtectiveMeasureEntry
- CalculateRiskTrajectory() in engine.go: berechnet schrittweise Risikoreduktion
  entlang ISO 12100 Hierarchie (design → protection → information)
- Kumulative Deltas pro Stufe, Clamp auf Minimum 1
- RiskTrajectoryStep mit Stage, S/E/P, Score, Level, IsAcceptable

101 Massnahmen mit RiskReduction-Profilen versehen:
- Design/Geometry (M001-M010): S-1, E-1 (Gefahrstelle eliminiert)
- Design/Force (M011-M022): S-2 (Energie/Kraft reduziert)
- Design/Control (M039-M050): P-2 (sichere Steuerung)
- Protection/Guards (M061-M072): E-2 (Zugang verhindert)
- Protection/Electro (M073-M079): E-1, P-1 (Erkennung)
- Protection/Safety (M105-M113): P-2 (sichere SPS)
- Protection/Monitoring (M114-M120): P-1 (Frueerkennung)
- Protection/Cyber (M121-M130): P-1
- Information/Training (M161-M168): P-1
- Information/PPE (M169-M175): S-1

8 neue Tests: NoMeasures, DesignReduce, FullHierarchy, ClampMin1,
  OnlyProtection, WithoutReduction, MandatoryAsProtective, LibraryCount

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-10 09:15:43 +02:00

162 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package iace
import "testing"
func TestRiskTrajectory_NoMeasures(t *testing.T) {
engine := NewRiskEngine()
steps := engine.CalculateRiskTrajectory(4, 4, 3, nil)
if len(steps) != 1 {
t.Fatalf("expected 1 step (inherent only), got %d", len(steps))
}
if steps[0].Stage != "inherent" {
t.Errorf("expected stage 'inherent', got %q", steps[0].Stage)
}
if steps[0].RiskScore != 48 {
t.Errorf("expected risk score 48 (4×4×3), got %.1f", steps[0].RiskScore)
}
}
func TestRiskTrajectory_DesignMeasuresReduce(t *testing.T) {
engine := NewRiskEngine()
measures := []ProtectiveMeasureEntry{
{ID: "M001", ReductionType: "design", RiskReduction: &RiskReduction{SeverityDelta: -1, ExposureDelta: -1}},
{ID: "M012", ReductionType: "design", RiskReduction: &RiskReduction{SeverityDelta: -2}},
}
steps := engine.CalculateRiskTrajectory(4, 4, 3, measures)
if len(steps) != 2 {
t.Fatalf("expected 2 steps (inherent + after_design), got %d", len(steps))
}
// After design: S=4+(-1)+(-2)=1, E=4+(-1)=3, P=3 → 1×3×3 = 9
after := steps[1]
if after.Stage != "after_design" {
t.Errorf("expected stage 'after_design', got %q", after.Stage)
}
if after.Severity != 1 {
t.Errorf("expected severity 1, got %d", after.Severity)
}
if after.Exposure != 3 {
t.Errorf("expected exposure 3, got %d", after.Exposure)
}
if after.RiskScore != 9 {
t.Errorf("expected risk score 9, got %.1f", after.RiskScore)
}
}
func TestRiskTrajectory_FullHierarchy(t *testing.T) {
engine := NewRiskEngine()
measures := []ProtectiveMeasureEntry{
{ID: "M001", ReductionType: "design", RiskReduction: &RiskReduction{ExposureDelta: -1}},
{ID: "M067", ReductionType: "protection", RiskReduction: &RiskReduction{ExposureDelta: -2, ProbabilityDelta: -1}},
{ID: "M161", ReductionType: "information", RiskReduction: &RiskReduction{ProbabilityDelta: -1}},
}
// Start: S=4, E=4, P=3 → 48
steps := engine.CalculateRiskTrajectory(4, 4, 3, measures)
if len(steps) != 4 {
t.Fatalf("expected 4 steps, got %d", len(steps))
}
// Inherent: 4×4×3 = 48
if steps[0].RiskScore != 48 {
t.Errorf("inherent: expected 48, got %.1f", steps[0].RiskScore)
}
// After design: S=4, E=3, P=3 → 36
if steps[1].RiskScore != 36 {
t.Errorf("after_design: expected 36, got %.1f", steps[1].RiskScore)
}
// After protection: S=4, E=1, P=2 → 8
if steps[2].RiskScore != 8 {
t.Errorf("after_protection: expected 8, got %.1f", steps[2].RiskScore)
}
// After information: S=4, E=1, P=1 → 4
if steps[3].RiskScore != 4 {
t.Errorf("after_information: expected 4, got %.1f", steps[3].RiskScore)
}
if !steps[3].IsAcceptable {
t.Error("final risk 4 should be acceptable")
}
}
func TestRiskTrajectory_ClampMinimum1(t *testing.T) {
engine := NewRiskEngine()
// Very aggressive reduction that would push below 1
measures := []ProtectiveMeasureEntry{
{ID: "M001", ReductionType: "design", RiskReduction: &RiskReduction{SeverityDelta: -5, ExposureDelta: -5, ProbabilityDelta: -5}},
}
steps := engine.CalculateRiskTrajectory(3, 3, 3, measures)
after := steps[1]
if after.Severity != 1 || after.Exposure != 1 || after.Probability != 1 {
t.Errorf("expected all clamped to 1, got S=%d E=%d P=%d", after.Severity, after.Exposure, after.Probability)
}
if after.RiskScore != 1 {
t.Errorf("expected minimum risk score 1, got %.1f", after.RiskScore)
}
}
func TestRiskTrajectory_OnlyProtectionMeasures(t *testing.T) {
engine := NewRiskEngine()
// No design measures, only protection
measures := []ProtectiveMeasureEntry{
{ID: "M067", ReductionType: "protection", RiskReduction: &RiskReduction{ExposureDelta: -2}},
}
steps := engine.CalculateRiskTrajectory(4, 4, 3, measures)
// Should have 2 steps: inherent + after_protection (no after_design)
if len(steps) != 2 {
t.Fatalf("expected 2 steps, got %d", len(steps))
}
if steps[1].Stage != "after_protection" {
t.Errorf("expected stage after_protection, got %q", steps[1].Stage)
}
// S=4, E=2, P=3 → 24
if steps[1].RiskScore != 24 {
t.Errorf("expected 24, got %.1f", steps[1].RiskScore)
}
}
func TestRiskTrajectory_MeasuresWithoutRiskReduction(t *testing.T) {
engine := NewRiskEngine()
// Measures without RiskReduction should be skipped (no delta)
measures := []ProtectiveMeasureEntry{
{ID: "M151", ReductionType: "information", Name: "Betriebsanleitung"},
}
steps := engine.CalculateRiskTrajectory(3, 3, 3, measures)
if len(steps) != 1 {
t.Fatalf("expected 1 step (measures without RiskReduction have no effect), got %d", len(steps))
}
}
func TestRiskTrajectory_MandatoryMeasuresAsProtective(t *testing.T) {
engine := NewRiskEngine()
// MN measures have ReductionType "protective" — should be grouped with "protection"
measures := []ProtectiveMeasureEntry{
{ID: "MN001", ReductionType: "protective", RiskReduction: &RiskReduction{ProbabilityDelta: -1}},
}
steps := engine.CalculateRiskTrajectory(4, 4, 3, measures)
if len(steps) != 2 {
t.Fatalf("expected 2 steps, got %d", len(steps))
}
if steps[1].Stage != "after_protection" {
t.Errorf("expected after_protection for 'protective' type, got %q", steps[1].Stage)
}
}
func TestRiskReduction_OnMeasureLibrary(t *testing.T) {
// Verify that the library has measures with RiskReduction profiles
measures := GetProtectiveMeasureLibrary()
withReduction := 0
for _, m := range measures {
if m.RiskReduction != nil {
withReduction++
}
}
if withReduction < 80 {
t.Errorf("expected at least 80 measures with RiskReduction profiles, got %d", withReduction)
}
}