Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
940
ai-compliance-sdk/internal/ucca/policy_engine_test.go
Normal file
940
ai-compliance-sdk/internal/ucca/policy_engine_test.go
Normal file
@@ -0,0 +1,940 @@
|
||||
package ucca
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Helper to get the project root for testing
|
||||
func getProjectRoot(t *testing.T) string {
|
||||
// Start from the current directory and walk up to find go.mod
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get working directory: %v", err)
|
||||
}
|
||||
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
||||
return dir
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
t.Fatalf("Could not find project root (no go.mod found)")
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPolicyEngineFromPath(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)
|
||||
}
|
||||
|
||||
if engine.GetPolicyVersion() != "1.0.0" {
|
||||
t.Errorf("Expected policy version 1.0.0, got %s", engine.GetPolicyVersion())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicyEngine_EvaluateSimpleCase(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)
|
||||
}
|
||||
|
||||
// Test case: Simple RAG chatbot for utilities (low risk)
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "Chatbot für Stadtwerke mit FAQ-Suche",
|
||||
Domain: DomainUtilities,
|
||||
DataTypes: DataTypes{
|
||||
PersonalData: false,
|
||||
PublicData: true,
|
||||
},
|
||||
Purpose: Purpose{
|
||||
CustomerSupport: true,
|
||||
},
|
||||
Automation: AutomationAssistive,
|
||||
ModelUsage: ModelUsage{
|
||||
RAG: true,
|
||||
Training: false,
|
||||
},
|
||||
Hosting: Hosting{
|
||||
Region: "eu",
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
if result.Feasibility != FeasibilityYES {
|
||||
t.Errorf("Expected feasibility YES, got %s", result.Feasibility)
|
||||
}
|
||||
|
||||
if result.RiskLevel != RiskLevelMINIMAL {
|
||||
t.Errorf("Expected risk level MINIMAL, got %s", result.RiskLevel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicyEngine_EvaluateHighRiskCase(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)
|
||||
}
|
||||
|
||||
// Test case: HR scoring with full automation (should be blocked)
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "Automatische Mitarbeiterbewertung",
|
||||
Domain: DomainHR,
|
||||
DataTypes: DataTypes{
|
||||
PersonalData: true,
|
||||
EmployeeData: true,
|
||||
},
|
||||
Purpose: Purpose{
|
||||
EvaluationScoring: true,
|
||||
},
|
||||
Automation: AutomationFullyAutomated,
|
||||
Outputs: Outputs{
|
||||
RankingsOrScores: true,
|
||||
},
|
||||
ModelUsage: ModelUsage{
|
||||
Training: true,
|
||||
},
|
||||
Hosting: Hosting{
|
||||
Region: "eu",
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
if result.Feasibility != FeasibilityNO {
|
||||
t.Errorf("Expected feasibility NO for HR scoring, got %s", result.Feasibility)
|
||||
}
|
||||
|
||||
// Should have at least one BLOCK severity rule triggered
|
||||
hasBlock := false
|
||||
for _, rule := range result.TriggeredRules {
|
||||
if rule.Severity == SeverityBLOCK {
|
||||
hasBlock = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasBlock {
|
||||
t.Error("Expected at least one BLOCK severity rule for HR scoring")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicyEngine_EvaluateConditionalCase(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)
|
||||
}
|
||||
|
||||
// Test case: Personal data with marketing (should be conditional)
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "Marketing-Personalisierung",
|
||||
Domain: DomainMarketing,
|
||||
DataTypes: DataTypes{
|
||||
PersonalData: true,
|
||||
},
|
||||
Purpose: Purpose{
|
||||
Marketing: true,
|
||||
},
|
||||
Automation: AutomationAssistive,
|
||||
ModelUsage: ModelUsage{
|
||||
RAG: true,
|
||||
},
|
||||
Hosting: Hosting{
|
||||
Region: "eu",
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
if result.Feasibility != FeasibilityCONDITIONAL {
|
||||
t.Errorf("Expected feasibility CONDITIONAL for marketing with PII, got %s", result.Feasibility)
|
||||
}
|
||||
|
||||
// Should require consent control
|
||||
hasConsentControl := false
|
||||
for _, ctrl := range result.RequiredControls {
|
||||
if ctrl.ID == "C_EXPLICIT_CONSENT" {
|
||||
hasConsentControl = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasConsentControl {
|
||||
t.Error("Expected C_EXPLICIT_CONSENT control for marketing with PII")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicyEngine_EvaluateArticle9Data(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)
|
||||
}
|
||||
|
||||
// Test case: Healthcare with Art. 9 data
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "Patientendaten-Analyse",
|
||||
Domain: DomainHealthcare,
|
||||
DataTypes: DataTypes{
|
||||
PersonalData: true,
|
||||
Article9Data: true,
|
||||
},
|
||||
Purpose: Purpose{
|
||||
Analytics: true,
|
||||
},
|
||||
Automation: AutomationAssistive,
|
||||
ModelUsage: ModelUsage{
|
||||
RAG: true,
|
||||
},
|
||||
Hosting: Hosting{
|
||||
Region: "eu",
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
// Art. 9 data should trigger DSFA recommendation
|
||||
if !result.DSFARecommended {
|
||||
t.Error("Expected DSFA recommended for Art. 9 data")
|
||||
}
|
||||
|
||||
// Should have triggered Art. 9 rule
|
||||
hasArt9Rule := false
|
||||
for _, rule := range result.TriggeredRules {
|
||||
if rule.Code == "R-A002" {
|
||||
hasArt9Rule = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasArt9Rule {
|
||||
t.Error("Expected R-A002 (Art. 9 data) rule to be triggered")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicyEngine_EvaluateLicensePlates(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)
|
||||
}
|
||||
|
||||
// Test case: Parking with license plates
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "Parkhaus-Kennzeichenerkennung",
|
||||
Domain: DomainRealEstate,
|
||||
DataTypes: DataTypes{
|
||||
PersonalData: true,
|
||||
LicensePlates: true,
|
||||
},
|
||||
Purpose: Purpose{
|
||||
Automation: true,
|
||||
},
|
||||
Automation: AutomationSemiAutomated,
|
||||
ModelUsage: ModelUsage{
|
||||
Inference: true,
|
||||
},
|
||||
Hosting: Hosting{
|
||||
Region: "eu",
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
// Should suggest pixelization pattern
|
||||
hasPixelization := false
|
||||
for _, pattern := range result.RecommendedArchitecture {
|
||||
if pattern.PatternID == "P_PIXELIZATION" {
|
||||
hasPixelization = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasPixelization {
|
||||
t.Error("Expected P_PIXELIZATION pattern to be recommended for license plates")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicyEngine_EvaluateThirdCountryTransfer(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)
|
||||
}
|
||||
|
||||
// Test case: Third country hosting with PII
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "US-hosted AI service",
|
||||
Domain: DomainITServices,
|
||||
DataTypes: DataTypes{
|
||||
PersonalData: true,
|
||||
},
|
||||
Purpose: Purpose{
|
||||
InternalTools: true,
|
||||
},
|
||||
Automation: AutomationAssistive,
|
||||
ModelUsage: ModelUsage{
|
||||
RAG: true,
|
||||
},
|
||||
Hosting: Hosting{
|
||||
Region: "third_country",
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
// Should require SCC control
|
||||
hasSCC := false
|
||||
for _, ctrl := range result.RequiredControls {
|
||||
if ctrl.ID == "C_SCC" {
|
||||
hasSCC = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasSCC {
|
||||
t.Error("Expected C_SCC control for third country transfer with PII")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicyEngine_GetAllRules(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)
|
||||
}
|
||||
|
||||
rules := engine.GetAllRules()
|
||||
if len(rules) == 0 {
|
||||
t.Error("Expected at least some rules")
|
||||
}
|
||||
|
||||
// Check that rules have required fields
|
||||
for _, rule := range rules {
|
||||
if rule.ID == "" {
|
||||
t.Error("Found rule without ID")
|
||||
}
|
||||
if rule.Category == "" {
|
||||
t.Error("Found rule without category")
|
||||
}
|
||||
if rule.Title == "" {
|
||||
t.Error("Found rule without title")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicyEngine_GetAllPatterns(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)
|
||||
}
|
||||
|
||||
patterns := engine.GetAllPatterns()
|
||||
if len(patterns) == 0 {
|
||||
t.Error("Expected at least some patterns")
|
||||
}
|
||||
|
||||
// Verify expected patterns exist
|
||||
expectedPatterns := []string{"P_RAG_ONLY", "P_PRE_ANON", "P_PIXELIZATION", "P_HITL_ENFORCED"}
|
||||
for _, expected := range expectedPatterns {
|
||||
if _, exists := patterns[expected]; !exists {
|
||||
t.Errorf("Expected pattern %s to exist", expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicyEngine_GetAllControls(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)
|
||||
}
|
||||
|
||||
controls := engine.GetAllControls()
|
||||
if len(controls) == 0 {
|
||||
t.Error("Expected at least some controls")
|
||||
}
|
||||
|
||||
// Verify expected controls exist
|
||||
expectedControls := []string{"C_EXPLICIT_CONSENT", "C_DSFA", "C_ENCRYPTION", "C_SCC"}
|
||||
for _, expected := range expectedControls {
|
||||
if _, exists := controls[expected]; !exists {
|
||||
t.Errorf("Expected control %s to exist", expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicyEngine_TrainingWithMinorData(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)
|
||||
}
|
||||
|
||||
// Test case: Training with minor data (should be blocked)
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "KI-Training mit Schülerdaten",
|
||||
Domain: DomainEducation,
|
||||
DataTypes: DataTypes{
|
||||
PersonalData: true,
|
||||
MinorData: true,
|
||||
},
|
||||
Purpose: Purpose{
|
||||
Research: true,
|
||||
},
|
||||
Automation: AutomationAssistive,
|
||||
ModelUsage: ModelUsage{
|
||||
Training: true,
|
||||
},
|
||||
Hosting: Hosting{
|
||||
Region: "eu",
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
if result.TrainingAllowed != TrainingNO {
|
||||
t.Errorf("Expected training NOT allowed for minor data, got %s", result.TrainingAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicyEngine_CompositeConditions(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)
|
||||
}
|
||||
|
||||
// Test case: Fully automated with legal effects (R-C004 uses all_of)
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "Automatische Vertragsgenehmigung",
|
||||
Domain: DomainLegal,
|
||||
DataTypes: DataTypes{
|
||||
PersonalData: true,
|
||||
},
|
||||
Purpose: Purpose{
|
||||
DecisionMaking: true,
|
||||
},
|
||||
Automation: AutomationFullyAutomated,
|
||||
Outputs: Outputs{
|
||||
LegalEffects: true,
|
||||
},
|
||||
ModelUsage: ModelUsage{
|
||||
Inference: true,
|
||||
},
|
||||
Hosting: Hosting{
|
||||
Region: "eu",
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
if result.Feasibility != FeasibilityNO {
|
||||
t.Errorf("Expected NO feasibility for fully automated legal decisions, got %s", result.Feasibility)
|
||||
}
|
||||
|
||||
// Check that R-C004 was triggered
|
||||
hasC004 := false
|
||||
for _, rule := range result.TriggeredRules {
|
||||
if rule.Code == "R-C004" {
|
||||
hasC004 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasC004 {
|
||||
t.Error("Expected R-C004 (automated legal effects) to be triggered")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Determinism Tests - Ensure consistent results
|
||||
// ============================================================================
|
||||
|
||||
func TestPolicyEngine_Determinism(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: "Test Case für Determinismus",
|
||||
Domain: DomainEducation,
|
||||
DataTypes: DataTypes{
|
||||
PersonalData: true,
|
||||
MinorData: true,
|
||||
},
|
||||
Purpose: Purpose{
|
||||
EvaluationScoring: true,
|
||||
},
|
||||
Automation: AutomationFullyAutomated,
|
||||
Outputs: Outputs{
|
||||
RankingsOrScores: true,
|
||||
},
|
||||
ModelUsage: ModelUsage{
|
||||
Training: true,
|
||||
},
|
||||
Hosting: Hosting{
|
||||
Region: "eu",
|
||||
},
|
||||
}
|
||||
|
||||
// Run evaluation 10 times and ensure identical results
|
||||
firstResult := engine.Evaluate(intake)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
if result.Feasibility != firstResult.Feasibility {
|
||||
t.Errorf("Run %d: Feasibility mismatch: %s vs %s", i, result.Feasibility, firstResult.Feasibility)
|
||||
}
|
||||
if result.RiskScore != firstResult.RiskScore {
|
||||
t.Errorf("Run %d: RiskScore mismatch: %d vs %d", i, result.RiskScore, firstResult.RiskScore)
|
||||
}
|
||||
if result.RiskLevel != firstResult.RiskLevel {
|
||||
t.Errorf("Run %d: RiskLevel mismatch: %s vs %s", i, result.RiskLevel, firstResult.RiskLevel)
|
||||
}
|
||||
if len(result.TriggeredRules) != len(firstResult.TriggeredRules) {
|
||||
t.Errorf("Run %d: TriggeredRules count mismatch: %d vs %d", i, len(result.TriggeredRules), len(firstResult.TriggeredRules))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Education Domain Specific Tests
|
||||
// ============================================================================
|
||||
|
||||
func TestPolicyEngine_EducationScoring_AlwaysBlocked(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)
|
||||
}
|
||||
|
||||
// Education + Scoring + Fully Automated = BLOCK (R-F001)
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "Automatische Schülerbewertung",
|
||||
Domain: DomainEducation,
|
||||
DataTypes: DataTypes{
|
||||
PersonalData: true,
|
||||
MinorData: true,
|
||||
},
|
||||
Purpose: Purpose{
|
||||
EvaluationScoring: true,
|
||||
},
|
||||
Automation: AutomationFullyAutomated,
|
||||
Outputs: Outputs{
|
||||
RankingsOrScores: true,
|
||||
},
|
||||
ModelUsage: ModelUsage{
|
||||
Inference: true,
|
||||
},
|
||||
Hosting: Hosting{
|
||||
Region: "eu",
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
if result.Feasibility != FeasibilityNO {
|
||||
t.Errorf("Expected NO for education automated scoring, got %s", result.Feasibility)
|
||||
}
|
||||
|
||||
// Should have Art. 22 risk flagged
|
||||
if !result.Art22Risk {
|
||||
t.Error("Expected Art. 22 risk for automated individual decisions in education")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RAG-Only Use Cases (Low Risk)
|
||||
// ============================================================================
|
||||
|
||||
func TestPolicyEngine_RAGOnly_LowRisk(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: "FAQ-Suche mit öffentlichen Dokumenten",
|
||||
Domain: DomainUtilities,
|
||||
DataTypes: DataTypes{
|
||||
PublicData: true,
|
||||
},
|
||||
Purpose: Purpose{
|
||||
CustomerSupport: true,
|
||||
},
|
||||
Automation: AutomationAssistive,
|
||||
ModelUsage: ModelUsage{
|
||||
RAG: true,
|
||||
},
|
||||
Hosting: Hosting{
|
||||
Region: "eu",
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
if result.Feasibility != FeasibilityYES {
|
||||
t.Errorf("Expected YES for RAG-only public data, got %s", result.Feasibility)
|
||||
}
|
||||
|
||||
if result.RiskLevel != RiskLevelMINIMAL {
|
||||
t.Errorf("Expected MINIMAL risk for RAG-only, got %s", result.RiskLevel)
|
||||
}
|
||||
|
||||
// Should recommend P_RAG_ONLY pattern
|
||||
hasRAGPattern := false
|
||||
for _, pattern := range result.RecommendedArchitecture {
|
||||
if pattern.PatternID == "P_RAG_ONLY" {
|
||||
hasRAGPattern = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasRAGPattern {
|
||||
t.Error("Expected P_RAG_ONLY pattern recommendation")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Risk Score Calculation Tests
|
||||
// ============================================================================
|
||||
|
||||
func TestPolicyEngine_RiskScoreCalculation(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)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
intake *UseCaseIntake
|
||||
minScore int
|
||||
maxScore int
|
||||
expectedRiskLevel RiskLevel
|
||||
}{
|
||||
{
|
||||
name: "Public data only → minimal risk",
|
||||
intake: &UseCaseIntake{
|
||||
Domain: DomainUtilities,
|
||||
DataTypes: DataTypes{
|
||||
PublicData: true,
|
||||
},
|
||||
Automation: AutomationAssistive,
|
||||
ModelUsage: ModelUsage{RAG: true},
|
||||
Hosting: Hosting{Region: "eu"},
|
||||
},
|
||||
minScore: 0,
|
||||
maxScore: 20,
|
||||
expectedRiskLevel: RiskLevelMINIMAL,
|
||||
},
|
||||
{
|
||||
name: "Personal data → low risk",
|
||||
intake: &UseCaseIntake{
|
||||
Domain: DomainITServices,
|
||||
DataTypes: DataTypes{
|
||||
PersonalData: true,
|
||||
},
|
||||
Automation: AutomationAssistive,
|
||||
ModelUsage: ModelUsage{RAG: true},
|
||||
Hosting: Hosting{Region: "eu"},
|
||||
},
|
||||
minScore: 5,
|
||||
maxScore: 40,
|
||||
expectedRiskLevel: RiskLevelLOW,
|
||||
},
|
||||
{
|
||||
name: "Art. 9 data → medium risk",
|
||||
intake: &UseCaseIntake{
|
||||
Domain: DomainHealthcare,
|
||||
DataTypes: DataTypes{
|
||||
PersonalData: true,
|
||||
Article9Data: true,
|
||||
},
|
||||
Automation: AutomationAssistive,
|
||||
ModelUsage: ModelUsage{RAG: true},
|
||||
Hosting: Hosting{Region: "eu"},
|
||||
},
|
||||
minScore: 20,
|
||||
maxScore: 60,
|
||||
expectedRiskLevel: RiskLevelMEDIUM,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := engine.Evaluate(tt.intake)
|
||||
|
||||
if result.RiskScore < tt.minScore || result.RiskScore > tt.maxScore {
|
||||
t.Errorf("RiskScore %d outside expected range [%d, %d]", result.RiskScore, tt.minScore, tt.maxScore)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Training Allowed Tests
|
||||
// ============================================================================
|
||||
|
||||
func TestPolicyEngine_TrainingAllowed_Scenarios(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)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
intake *UseCaseIntake
|
||||
expectedAllowed TrainingAllowed
|
||||
}{
|
||||
{
|
||||
name: "Public data training → allowed",
|
||||
intake: &UseCaseIntake{
|
||||
Domain: DomainUtilities,
|
||||
DataTypes: DataTypes{
|
||||
PublicData: true,
|
||||
},
|
||||
Automation: AutomationAssistive,
|
||||
ModelUsage: ModelUsage{Training: true},
|
||||
Hosting: Hosting{Region: "eu"},
|
||||
},
|
||||
expectedAllowed: TrainingYES,
|
||||
},
|
||||
{
|
||||
name: "Minor data training → not allowed",
|
||||
intake: &UseCaseIntake{
|
||||
Domain: DomainEducation,
|
||||
DataTypes: DataTypes{
|
||||
PersonalData: true,
|
||||
MinorData: true,
|
||||
},
|
||||
Automation: AutomationAssistive,
|
||||
ModelUsage: ModelUsage{Training: true},
|
||||
Hosting: Hosting{Region: "eu"},
|
||||
},
|
||||
expectedAllowed: TrainingNO,
|
||||
},
|
||||
{
|
||||
name: "Art. 9 data training → not allowed",
|
||||
intake: &UseCaseIntake{
|
||||
Domain: DomainHealthcare,
|
||||
DataTypes: DataTypes{
|
||||
PersonalData: true,
|
||||
Article9Data: true,
|
||||
},
|
||||
Automation: AutomationAssistive,
|
||||
ModelUsage: ModelUsage{Training: true},
|
||||
Hosting: Hosting{Region: "eu"},
|
||||
},
|
||||
expectedAllowed: TrainingNO,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := engine.Evaluate(tt.intake)
|
||||
|
||||
if result.TrainingAllowed != tt.expectedAllowed {
|
||||
t.Errorf("Expected TrainingAllowed=%s, got %s", tt.expectedAllowed, result.TrainingAllowed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DSFA Recommendation Tests
|
||||
// ============================================================================
|
||||
|
||||
func TestPolicyEngine_DSFARecommendation(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)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
intake *UseCaseIntake
|
||||
expectDSFA bool
|
||||
expectArt22 bool
|
||||
}{
|
||||
{
|
||||
name: "Art. 9 data → DSFA required",
|
||||
intake: &UseCaseIntake{
|
||||
Domain: DomainHealthcare,
|
||||
DataTypes: DataTypes{
|
||||
PersonalData: true,
|
||||
Article9Data: true,
|
||||
},
|
||||
Automation: AutomationAssistive,
|
||||
ModelUsage: ModelUsage{RAG: true},
|
||||
Hosting: Hosting{Region: "eu"},
|
||||
},
|
||||
expectDSFA: true,
|
||||
expectArt22: false,
|
||||
},
|
||||
{
|
||||
name: "Systematic evaluation → DSFA + Art. 22",
|
||||
intake: &UseCaseIntake{
|
||||
Domain: DomainHR,
|
||||
DataTypes: DataTypes{
|
||||
PersonalData: true,
|
||||
EmployeeData: true,
|
||||
},
|
||||
Purpose: Purpose{
|
||||
EvaluationScoring: true,
|
||||
},
|
||||
Automation: AutomationFullyAutomated,
|
||||
Outputs: Outputs{
|
||||
RankingsOrScores: true,
|
||||
},
|
||||
ModelUsage: ModelUsage{Inference: true},
|
||||
Hosting: Hosting{Region: "eu"},
|
||||
},
|
||||
expectDSFA: true,
|
||||
expectArt22: true,
|
||||
},
|
||||
{
|
||||
name: "Public data RAG → no DSFA",
|
||||
intake: &UseCaseIntake{
|
||||
Domain: DomainUtilities,
|
||||
DataTypes: DataTypes{
|
||||
PublicData: true,
|
||||
},
|
||||
Automation: AutomationAssistive,
|
||||
ModelUsage: ModelUsage{RAG: true},
|
||||
Hosting: Hosting{Region: "eu"},
|
||||
},
|
||||
expectDSFA: false,
|
||||
expectArt22: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := engine.Evaluate(tt.intake)
|
||||
|
||||
if result.DSFARecommended != tt.expectDSFA {
|
||||
t.Errorf("Expected DSFARecommended=%v, got %v", tt.expectDSFA, result.DSFARecommended)
|
||||
}
|
||||
if result.Art22Risk != tt.expectArt22 {
|
||||
t.Errorf("Expected Art22Risk=%v, got %v", tt.expectArt22, result.Art22Risk)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Required Controls Tests
|
||||
// ============================================================================
|
||||
|
||||
func TestPolicyEngine_RequiredControls(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)
|
||||
}
|
||||
|
||||
// Third country with PII should require SCC
|
||||
intake := &UseCaseIntake{
|
||||
Domain: DomainITServices,
|
||||
DataTypes: DataTypes{
|
||||
PersonalData: true,
|
||||
},
|
||||
Automation: AutomationAssistive,
|
||||
ModelUsage: ModelUsage{RAG: true},
|
||||
Hosting: Hosting{
|
||||
Region: "third_country",
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
controlIDs := make(map[string]bool)
|
||||
for _, ctrl := range result.RequiredControls {
|
||||
controlIDs[ctrl.ID] = true
|
||||
}
|
||||
|
||||
if !controlIDs["C_SCC"] {
|
||||
t.Error("Expected C_SCC control for third country transfer")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Policy Version Tests
|
||||
// ============================================================================
|
||||
|
||||
func TestPolicyEngine_PolicyVersion(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)
|
||||
}
|
||||
|
||||
version := engine.GetPolicyVersion()
|
||||
|
||||
// Version should be non-empty and follow semver pattern
|
||||
if version == "" {
|
||||
t.Error("Policy version should not be empty")
|
||||
}
|
||||
|
||||
// Check for basic semver pattern (x.y.z)
|
||||
parts := 0
|
||||
for _, c := range version {
|
||||
if c == '.' {
|
||||
parts++
|
||||
}
|
||||
}
|
||||
if parts < 2 {
|
||||
t.Errorf("Policy version should follow semver (x.y.z), got %s", version)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user