package iace import "testing" func mkPM(id, cat, zone, scenario string, prio int, measures, opstates []string) PatternMatch { return PatternMatch{ PatternID: id, PatternName: id, Priority: prio, HazardCats: []string{cat}, ZoneDE: zone, ScenarioDE: scenario, SuggestedMeasureIDs: measures, OperationalStates: opstates, } } func TestFindDedupCandidates_FindsOverlappingPair(t *testing.T) { fired := []PatternMatch{ mkPM("HPa", "update_failure", "Steuerung, SPS", "Software-Update der Steuerung scheitert nach Abbruch", 80, []string{"M138", "M146"}, nil), mkPM("HPb", "update_failure", "Steuerung, Antriebsregler", "Software-Update der Steuerung schlaegt fehl", 75, []string{"M138", "M146", "M141"}, nil), mkPM("HPc", "mechanical_hazard", "Tuer", "Quetschen der Finger an der Tuer", 70, []string{"M003"}, nil), } got := FindDedupCandidates(fired, 0.4) if len(got) != 1 { t.Fatalf("want 1 candidate, got %d: %+v", len(got), got) } // Higher-priority pattern survives, lower one is the drop target. if got[0].KeepPattern != "HPa" || got[0].DropPattern != "HPb" { t.Errorf("want keep HPa / drop HPb, got keep %s / drop %s", got[0].KeepPattern, got[0].DropPattern) } if got[0].DropName != "Software-Update der Steuerung schlaegt fehl" { t.Errorf("DropName must equal drop pattern ScenarioDE, got %q", got[0].DropName) } } func TestFindDedupCandidates_LifecycleGuard(t *testing.T) { // Same category, zone and measures — but normal-operation vs maintenance. // These are legitimate variants (HP011 vs HP077) and must NOT be proposed. fired := []PatternMatch{ mkPM("HP011", "electrical_hazard", "Schaltschrank, Klemmenkasten", "Person beruehrt spannungsfuehrende Teile", 95, []string{"M481", "M482"}, nil), mkPM("HP077", "electrical_hazard", "Schaltschrank, Klemmenkasten", "Person beruehrt spannungsfuehrende Teile", 80, []string{"M481", "M482"}, []string{"maintenance"}), } if got := FindDedupCandidates(fired, 0.4); len(got) != 0 { t.Fatalf("lifecycle guard failed: want 0 candidates, got %d: %+v", len(got), got) } } func TestFindDedupCandidates_DifferentCategoryIgnored(t *testing.T) { fired := []PatternMatch{ mkPM("HPa", "thermal_hazard", "Boiler", "Heisse Oberflaeche am Boiler", 80, []string{"M071"}, nil), mkPM("HPb", "mechanical_hazard", "Boiler", "Heisse Oberflaeche am Boiler", 80, []string{"M071"}, nil), } if got := FindDedupCandidates(fired, 0.3); len(got) != 0 { t.Fatalf("cross-category pair must not be proposed, got %d", len(got)) } } func TestFindDedupCandidates_BelowThresholdDropped(t *testing.T) { fired := []PatternMatch{ mkPM("HPa", "mechanical_hazard", "Tuer", "Quetschen an der Tuer", 80, []string{"M003"}, nil), mkPM("HPb", "mechanical_hazard", "Foerderband", "Einzug am Foerderband", 80, []string{"M540"}, nil), } if got := FindDedupCandidates(fired, 0.4); len(got) != 0 { t.Fatalf("disjoint pair must be below threshold, got %d: %+v", len(got), got) } }