using LehrerApp.Core.AiPlanning; using LehrerApp.Core.Models; using LehrerApp.Desktop.Services; using LehrerApp.Desktop.ViewModels.Groups; using System.Net; using System.Text; using Xunit; namespace LehrerApp.Desktop.Tests; /// Tests für die reinen (nur Repository-Lesezugriff, kein Netzwerk) Teile von AiPlanningService: /// BuildContext (Export) und ApplyResponse (Import). Login/GetBalanceAsync/RequestPlanAsync /// brauchen einen echten HTTP-Endpunkt und sind nicht Teil dieser Tests (siehe Planungsdokument, /// Abschnitt "Nicht ohne echtes Deployment ... verifizierbar"). public sealed class AiPlanningServiceTests { private static AiPlanningService Build(FakeLessons lessons, FakeGroups groups, FakeSubjects subjects, FakeCompetencyDomains competencyDomains, FakeAlternativeLessonPaths altPaths) => new(new HttpClient(), lessons, groups, subjects, competencyDomains, altPaths); [Fact] public void ParsePlanningLessons_AkzeptiertNormaleAntwortUndDeutschesDatum() { const string json = """ {"lessons":[{"id":null,"date":"15.09.2026","lessonNumber":2,"topic":"Redox","startTime":"08:35","phases":[]}],"summary":"ok"} """; var lesson = Assert.Single(AiPlanningService.ParsePlanningLessons(json)); Assert.Equal("Redox", lesson.Topic); Assert.Equal(new DateOnly(2026, 9, 15), lesson.Date); Assert.Equal(new TimeOnly(8, 35), lesson.StartTime); } [Fact] public void ParsePlanningLessons_AkzeptiertEinzelneMarkierteStunde() { const string json = """ {"topic":"Nur diese Stunde","phases":[{"name":"Einstieg","durationMinutes":5,"activity":"Impuls","material":"Bild","shorthand":"UG"}]} """; var lesson = Assert.Single(AiPlanningService.ParsePlanningLessons(json)); Assert.Equal("Nur diese Stunde", lesson.Topic); Assert.Single(lesson.Phases); } [Fact] public void ParsePlanningLessons_UngueltigeMarkierung_LiefertVerstaendlichenFehler() { var error = Assert.Throws(() => AiPlanningService.ParsePlanningLessons("Hier kommt das JSON: { kaputt }")); Assert.Contains("noch kein gültiges Stunden-JSON", error.Message); } [Fact] public async Task RequestPlanAsync_Backendfehler_BewahrtOriginalantwortFuerRettung() { const string body = """ {"error":"Kein gültiges JSON.","rawResponse":"Vorspann\n{kaputt}"} """; var service = BuildWithResponse(HttpStatusCode.BadGateway, body); var error = await Assert.ThrowsAsync(() => service.RequestPlanAsync(new Unit(), "", "token")); Assert.Equal("Vorspann\n{kaputt}", error.RawResponse); } [Fact] public async Task RequestPlanAsync_Typfehler_BewahrtEingebetteteOriginalantwortFuerRettung() { const string body = """ {"lessons":[{"topic":"Test","date":"kein Datum"}],"rawResponse":"DIE ORIGINALANTWORT"} """; var service = BuildWithResponse(HttpStatusCode.OK, body); var error = await Assert.ThrowsAsync(() => service.RequestPlanAsync(new Unit(), "", "token")); Assert.Equal("DIE ORIGINALANTWORT", error.RawResponse); } [Fact] public async Task RequestPlanAsync_Zeitueberschreitung_LiefertKonkreteProxyMeldung() { var http = new HttpClient(new CanceledResponseHandler()) { BaseAddress = new Uri("https://example.invalid/"), }; var service = new AiPlanningService(http, new FakeLessons(), new FakeGroups([]), new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); var error = await Assert.ThrowsAsync(() => service.RequestPlanAsync(new Unit(), "", "token")); Assert.Contains("3½ Minuten", error.Message); Assert.Contains("Webserver-Proxys", error.Message); } [Fact] public void Rettungsdialog_ImportiertMarkierteStundeAlsNeu() { var group = new LearningGroup(); var unit = new Unit { GroupId = group.Id, Title = "T" }; var lessons = new FakeLessons(); var service = Build(lessons, new FakeGroups([group]), new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); var vm = new AiResponseRescueDialogViewModel(service, lessons, unit, "Rohantwort"); vm.ParseSelection("""{"topic":"Gerettete Stunde","phases":[]}"""); vm.ImportAsNewCommand.Execute(null); Assert.True(vm.Result); Assert.Equal("Gerettete Stunde", Assert.Single(lessons.GetByUnit(unit.Id)).Topic); } [Fact] public async Task RequestUntisStatusSuggestionsAsync_PassendeIds_LiefertZuordnung() { const string body = """ {"suggestions":[{"id":"0","status":"Excused"},{"id":"1","status":"Unexcused"}]} """; var service = BuildWithResponse(HttpStatusCode.OK, body); var rows = new[] { new AiUntisStatusRow { Id = "0", CurrentGuess = "ExcusePending" }, new AiUntisStatusRow { Id = "1", CurrentGuess = "ExcusePending" }, }; var suggestions = await service.RequestUntisStatusSuggestionsAsync(rows, "token"); Assert.Equal("Excused", suggestions["0"]); Assert.Equal("Unexcused", suggestions["1"]); } // Kernabsicherung gegen Verwechslung (Nutzer-Feedback zum Untis-Hub, siehe // AiPlanningService.RequestUntisStatusSuggestionsAsync): weicht die zurückgegebene Id-Menge // auch nur minimal von der gesendeten ab (hier: eine erfundene Id "2" statt "1"), wird die // gesamte Antwort verworfen statt sich auf eine möglicherweise vermischte Zuordnung zu verlassen. [Fact] public async Task RequestUntisStatusSuggestionsAsync_AbweichendeIdMenge_VerwirftKomplett() { const string body = """ {"suggestions":[{"id":"0","status":"Excused"},{"id":"2","status":"Unexcused"}]} """; var service = BuildWithResponse(HttpStatusCode.OK, body); var rows = new[] { new AiUntisStatusRow { Id = "0", CurrentGuess = "ExcusePending" }, new AiUntisStatusRow { Id = "1", CurrentGuess = "ExcusePending" }, }; var suggestions = await service.RequestUntisStatusSuggestionsAsync(rows, "token"); Assert.Empty(suggestions); } [Fact] public async Task RequestUntisStatusSuggestionsAsync_DoppelteId_VerwirftKomplett() { const string body = """ {"suggestions":[{"id":"0","status":"Excused"},{"id":"0","status":"Unexcused"}]} """; var service = BuildWithResponse(HttpStatusCode.OK, body); var rows = new[] { new AiUntisStatusRow { Id = "0", CurrentGuess = "ExcusePending" } }; var suggestions = await service.RequestUntisStatusSuggestionsAsync(rows, "token"); Assert.Empty(suggestions); } [Fact] public async Task RequestUntisStatusSuggestionsAsync_KeineZeilen_KeinNetzwerkaufruf() { var service = Build(new FakeLessons(), new FakeGroups([]), new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); var suggestions = await service.RequestUntisStatusSuggestionsAsync([], "token"); Assert.Empty(suggestions); } private static AiPlanningService BuildWithResponse(HttpStatusCode status, string body) { var http = new HttpClient(new StaticResponseHandler(status, body)) { BaseAddress = new Uri("https://example.invalid/"), }; return new AiPlanningService(http, new FakeLessons(), new FakeGroups([]), new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); } private sealed class StaticResponseHandler(HttpStatusCode status, string body) : HttpMessageHandler { protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => Task.FromResult(new HttpResponseMessage(status) { Content = new StringContent(body, Encoding.UTF8, "application/json"), }); } private sealed class CanceledResponseHandler : HttpMessageHandler { protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => Task.FromCanceled( new CancellationToken(canceled: true)); } [Fact] public void BuildContext_FuelltGruppenUndFachKontext() { var subject = new Subject { Name = "Chemie" }; var group = new LearningGroup { Name = "9c", SubjectId = subject.Id, GradeLevel = 9 }; var unit = new Unit { GroupId = group.Id, Title = "Redoxreaktionen" }; var service = Build(new FakeLessons(), new FakeGroups([group]), new FakeSubjects([subject]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); var context = service.BuildContext(unit, "Testanweisung"); Assert.Equal(unit.Id, context.Id); Assert.Equal("Redoxreaktionen", context.Title); Assert.Equal("Chemie", context.SubjectName); Assert.Equal(9, context.GradeLevel); Assert.Equal("9c", context.GroupName); } [Fact] public void BuildContext_FiltertKompetenzkatalogNachFachUndStufe() { var subject = new Subject { Name = "Chemie" }; var group = new LearningGroup { SubjectId = subject.Id, GradeLevel = 9 }; var unit = new Unit { GroupId = group.Id, Title = "T" }; var domains = new FakeCompetencyDomains(); domains.Add(new CompetencyDomain { SubjectId = subject.Id, GradeLevel = 9, Name = "Chemische Reaktionen", Items = [new CompetencyItem { Code = "C1", Description = "Redoxreaktionen erklären" }], }); var service = Build(new FakeLessons(), new FakeGroups([group]), new FakeSubjects([subject]), domains, new FakeAlternativeLessonPaths([])); var context = service.BuildContext(unit, ""); var catalogDomain = Assert.Single(context.CompetencyCatalog); Assert.Equal("Chemische Reaktionen", catalogDomain.Name); var item = Assert.Single(catalogDomain.Items); Assert.Equal("C1", item.Code); Assert.Equal("Redoxreaktionen erklären", item.Description); } [Fact] public void BuildContext_EnthaeltVorhandeneLessonsMitEchtenIds() { var group = new LearningGroup(); var unit = new Unit { GroupId = group.Id, Title = "T" }; var lesson = new Lesson { UnitId = unit.Id, GroupId = group.Id, Topic = "Erste Stunde" }; var lessons = new FakeLessons(); lessons.Add(lesson); var service = Build(lessons, new FakeGroups([group]), new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); var context = service.BuildContext(unit, ""); var aiLesson = Assert.Single(context.Lessons); Assert.Equal(lesson.Id, aiLesson.Id); Assert.Equal("Erste Stunde", aiLesson.Topic); } [Fact] public void BuildContext_LoestAlternativePathNameAuf() { var group = new LearningGroup(); var unit = new Unit { GroupId = group.Id, Title = "T" }; var path = new AlternativeLessonPath { Name = "Vertiefung" }; var lesson = new Lesson { UnitId = unit.Id, GroupId = group.Id, Topic = "Stunde", Phases = [new LessonPhaseStep { Name = "Erarbeitung", AlternativePathId = path.Id }], }; var lessons = new FakeLessons(); lessons.Add(lesson); var service = Build(lessons, new FakeGroups([group]), new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([path])); var context = service.BuildContext(unit, ""); var phase = Assert.Single(Assert.Single(context.Lessons).Phases); Assert.Equal("Vertiefung", phase.AlternativePathName); Assert.Contains(context.AlternativePathCatalog, p => p.Name == "Vertiefung" && p.Id == path.Id); } [Fact] public void BuildContext_DraftOverride_ErsetztBestehendeLessonMitGleicherId() { var group = new LearningGroup(); var unit = new Unit { GroupId = group.Id, Title = "T" }; var existing = new Lesson { UnitId = unit.Id, GroupId = group.Id, Topic = "Alter Titel" }; var lessons = new FakeLessons(); lessons.Add(existing); var service = Build(lessons, new FakeGroups([group]), new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); var draft = new List { new() { Id = existing.Id, Topic = "Neuer Entwurfstitel", Phases = [] } }; var context = service.BuildContext(unit, "", draft); var aiLesson = Assert.Single(context.Lessons); Assert.Equal(existing.Id, aiLesson.Id); Assert.Equal("Neuer Entwurfstitel", aiLesson.Topic); } [Fact] public void BuildContext_DraftOverride_HaengtNeueEntwurfLessonsOhneIdAn() { var group = new LearningGroup(); var unit = new Unit { GroupId = group.Id, Title = "T" }; var existing = new Lesson { UnitId = unit.Id, GroupId = group.Id, Topic = "Bestehend" }; var lessons = new FakeLessons(); lessons.Add(existing); var service = Build(lessons, new FakeGroups([group]), new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); var draft = new List { new() { Id = null, Topic = "Neu aus Runde 1", Phases = [] } }; var context = service.BuildContext(unit, "", draft); Assert.Equal(2, context.Lessons.Count); Assert.Contains(context.Lessons, l => l.Topic == "Bestehend"); Assert.Contains(context.Lessons, l => l.Topic == "Neu aus Runde 1" && l.Id == null); } [Fact] public void DescribeChanges_ErkenntNurTatsaechlichGeaenderteFelder() { var existing = new Lesson { Topic = "Alt", Date = new DateOnly(2026, 3, 10), StartTime = new TimeOnly(8, 0), Homework = "Alte HA", Reflection = "Alte Reflexion", }; var proposed = new AiLesson { Id = existing.Id, Topic = "Neu", Date = new DateOnly(2026, 3, 17), StartTime = new TimeOnly(8, 0), Homework = "Alte HA", Reflection = "Neue Reflexion", Phases = [], }; var service = Build(new FakeLessons(), new FakeGroups([]), new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); var diffs = service.DescribeChanges(existing, proposed); Assert.Contains(diffs, d => d.Contains("Thema")); Assert.Contains(diffs, d => d.Contains("Datum")); Assert.Contains(diffs, d => d.Contains("Reflexion")); Assert.DoesNotContain(diffs, d => d.Contains("Beginn")); Assert.DoesNotContain(diffs, d => d.Contains("Hausaufgabe")); } [Fact] public void DescribeChanges_IdentischeLesson_LiefertKeineUnterschiede() { var existing = new Lesson { Topic = "Gleich", Date = new DateOnly(2026, 3, 10) }; var proposed = new AiLesson { Id = existing.Id, Topic = "Gleich", Date = new DateOnly(2026, 3, 10), Phases = [] }; var service = Build(new FakeLessons(), new FakeGroups([]), new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); Assert.Empty(service.DescribeChanges(existing, proposed)); } [Fact] public void DescribeChanges_UnterschiedlicheAnzahlPhasen_WirdAlsVerlaufsplanAenderungErkannt() { var existing = new Lesson { Topic = "T", Phases = [new LessonPhaseStep { Name = "Einstieg", DurationMinutes = 10 }], }; var proposed = new AiLesson { Id = existing.Id, Topic = "T", Phases = [ new AiPhaseStep { Name = "Einstieg", DurationMinutes = 10 }, new AiPhaseStep { Name = "Erarbeitung", DurationMinutes = 20 }, ], }; var service = Build(new FakeLessons(), new FakeGroups([]), new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); Assert.Contains(service.DescribeChanges(existing, proposed), d => d.Contains("Verlaufsplan")); } [Fact] public void BuildMaterialPrompt_EnthaeltKontextUndVorschlag() { var subject = new Subject { Name = "Chemie" }; var group = new LearningGroup { Name = "9c", SubjectId = subject.Id, GradeLevel = 9 }; var unit = new Unit { GroupId = group.Id, Title = "Redoxreaktionen" }; var lesson = new AiLesson { Topic = "Elektrolyse", Date = new DateOnly(2026, 3, 17), Phases = [] }; var phase = new AiPhaseStep { Name = "Erarbeitung", DurationMinutes = 20, Activity = "Experiment auswerten", Material = "AB003", MaterialSuggestion = "Tafelbild mit dem Aufbau der Elektrolysezelle.", }; var service = Build(new FakeLessons(), new FakeGroups([group]), new FakeSubjects([subject]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); var prompt = service.BuildMaterialPrompt(unit, lesson, phase); Assert.Contains("Chemie", prompt); Assert.Contains("9c", prompt); Assert.Contains("Redoxreaktionen", prompt); Assert.Contains("Elektrolyse", prompt); Assert.Contains("Erarbeitung", prompt); Assert.Contains("AB003", prompt); Assert.Contains("Tafelbild mit dem Aufbau der Elektrolysezelle.", prompt); } [Fact] public void BuildMaterialPrompt_OhneBisherigesMaterial_LaesstZeileWeg() { var group = new LearningGroup(); var unit = new Unit { GroupId = group.Id, Title = "T" }; var lesson = new AiLesson { Topic = "Stunde", Phases = [] }; var phase = new AiPhaseStep { Name = "Einstieg", MaterialSuggestion = "Kurzer Impuls per Bild." }; var service = Build(new FakeLessons(), new FakeGroups([group]), new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); var prompt = service.BuildMaterialPrompt(unit, lesson, phase); Assert.DoesNotContain("Bisheriges Material", prompt); Assert.Contains("Kurzer Impuls per Bild.", prompt); } [Fact] public void ApplyResponse_BekannteId_WirdAlsUpdateBehandelt() { var group = new LearningGroup(); var unit = new Unit { GroupId = group.Id, Title = "T" }; var existing = new Lesson { UnitId = unit.Id, GroupId = group.Id, Topic = "Alt" }; var lessons = new FakeLessons(); lessons.Add(existing); var service = Build(lessons, new FakeGroups([group]), new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); var accepted = new List { new() { Id = existing.Id, Topic = "Neu formuliert" } }; var result = service.ApplyResponse(unit, accepted); var lesson = Assert.Single(result); Assert.Equal(existing.Id, lesson.Id); Assert.Equal("Neu formuliert", lesson.Topic); Assert.Equal(unit.Id, lesson.UnitId); Assert.Equal(group.Id, lesson.GroupId); } /// Die KI kennt/liefert keinen Status — ohne diese Absicherung würde eine bereits gehaltene /// Stunde durch eine übernommene KI-Änderung stillschweigend auf "Geplant" zurückgesetzt. [Fact] public void ApplyResponse_BekannteId_BehaeltStatusDerBestehendenLessonBei() { var group = new LearningGroup(); var unit = new Unit { GroupId = group.Id, Title = "T" }; var existing = new Lesson { UnitId = unit.Id, GroupId = group.Id, Topic = "Alt", Status = LessonStatus.Conducted }; var lessons = new FakeLessons(); lessons.Add(existing); var service = Build(lessons, new FakeGroups([group]), new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); var accepted = new List { new() { Id = existing.Id, Topic = "Nachträglich präzisiert" } }; var result = service.ApplyResponse(unit, accepted); Assert.Equal(LessonStatus.Conducted, Assert.Single(result).Status); } /// Umfangs-Umschalter (Nutzer-Nachtrag): "Einheit umplanen ohne Stunden zu ändern" muss auch /// dann greifen, wenn die KI die Anweisung im Systemprompt ignoriert und trotzdem eine /// bestehende Id zurückgibt — client-seitig hart durchgesetzt, nicht nur per Prompt erbeten. [Fact] public void ApplyResponse_AenderungVerboten_VerwirftUpdateBestehenderLesson() { var group = new LearningGroup(); var unit = new Unit { GroupId = group.Id, Title = "T" }; var existing = new Lesson { UnitId = unit.Id, GroupId = group.Id, Topic = "Alt" }; var lessons = new FakeLessons(); lessons.Add(existing); var service = Build(lessons, new FakeGroups([group]), new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); var accepted = new List { new() { Id = existing.Id, Topic = "Sollte verworfen werden" }, new() { Id = null, Topic = "Neue Stunde bleibt erlaubt" }, }; var result = service.ApplyResponse(unit, accepted, allowModifyingExistingLessons: false); var lesson = Assert.Single(result); Assert.Equal("Neue Stunde bleibt erlaubt", lesson.Topic); } /// Fokus-Modus (4.5.22, KI-Unterstützung aus dem Stunden-Editor heraus): auch wenn die KI die /// Anweisung ignoriert und weitere/andere Stunden zurückgibt, darf clientseitig nur die /// angefragte Fokus-Stunde übernommen werden. [Fact] public void ApplyResponse_FocusLessonId_VerwirftAlleAnderenLessons() { var group = new LearningGroup(); var unit = new Unit { GroupId = group.Id, Title = "T" }; var focus = new Lesson { UnitId = unit.Id, GroupId = group.Id, Topic = "Fokus-Stunde" }; var other = new Lesson { UnitId = unit.Id, GroupId = group.Id, Topic = "Andere Stunde" }; var lessons = new FakeLessons(); lessons.Add(focus); lessons.Add(other); var service = Build(lessons, new FakeGroups([group]), new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); var accepted = new List { new() { Id = focus.Id, Topic = "Fokus-Stunde, geändert" }, new() { Id = other.Id, Topic = "Sollte verworfen werden" }, new() { Id = null, Topic = "Neue Stunde sollte verworfen werden" }, }; var result = service.ApplyResponse(unit, accepted, focusLessonId: focus.Id); var lesson = Assert.Single(result); Assert.Equal(focus.Id, lesson.Id); Assert.Equal("Fokus-Stunde, geändert", lesson.Topic); } [Fact] public void ApplyResponse_NullId_WirdAlsNeueLessonBehandelt() { var group = new LearningGroup(); var unit = new Unit { GroupId = group.Id, Title = "T" }; var service = Build(new FakeLessons(), new FakeGroups([group]), new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); var accepted = new List { new() { Id = null, Topic = "Ganz neu" } }; var result = service.ApplyResponse(unit, accepted); var lesson = Assert.Single(result); Assert.NotEqual(Guid.Empty, lesson.Id); Assert.Equal("Ganz neu", lesson.Topic); } /// Zentraler Sicherheitstest (siehe Planungsdokument): eine von der KI zurückgegebene Id, die /// zu keiner tatsächlich zur Einheit gehörenden Lesson passt, darf NIE als Update interpretiert /// werden — sonst könnte eine halluzinierte/fremde Id im schlimmsten Fall eine fremde Lesson /// überschreiben. Sie muss stattdessen wie eine neue Lesson behandelt werden (frische Id). [Fact] public void ApplyResponse_UnbekannteFremdeId_WirdNieAlsUpdateUebernommen() { var group = new LearningGroup(); var unit = new Unit { GroupId = group.Id, Title = "T" }; var existing = new Lesson { UnitId = unit.Id, GroupId = group.Id, Topic = "Bestehende Stunde" }; var lessons = new FakeLessons(); lessons.Add(existing); var service = Build(lessons, new FakeGroups([group]), new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); var foreignId = Guid.NewGuid(); // gehört zu keiner Lesson dieser Einheit var accepted = new List { new() { Id = foreignId, Topic = "Verdächtig" } }; var result = service.ApplyResponse(unit, accepted); var lesson = Assert.Single(result); Assert.NotEqual(foreignId, lesson.Id); Assert.NotEqual(existing.Id, lesson.Id); } [Fact] public void ApplyResponse_LoestAlternativePathNameZurueckZuId_KeinTrefferBleibtNull() { var group = new LearningGroup(); var unit = new Unit { GroupId = group.Id, Title = "T" }; var path = new AlternativeLessonPath { Name = "Förderung" }; var service = Build(new FakeLessons(), new FakeGroups([group]), new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([path])); var accepted = new List { new() { Topic = "T", Phases = [ new AiPhaseStep { Name = "A", AlternativePathName = "Förderung" }, new AiPhaseStep { Name = "B", AlternativePathName = "Unbekannter Pfad" }, ], }, }; var result = service.ApplyResponse(unit, accepted); var phases = Assert.Single(result).Phases; Assert.Equal(path.Id, phases[0].AlternativePathId); Assert.Null(phases[1].AlternativePathId); } }