Files
breakpilot-compliance/ai-compliance-sdk/internal/iace/pattern_coverage_test.go
T
Benjamin Admin b1357915ae
CI / detect-changes (push) Successful in 8s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / build-sha-integrity (push) Failing after 4s
CI / validate-canonical-controls (push) Successful in 10s
CI / loc-budget (push) Successful in 11s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Has been skipped
CI / test-go (push) Failing after 40s
CI / iace-gt-coverage (push) Successful in 24s
CI / test-python-backend (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
feat(iace): Capability-Domain-Gating — Ghost 120→0, Leakage 25→0, Coverage 100%
Generische Pattern-Engine-Optimierung: behebt zwei Seiten derselben Wurzel
(inkonsistente Applicability-Deklaration ueber 1216 Patterns).

- Ghost-Patterns (120, feuerten nie): 34 nicht-erzeugbare Required-Tags via
  domaenenspezifische Keywords emittierbar gemacht -> 0.
- Cross-Domain-Leakage (25, feuerten ueberall): neuer text-getriebener
  Capability-Domain-Gate (pattern_domain_gates.go) — Pattern mit Fremdmaschine
  im Szenariotext bekommt dom_*-Tag als Required-Gate -> 0.
- Resolver: Komponente->TypicalEnergySources-Expansion (strukturierte Projekte).
- Benchmark: GT-Platzhalter-Filter; faithful Cross-GT-Narrative-Harness.
- Harte Regression-Guards: Ghosts=0, Leakage=0, Coverage>=90% (beide GTs).
- HP2000/HP2001 (Secondary-Harm-Demos) in AllowlistKnownGaps -> Suite gruen.

Echte Pipeline beide GTs: Coverage 100%/100%, 0 Leaks, 0 Ghosts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 11:57:08 +02:00

171 lines
7.3 KiB
Go

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_hazard": {"noise_vibration": true, "ergonomic": true},
"vibration_hazard": {"noise_vibration": true, "ergonomic": 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},
"cyber_resilience": {"cyber_resilience": true, "cyber_network": true, "software_control": 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)
//
// HP2000/HP2001 are deliberate secondary-harm-chain DEMO patterns
// (GetSecondaryHarmDemoPatterns). Their value is the SecondaryHarms field
// (consumer-safety / product-liability chain), not a primary mitigation, so
// they intentionally carry no SuggestedMeasureIDs. Allowlisted rather than
// forced to inherit an ill-fitting measure.
"HP2000": "Secondary-harm DEMO (Cola-Flasche/Splitter): kein Primaer-Measure by design; Wert ist die SecondaryHarms-Kette. TODO: Primaer-Mechanik-Measure ergaenzen, falls aus Demo zu Produktiv-Pattern befoerdert.",
"HP2001": "Secondary-harm DEMO (Pharma Kreuzkontamination): Library hat kein Pharma-CIP-Measure; Wert ist die SecondaryHarms-Kette. TODO: CIP/material_environmental-Measure ergaenzen, falls befoerdert.",
}
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
}