Interaktiver 12-Fragen-Entscheidungsbaum für die AI Act Klassifikation auf zwei Achsen: High-Risk (Anhang III, Q1-Q7) und GPAI (Art. 51-56, Q8-Q12). Deterministische Auswertung ohne LLM. Backend (Go): - Neue Structs: GPAIClassification, DecisionTreeAnswer, DecisionTreeResult - Decision Tree Engine mit BuildDecisionTreeDefinition() und EvaluateDecisionTree() - Store-Methoden für CRUD der Ergebnisse - API-Endpoints: GET/POST /decision-tree, GET/DELETE /decision-tree/results - 12 Unit Tests (alle bestanden) Frontend (Next.js): - DecisionTreeWizard: Wizard-UI mit Ja/Nein-Fragen, Dual-Progress-Bar, Ergebnis-Ansicht - AI Act Page refactored: Tabs (Übersicht | Entscheidungsbaum | Ergebnisse) - Proxy-Route für decision-tree Endpoints Migration 083: ai_act_decision_tree_results Tabelle Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
421 lines
12 KiB
Go
421 lines
12 KiB
Go
package ucca
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestBuildDecisionTreeDefinition_ReturnsValidTree(t *testing.T) {
|
|
tree := BuildDecisionTreeDefinition()
|
|
|
|
if tree == nil {
|
|
t.Fatal("Expected non-nil tree definition")
|
|
}
|
|
if tree.ID != "ai_act_two_axis" {
|
|
t.Errorf("Expected ID 'ai_act_two_axis', got '%s'", tree.ID)
|
|
}
|
|
if tree.Version != "1.0.0" {
|
|
t.Errorf("Expected version '1.0.0', got '%s'", tree.Version)
|
|
}
|
|
if len(tree.Questions) != 12 {
|
|
t.Errorf("Expected 12 questions, got %d", len(tree.Questions))
|
|
}
|
|
|
|
// Check axis distribution
|
|
hrCount := 0
|
|
gpaiCount := 0
|
|
for _, q := range tree.Questions {
|
|
switch q.Axis {
|
|
case "high_risk":
|
|
hrCount++
|
|
case "gpai":
|
|
gpaiCount++
|
|
default:
|
|
t.Errorf("Unexpected axis '%s' for question %s", q.Axis, q.ID)
|
|
}
|
|
}
|
|
if hrCount != 7 {
|
|
t.Errorf("Expected 7 high_risk questions, got %d", hrCount)
|
|
}
|
|
if gpaiCount != 5 {
|
|
t.Errorf("Expected 5 gpai questions, got %d", gpaiCount)
|
|
}
|
|
|
|
// Check all questions have required fields
|
|
for _, q := range tree.Questions {
|
|
if q.ID == "" {
|
|
t.Error("Question has empty ID")
|
|
}
|
|
if q.Question == "" {
|
|
t.Errorf("Question %s has empty question text", q.ID)
|
|
}
|
|
if q.Description == "" {
|
|
t.Errorf("Question %s has empty description", q.ID)
|
|
}
|
|
if q.ArticleRef == "" {
|
|
t.Errorf("Question %s has empty article_ref", q.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEvaluateDecisionTree_NotApplicable(t *testing.T) {
|
|
// Q1=No → AI Act not applicable
|
|
req := &DecisionTreeEvalRequest{
|
|
SystemName: "Test System",
|
|
Answers: map[string]DecisionTreeAnswer{
|
|
Q1: {QuestionID: Q1, Value: false},
|
|
},
|
|
}
|
|
|
|
result := EvaluateDecisionTree(req)
|
|
|
|
if result.HighRiskResult != AIActNotApplicable {
|
|
t.Errorf("Expected not_applicable, got %s", result.HighRiskResult)
|
|
}
|
|
if result.GPAIResult.IsGPAI {
|
|
t.Error("Expected GPAI to be false when Q8 is not answered")
|
|
}
|
|
if result.SystemName != "Test System" {
|
|
t.Errorf("Expected system name 'Test System', got '%s'", result.SystemName)
|
|
}
|
|
}
|
|
|
|
func TestEvaluateDecisionTree_MinimalRisk(t *testing.T) {
|
|
// Q1=Yes, Q2-Q7=No → minimal risk
|
|
req := &DecisionTreeEvalRequest{
|
|
SystemName: "Simple Tool",
|
|
Answers: map[string]DecisionTreeAnswer{
|
|
Q1: {QuestionID: Q1, Value: true},
|
|
Q2: {QuestionID: Q2, Value: false},
|
|
Q3: {QuestionID: Q3, Value: false},
|
|
Q4: {QuestionID: Q4, Value: false},
|
|
Q5: {QuestionID: Q5, Value: false},
|
|
Q6: {QuestionID: Q6, Value: false},
|
|
Q7: {QuestionID: Q7, Value: false},
|
|
Q8: {QuestionID: Q8, Value: false},
|
|
},
|
|
}
|
|
|
|
result := EvaluateDecisionTree(req)
|
|
|
|
if result.HighRiskResult != AIActMinimalRisk {
|
|
t.Errorf("Expected minimal_risk, got %s", result.HighRiskResult)
|
|
}
|
|
if result.GPAIResult.IsGPAI {
|
|
t.Error("Expected GPAI to be false")
|
|
}
|
|
if result.GPAIResult.Category != GPAICategoryNone {
|
|
t.Errorf("Expected GPAI category 'none', got '%s'", result.GPAIResult.Category)
|
|
}
|
|
}
|
|
|
|
func TestEvaluateDecisionTree_HighRisk_Biometric(t *testing.T) {
|
|
// Q1=Yes, Q2=Yes → high risk (biometric)
|
|
req := &DecisionTreeEvalRequest{
|
|
SystemName: "Face Recognition",
|
|
Answers: map[string]DecisionTreeAnswer{
|
|
Q1: {QuestionID: Q1, Value: true},
|
|
Q2: {QuestionID: Q2, Value: true},
|
|
Q3: {QuestionID: Q3, Value: false},
|
|
Q4: {QuestionID: Q4, Value: false},
|
|
Q5: {QuestionID: Q5, Value: false},
|
|
Q6: {QuestionID: Q6, Value: false},
|
|
Q7: {QuestionID: Q7, Value: false},
|
|
},
|
|
}
|
|
|
|
result := EvaluateDecisionTree(req)
|
|
|
|
if result.HighRiskResult != AIActHighRisk {
|
|
t.Errorf("Expected high_risk, got %s", result.HighRiskResult)
|
|
}
|
|
|
|
// Should have high-risk obligations
|
|
if len(result.CombinedObligations) == 0 {
|
|
t.Error("Expected non-empty obligations for high-risk system")
|
|
}
|
|
}
|
|
|
|
func TestEvaluateDecisionTree_HighRisk_CriticalInfrastructure(t *testing.T) {
|
|
// Q1=Yes, Q3=Yes → high risk (critical infrastructure)
|
|
req := &DecisionTreeEvalRequest{
|
|
SystemName: "Energy Grid AI",
|
|
Answers: map[string]DecisionTreeAnswer{
|
|
Q1: {QuestionID: Q1, Value: true},
|
|
Q2: {QuestionID: Q2, Value: false},
|
|
Q3: {QuestionID: Q3, Value: true},
|
|
Q4: {QuestionID: Q4, Value: false},
|
|
Q5: {QuestionID: Q5, Value: false},
|
|
Q6: {QuestionID: Q6, Value: false},
|
|
Q7: {QuestionID: Q7, Value: false},
|
|
},
|
|
}
|
|
|
|
result := EvaluateDecisionTree(req)
|
|
|
|
if result.HighRiskResult != AIActHighRisk {
|
|
t.Errorf("Expected high_risk, got %s", result.HighRiskResult)
|
|
}
|
|
}
|
|
|
|
func TestEvaluateDecisionTree_HighRisk_Education(t *testing.T) {
|
|
// Q1=Yes, Q4=Yes → high risk (education/employment)
|
|
req := &DecisionTreeEvalRequest{
|
|
SystemName: "Exam Grading AI",
|
|
Answers: map[string]DecisionTreeAnswer{
|
|
Q1: {QuestionID: Q1, Value: true},
|
|
Q2: {QuestionID: Q2, Value: false},
|
|
Q3: {QuestionID: Q3, Value: false},
|
|
Q4: {QuestionID: Q4, Value: true},
|
|
},
|
|
}
|
|
|
|
result := EvaluateDecisionTree(req)
|
|
|
|
if result.HighRiskResult != AIActHighRisk {
|
|
t.Errorf("Expected high_risk, got %s", result.HighRiskResult)
|
|
}
|
|
}
|
|
|
|
func TestEvaluateDecisionTree_HighRisk_AutonomousDecisions(t *testing.T) {
|
|
// Q1=Yes, Q7=Yes → high risk (autonomous decisions)
|
|
req := &DecisionTreeEvalRequest{
|
|
SystemName: "Credit Scoring AI",
|
|
Answers: map[string]DecisionTreeAnswer{
|
|
Q1: {QuestionID: Q1, Value: true},
|
|
Q2: {QuestionID: Q2, Value: false},
|
|
Q3: {QuestionID: Q3, Value: false},
|
|
Q4: {QuestionID: Q4, Value: false},
|
|
Q5: {QuestionID: Q5, Value: false},
|
|
Q6: {QuestionID: Q6, Value: false},
|
|
Q7: {QuestionID: Q7, Value: true},
|
|
},
|
|
}
|
|
|
|
result := EvaluateDecisionTree(req)
|
|
|
|
if result.HighRiskResult != AIActHighRisk {
|
|
t.Errorf("Expected high_risk, got %s", result.HighRiskResult)
|
|
}
|
|
}
|
|
|
|
func TestEvaluateDecisionTree_GPAI_Standard(t *testing.T) {
|
|
// Q8=Yes, Q10=No → GPAI standard
|
|
req := &DecisionTreeEvalRequest{
|
|
SystemName: "Custom LLM",
|
|
Answers: map[string]DecisionTreeAnswer{
|
|
Q1: {QuestionID: Q1, Value: true},
|
|
Q8: {QuestionID: Q8, Value: true},
|
|
Q9: {QuestionID: Q9, Value: true},
|
|
Q10: {QuestionID: Q10, Value: false},
|
|
Q11: {QuestionID: Q11, Value: false},
|
|
Q12: {QuestionID: Q12, Value: false},
|
|
},
|
|
}
|
|
|
|
result := EvaluateDecisionTree(req)
|
|
|
|
if !result.GPAIResult.IsGPAI {
|
|
t.Error("Expected IsGPAI to be true")
|
|
}
|
|
if result.GPAIResult.Category != GPAICategoryStandard {
|
|
t.Errorf("Expected category 'standard', got '%s'", result.GPAIResult.Category)
|
|
}
|
|
if result.GPAIResult.IsSystemicRisk {
|
|
t.Error("Expected IsSystemicRisk to be false")
|
|
}
|
|
|
|
// Should have Art. 51, 53, 50 (generative)
|
|
hasArt51 := false
|
|
hasArt53 := false
|
|
hasArt50 := false
|
|
for _, a := range result.GPAIResult.ApplicableArticles {
|
|
if a == "Art. 51" {
|
|
hasArt51 = true
|
|
}
|
|
if a == "Art. 53" {
|
|
hasArt53 = true
|
|
}
|
|
if a == "Art. 50" {
|
|
hasArt50 = true
|
|
}
|
|
}
|
|
if !hasArt51 {
|
|
t.Error("Expected Art. 51 in applicable articles")
|
|
}
|
|
if !hasArt53 {
|
|
t.Error("Expected Art. 53 in applicable articles")
|
|
}
|
|
if !hasArt50 {
|
|
t.Error("Expected Art. 50 in applicable articles (generative AI)")
|
|
}
|
|
}
|
|
|
|
func TestEvaluateDecisionTree_GPAI_SystemicRisk(t *testing.T) {
|
|
// Q8=Yes, Q10=Yes → GPAI systemic risk
|
|
req := &DecisionTreeEvalRequest{
|
|
SystemName: "GPT-5",
|
|
Answers: map[string]DecisionTreeAnswer{
|
|
Q1: {QuestionID: Q1, Value: true},
|
|
Q8: {QuestionID: Q8, Value: true},
|
|
Q9: {QuestionID: Q9, Value: true},
|
|
Q10: {QuestionID: Q10, Value: true},
|
|
Q11: {QuestionID: Q11, Value: true},
|
|
Q12: {QuestionID: Q12, Value: true},
|
|
},
|
|
}
|
|
|
|
result := EvaluateDecisionTree(req)
|
|
|
|
if !result.GPAIResult.IsGPAI {
|
|
t.Error("Expected IsGPAI to be true")
|
|
}
|
|
if result.GPAIResult.Category != GPAICategorySystemic {
|
|
t.Errorf("Expected category 'systemic', got '%s'", result.GPAIResult.Category)
|
|
}
|
|
if !result.GPAIResult.IsSystemicRisk {
|
|
t.Error("Expected IsSystemicRisk to be true")
|
|
}
|
|
|
|
// Should have Art. 55
|
|
hasArt55 := false
|
|
for _, a := range result.GPAIResult.ApplicableArticles {
|
|
if a == "Art. 55" {
|
|
hasArt55 = true
|
|
}
|
|
}
|
|
if !hasArt55 {
|
|
t.Error("Expected Art. 55 in applicable articles (systemic risk)")
|
|
}
|
|
}
|
|
|
|
func TestEvaluateDecisionTree_Combined_HighRiskAndGPAI(t *testing.T) {
|
|
// Q1=Yes, Q4=Yes (high risk) + Q8=Yes, Q9=Yes (GPAI standard)
|
|
req := &DecisionTreeEvalRequest{
|
|
SystemName: "HR Screening with LLM",
|
|
SystemDescription: "LLM-based applicant screening system",
|
|
Answers: map[string]DecisionTreeAnswer{
|
|
Q1: {QuestionID: Q1, Value: true},
|
|
Q2: {QuestionID: Q2, Value: false},
|
|
Q3: {QuestionID: Q3, Value: false},
|
|
Q4: {QuestionID: Q4, Value: true},
|
|
Q5: {QuestionID: Q5, Value: false},
|
|
Q6: {QuestionID: Q6, Value: false},
|
|
Q7: {QuestionID: Q7, Value: true},
|
|
Q8: {QuestionID: Q8, Value: true},
|
|
Q9: {QuestionID: Q9, Value: true},
|
|
Q10: {QuestionID: Q10, Value: false},
|
|
Q11: {QuestionID: Q11, Value: false},
|
|
Q12: {QuestionID: Q12, Value: false},
|
|
},
|
|
}
|
|
|
|
result := EvaluateDecisionTree(req)
|
|
|
|
// Both axes should be triggered
|
|
if result.HighRiskResult != AIActHighRisk {
|
|
t.Errorf("Expected high_risk, got %s", result.HighRiskResult)
|
|
}
|
|
if !result.GPAIResult.IsGPAI {
|
|
t.Error("Expected GPAI to be true")
|
|
}
|
|
if result.GPAIResult.Category != GPAICategoryStandard {
|
|
t.Errorf("Expected GPAI category 'standard', got '%s'", result.GPAIResult.Category)
|
|
}
|
|
|
|
// Combined obligations should include both axes
|
|
if len(result.CombinedObligations) < 5 {
|
|
t.Errorf("Expected at least 5 combined obligations, got %d", len(result.CombinedObligations))
|
|
}
|
|
|
|
// Should have articles from both axes
|
|
if len(result.ApplicableArticles) < 3 {
|
|
t.Errorf("Expected at least 3 applicable articles, got %d", len(result.ApplicableArticles))
|
|
}
|
|
|
|
// Check system name preserved
|
|
if result.SystemName != "HR Screening with LLM" {
|
|
t.Errorf("Expected system name preserved, got '%s'", result.SystemName)
|
|
}
|
|
if result.SystemDescription != "LLM-based applicant screening system" {
|
|
t.Errorf("Expected description preserved, got '%s'", result.SystemDescription)
|
|
}
|
|
}
|
|
|
|
func TestEvaluateDecisionTree_GPAI_MarketPenetration(t *testing.T) {
|
|
// Q8=Yes, Q10=No, Q12=Yes → GPAI standard with market penetration warning
|
|
req := &DecisionTreeEvalRequest{
|
|
SystemName: "Popular Chatbot",
|
|
Answers: map[string]DecisionTreeAnswer{
|
|
Q1: {QuestionID: Q1, Value: true},
|
|
Q8: {QuestionID: Q8, Value: true},
|
|
Q9: {QuestionID: Q9, Value: true},
|
|
Q10: {QuestionID: Q10, Value: false},
|
|
Q11: {QuestionID: Q11, Value: true},
|
|
Q12: {QuestionID: Q12, Value: true},
|
|
},
|
|
}
|
|
|
|
result := EvaluateDecisionTree(req)
|
|
|
|
if result.GPAIResult.Category != GPAICategoryStandard {
|
|
t.Errorf("Expected category 'standard' (not systemic because Q10=No), got '%s'", result.GPAIResult.Category)
|
|
}
|
|
|
|
// Should have Art. 51 Abs. 3 warning
|
|
hasArt51_3 := false
|
|
for _, a := range result.GPAIResult.ApplicableArticles {
|
|
if a == "Art. 51 Abs. 3" {
|
|
hasArt51_3 = true
|
|
}
|
|
}
|
|
if !hasArt51_3 {
|
|
t.Error("Expected Art. 51 Abs. 3 in applicable articles for high market penetration")
|
|
}
|
|
}
|
|
|
|
func TestEvaluateDecisionTree_NoGPAI(t *testing.T) {
|
|
// Q8=No → No GPAI classification
|
|
req := &DecisionTreeEvalRequest{
|
|
SystemName: "Traditional ML",
|
|
Answers: map[string]DecisionTreeAnswer{
|
|
Q1: {QuestionID: Q1, Value: true},
|
|
Q8: {QuestionID: Q8, Value: false},
|
|
},
|
|
}
|
|
|
|
result := EvaluateDecisionTree(req)
|
|
|
|
if result.GPAIResult.IsGPAI {
|
|
t.Error("Expected IsGPAI to be false")
|
|
}
|
|
if result.GPAIResult.Category != GPAICategoryNone {
|
|
t.Errorf("Expected category 'none', got '%s'", result.GPAIResult.Category)
|
|
}
|
|
if len(result.GPAIResult.Obligations) != 0 {
|
|
t.Errorf("Expected 0 GPAI obligations, got %d", len(result.GPAIResult.Obligations))
|
|
}
|
|
}
|
|
|
|
func TestAnswerIsYes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
answers map[string]DecisionTreeAnswer
|
|
qID string
|
|
expected bool
|
|
}{
|
|
{"yes answer", map[string]DecisionTreeAnswer{"Q1": {Value: true}}, "Q1", true},
|
|
{"no answer", map[string]DecisionTreeAnswer{"Q1": {Value: false}}, "Q1", false},
|
|
{"missing answer", map[string]DecisionTreeAnswer{}, "Q1", false},
|
|
{"different question", map[string]DecisionTreeAnswer{"Q2": {Value: true}}, "Q1", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := answerIsYes(tt.answers, tt.qID)
|
|
if result != tt.expected {
|
|
t.Errorf("Expected %v, got %v", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|