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