package training import ( "encoding/json" "testing" ) // ============================================================================= // parseNarratorScript Tests // ============================================================================= func TestParseNarratorScript_ValidJSON(t *testing.T) { input := `{ "title": "DSGVO Grundlagen", "intro": "Hallo, ich bin Ihr AI Teacher.", "sections": [ { "heading": "Einfuehrung", "narrator_text": "Willkommen zur Schulung ueber die DSGVO.", "bullet_points": ["Punkt 1", "Punkt 2"], "transition": "Bevor wir fortfahren...", "checkpoint": { "title": "Checkpoint 1", "questions": [ { "question": "Was ist die DSGVO?", "options": ["EU-Verordnung", "Bundesgesetz", "Landesgesetz", "Internationale Konvention"], "correct_index": 0, "explanation": "Die DSGVO ist eine EU-Verordnung." } ] } } ], "outro": "Vielen Dank fuer Ihre Aufmerksamkeit.", "total_duration_estimate": 600 }` script, err := parseNarratorScript(input) if err != nil { t.Fatalf("Expected no error, got %v", err) } if script.Title != "DSGVO Grundlagen" { t.Errorf("Expected title 'DSGVO Grundlagen', got '%s'", script.Title) } if script.Intro != "Hallo, ich bin Ihr AI Teacher." { t.Errorf("Expected intro text, got '%s'", script.Intro) } if len(script.Sections) != 1 { t.Fatalf("Expected 1 section, got %d", len(script.Sections)) } if script.Sections[0].Heading != "Einfuehrung" { t.Errorf("Expected heading 'Einfuehrung', got '%s'", script.Sections[0].Heading) } if script.Sections[0].Checkpoint == nil { t.Fatal("Expected checkpoint, got nil") } if len(script.Sections[0].Checkpoint.Questions) != 1 { t.Fatalf("Expected 1 question, got %d", len(script.Sections[0].Checkpoint.Questions)) } if script.Sections[0].Checkpoint.Questions[0].CorrectIndex != 0 { t.Errorf("Expected correct_index 0, got %d", script.Sections[0].Checkpoint.Questions[0].CorrectIndex) } if script.Outro != "Vielen Dank fuer Ihre Aufmerksamkeit." { t.Errorf("Expected outro text, got '%s'", script.Outro) } if script.TotalDurationEstimate != 600 { t.Errorf("Expected 600 seconds estimate, got %d", script.TotalDurationEstimate) } } func TestParseNarratorScript_WithSurroundingText(t *testing.T) { input := `Here is the narrator script: { "title": "NIS-2 Schulung", "intro": "Willkommen", "sections": [ { "heading": "Abschnitt 1", "narrator_text": "Text hier.", "bullet_points": ["BP1"], "transition": "Weiter" } ], "outro": "Ende", "total_duration_estimate": 300 } I hope this helps!` script, err := parseNarratorScript(input) if err != nil { t.Fatalf("Expected no error, got %v", err) } if script.Title != "NIS-2 Schulung" { t.Errorf("Expected title 'NIS-2 Schulung', got '%s'", script.Title) } } func TestParseNarratorScript_InvalidJSON(t *testing.T) { _, err := parseNarratorScript("not valid json") if err == nil { t.Error("Expected error for invalid JSON") } } func TestParseNarratorScript_NoSections(t *testing.T) { input := `{"title": "Test", "intro": "Hi", "sections": [], "outro": "Bye", "total_duration_estimate": 0}` _, err := parseNarratorScript(input) if err == nil { t.Error("Expected error for empty sections") } } func TestParseNarratorScript_NoJSON(t *testing.T) { _, err := parseNarratorScript("Just plain text without any JSON") if err == nil { t.Error("Expected error when no JSON object found") } } func TestParseNarratorScript_SectionWithoutCheckpoint(t *testing.T) { input := `{ "title": "Test", "intro": "Hi", "sections": [ { "heading": "Section 1", "narrator_text": "Some text", "bullet_points": ["P1"], "transition": "Next" } ], "outro": "Bye", "total_duration_estimate": 180 }` script, err := parseNarratorScript(input) if err != nil { t.Fatalf("Expected no error, got %v", err) } if script.Sections[0].Checkpoint != nil { t.Error("Section without checkpoint definition should have nil Checkpoint") } } func TestParseNarratorScript_MultipleSectionsWithCheckpoints(t *testing.T) { input := `{ "title": "Multi-Section", "intro": "Start", "sections": [ { "heading": "S1", "narrator_text": "Text 1", "bullet_points": [], "transition": "T1", "checkpoint": { "title": "CP1", "questions": [ {"question": "Q1?", "options": ["A", "B", "C", "D"], "correct_index": 0, "explanation": "E1"}, {"question": "Q2?", "options": ["A", "B", "C", "D"], "correct_index": 1, "explanation": "E2"} ] } }, { "heading": "S2", "narrator_text": "Text 2", "bullet_points": ["BP"], "transition": "T2", "checkpoint": { "title": "CP2", "questions": [ {"question": "Q3?", "options": ["A", "B", "C", "D"], "correct_index": 2, "explanation": "E3"} ] } }, { "heading": "S3", "narrator_text": "Text 3", "bullet_points": [], "transition": "T3" } ], "outro": "End", "total_duration_estimate": 900 }` script, err := parseNarratorScript(input) if err != nil { t.Fatalf("Expected no error, got %v", err) } if len(script.Sections) != 3 { t.Fatalf("Expected 3 sections, got %d", len(script.Sections)) } if script.Sections[0].Checkpoint == nil { t.Error("Section 0 should have a checkpoint") } if len(script.Sections[0].Checkpoint.Questions) != 2 { t.Errorf("Section 0 checkpoint should have 2 questions, got %d", len(script.Sections[0].Checkpoint.Questions)) } if script.Sections[1].Checkpoint == nil { t.Error("Section 1 should have a checkpoint") } if script.Sections[2].Checkpoint != nil { t.Error("Section 2 should not have a checkpoint") } } // ============================================================================= // countCheckpoints Tests // ============================================================================= func TestCountCheckpoints_WithCheckpoints(t *testing.T) { script := &NarratorScript{ Sections: []NarratorSection{ {Checkpoint: &CheckpointDefinition{Title: "CP1"}}, {Checkpoint: nil}, {Checkpoint: &CheckpointDefinition{Title: "CP3"}}, }, } count := countCheckpoints(script) if count != 2 { t.Errorf("Expected 2 checkpoints, got %d", count) } } func TestCountCheckpoints_NoCheckpoints(t *testing.T) { script := &NarratorScript{ Sections: []NarratorSection{ {Heading: "S1"}, {Heading: "S2"}, }, } count := countCheckpoints(script) if count != 0 { t.Errorf("Expected 0 checkpoints, got %d", count) } } func TestCountCheckpoints_EmptySections(t *testing.T) { script := &NarratorScript{} count := countCheckpoints(script) if count != 0 { t.Errorf("Expected 0 checkpoints, got %d", count) } } // ============================================================================= // NarratorScript JSON Serialization Tests // ============================================================================= func TestNarratorScript_JSONRoundTrip(t *testing.T) { original := NarratorScript{ Title: "Test", Intro: "Hello", Sections: []NarratorSection{ { Heading: "H1", NarratorText: "NT1", BulletPoints: []string{"BP1"}, Transition: "T1", Checkpoint: &CheckpointDefinition{ Title: "CP1", Questions: []CheckpointQuestion{ { Question: "Q?", Options: []string{"A", "B", "C", "D"}, CorrectIndex: 2, Explanation: "C is correct", }, }, }, }, }, Outro: "Bye", TotalDurationEstimate: 600, } data, err := json.Marshal(original) if err != nil { t.Fatalf("Marshal failed: %v", err) } var decoded NarratorScript if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("Unmarshal failed: %v", err) } if decoded.Title != original.Title { t.Errorf("Title mismatch: %s != %s", decoded.Title, original.Title) } if len(decoded.Sections) != 1 { t.Fatalf("Expected 1 section, got %d", len(decoded.Sections)) } if decoded.Sections[0].Checkpoint == nil { t.Fatal("Checkpoint should not be nil after round-trip") } if decoded.Sections[0].Checkpoint.Questions[0].CorrectIndex != 2 { t.Errorf("CorrectIndex mismatch: got %d", decoded.Sections[0].Checkpoint.Questions[0].CorrectIndex) } } // ============================================================================= // InteractiveVideoManifest Tests // ============================================================================= func TestInteractiveVideoManifest_JSON(t *testing.T) { manifest := InteractiveVideoManifest{ StreamURL: "https://example.com/video.mp4", Checkpoints: []CheckpointManifestEntry{ { Index: 0, Title: "CP1", TimestampSeconds: 180.5, Questions: []CheckpointQuestion{ { Question: "Q?", Options: []string{"A", "B", "C", "D"}, CorrectIndex: 1, Explanation: "B", }, }, }, }, } data, err := json.Marshal(manifest) if err != nil { t.Fatalf("Marshal failed: %v", err) } var decoded InteractiveVideoManifest if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("Unmarshal failed: %v", err) } if len(decoded.Checkpoints) != 1 { t.Fatalf("Expected 1 checkpoint, got %d", len(decoded.Checkpoints)) } if decoded.Checkpoints[0].TimestampSeconds != 180.5 { t.Errorf("Timestamp mismatch: got %f", decoded.Checkpoints[0].TimestampSeconds) } } // ============================================================================= // SubmitCheckpointQuizRequest/Response Tests // ============================================================================= func TestSubmitCheckpointQuizResponse_JSON(t *testing.T) { resp := SubmitCheckpointQuizResponse{ Passed: true, Score: 80.0, Feedback: []CheckpointQuizFeedback{ {Question: "Q1?", Correct: true, Explanation: "Correct!"}, {Question: "Q2?", Correct: false, Explanation: "Wrong answer."}, }, } data, err := json.Marshal(resp) if err != nil { t.Fatalf("Marshal failed: %v", err) } var decoded SubmitCheckpointQuizResponse if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("Unmarshal failed: %v", err) } if !decoded.Passed { t.Error("Expected passed=true") } if decoded.Score != 80.0 { t.Errorf("Expected score 80.0, got %f", decoded.Score) } if len(decoded.Feedback) != 2 { t.Fatalf("Expected 2 feedback items, got %d", len(decoded.Feedback)) } if decoded.Feedback[1].Correct { t.Error("Second feedback should be incorrect") } } // ============================================================================= // narratorSystemPrompt Tests // ============================================================================= func TestNarratorSystemPrompt_ContainsKeyPhrases(t *testing.T) { if !containsSubstring(narratorSystemPrompt, "AI Teacher") { t.Error("System prompt should mention AI Teacher") } if !containsSubstring(narratorSystemPrompt, "Checkpoint") { t.Error("System prompt should mention Checkpoint") } if !containsSubstring(narratorSystemPrompt, "JSON") { t.Error("System prompt should mention JSON format") } if !containsSubstring(narratorSystemPrompt, "correct_index") { t.Error("System prompt should mention correct_index") } } // ============================================================================= // Checkpoint Grading Logic Tests (User Journey: Learner scores quiz) // ============================================================================= func TestCheckpointGrading_AllCorrect_ScoreIs100(t *testing.T) { questions := []CheckpointQuestion{ {Question: "Q1?", Options: []string{"A", "B", "C", "D"}, CorrectIndex: 0}, {Question: "Q2?", Options: []string{"A", "B", "C", "D"}, CorrectIndex: 1}, {Question: "Q3?", Options: []string{"A", "B", "C", "D"}, CorrectIndex: 2}, } answers := []int{0, 1, 2} correctCount := 0 for i, q := range questions { if i < len(answers) && answers[i] == q.CorrectIndex { correctCount++ } } score := float64(correctCount) / float64(len(questions)) * 100 passed := score >= 70 if score != 100.0 { t.Errorf("Expected score 100, got %f", score) } if !passed { t.Error("Expected passed=true with 100% score") } } func TestCheckpointGrading_NoneCorrect_ScoreIs0(t *testing.T) { questions := []CheckpointQuestion{ {Question: "Q1?", Options: []string{"A", "B", "C", "D"}, CorrectIndex: 0}, {Question: "Q2?", Options: []string{"A", "B", "C", "D"}, CorrectIndex: 1}, {Question: "Q3?", Options: []string{"A", "B", "C", "D"}, CorrectIndex: 2}, } answers := []int{3, 3, 3} correctCount := 0 for i, q := range questions { if i < len(answers) && answers[i] == q.CorrectIndex { correctCount++ } } score := float64(correctCount) / float64(len(questions)) * 100 passed := score >= 70 if score != 0.0 { t.Errorf("Expected score 0, got %f", score) } if passed { t.Error("Expected passed=false with 0% score") } } func TestCheckpointGrading_ExactlyAt70Percent_Passes(t *testing.T) { // 7 out of 10 correct = 70% — exactly at threshold questions := make([]CheckpointQuestion, 10) answers := make([]int, 10) for i := 0; i < 10; i++ { questions[i] = CheckpointQuestion{ Question: "Q?", Options: []string{"A", "B", "C", "D"}, CorrectIndex: 0, } if i < 7 { answers[i] = 0 // correct } else { answers[i] = 1 // wrong } } correctCount := 0 for i, q := range questions { if i < len(answers) && answers[i] == q.CorrectIndex { correctCount++ } } score := float64(correctCount) / float64(len(questions)) * 100 passed := score >= 70 if score != 70.0 { t.Errorf("Expected score 70, got %f", score) } if !passed { t.Error("Expected passed=true at exactly 70%") } } func TestCheckpointGrading_JustBelow70Percent_Fails(t *testing.T) { // 2 out of 3 correct = 66.67% — below threshold questions := []CheckpointQuestion{ {Question: "Q1?", Options: []string{"A", "B", "C", "D"}, CorrectIndex: 0}, {Question: "Q2?", Options: []string{"A", "B", "C", "D"}, CorrectIndex: 1}, {Question: "Q3?", Options: []string{"A", "B", "C", "D"}, CorrectIndex: 2}, } answers := []int{0, 1, 3} // 2 correct, 1 wrong correctCount := 0 for i, q := range questions { if i < len(answers) && answers[i] == q.CorrectIndex { correctCount++ } } score := float64(correctCount) / float64(len(questions)) * 100 passed := score >= 70 if passed { t.Errorf("Expected passed=false at %.2f%%", score) } } func TestCheckpointGrading_FewerAnswersThanQuestions_MarksUnansweredWrong(t *testing.T) { questions := []CheckpointQuestion{ {Question: "Q1?", Options: []string{"A", "B", "C", "D"}, CorrectIndex: 0}, {Question: "Q2?", Options: []string{"A", "B", "C", "D"}, CorrectIndex: 1}, {Question: "Q3?", Options: []string{"A", "B", "C", "D"}, CorrectIndex: 2}, } answers := []int{0} // Only 1 answer for 3 questions correctCount := 0 for i, q := range questions { if i < len(answers) && answers[i] == q.CorrectIndex { correctCount++ } } if correctCount != 1 { t.Errorf("Expected 1 correct, got %d", correctCount) } score := float64(correctCount) / float64(len(questions)) * 100 if score > 34 { t.Errorf("Expected score ~33.3%%, got %f", score) } } func TestCheckpointGrading_EmptyAnswers_AllWrong(t *testing.T) { questions := []CheckpointQuestion{ {Question: "Q1?", Options: []string{"A", "B"}, CorrectIndex: 0}, {Question: "Q2?", Options: []string{"A", "B"}, CorrectIndex: 1}, } answers := []int{} correctCount := 0 for i, q := range questions { if i < len(answers) && answers[i] == q.CorrectIndex { correctCount++ } } if correctCount != 0 { t.Errorf("Expected 0 correct with empty answers, got %d", correctCount) } } // ============================================================================= // Feedback Generation Tests (User Journey: Learner sees feedback) // ============================================================================= func TestCheckpointFeedback_CorrectAnswerGetsCorrectFlag(t *testing.T) { questions := []CheckpointQuestion{ {Question: "Was ist DSGVO?", Options: []string{"EU-Verordnung", "Bundesgesetz"}, CorrectIndex: 0, Explanation: "EU-Verordnung"}, {Question: "Wer ist DSB?", Options: []string{"IT-Leiter", "Datenschutzbeauftragter"}, CorrectIndex: 1, Explanation: "DSB Rolle"}, } answers := []int{0, 0} // First correct, second wrong feedback := make([]CheckpointQuizFeedback, len(questions)) for i, q := range questions { isCorrect := false if i < len(answers) && answers[i] == q.CorrectIndex { isCorrect = true } feedback[i] = CheckpointQuizFeedback{ Question: q.Question, Correct: isCorrect, Explanation: q.Explanation, } } if !feedback[0].Correct { t.Error("First answer should be marked correct") } if feedback[1].Correct { t.Error("Second answer should be marked incorrect") } if feedback[0].Question != "Was ist DSGVO?" { t.Errorf("Unexpected question text: %s", feedback[0].Question) } if feedback[1].Explanation != "DSB Rolle" { t.Errorf("Explanation should be preserved: got %s", feedback[1].Explanation) } } // ============================================================================= // NarratorScript Pipeline Tests (User Journey: Admin generates video) // ============================================================================= func TestNarratorScript_SectionCounting(t *testing.T) { tests := []struct { name string sectionCount int checkpointCount int }{ {"3 sections, all with checkpoints", 3, 3}, {"4 sections, 2 with checkpoints", 4, 2}, {"1 section, no checkpoint", 1, 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { sections := make([]NarratorSection, tt.sectionCount) cpAdded := 0 for i := 0; i < tt.sectionCount; i++ { sections[i] = NarratorSection{ Heading: "Section", NarratorText: "Text", BulletPoints: []string{}, Transition: "Next", } if cpAdded < tt.checkpointCount { sections[i].Checkpoint = &CheckpointDefinition{ Title: "CP", Questions: []CheckpointQuestion{{Question: "Q?", Options: []string{"A", "B"}, CorrectIndex: 0}}, } cpAdded++ } } script := &NarratorScript{ Title: "Test", Intro: "Hi", Sections: sections, Outro: "Bye", } if len(script.Sections) != tt.sectionCount { t.Errorf("Expected %d sections, got %d", tt.sectionCount, len(script.Sections)) } if countCheckpoints(script) != tt.checkpointCount { t.Errorf("Expected %d checkpoints, got %d", tt.checkpointCount, countCheckpoints(script)) } }) } } func TestNarratorScript_SectionAudioConversion(t *testing.T) { // Verify NarratorSection can be converted to SectionAudio for TTS sections := []NarratorSection{ {Heading: "Einleitung", NarratorText: "Willkommen zur Schulung."}, {Heading: "Hauptteil", NarratorText: "Hier lernen Sie die Grundlagen."}, } audioSections := make([]SectionAudio, len(sections)) for i, s := range sections { audioSections[i] = SectionAudio{ Text: s.NarratorText, Heading: s.Heading, } } if len(audioSections) != 2 { t.Fatalf("Expected 2 audio sections, got %d", len(audioSections)) } if audioSections[0].Heading != "Einleitung" { t.Errorf("Expected heading 'Einleitung', got '%s'", audioSections[0].Heading) } if audioSections[1].Text != "Hier lernen Sie die Grundlagen." { t.Errorf("Unexpected text: '%s'", audioSections[1].Text) } } // ============================================================================= // InteractiveVideoManifest Progress Tests (User Journey: Learner resumes) // ============================================================================= func TestManifest_IdentifiesNextUnpassedCheckpoint(t *testing.T) { manifest := InteractiveVideoManifest{ StreamURL: "https://example.com/video.mp4", Checkpoints: []CheckpointManifestEntry{ {Index: 0, Title: "CP1", TimestampSeconds: 180, Progress: &CheckpointProgress{Passed: true}}, {Index: 1, Title: "CP2", TimestampSeconds: 360, Progress: &CheckpointProgress{Passed: false}}, {Index: 2, Title: "CP3", TimestampSeconds: 540, Progress: nil}, }, } var nextUnpassed *CheckpointManifestEntry for i := range manifest.Checkpoints { cp := &manifest.Checkpoints[i] if cp.Progress == nil || !cp.Progress.Passed { nextUnpassed = cp break } } if nextUnpassed == nil { t.Fatal("Expected to find an unpassed checkpoint") } if nextUnpassed.Index != 1 { t.Errorf("Expected next unpassed at index 1, got %d", nextUnpassed.Index) } if nextUnpassed.Title != "CP2" { t.Errorf("Expected CP2, got %s", nextUnpassed.Title) } } func TestManifest_AllCheckpointsPassed(t *testing.T) { manifest := InteractiveVideoManifest{ Checkpoints: []CheckpointManifestEntry{ {Index: 0, Progress: &CheckpointProgress{Passed: true}}, {Index: 1, Progress: &CheckpointProgress{Passed: true}}, }, } allPassed := true for _, cp := range manifest.Checkpoints { if cp.Progress == nil || !cp.Progress.Passed { allPassed = false break } } if !allPassed { t.Error("Expected all checkpoints to be passed") } } func TestManifest_NoCheckpoints_AllPassedIsTrue(t *testing.T) { manifest := InteractiveVideoManifest{ Checkpoints: []CheckpointManifestEntry{}, } allPassed := true for _, cp := range manifest.Checkpoints { if cp.Progress == nil || !cp.Progress.Passed { allPassed = false break } } if !allPassed { t.Error("Empty checkpoint list should be considered all-passed") } } func TestManifest_SeekProtection_BlocksSkippingPastUnpassed(t *testing.T) { // Simulates seek protection logic from InteractiveVideoPlayer checkpoints := []CheckpointManifestEntry{ {Index: 0, TimestampSeconds: 180, Progress: &CheckpointProgress{Passed: true}}, {Index: 1, TimestampSeconds: 360, Progress: nil}, // Not yet attempted {Index: 2, TimestampSeconds: 540, Progress: nil}, } seekTarget := 500.0 // User tries to seek to 500s // Find first unpassed checkpoint var firstUnpassed *CheckpointManifestEntry for i := range checkpoints { if checkpoints[i].Progress == nil || !checkpoints[i].Progress.Passed { firstUnpassed = &checkpoints[i] break } } blocked := false if firstUnpassed != nil && seekTarget > firstUnpassed.TimestampSeconds { blocked = true } if !blocked { t.Error("Seek past unpassed checkpoint should be blocked") } if firstUnpassed.TimestampSeconds != 360 { t.Errorf("Expected block at 360s, got %f", firstUnpassed.TimestampSeconds) } } func TestManifest_SeekProtection_AllowsSeekBeforeFirstUnpassed(t *testing.T) { checkpoints := []CheckpointManifestEntry{ {Index: 0, TimestampSeconds: 180, Progress: &CheckpointProgress{Passed: true}}, {Index: 1, TimestampSeconds: 360, Progress: nil}, } seekTarget := 200.0 // User seeks to 200s — before unpassed checkpoint at 360s var firstUnpassed *CheckpointManifestEntry for i := range checkpoints { if checkpoints[i].Progress == nil || !checkpoints[i].Progress.Passed { firstUnpassed = &checkpoints[i] break } } blocked := false if firstUnpassed != nil && seekTarget > firstUnpassed.TimestampSeconds { blocked = true } if blocked { t.Error("Seek before unpassed checkpoint should be allowed") } }