From 1989c410a945e066d359f211bbaf7c723b74f6f9 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sun, 12 Apr 2026 11:11:33 +0200 Subject: [PATCH] =?UTF-8?q?test:=20BetrVG-Modul=20Tests=20=E2=80=94=20Konf?= =?UTF-8?q?likt-Score,=20Escalation,=20Obligations,=20Applicability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 10 Tests: Score-Berechnung (no data, monitoring, HR, consulted), Escalation (E2/E3 Trigger), V2-Obligations-Loading, Applicability (DE/US/small). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../internal/ucca/betrvg_test.go | 305 ++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 ai-compliance-sdk/internal/ucca/betrvg_test.go diff --git a/ai-compliance-sdk/internal/ucca/betrvg_test.go b/ai-compliance-sdk/internal/ucca/betrvg_test.go new file mode 100644 index 0000000..ecbe8d3 --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/betrvg_test.go @@ -0,0 +1,305 @@ +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") + } +}