9c0d471277
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>
125 lines
3.6 KiB
Go
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()))
|
|
}
|