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) } }