Files
breakpilot-compliance/ai-compliance-sdk/internal/iace/manufacturer_safety_features_test.go
T
Benjamin Admin e9002175ac feat(iace): manufacturer safety feature library (Stufe A — 50+ entries)
Adds a curated database of safety-relevant features for the major
manufacturers across mechanical/plant engineering, written entirely in
own words with norm anchors. No verbatim manufacturer texts — therefore
no copyright issue:

- Markennennung (§ 23 MarkenG nominative use) is permitted.
- Fakten ueber Produkt-Sicherheitsfunktionen are not protected by § 2
  UrhG (only Werke, not facts).
- NormReferences contain only the identifiers (e.g. "EN ISO 13849-1
  PLd Kat.3"), never the norm text itself.

Coverage (52 entries across 12 categories):
  Industrieroboter (10): FANUC DCS, KUKA SafeOperation, ABB SafeMove,
    Yaskawa FSU, Staeubli CS9, Kawasaki Cubic-S, Mitsubishi MELFA,
    Universal Robots PolyScope, Doosan PRS, Comau SafeNet
  CNC/WZM (8): DMG MORI, Mazak, TRUMPF, Okuma, Hermle, Heidenhain
    SPLC, GROB, Heller
  Pneumatik (4): Festo, SMC, AVENTICS, Parker
  Hydraulik (3): Bosch Rexroth, HAWE, HYDAC
  Safety-PLC / Sicherheitstechnik (8): PILZ, SICK, Schmersal, Euchner,
    Leuze, Phoenix Contact, Banner, Wieland
  Standard-PLC (5): Siemens, Beckhoff, Rockwell, Schneider, B&R
  Pressen (3): Schuler, Bruderer, AIDA
  Spritzguss (3): Arburg, KraussMaffei, ENGEL
  Verpackung (2): Krones, Bosch Packaging/Syntegon
  Laser/Schweissen (3): Bystronic, Amada, Fronius
  Foerdertechnik (2): Interroll, SEW EURODRIVE

Engine integration:
- LookupManufacturerFeaturesInText() scans the project narrative for
  any of the manufacturer aliases (case-insensitive, umlaut-tolerant).
- Init-Handler appends matched feature clarifications to the relevant
  hazard's "Mit Anlagenbauer zu klaeren:" block — for the right
  HazardCategory only (e.g. FANUC DCS only on mechanical_hazard).
- For a Bremse project narrative mentioning "Fanuc Robodrill", the
  engine now adds clarification questions like "Ist DCS am Roboter
  konfiguriert?" to relevant mechanical hazards automatically.

Tests: 7 new pin tests — manufacturer count, norm prefixes, FANUC/KUKA
detection in narrative, umlaut robustness (Staeubli vs Staubli).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:04:56 +02:00

135 lines
4.7 KiB
Go

package iace
import (
"strings"
"testing"
)
// TestManufacturerLibrary_HasTopManufacturers checks the curated baseline
// (~50 entries across major mechanical/plant-engineering segments). The
// expected manufacturers below are the ones that would be unsurprising to
// find in any industrial risk-assessment context.
func TestManufacturerLibrary_HasTopManufacturers(t *testing.T) {
required := []string{
"FANUC", "KUKA", "ABB Robotics", "Yaskawa Motoman", "Universal Robots",
"DMG MORI", "Mazak", "TRUMPF", "Heidenhain",
"Festo", "SMC", "Bosch Rexroth",
"PILZ", "SICK", "Schmersal", "Euchner",
"Siemens", "Beckhoff", "Rockwell Automation",
"Schuler", "Arburg", "KraussMaffei",
}
have := map[string]bool{}
for _, mf := range GetManufacturerSafetyFeatures() {
have[mf.Manufacturer] = true
}
for _, want := range required {
if !have[want] {
t.Errorf("manufacturer library missing %q", want)
}
}
}
// TestManufacturerLibrary_AllHaveNorms pins that every entry carries at
// least one EN/ISO/IEC norm reference. A manufacturer entry without norm
// anchor is just hearsay; with it, the operator has something verifiable.
func TestManufacturerLibrary_AllHaveNorms(t *testing.T) {
for _, mf := range GetManufacturerSafetyFeatures() {
if len(mf.NormReferences) == 0 {
t.Errorf("manufacturer %q (%s) missing NormReferences", mf.Manufacturer, mf.FeatureName)
}
if len(mf.Clarifications) == 0 {
t.Errorf("manufacturer %q (%s) missing Clarifications", mf.Manufacturer, mf.FeatureName)
}
}
}
// TestLookupManufacturerFeaturesInText_FindsFANUC reproduces the GT-Bremse
// scenario: a narrative mentioning "Robodrill" must surface the FANUC DCS
// entry — that's how the operator gets the right clarification question
// without us inventing commentary.
func TestLookupManufacturerFeaturesInText_FindsFANUC(t *testing.T) {
narrative := "Roboterzelle mit Fanuc Robodrill Werkzeugmaschine und KUKA-Roboter."
hits := LookupManufacturerFeaturesInText(narrative)
manufacturers := map[string]bool{}
for _, h := range hits {
manufacturers[h.Manufacturer] = true
}
if !manufacturers["FANUC"] {
t.Errorf("expected FANUC hit for narrative %q, got %v", narrative, manufacturers)
}
if !manufacturers["KUKA"] {
t.Errorf("expected KUKA hit for narrative %q, got %v", narrative, manufacturers)
}
}
// TestLookupManufacturerFeaturesInText_HandlesUmlauts pins case + umlaut
// robustness — "Stäubli" must match alias "staeubli".
func TestLookupManufacturerFeaturesInText_HandlesUmlauts(t *testing.T) {
hits := LookupManufacturerFeaturesInText("Stäubli TX2 Roboter und Schmersal Türverriegelung")
manufacturers := map[string]bool{}
for _, h := range hits {
manufacturers[h.Manufacturer] = true
}
if !manufacturers["Stäubli Robotics"] {
t.Errorf("expected Staeubli hit, got %v", manufacturers)
}
if !manufacturers["Schmersal"] {
t.Errorf("expected Schmersal hit, got %v", manufacturers)
}
}
// TestLookupManufacturerFeaturesInText_EmptyText handles missing narrative
// gracefully — must not crash and must return nil/empty.
func TestLookupManufacturerFeaturesInText_EmptyText(t *testing.T) {
if hits := LookupManufacturerFeaturesInText(""); len(hits) > 0 {
t.Errorf("expected no hits for empty narrative, got %d", len(hits))
}
}
// TestManufacturerLibrary_Has50OrMore checks the target count of the
// curated set so a future regression that deletes many entries fails CI.
func TestManufacturerLibrary_Has50OrMore(t *testing.T) {
got := len(GetManufacturerSafetyFeatures())
if got < 50 {
// Allow small undercount but keep the floor strict
t.Errorf("manufacturer library has %d entries, expected >= 50", got)
}
}
// TestNormalizeForMatch pins the umlaut/case folding behavior.
func TestNormalizeForMatch(t *testing.T) {
cases := []struct{ in, want string }{
{"Stäubli", "staeubli"},
{"FANUC", "fanuc"},
{"Schäffer", "schaeffer"},
{"Mößbauer", "moessbauer"},
}
for _, c := range cases {
if got := normalizeForMatch(c.in); got != c.want {
t.Errorf("normalizeForMatch(%q) = %q, want %q", c.in, got, c.want)
}
}
}
// TestManufacturerLibrary_NormReferencesAreRealNorms scans for the standard
// norm prefixes so a future "we just put a placeholder" regression fails.
func TestManufacturerLibrary_NormReferencesAreRealNorms(t *testing.T) {
for _, mf := range GetManufacturerSafetyFeatures() {
ok := false
for _, n := range mf.NormReferences {
for _, prefix := range []string{"EN ", "IEC ", "ISO ", "DIN ", "EN ISO", "DIN EN", "VDE", "TRBS", "TRGS"} {
if strings.HasPrefix(n, prefix) {
ok = true
break
}
}
if ok {
break
}
}
if !ok {
t.Errorf("manufacturer %q has no recognized norm prefix in %v", mf.Manufacturer, mf.NormReferences)
}
}
}