Files
breakpilot-compliance/ai-compliance-sdk/internal/iace/failure_mode_test.go
T
Benjamin Admin 9c0d471277 feat(iace): Sprint 4D — Failure Mode Layer (FMEA-Faehigkeit)
150 Failure Modes in 11 ComponentTypes:
- Sensor (20): Signalverlust, Drift, Falschmeldung, Encoder-spezifisch
- Controller (20): Watchdog, Speicher, Bus, Safety-SPS CCF, Antrieb
- Actuator (15): Blockiert, Ueberlast, Haltekraftverlust, Schuetz verschweisst
- Mechanical (20): Ermuedungsbruch, Lagerschaden, Kettenriss, Werkzeugbruch
- Electrical (15): Isolation, Kurzschluss, Erdschluss, Lichtbogen
- Software (15): Exception, Race Condition, Buffer Overflow, Timing
- Hydraulic/Pneumatic (15): Schlauchplatzer, Ventil blockiert, Kavitation
- Safety Device (15): Failure-to-trip, CCF, Bremsenverschleiss, PL-Degradation
- Network (10): Paketverlust, Latenz, Man-in-the-Middle
- AI/ML (5): Model Drift, Adversarial Input, Bias

Architektur:
- FailureModeEntry Struct mit FMEA-Scores (Severity/Occurrence/Detection 1-10)
- RPZ = S x O x D (max 1000, Schwelle >= 100 = Massnahme erforderlich)
- RequiredFailureModes auf HazardPattern fuer FM-gesteuertes Pattern-Matching
- MatchInput.FailureModes + MatchReason "failure_mode" (Explainability)
- GET /failure-modes?component_type= API-Endpoint

10 Tests: Count, UniqueIDs, ValidTypes, NonEmpty, Distribution, RPZ (3x), NilFires, RPZDistribution

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

125 lines
3.6 KiB
Go

package iace
import "testing"
func TestFailureModeLibrary_Count(t *testing.T) {
fms := GetFailureModeLibrary()
if len(fms) < 140 {
t.Errorf("expected at least 140 failure modes, got %d", len(fms))
}
t.Logf("Total failure modes: %d", len(fms))
}
func TestFailureModeLibrary_UniqueIDs(t *testing.T) {
seen := make(map[string]bool)
for _, fm := range GetFailureModeLibrary() {
if seen[fm.ID] {
t.Errorf("duplicate failure mode ID: %s", fm.ID)
}
seen[fm.ID] = true
}
}
func TestFailureModeLibrary_ValidComponentTypes(t *testing.T) {
validTypes := map[string]bool{
"sensor": true, "controller": true, "actuator": true,
"mechanical": true, "electrical": true, "software": true,
"hydraulic": true, "pneumatic": true, "safety_device": true,
"network": true, "ai_model": true,
}
for _, fm := range GetFailureModeLibrary() {
if !validTypes[fm.ComponentType] {
t.Errorf("FM %s has invalid ComponentType: %s", fm.ID, fm.ComponentType)
}
}
}
func TestFailureModeLibrary_NonEmptyFields(t *testing.T) {
for _, fm := range GetFailureModeLibrary() {
if fm.NameDE == "" {
t.Errorf("FM %s has empty NameDE", fm.ID)
}
if fm.Effect == "" {
t.Errorf("FM %s has empty Effect", fm.ID)
}
if fm.DefaultSeverity < 1 || fm.DefaultSeverity > 10 {
t.Errorf("FM %s has invalid Severity: %d", fm.ID, fm.DefaultSeverity)
}
if fm.DefaultOccurrence < 1 || fm.DefaultOccurrence > 10 {
t.Errorf("FM %s has invalid Occurrence: %d", fm.ID, fm.DefaultOccurrence)
}
if fm.DefaultDetection < 1 || fm.DefaultDetection > 10 {
t.Errorf("FM %s has invalid Detection: %d", fm.ID, fm.DefaultDetection)
}
}
}
func TestFailureModeLibrary_ComponentTypeDistribution(t *testing.T) {
counts := make(map[string]int)
for _, fm := range GetFailureModeLibrary() {
counts[fm.ComponentType]++
}
for ct, n := range counts {
t.Logf(" %s: %d failure modes", ct, n)
}
if len(counts) < 8 {
t.Errorf("expected at least 8 component types, got %d", len(counts))
}
}
func TestRPZ_Calculation(t *testing.T) {
fm := FailureModeEntry{DefaultSeverity: 8, DefaultOccurrence: 3, DefaultDetection: 4}
rpz := fm.CalculateRPZ()
if rpz != 96 {
t.Errorf("expected RPZ 96 (8*3*4), got %d", rpz)
}
}
func TestRPZ_Maximum(t *testing.T) {
fm := FailureModeEntry{DefaultSeverity: 10, DefaultOccurrence: 10, DefaultDetection: 10}
rpz := fm.CalculateRPZ()
if rpz != 1000 {
t.Errorf("expected RPZ 1000 (10*10*10), got %d", rpz)
}
}
func TestRPZ_Threshold(t *testing.T) {
below := FailureModeEntry{DefaultSeverity: 5, DefaultOccurrence: 2, DefaultDetection: 3}
above := FailureModeEntry{DefaultSeverity: 8, DefaultOccurrence: 4, DefaultDetection: 4}
if below.CalculateRPZ() >= RPZThresholdAction {
t.Error("RPZ 30 should be below threshold 100")
}
if above.CalculateRPZ() < RPZThresholdAction {
t.Errorf("RPZ %d should be above threshold 100", above.CalculateRPZ())
}
}
func TestPatternEngine_FailureMode_NilFiresAlways(t *testing.T) {
engine := NewPatternEngine()
// Without FailureModes in input — all patterns should fire normally
result := engine.Match(MatchInput{
ComponentLibraryIDs: []string{"C001"},
EnergySourceIDs: []string{"EN01"},
})
if len(result.MatchedPatterns) == 0 {
t.Fatal("expected patterns without failure mode filter")
}
}
func TestFailureModeLibrary_RPZDistribution(t *testing.T) {
actionRequired := 0
critical := 0
for _, fm := range GetFailureModeLibrary() {
rpz := fm.CalculateRPZ()
if rpz >= RPZThresholdAction {
actionRequired++
}
if rpz >= 200 {
critical++
}
}
t.Logf("RPZ distribution: %d action required (>=100), %d critical (>=200), of %d total",
actionRequired, critical, len(GetFailureModeLibrary()))
}