Files
breakpilot-compliance/ai-compliance-sdk/internal/iace/controls_library_test.go
T
Benjamin Admin 2e29b611c9 feat(iace): Phase 1 — Haftungs-Fixes, Massnahmen-Verkabelung, Explainability Engine
Phase 1A — Haftungs-kritische Fixes:
- SIL/PL-Badges als "Vorab-Einschaetzung" mit Tooltip gekennzeichnet
- Coverage-Disclaimer in CE-Akte, Projekt-Uebersicht und Print-Export
- Norm-Referenzen: 42 Kapitelverweise durch Themen-Deskriptoren ersetzt

Phase 1B — Massnahmen-Verkabelung:
- 16 neue Massnahmen (M201-M216) fuer bisher unabgedeckte Kategorien
  (communication_failure, hmi_error, firmware_corruption, maintenance,
  sensor_fault, mode_confusion)
- Kategorie-Fallback im Initialize-Endpoint: ordnet Massnahmen aus der
  Bibliothek automatisch per HazardCategory zu (max 8 pro Kategorie)
- Total: 225 → 241 Massnahmen, 0 Kategorien ohne Massnahmen

Phase 1C — Explainability Engine:
- MatchReason Struct in PatternMatch (type, tag, met)
- Pattern Engine schreibt fuer jeden Match strukturierte Begruendungen
- Frontend zeigt "Erkannt weil: Komponente X, Energie Y, Kein Ausschluss Z"

Weitere Aenderungen:
- BAuA/OSHA Regulatory Hints: 3 Enrich-Endpoints (per Hazard, per Measure, Batch)
- Dokumente-Tab in IACE-Bibliothek (36.708 Chunks aus Qdrant)
- Varianten-UX: Basis-Projekt-Summary auf Varianten-Seite
- Projekt-Initialisierung: POST /initialize kettet Parse→Komponenten→Patterns→Hazards→Massnahmen→Normen
- 18 pre-existing TS-Fehler gefixt, Route-Konflikt behoben
- Component-Library + Measures-Library Tests aktualisiert

Tests: Go alle bestanden, TS 0 Fehler, Playwright 141+ bestanden

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-09 21:32:23 +02:00

178 lines
4.9 KiB
Go

package iace
import "testing"
// TestControlsLibrary_UniqueIDs verifies all control IDs are unique.
func TestControlsLibrary_UniqueIDs(t *testing.T) {
seen := make(map[string]bool)
for _, e := range GetControlsLibrary() {
if e.ID == "" {
t.Errorf("control has empty ID")
continue
}
if seen[e.ID] {
t.Errorf("duplicate control ID: %s", e.ID)
}
seen[e.ID] = true
}
}
// TestProtectiveMeasures_HasExamples verifies measures have examples.
func TestProtectiveMeasures_HasExamples(t *testing.T) {
withExamples := 0
for _, e := range GetProtectiveMeasureLibrary() {
if len(e.Examples) > 0 {
withExamples++
}
}
total := len(GetProtectiveMeasureLibrary())
threshold := total * 80 / 100
if withExamples < threshold {
t.Errorf("only %d/%d measures have examples, want at least %d", withExamples, total, threshold)
}
}
// TestProtectiveMeasures_ThreeReductionTypesPresent verifies all 3 types exist.
func TestProtectiveMeasures_ThreeReductionTypesPresent(t *testing.T) {
types := make(map[string]int)
for _, e := range GetProtectiveMeasureLibrary() {
types[e.ReductionType]++
}
// Accept both naming variants
designCount := types["design"]
protectiveCount := types["protective"] + types["protection"]
infoCount := types["information"]
if designCount == 0 {
t.Error("no measures with reduction type design")
}
if protectiveCount == 0 {
t.Error("no measures with reduction type protective/protection")
}
if infoCount == 0 {
t.Error("no measures with reduction type information")
}
}
// TestProtectiveMeasures_TagFieldAccessible verifies the Tags field is accessible.
func TestProtectiveMeasures_TagFieldAccessible(t *testing.T) {
measures := GetProtectiveMeasureLibrary()
if len(measures) == 0 {
t.Fatal("no measures returned")
}
// Tags field exists but may not be populated yet
_ = measures[0].Tags
}
// TestProtectiveMeasures_HazardCategoryNotEmpty verifies HazardCategory is populated.
func TestProtectiveMeasures_HazardCategoryNotEmpty(t *testing.T) {
for _, e := range GetProtectiveMeasureLibrary() {
if e.HazardCategory == "" {
t.Errorf("measure %s (%s): HazardCategory is empty", e.ID, e.Name)
}
}
}
// TestProtectiveMeasures_Count241 verifies at least 241 measures exist (200 base + 25 mandatory + 16 Phase1B).
func TestProtectiveMeasures_Count241(t *testing.T) {
entries := GetProtectiveMeasureLibrary()
if len(entries) < 241 {
t.Fatalf("got %d protective measures, want at least 241", len(entries))
}
}
// TestProtectiveMeasures_Count160 verifies at least 160 measures exist (legacy gate).
func TestProtectiveMeasures_Count160(t *testing.T) {
entries := GetProtectiveMeasureLibrary()
if len(entries) < 160 {
t.Fatalf("got %d protective measures, want at least 160", len(entries))
}
}
// TestProtectiveMeasures_NormReferencesPopulated verifies most measures have norm refs.
func TestProtectiveMeasures_NormReferencesPopulated(t *testing.T) {
withRefs := 0
for _, e := range GetProtectiveMeasureLibrary() {
if len(e.NormReferences) > 0 {
withRefs++
}
}
total := len(GetProtectiveMeasureLibrary())
threshold := total * 90 / 100
if withRefs < threshold {
t.Errorf("only %d/%d measures have NormReferences, want at least %d", withRefs, total, threshold)
}
}
// TestProtectiveMeasures_DesignProtectionInfoDistribution verifies balanced distribution.
func TestProtectiveMeasures_DesignProtectionInfoDistribution(t *testing.T) {
types := make(map[string]int)
for _, e := range GetProtectiveMeasureLibrary() {
types[e.ReductionType]++
}
design := types["design"]
protection := types["protection"]
information := types["information"]
if design < 50 {
t.Errorf("design measures: %d, want >= 50", design)
}
if protection < 70 {
t.Errorf("protection measures: %d, want >= 70", protection)
}
if information < 50 {
t.Errorf("information measures: %d, want >= 50", information)
}
t.Logf("Distribution: design=%d, protection=%d, information=%d", design, protection, information)
}
// TestProtectiveMeasures_UniqueIDs verifies all measure IDs are unique.
func TestProtectiveMeasures_UniqueIDs(t *testing.T) {
entries := GetProtectiveMeasureLibrary()
seen := make(map[string]bool)
for _, e := range entries {
if seen[e.ID] {
t.Errorf("duplicate measure ID: %s", e.ID)
}
seen[e.ID] = true
if e.ID == "" {
t.Error("empty measure ID found")
}
}
}
func padID(n int) string {
if n < 10 {
return "00" + itoa(n)
}
if n < 100 {
return "0" + itoa(n)
}
return itoa(n)
}
func itoa(n int) string {
if n == 0 {
return "0"
}
s := ""
for n > 0 {
s = string(rune('0'+n%10)) + s
n /= 10
}
return s
}
// TestProtectiveMeasures_SubTypesPresent verifies subtypes are used.
func TestProtectiveMeasures_SubTypesPresent(t *testing.T) {
subtypes := make(map[string]int)
for _, e := range GetProtectiveMeasureLibrary() {
if e.SubType != "" {
subtypes[e.SubType]++
}
}
if len(subtypes) < 3 {
t.Errorf("expected at least 3 different subtypes, got %d: %v", len(subtypes), subtypes)
}
}