package ucca import ( "os" "path/filepath" "testing" ) // ============================================================================ // BetrVG Conflict Score Tests // ============================================================================ func TestCalculateBetrvgConflictScore_NoEmployeeData(t *testing.T) { root := getProjectRoot(t) policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml") engine, err := NewPolicyEngineFromPath(policyPath) if err != nil { t.Fatalf("Failed to create policy engine: %v", err) } intake := &UseCaseIntake{ UseCaseText: "Chatbot fuer Kunden-FAQ", Domain: DomainUtilities, DataTypes: DataTypes{ PersonalData: false, PublicData: true, }, } result := engine.Evaluate(intake) if result.BetrvgConflictScore != 0 { t.Errorf("Expected BetrvgConflictScore 0 for non-employee case, got %d", result.BetrvgConflictScore) } if result.BetrvgConsultationRequired { t.Error("Expected BetrvgConsultationRequired=false for non-employee case") } } func TestCalculateBetrvgConflictScore_EmployeeMonitoring(t *testing.T) { root := getProjectRoot(t) policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml") engine, err := NewPolicyEngineFromPath(policyPath) if err != nil { t.Fatalf("Failed to create policy engine: %v", err) } intake := &UseCaseIntake{ UseCaseText: "Teams Analytics mit Nutzungsstatistiken pro Mitarbeiter", Domain: DomainIT, DataTypes: DataTypes{ PersonalData: true, EmployeeData: true, }, EmployeeMonitoring: true, } result := engine.Evaluate(intake) // employee_data(+10) + employee_monitoring(+20) + not_consulted(+5) = 35 if result.BetrvgConflictScore < 30 { t.Errorf("Expected BetrvgConflictScore >= 30 for employee monitoring, got %d", result.BetrvgConflictScore) } if !result.BetrvgConsultationRequired { t.Error("Expected BetrvgConsultationRequired=true for employee monitoring") } } func TestCalculateBetrvgConflictScore_HRDecisionSupport(t *testing.T) { root := getProjectRoot(t) policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml") engine, err := NewPolicyEngineFromPath(policyPath) if err != nil { t.Fatalf("Failed to create policy engine: %v", err) } intake := &UseCaseIntake{ UseCaseText: "KI-gestuetztes Bewerber-Screening", Domain: DomainHR, DataTypes: DataTypes{ PersonalData: true, EmployeeData: true, }, EmployeeMonitoring: true, HRDecisionSupport: true, Automation: "fully_automated", Outputs: Outputs{ Rankings: true, }, } result := engine.Evaluate(intake) // employee_data(+10) + monitoring(+20) + hr(+20) + rankings(+10) + fully_auto(+10) + not_consulted(+5) = 75 if result.BetrvgConflictScore < 70 { t.Errorf("Expected BetrvgConflictScore >= 70 for HR+monitoring+automated, got %d", result.BetrvgConflictScore) } if !result.BetrvgConsultationRequired { t.Error("Expected BetrvgConsultationRequired=true") } } func TestCalculateBetrvgConflictScore_ConsultedReducesScore(t *testing.T) { root := getProjectRoot(t) policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml") engine, err := NewPolicyEngineFromPath(policyPath) if err != nil { t.Fatalf("Failed to create policy engine: %v", err) } // Same as above but works council consulted intakeNotConsulted := &UseCaseIntake{ UseCaseText: "Teams mit Nutzungsstatistiken", Domain: DomainIT, DataTypes: DataTypes{ PersonalData: true, EmployeeData: true, }, EmployeeMonitoring: true, WorksCouncilConsulted: false, } intakeConsulted := &UseCaseIntake{ UseCaseText: "Teams mit Nutzungsstatistiken", Domain: DomainIT, DataTypes: DataTypes{ PersonalData: true, EmployeeData: true, }, EmployeeMonitoring: true, WorksCouncilConsulted: true, } resultNot := engine.Evaluate(intakeNotConsulted) resultYes := engine.Evaluate(intakeConsulted) if resultYes.BetrvgConflictScore >= resultNot.BetrvgConflictScore { t.Errorf("Expected consulted score (%d) < not-consulted score (%d)", resultYes.BetrvgConflictScore, resultNot.BetrvgConflictScore) } } // ============================================================================ // BetrVG Escalation Tests // ============================================================================ func TestEscalation_BetrvgHighConflict_E3(t *testing.T) { trigger := DefaultEscalationTrigger() result := &AssessmentResult{ Feasibility: FeasibilityCONDITIONAL, RiskLevel: RiskLevelMEDIUM, RiskScore: 45, BetrvgConflictScore: 80, BetrvgConsultationRequired: true, Intake: UseCaseIntake{ WorksCouncilConsulted: false, }, TriggeredRules: []TriggeredRule{ {Code: "R-WARN-001", Severity: "WARN"}, }, } level, reason := trigger.DetermineEscalationLevel(result) if level != EscalationLevelE3 { t.Errorf("Expected E3 for high BR conflict without consultation, got %s (reason: %s)", level, reason) } } func TestEscalation_BetrvgMediumConflict_E2(t *testing.T) { trigger := DefaultEscalationTrigger() result := &AssessmentResult{ Feasibility: FeasibilityCONDITIONAL, RiskLevel: RiskLevelLOW, RiskScore: 25, BetrvgConflictScore: 55, BetrvgConsultationRequired: true, Intake: UseCaseIntake{ WorksCouncilConsulted: false, }, TriggeredRules: []TriggeredRule{ {Code: "R-WARN-001", Severity: "WARN"}, }, } level, reason := trigger.DetermineEscalationLevel(result) if level != EscalationLevelE2 { t.Errorf("Expected E2 for medium BR conflict without consultation, got %s (reason: %s)", level, reason) } } func TestEscalation_BetrvgConsulted_NoEscalation(t *testing.T) { trigger := DefaultEscalationTrigger() result := &AssessmentResult{ Feasibility: FeasibilityYES, RiskLevel: RiskLevelLOW, RiskScore: 15, BetrvgConflictScore: 55, BetrvgConsultationRequired: true, Intake: UseCaseIntake{ WorksCouncilConsulted: true, }, TriggeredRules: []TriggeredRule{}, } level, _ := trigger.DetermineEscalationLevel(result) // With consultation done and low risk, should not escalate for BR reasons if level == EscalationLevelE3 { t.Error("Should not escalate to E3 when works council is consulted") } } // ============================================================================ // BetrVG V2 Obligations Loading Test // ============================================================================ func TestBetrvgV2_LoadsFromManifest(t *testing.T) { root := getProjectRoot(t) v2Dir := filepath.Join(root, "policies", "obligations", "v2") // Check file exists betrvgPath := filepath.Join(v2Dir, "betrvg_v2.json") if _, err := os.Stat(betrvgPath); os.IsNotExist(err) { t.Fatal("betrvg_v2.json not found in policies/obligations/v2/") } // Load all v2 regulations regs, err := LoadAllV2Regulations() if err != nil { t.Fatalf("Failed to load v2 regulations: %v", err) } betrvg, ok := regs["betrvg"] if !ok { t.Fatal("betrvg not found in loaded regulations") } if betrvg.Regulation != "betrvg" { t.Errorf("Expected regulation 'betrvg', got '%s'", betrvg.Regulation) } if len(betrvg.Obligations) < 10 { t.Errorf("Expected at least 10 BetrVG obligations, got %d", len(betrvg.Obligations)) } // Check first obligation has correct structure obl := betrvg.Obligations[0] if obl.ID != "BETRVG-OBL-001" { t.Errorf("Expected first obligation ID 'BETRVG-OBL-001', got '%s'", obl.ID) } if len(obl.LegalBasis) == 0 { t.Error("Expected legal basis for first obligation") } if obl.LegalBasis[0].Norm != "BetrVG" { t.Errorf("Expected norm 'BetrVG', got '%s'", obl.LegalBasis[0].Norm) } } func TestBetrvgApplicability_Germany(t *testing.T) { regs, err := LoadAllV2Regulations() if err != nil { t.Fatalf("Failed to load v2 regulations: %v", err) } betrvgReg := regs["betrvg"] module := NewJSONRegulationModule(betrvgReg) // German company with 50 employees — should be applicable factsDE := &UnifiedFacts{ Organization: OrganizationFacts{ Country: "DE", EmployeeCount: 50, }, } if !module.IsApplicable(factsDE) { t.Error("BetrVG should be applicable for German company with 50 employees") } // US company — should NOT be applicable factsUS := &UnifiedFacts{ Organization: OrganizationFacts{ Country: "US", EmployeeCount: 50, }, } if module.IsApplicable(factsUS) { t.Error("BetrVG should NOT be applicable for US company") } // German company with 3 employees — should NOT be applicable (threshold 5) factsSmall := &UnifiedFacts{ Organization: OrganizationFacts{ Country: "DE", EmployeeCount: 3, }, } if module.IsApplicable(factsSmall) { t.Error("BetrVG should NOT be applicable for company with < 5 employees") } }