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) <noreply@anthropic.com>
306 lines
8.7 KiB
Go
306 lines
8.7 KiB
Go
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")
|
|
}
|
|
}
|