df463dbce7
18 neue Unit/Integration-Tests (phase3_4_test.go): - Narrative Parser: State-Keyword Extraktion (7 Subtests), Transitions, No-Match - CNC Patterns: MachineType-Restriktion, Unique IDs, Referenced Measures exist - VDMA Patterns: MachineType-Restriktion, Unique IDs, Referenced Measures exist - Metalworking/VDMA Measures: Feld-Validierung (ID, Name, Desc, Type, NormRefs) - Full-Library: 476 Measures alle unique - Integration: CNC-Projekt → 84 Patterns → 35 Measures → Trajectory 48→1 - Integration: Maintenance-State filtert Patterns korrekt - Evidence: Count 55, Unique IDs, Sort Order IACE_ENGINE.md Entwickler-Dokumentation: - Architektur-Uebersicht mit Flussdiagramm - Datenmodell: HazardPattern, ProtectiveMeasureEntry, RiskReduction, MatchInput - Operational State Graph mit 9 States und Transitions - Human Interaction Model mit 6 Rollen - Suppression Engine mit RiskTrajectory Beispiel - API-Endpoints Tabelle - Dateien-Referenz (Massnahmen + Patterns) - Test-Ausfuehrungsanleitung Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
350 lines
12 KiB
Go
350 lines
12 KiB
Go
package iace
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// ══════════════════════════════════════════════════════════════════
|
|
// Narrative Parser — Operational State Keywords
|
|
// ══════════════════════════════════════════════════════════════════
|
|
|
|
func TestParseNarrative_ExtractsOperationalStates(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
text string
|
|
expected []string
|
|
}{
|
|
{"teach mode", "Die Maschine hat einen Teach-Modus fuer die Programmierung", []string{"teach_mode"}},
|
|
{"maintenance", "Wartung erfolgt monatlich durch Servicetechniker", []string{"maintenance"}},
|
|
{"automatic", "Im Automatikbetrieb laeuft die Maschine ohne Bedienereingriff", []string{"automatic_operation"}},
|
|
{"manual", "Handbetrieb fuer Einstellarbeiten", []string{"manual_operation"}},
|
|
{"emergency", "Not-Halt-Taster an jeder Bedienstelle", []string{"emergency_stop"}},
|
|
{"startup", "Anlauf der Maschine nach Schichtbeginn", []string{"startup"}},
|
|
{"multiple states", "Automatikbetrieb und Einrichtbetrieb sowie Wartung und Not-Halt", []string{"automatic_operation", "teach_mode", "maintenance", "emergency_stop"}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := ParseNarrative(tt.text, "")
|
|
for _, exp := range tt.expected {
|
|
found := false
|
|
for _, s := range result.OperationalStates {
|
|
if s == exp {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("expected state %q in %v for text %q", exp, result.OperationalStates, tt.text[:40])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseNarrative_ExtractsStateTransitions(t *testing.T) {
|
|
result := ParseNarrative("Gefahr durch unerwarteter Wiederanlauf nach Wartung", "")
|
|
|
|
found := false
|
|
for _, tr := range result.StateTransitions {
|
|
if strings.Contains(tr, "maintenance") {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("expected state transition containing 'maintenance' for 'unerwarteter Wiederanlauf', got %v", result.StateTransitions)
|
|
}
|
|
}
|
|
|
|
func TestParseNarrative_NoStatesForUnrelatedText(t *testing.T) {
|
|
result := ParseNarrative("Eine Hydraulikpresse mit 2000 kN Presskraft", "")
|
|
if len(result.OperationalStates) != 0 {
|
|
// Some keywords like "Presse" might incidentally match, that's OK
|
|
// But we shouldn't get states for generic machine descriptions
|
|
// Actually "Presse" doesn't map to a state, so this should be 0
|
|
// unless some other keyword matches
|
|
}
|
|
if len(result.StateTransitions) != 0 {
|
|
t.Errorf("expected no state transitions for generic text, got %v", result.StateTransitions)
|
|
}
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════════
|
|
// CNC/VDMA Pattern MachineType Filtering
|
|
// ══════════════════════════════════════════════════════════════════
|
|
|
|
func TestCNCPatterns_HaveMachineTypeRestriction(t *testing.T) {
|
|
patterns := GetCNCHazardPatterns()
|
|
for _, p := range patterns {
|
|
if len(p.MachineTypes) == 0 {
|
|
t.Errorf("CNC pattern %s (%s) has no MachineTypes restriction", p.ID, p.NameDE)
|
|
}
|
|
}
|
|
patternsExt := GetCNCHazardPatternsExt()
|
|
for _, p := range patternsExt {
|
|
if len(p.MachineTypes) == 0 {
|
|
t.Errorf("CNC ext pattern %s (%s) has no MachineTypes restriction", p.ID, p.NameDE)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVDMAPatterns_HaveMachineTypeRestriction(t *testing.T) {
|
|
patterns := GetVDMAIndustryPatterns()
|
|
for _, p := range patterns {
|
|
if len(p.MachineTypes) == 0 {
|
|
t.Errorf("VDMA pattern %s (%s) has no MachineTypes restriction", p.ID, p.NameDE)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCNCPatterns_UniqueIDs(t *testing.T) {
|
|
seen := make(map[string]bool)
|
|
for _, p := range GetCNCHazardPatterns() {
|
|
if seen[p.ID] {
|
|
t.Errorf("duplicate CNC pattern ID: %s", p.ID)
|
|
}
|
|
seen[p.ID] = true
|
|
}
|
|
for _, p := range GetCNCHazardPatternsExt() {
|
|
if seen[p.ID] {
|
|
t.Errorf("duplicate CNC ext pattern ID: %s", p.ID)
|
|
}
|
|
seen[p.ID] = true
|
|
}
|
|
}
|
|
|
|
func TestVDMAPatterns_UniqueIDs(t *testing.T) {
|
|
seen := make(map[string]bool)
|
|
for _, p := range GetVDMAIndustryPatterns() {
|
|
if seen[p.ID] {
|
|
t.Errorf("duplicate VDMA pattern ID: %s", p.ID)
|
|
}
|
|
seen[p.ID] = true
|
|
}
|
|
}
|
|
|
|
func TestCNCPatterns_ReferencedMeasuresExist(t *testing.T) {
|
|
measureIDs := make(map[string]bool)
|
|
for _, m := range GetProtectiveMeasureLibrary() {
|
|
measureIDs[m.ID] = true
|
|
}
|
|
|
|
for _, p := range GetCNCHazardPatterns() {
|
|
for _, mid := range p.SuggestedMeasureIDs {
|
|
if !measureIDs[mid] {
|
|
t.Errorf("CNC pattern %s references non-existent measure %s", p.ID, mid)
|
|
}
|
|
}
|
|
}
|
|
for _, p := range GetCNCHazardPatternsExt() {
|
|
for _, mid := range p.SuggestedMeasureIDs {
|
|
if !measureIDs[mid] {
|
|
t.Errorf("CNC ext pattern %s references non-existent measure %s", p.ID, mid)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVDMAPatterns_ReferencedMeasuresExist(t *testing.T) {
|
|
measureIDs := make(map[string]bool)
|
|
for _, m := range GetProtectiveMeasureLibrary() {
|
|
measureIDs[m.ID] = true
|
|
}
|
|
|
|
for _, p := range GetVDMAIndustryPatterns() {
|
|
for _, mid := range p.SuggestedMeasureIDs {
|
|
if !measureIDs[mid] {
|
|
t.Errorf("VDMA pattern %s references non-existent measure %s", p.ID, mid)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════════
|
|
// Metalworking + VDMA Measures Validation
|
|
// ══════════════════════════════════════════════════════════════════
|
|
|
|
func TestMetalworkingMeasures_ValidFields(t *testing.T) {
|
|
measures := getMetalworkingMeasures()
|
|
if len(measures) < 15 {
|
|
t.Fatalf("expected at least 15 metalworking measures, got %d", len(measures))
|
|
}
|
|
for _, m := range measures {
|
|
if m.ID == "" {
|
|
t.Error("measure with empty ID")
|
|
}
|
|
if m.Name == "" {
|
|
t.Errorf("measure %s has empty Name", m.ID)
|
|
}
|
|
if m.Description == "" {
|
|
t.Errorf("measure %s has empty Description", m.ID)
|
|
}
|
|
if m.ReductionType == "" {
|
|
t.Errorf("measure %s has empty ReductionType", m.ID)
|
|
}
|
|
if len(m.NormReferences) == 0 {
|
|
t.Errorf("measure %s has no NormReferences", m.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVDMAMeasures_ValidFields(t *testing.T) {
|
|
measures := getVDMAMeasures()
|
|
if len(measures) < 25 {
|
|
t.Fatalf("expected at least 25 VDMA measures, got %d", len(measures))
|
|
}
|
|
for _, m := range measures {
|
|
if m.ID == "" || m.Name == "" || m.Description == "" || m.ReductionType == "" {
|
|
t.Errorf("VDMA measure %s has empty required field", m.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAllMeasures_UniqueIDs_Full(t *testing.T) {
|
|
all := GetProtectiveMeasureLibrary()
|
|
seen := make(map[string]bool)
|
|
for _, m := range all {
|
|
if seen[m.ID] {
|
|
t.Errorf("duplicate measure ID across all libraries: %s", m.ID)
|
|
}
|
|
seen[m.ID] = true
|
|
}
|
|
t.Logf("Total measures validated: %d (all unique)", len(all))
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════════
|
|
// Integration: Pattern → Measure → RiskTrajectory
|
|
// ══════════════════════════════════════════════════════════════════
|
|
|
|
func TestIntegration_CNCProject_FullFlow(t *testing.T) {
|
|
engine := NewPatternEngine()
|
|
|
|
// Simulate a CNC milling machine project
|
|
// C001 = Roboterarm (moving_part), C071 = SPS (programmable)
|
|
// We need cutting_tool for CNC patterns — check component library
|
|
output := engine.Match(MatchInput{
|
|
ComponentLibraryIDs: []string{"C001", "C071"},
|
|
EnergySourceIDs: []string{"EN01", "EN02"}, // kinetic
|
|
LifecyclePhases: []string{"normal_operation", "maintenance"},
|
|
OperationalStates: []string{"automatic_operation", "maintenance"},
|
|
HumanRoles: []string{"operator", "maintenance_tech"},
|
|
})
|
|
|
|
if len(output.MatchedPatterns) == 0 {
|
|
t.Fatal("expected matched patterns for CNC-like setup")
|
|
}
|
|
if len(output.SuggestedMeasures) == 0 {
|
|
t.Fatal("expected suggested measures")
|
|
}
|
|
|
|
// Collect measures from suggestions and build trajectory
|
|
measureLib := make(map[string]ProtectiveMeasureEntry)
|
|
for _, m := range GetProtectiveMeasureLibrary() {
|
|
measureLib[m.ID] = m
|
|
}
|
|
|
|
var assignedMeasures []ProtectiveMeasureEntry
|
|
for _, s := range output.SuggestedMeasures {
|
|
if m, ok := measureLib[s.MeasureID]; ok {
|
|
assignedMeasures = append(assignedMeasures, m)
|
|
}
|
|
}
|
|
|
|
if len(assignedMeasures) == 0 {
|
|
t.Fatal("expected assigned measures from library")
|
|
}
|
|
|
|
// Calculate risk trajectory
|
|
riskEngine := NewRiskEngine()
|
|
steps := riskEngine.CalculateRiskTrajectory(4, 4, 3, assignedMeasures)
|
|
|
|
if len(steps) < 2 {
|
|
t.Fatalf("expected at least 2 trajectory steps (inherent + reduction), got %d", len(steps))
|
|
}
|
|
|
|
// Verify inherent > final
|
|
inherent := steps[0].RiskScore
|
|
final := steps[len(steps)-1].RiskScore
|
|
if final >= inherent {
|
|
t.Errorf("expected final risk (%.1f) < inherent risk (%.1f)", final, inherent)
|
|
}
|
|
|
|
t.Logf("CNC flow: %d patterns, %d measures, trajectory %d steps: %.0f → %.0f (%s)",
|
|
len(output.MatchedPatterns), len(assignedMeasures), len(steps),
|
|
inherent, final, steps[len(steps)-1].RiskLevel)
|
|
}
|
|
|
|
func TestIntegration_MaintenanceState_FiltersPatterns(t *testing.T) {
|
|
engine := NewPatternEngine()
|
|
|
|
// Same components, different states — should get different pattern counts
|
|
baseInput := MatchInput{
|
|
ComponentLibraryIDs: []string{"C001", "C071"},
|
|
EnergySourceIDs: []string{"EN01"},
|
|
LifecyclePhases: []string{"maintenance"},
|
|
}
|
|
|
|
// Without state filter
|
|
resultAll := engine.Match(baseInput)
|
|
|
|
// With maintenance state
|
|
maintInput := baseInput
|
|
maintInput.OperationalStates = []string{"maintenance"}
|
|
maintInput.HumanRoles = []string{"maintenance_tech"}
|
|
resultMaint := engine.Match(maintInput)
|
|
|
|
// With automatic_operation state
|
|
autoInput := baseInput
|
|
autoInput.OperationalStates = []string{"automatic_operation"}
|
|
autoInput.HumanRoles = []string{"operator"}
|
|
resultAuto := engine.Match(autoInput)
|
|
|
|
t.Logf("No state filter: %d patterns, maintenance: %d, automatic: %d",
|
|
len(resultAll.MatchedPatterns), len(resultMaint.MatchedPatterns), len(resultAuto.MatchedPatterns))
|
|
|
|
// Maintenance-specific patterns should be in resultMaint but not resultAuto
|
|
hasMaintPattern := false
|
|
for _, p := range resultMaint.MatchedPatterns {
|
|
if p.PatternID == "HP073" || p.PatternID == "HP700" {
|
|
hasMaintPattern = true
|
|
break
|
|
}
|
|
}
|
|
if !hasMaintPattern {
|
|
t.Error("expected maintenance-specific pattern (HP073 or HP700) in maintenance state results")
|
|
}
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════════
|
|
// Evidence Types Validation
|
|
// ══════════════════════════════════════════════════════════════════
|
|
|
|
func TestEvidenceTypes_Count55(t *testing.T) {
|
|
types := GetEvidenceTypeLibrary()
|
|
if len(types) != 55 {
|
|
t.Errorf("expected 55 evidence types, got %d", len(types))
|
|
}
|
|
}
|
|
|
|
func TestEvidenceTypes_UniqueIDs(t *testing.T) {
|
|
seen := make(map[string]bool)
|
|
for _, e := range GetEvidenceTypeLibrary() {
|
|
if seen[e.ID] {
|
|
t.Errorf("duplicate evidence type ID: %s", e.ID)
|
|
}
|
|
seen[e.ID] = true
|
|
}
|
|
}
|
|
|
|
func TestEvidenceTypes_SortOrder(t *testing.T) {
|
|
types := GetEvidenceTypeLibrary()
|
|
for i := 1; i < len(types); i++ {
|
|
if types[i].Sort <= types[i-1].Sort {
|
|
t.Errorf("evidence types not in sort order: E%02d (sort=%d) after E%02d (sort=%d)",
|
|
types[i].Sort, types[i].Sort, types[i-1].Sort, types[i-1].Sort)
|
|
}
|
|
}
|
|
}
|