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 } } // ══════════════════════════════════════════════════════════════════ // Sprint 4B: HazardType / ISO 12100 Trennung // ══════════════════════════════════════════════════════════════════ func TestDeriveHazardType_WithScenarioAndHarm(t *testing.T) { h := &Hazard{Scenario: "Person im Bewegungsraum", PossibleHarm: "Quetschung"} if got := DeriveHazardType(h); got != HazardTypeHazardousSituation { t.Errorf("expected hazardous_situation, got %s", got) } } func TestDeriveHazardType_HarmOnly(t *testing.T) { h := &Hazard{PossibleHarm: "Quetschung", Category: "mechanical"} if got := DeriveHazardType(h); got != HazardTypeHarm { t.Errorf("expected harm, got %s", got) } } func TestDeriveHazardType_CategoryOnly(t *testing.T) { h := &Hazard{Category: "mechanical_hazard"} if got := DeriveHazardType(h); got != HazardTypeHazard { t.Errorf("expected hazard, got %s", got) } } func TestDeriveHazardType_ExplicitOverride(t *testing.T) { h := &Hazard{HazardType: "harm", Scenario: "ignored", PossibleHarm: "ignored"} if got := DeriveHazardType(h); got != "harm" { t.Errorf("expected explicit harm, got %s", got) } } func TestDeriveHazardType_EmptyFallback(t *testing.T) { h := &Hazard{} if got := DeriveHazardType(h); got != DefaultHazardType { t.Errorf("expected default %s, got %s", DefaultHazardType, got) } } func TestPatternMatch_GeneratedHazardType(t *testing.T) { // Verify PatternMatch carries GeneratedHazardType engine := NewPatternEngine() result := engine.Match(MatchInput{ ComponentLibraryIDs: []string{"C001"}, EnergySourceIDs: []string{"EN01"}, }) // Most patterns don't set GeneratedHazardType (empty = default) // Just verify the field exists and doesn't crash for _, p := range result.MatchedPatterns { _ = p.GeneratedHazardType // Should compile and not panic } if len(result.MatchedPatterns) == 0 { t.Fatal("expected matched patterns") } } 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) } } }