All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 34s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 23s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 2s
Phase 5 — Frontend Integration: - components/page.tsx: ComponentLibraryModal with 120 components + 20 energy sources - hazards/page.tsx: AutoSuggestPanel with 3-column pattern matching review - mitigations/page.tsx: SuggestMeasuresModal per hazard with 3-level grouping - verification/page.tsx: SuggestEvidenceModal per mitigation with evidence types Phase 6 — RAG Library Search: - Added bp_iace_libraries to AllowedCollections whitelist in rag_handlers.go - SearchLibrary endpoint: POST /iace/library-search (semantic search across libraries) - EnrichTechFileSection endpoint: POST /projects/:id/tech-file/:section/enrich - Created ingest-iace-libraries.sh ingestion script for Qdrant collection Tests (123 passing): - tag_taxonomy_test.go: 8 tests for taxonomy entries, domains, essential tags - controls_library_test.go: 7 tests for measures, reduction types, subtypes - integration_test.go: 7 integration tests for full match flow and library consistency - Extended tag_resolver_test.go: 9 new tests for FindByTags and cross-category resolution Documentation: - Updated iace.md with Hazard-Matching-Engine, RAG enrichment, and new DB tables Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
258 lines
8.0 KiB
Go
258 lines
8.0 KiB
Go
package iace
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
// TestIntegration_FullMatchFlow tests the complete pattern matching flow:
|
|
// components → tags → patterns → hazards/measures/evidence
|
|
func TestIntegration_FullMatchFlow(t *testing.T) {
|
|
engine := NewPatternEngine()
|
|
|
|
// Simulate a robot arm with electrical components and kinetic energy
|
|
input := MatchInput{
|
|
ComponentLibraryIDs: []string{"C001", "C061", "C071"}, // Roboterarm, Schaltschrank, SPS
|
|
EnergySourceIDs: []string{"EN01", "EN04"}, // Kinetic, Electrical
|
|
LifecyclePhases: []string{},
|
|
CustomTags: []string{},
|
|
}
|
|
|
|
output := engine.Match(input)
|
|
|
|
// Should have matched patterns
|
|
if len(output.MatchedPatterns) == 0 {
|
|
t.Fatal("expected matched patterns for robot arm + electrical + SPS setup, got none")
|
|
}
|
|
|
|
// Should have suggested hazards
|
|
if len(output.SuggestedHazards) == 0 {
|
|
t.Fatal("expected suggested hazards, got none")
|
|
}
|
|
|
|
// Should have suggested measures
|
|
if len(output.SuggestedMeasures) == 0 {
|
|
t.Fatal("expected suggested measures, got none")
|
|
}
|
|
|
|
// Should have suggested evidence
|
|
if len(output.SuggestedEvidence) == 0 {
|
|
t.Fatal("expected suggested evidence, got none")
|
|
}
|
|
|
|
// Should have resolved tags
|
|
if len(output.ResolvedTags) == 0 {
|
|
t.Fatal("expected resolved tags, got none")
|
|
}
|
|
|
|
// Verify mechanical hazards are present (robot arm has moving_part, rotating_part)
|
|
hasMechanical := false
|
|
for _, h := range output.SuggestedHazards {
|
|
if h.Category == "mechanical" || h.Category == "mechanical_hazard" {
|
|
hasMechanical = true
|
|
break
|
|
}
|
|
}
|
|
if !hasMechanical {
|
|
cats := make(map[string]bool)
|
|
for _, h := range output.SuggestedHazards {
|
|
cats[h.Category] = true
|
|
}
|
|
t.Errorf("expected mechanical hazards for robot arm, got categories: %v", cats)
|
|
}
|
|
|
|
// Verify electrical hazards are present (Schaltschrank has high_voltage)
|
|
hasElectrical := false
|
|
for _, h := range output.SuggestedHazards {
|
|
if h.Category == "electrical" || h.Category == "electrical_hazard" {
|
|
hasElectrical = true
|
|
break
|
|
}
|
|
}
|
|
if !hasElectrical {
|
|
cats := make(map[string]bool)
|
|
for _, h := range output.SuggestedHazards {
|
|
cats[h.Category] = true
|
|
}
|
|
t.Errorf("expected electrical hazards for Schaltschrank, got categories: %v", cats)
|
|
}
|
|
}
|
|
|
|
// TestIntegration_TagResolverToPatternEngine verifies the tag resolver output
|
|
// feeds correctly into the pattern engine.
|
|
func TestIntegration_TagResolverToPatternEngine(t *testing.T) {
|
|
resolver := NewTagResolver()
|
|
engine := NewPatternEngine()
|
|
|
|
// Resolve tags for a hydraulic setup
|
|
componentTags := resolver.ResolveComponentTags([]string{"C041"}) // Hydraulikpumpe
|
|
energyTags := resolver.ResolveEnergyTags([]string{"EN05"}) // Hydraulische Energie
|
|
|
|
allTags := resolver.ResolveTags([]string{"C041"}, []string{"EN05"}, nil)
|
|
|
|
// All tags should be non-empty
|
|
if len(componentTags) == 0 {
|
|
t.Error("expected component tags for C041")
|
|
}
|
|
if len(energyTags) == 0 {
|
|
t.Error("expected energy tags for EN05")
|
|
}
|
|
|
|
// Merged tags should include both
|
|
tagSet := toSet(allTags)
|
|
if !tagSet["hydraulic_part"] {
|
|
t.Error("expected 'hydraulic_part' in merged tags")
|
|
}
|
|
if !tagSet["hydraulic_pressure"] {
|
|
t.Error("expected 'hydraulic_pressure' in merged tags")
|
|
}
|
|
|
|
// Feed into pattern engine
|
|
output := engine.Match(MatchInput{
|
|
ComponentLibraryIDs: []string{"C041"},
|
|
EnergySourceIDs: []string{"EN05"},
|
|
})
|
|
|
|
if len(output.MatchedPatterns) == 0 {
|
|
t.Error("expected patterns to match for hydraulic setup")
|
|
}
|
|
}
|
|
|
|
// TestIntegration_AllComponentCategoriesProduceMatches verifies that every
|
|
// component category, when paired with its typical energy source, produces
|
|
// at least one pattern match.
|
|
func TestIntegration_AllComponentCategoriesProduceMatches(t *testing.T) {
|
|
engine := NewPatternEngine()
|
|
|
|
tests := []struct {
|
|
name string
|
|
componentIDs []string
|
|
energyIDs []string
|
|
}{
|
|
{"mechanical", []string{"C001"}, []string{"EN01"}}, // Roboterarm + Kinetic
|
|
{"drive", []string{"C031"}, []string{"EN02"}}, // Elektromotor + Rotational
|
|
{"hydraulic", []string{"C041"}, []string{"EN05"}}, // Hydraulikpumpe + Hydraulic
|
|
{"pneumatic", []string{"C051"}, []string{"EN06"}}, // Pneumatikzylinder + Pneumatic
|
|
{"electrical", []string{"C061"}, []string{"EN04"}}, // Schaltschrank + Electrical
|
|
{"control", []string{"C071"}, []string{"EN04"}}, // SPS + Electrical
|
|
{"safety", []string{"C101"}, []string{"EN04"}}, // Not-Halt + Electrical
|
|
{"it_network", []string{"C111"}, []string{"EN04", "EN19"}}, // Switch + Electrical + Data
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
output := engine.Match(MatchInput{
|
|
ComponentLibraryIDs: tt.componentIDs,
|
|
EnergySourceIDs: tt.energyIDs,
|
|
})
|
|
if len(output.MatchedPatterns) == 0 {
|
|
t.Errorf("category %s: expected at least one pattern match, got none (resolved tags: %v)",
|
|
tt.name, output.ResolvedTags)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIntegration_PatternsSuggestHazardCategories verifies that pattern-suggested
|
|
// hazard categories cover the main safety domains.
|
|
func TestIntegration_PatternsSuggestHazardCategories(t *testing.T) {
|
|
engine := NewPatternEngine()
|
|
|
|
// Full industrial setup: robot arm + electrical panel + PLC + network
|
|
output := engine.Match(MatchInput{
|
|
ComponentLibraryIDs: []string{"C001", "C061", "C071", "C111"},
|
|
EnergySourceIDs: []string{"EN01", "EN04"},
|
|
})
|
|
|
|
categories := make(map[string]bool)
|
|
for _, h := range output.SuggestedHazards {
|
|
categories[h.Category] = true
|
|
}
|
|
|
|
// Should cover mechanical and electrical hazards (naming may use _hazard suffix)
|
|
hasMech := categories["mechanical"] || categories["mechanical_hazard"]
|
|
hasElec := categories["electrical"] || categories["electrical_hazard"]
|
|
if !hasMech {
|
|
t.Errorf("expected mechanical hazard category in suggestions, got: %v", categories)
|
|
}
|
|
if !hasElec {
|
|
t.Errorf("expected electrical hazard category in suggestions, got: %v", categories)
|
|
}
|
|
}
|
|
|
|
// TestIntegration_EvidenceSuggestionsPerReductionType tests that evidence
|
|
// can be found for each reduction type.
|
|
func TestIntegration_EvidenceSuggestionsPerReductionType(t *testing.T) {
|
|
resolver := NewTagResolver()
|
|
|
|
tests := []struct {
|
|
reductionType string
|
|
evidenceTags []string
|
|
}{
|
|
{"design", []string{"design_evidence", "analysis_evidence"}},
|
|
{"protective", []string{"test_evidence", "inspection_evidence"}},
|
|
{"information", []string{"training_evidence", "operational_evidence"}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.reductionType, func(t *testing.T) {
|
|
evidence := resolver.FindEvidenceByTags(tt.evidenceTags)
|
|
if len(evidence) == 0 {
|
|
t.Errorf("no evidence found for %s reduction type (tags: %v)", tt.reductionType, tt.evidenceTags)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIntegration_LibraryConsistency verifies components and energy sources have tags.
|
|
func TestIntegration_LibraryConsistency(t *testing.T) {
|
|
components := GetComponentLibrary()
|
|
energySources := GetEnergySources()
|
|
taxonomy := GetTagTaxonomy()
|
|
|
|
// Taxonomy should be populated
|
|
if len(taxonomy) == 0 {
|
|
t.Fatal("tag taxonomy is empty")
|
|
}
|
|
|
|
// All components should have at least one tag
|
|
for _, comp := range components {
|
|
if len(comp.Tags) == 0 {
|
|
t.Errorf("component %s has no tags", comp.ID)
|
|
}
|
|
}
|
|
|
|
// All energy sources should have at least one tag
|
|
for _, es := range energySources {
|
|
if len(es.Tags) == 0 {
|
|
t.Errorf("energy source %s has no tags", es.ID)
|
|
}
|
|
}
|
|
|
|
// Component tags should mostly exist in taxonomy (allow some flexibility)
|
|
taxonomyIDs := toSet(func() []string {
|
|
ids := make([]string, len(taxonomy))
|
|
for i, tag := range taxonomy {
|
|
ids[i] = tag.ID
|
|
}
|
|
return ids
|
|
}())
|
|
|
|
missingCount := 0
|
|
totalTags := 0
|
|
for _, comp := range components {
|
|
for _, tag := range comp.Tags {
|
|
totalTags++
|
|
if !taxonomyIDs[tag] {
|
|
missingCount++
|
|
}
|
|
}
|
|
}
|
|
// At least 90% of component tags should be in taxonomy
|
|
if totalTags > 0 {
|
|
coverage := float64(totalTags-missingCount) / float64(totalTags) * 100
|
|
if coverage < 90 {
|
|
t.Errorf("only %.0f%% of component tags exist in taxonomy (%d/%d)", coverage, totalTags-missingCount, totalTags)
|
|
}
|
|
}
|
|
}
|