547 lines
23 KiB
C#
547 lines
23 KiB
C#
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<AiBackendException>(() =>
|
|
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<AiBackendException>(() =>
|
|
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<AiBackendException>(() =>
|
|
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<AiBackendException>(() =>
|
|
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);
|
|
}
|
|
|
|
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<HttpResponseMessage> 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<HttpResponseMessage> SendAsync(HttpRequestMessage request,
|
|
CancellationToken cancellationToken) => Task.FromCanceled<HttpResponseMessage>(
|
|
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<AiLesson> { 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<AiLesson> { 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<AiLesson> { 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<AiLesson> { 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<AiLesson>
|
|
{
|
|
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<AiLesson>
|
|
{
|
|
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<AiLesson> { 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<AiLesson> { 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<AiLesson>
|
|
{
|
|
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);
|
|
}
|
|
}
|