package iace import ( "sort" "testing" ) // patternCategoryCompatibility mirrors the Go runtime helper // `acceptableMeasureCategories` in internal/api/handlers/iace_handler_init_helpers.go. // Kept here so the test does not depend on the handlers package (which would // introduce an import cycle). If the runtime map changes, this table must be // updated in lockstep — there is no fallback any more, so a drift would mean // hazards going to the operator with zero mitigations. var patternCategoryCompatibility = map[string]map[string]bool{ "mechanical_hazard": {"mechanical": true}, "electrical_hazard": {"electrical": true}, "thermal_hazard": {"thermal": true, "material_environmental": true}, "noise_vibration": {"noise_vibration": true, "ergonomic": true}, "pneumatic_hydraulic": {"pneumatic_hydraulic": true, "mechanical": true}, "material_environmental": {"material_environmental": true}, "chemical_risk": {"material_environmental": true, "thermal": true}, "ergonomic": {"ergonomic": true}, "ergonomic_hazard": {"ergonomic": true}, "fire_explosion": {"thermal": true, "material_environmental": true}, "radiation_hazard": {"material_environmental": true}, "emc_hazard": {"electrical": true, "software_control": true}, "maintenance_hazard": {"mechanical": true}, "safety_function_failure": {"safety_function": true, "software_control": true}, "software_fault": {"software_control": true}, "sensor_fault": {"software_control": true}, "configuration_error": {"software_control": true}, "update_failure": {"software_control": true}, "hmi_error": {"software_control": true}, "mode_confusion": {"software_control": true}, "unauthorized_access": {"cyber_network": true, "software_control": true}, "communication_failure": {"cyber_network": true, "software_control": true}, "firmware_corruption": {"cyber_network": true, "software_control": true}, "logging_audit_failure": {"cyber_network": true, "software_control": true}, "ai_misclassification": {"ai_specific": true, "software_control": true}, "false_classification": {"ai_specific": true, "software_control": true}, "model_drift": {"ai_specific": true, "software_control": true}, "data_poisoning": {"ai_specific": true, "software_control": true}, "sensor_spoofing": {"ai_specific": true, "software_control": true}, "unintended_bias": {"ai_specific": true, "software_control": true}, "noise_source": {"noise_vibration": true, "ergonomic": true}, "vibration_source": {"noise_vibration": true, "ergonomic": true}, "high_temperature": {"thermal": true, "material_environmental": true}, "material_environmental_hazard": {"material_environmental": true}, } // TestEveryPattern_HasCategoryCompatibleMeasure is the contract that replaces // the old category fallback: every hazard pattern must reference at least one // measure whose HazardCategory is compatible with the pattern's hazard cats, // or with "general". Without this, the operator-facing UI shows a hazard with // zero mitigations and a "consult expert" placeholder — which is fine for // rare edge cases but must not become the default state. // // New patterns added to the library must satisfy this test. The 2026-05 // Bremse benchmark drove down zero-surv from 237 → 0 — that floor is now // enforced. // // Patterns explicitly known to have no library coverage today should be added // to AllowlistKnownGaps with a TODO and an issue link; the test still fails // if anything not on that list has zero coverage. var AllowlistKnownGaps = map[string]string{ // hp-id -> rationale (must be filled when adding) } func TestEveryPattern_HasCategoryCompatibleMeasure(t *testing.T) { measureCat := map[string]string{} for _, m := range GetProtectiveMeasureLibrary() { measureCat[m.ID] = m.HazardCategory } patterns := collectAllPatterns() gaps := []string{} for _, p := range patterns { if _, allowed := AllowlistKnownGaps[p.ID]; allowed { continue } accepted := map[string]bool{"general": true} for _, hc := range p.GeneratedHazardCats { for c := range patternCategoryCompatibility[hc] { accepted[c] = true } } hasCompatible := false for _, mid := range p.SuggestedMeasureIDs { mc, ok := measureCat[mid] if !ok { continue } if mc == "" || accepted[mc] { hasCompatible = true break } } if !hasCompatible { gaps = append(gaps, p.ID+" (cats="+joinCats(p.GeneratedHazardCats)+")") } } sort.Strings(gaps) if len(gaps) > 0 { t.Errorf("%d patterns have no category-compatible measure (add to AllowlistKnownGaps or fix SuggestedMeasureIDs):", len(gaps)) const maxList = 20 for i, g := range gaps { if i >= maxList { t.Errorf(" ... and %d more", len(gaps)-maxList) break } t.Errorf(" - %s", g) } } } // TestAllPatterns_UniqueIDs pins that collectAllPatterns() — i.e. every // pattern that the engine actually sees at runtime — has globally unique // HP-IDs. The older TestGetBuiltinHazardPatterns_UniqueIDs only checks the // 44 builtin ones and missed 58 duplicates between extended.go/extended2.go // and cobot/press/operational/extended_dguv.go that lived undetected for // months (a hazard with the same HP-ID but different scenario would silently // shadow its sibling in the persistence layer). func TestAllPatterns_UniqueIDs(t *testing.T) { seen := map[string]string{} // HP-ID -> first NameDE dups := []string{} for _, p := range collectAllPatterns() { if p.ID == "" { t.Errorf("pattern with empty ID: %s", p.NameDE) continue } if prev, ok := seen[p.ID]; ok { dups = append(dups, p.ID+" ("+prev+" vs "+p.NameDE+")") continue } seen[p.ID] = p.NameDE } if len(dups) > 0 { t.Errorf("%d duplicate HP-IDs across all pattern sources:", len(dups)) const maxList = 20 for i, d := range dups { if i >= maxList { t.Errorf(" ... and %d more", len(dups)-maxList) break } t.Errorf(" - %s", d) } } } func joinCats(cats []string) string { out := "" for i, c := range cats { if i > 0 { out += "," } out += c } return out }