using LehrerApp.Core.Models; using LehrerApp.Desktop.Services.Mcp; using LehrerApp.Desktop.Services.Mcp.Tools; using Xunit; namespace LehrerApp.Desktop.Tests; public sealed class McpToolsTests { // ── McpToolScope ───────────────────────────────────────────────────────────────────────── [Fact] public void AllowedReadTools_EnthaeltGenauDieErwartetenReadTools() { Assert.Equal( new[] { "download_lesson_attachment", "get_competency_catalog", "get_exams", "get_grades", "get_groups", "get_lesson_plans", "get_named_untis_absence_pattern", "get_schedule", "get_students", "get_subjects", "get_time_entries", "get_untis_absence_rows", "get_untis_hub_status", "list_letter_templates", "render_letter", }, McpToolScope.AllowedReadTools.OrderBy(n => n, StringComparer.Ordinal)); } [Fact] public void AllowedWriteTools_EnthaeltGenauDieErwartetenWriteTools() { Assert.Equal( new[] { "add_competency_item", "add_lesson_attachment", "add_lesson_competency", "add_lesson_phase", "apply_untis_absence_status", "create_competency_domain", "create_grade_entry", "create_lesson", "create_subject", "create_time_entry", "create_unit", "move_lesson", "remove_competency_item", "remove_lesson_competency", "remove_lesson_phase", "update_competency_domain", "update_competency_item", "update_lesson", "update_lesson_phase", "update_student_group_assignment", "update_subject", "update_unit", }, McpToolScope.AllowedWriteTools.OrderBy(n => n, StringComparer.Ordinal)); } [Fact] public void AllowedDestructiveWriteTools_EnthaeltDeleteLessonSubjectUndCompetencyDomain() { Assert.Equal( new[] { "delete_competency_domain", "delete_lesson", "delete_subject" }, McpToolScope.AllowedDestructiveWriteTools.OrderBy(n => n, StringComparer.Ordinal)); } [Fact] public void AllowedTools_EnthaeltKeineDokumentationstypen() { // Gesprächsnotizen/Vorfälle/Förderpläne dürfen technisch nie über MCP erreichbar sein // (siehe Planungsdokument) - die Namenskonvention "documentation"/"vorgang" darf nie auftauchen. var allNames = McpToolScope.AllowedReadTools.Concat(McpToolScope.AllowedWriteTools); Assert.DoesNotContain(allNames, n => n.Contains("documentation", StringComparison.OrdinalIgnoreCase) || n.Contains("vorgang", StringComparison.OrdinalIgnoreCase) || n.Contains("note", StringComparison.OrdinalIgnoreCase)); } // ── GroupTools ─────────────────────────────────────────────────────────────────────────── [Fact] public void GetGroups_LiefertIdNameUndSchuljahrJeGruppe() { var group = new LearningGroup { Name = "9a", Type = GroupType.Class, SchoolYear = "2025/26", GradeLevel = 9, IsActive = true, }; var tool = new GroupTools(new FakeGroups([group])); var result = Assert.Single(tool.GetGroups()); Assert.Equal(group.Id, result.Id); Assert.Equal("9a", result.Name); Assert.Equal("2025/26", result.SchoolYear); Assert.Equal(9, result.GradeLevel); Assert.True(result.IsActive); } // ── StudentTools ───────────────────────────────────────────────────────────────────────── [Fact] public void GetStudents_OhneFilter_LiefertNurAktiveSchueler() { var active = new Student { FirstName = "Anna", LastName = "Aktiv", IsActive = true }; var inactive = new Student { FirstName = "Ida", LastName = "Inaktiv", IsActive = false }; var tool = new StudentTools(new FakeStudents([active, inactive])); var result = tool.GetStudents(); Assert.Single(result); Assert.Equal(active.Id, result[0].Id); } [Fact] public void GetStudents_IncludeInactive_LiefertAuchInaktive() { var active = new Student { FirstName = "Anna", LastName = "Aktiv", IsActive = true }; var inactive = new Student { FirstName = "Ida", LastName = "Inaktiv", IsActive = false }; var tool = new StudentTools(new FakeStudents([active, inactive])); var result = tool.GetStudents(includeInactive: true); Assert.Equal(2, result.Count); } // ── ExamTools ──────────────────────────────────────────────────────────────────────────── [Fact] public void GetExams_OhneIncludeResults_LiefertKeineErgebnisse() { var groupId = Guid.NewGuid(); var exam = new Exam { GroupId = groupId, Title = "Klausur 1" }; var results = new FakeResults(); results.Add(new ExamResult { ExamId = exam.Id, StudentId = Guid.NewGuid(), TotalPoints = 10 }); var tool = new ExamTools(new FakeExams([exam]), results); var dto = Assert.Single(tool.GetExams(groupId)); Assert.Null(dto.Results); } [Fact] public void GetExams_MitIncludeResults_LiefertErgebnisseJeSchueler() { var groupId = Guid.NewGuid(); var studentId = Guid.NewGuid(); var exam = new Exam { GroupId = groupId, Title = "Klausur 1" }; var results = new FakeResults(); results.Add(new ExamResult { ExamId = exam.Id, StudentId = studentId, TotalPoints = 12.5, Grade = "2+" }); var tool = new ExamTools(new FakeExams([exam]), results); var dto = Assert.Single(tool.GetExams(groupId, includeResults: true)); var result = Assert.Single(dto.Results!); Assert.Equal(studentId, result.StudentId); Assert.Equal("2+", result.Grade); } // ── GradeTools ─────────────────────────────────────────────────────────────────────────── [Fact] public void GetGrades_MitStudentId_FiltertAufEinenSchueler() { var groupId = Guid.NewGuid(); var studentA = Guid.NewGuid(); var studentB = Guid.NewGuid(); var grades = new FakeGrades(); grades.Add(new Grade { GroupId = groupId, StudentId = studentA, Value = "2" }); grades.Add(new Grade { GroupId = groupId, StudentId = studentB, Value = "3" }); var tool = new GradeTools(grades, new FakeStudents([]), new FakeMcpConfirmation()); var dto = Assert.Single(tool.GetGrades(groupId, studentA)); Assert.Equal(studentA, dto.StudentId); } [Fact] public async Task CreateGradeEntry_NutzerBestaetigt_SpeichertNote() { var student = new Student { FirstName = "Anna", LastName = "Aktiv" }; var grades = new FakeGrades(); var confirmation = new FakeMcpConfirmation { Response = true }; var tool = new GradeTools(grades, new FakeStudents([student]), confirmation); var groupId = Guid.NewGuid(); var result = await tool.CreateGradeEntry( student.Id, groupId, GradeCategory.Oral, "2+", new DateOnly(2026, 1, 10)); Assert.True(result.Applied); Assert.NotNull(result.Id); Assert.Single(grades.GetByGroup(groupId)); // Bestätigungstext muss für einen Menschen lesbar sein (Name statt bloßer GUID). Assert.Contains("Anna", confirmation.LastMessage); Assert.DoesNotContain(student.Id.ToString(), confirmation.LastMessage); } [Fact] public async Task CreateGradeEntry_NutzerLehntAb_SpeichertNichts() { var student = new Student { FirstName = "Anna", LastName = "Aktiv" }; var grades = new FakeGrades(); var confirmation = new FakeMcpConfirmation { Response = false }; var tool = new GradeTools(grades, new FakeStudents([student]), confirmation); var groupId = Guid.NewGuid(); var result = await tool.CreateGradeEntry( student.Id, groupId, GradeCategory.Oral, "2+", new DateOnly(2026, 1, 10)); Assert.False(result.Applied); Assert.Empty(grades.GetByGroup(groupId)); Assert.Equal(1, confirmation.CallCount); } // ── ScheduleTools ──────────────────────────────────────────────────────────────────────── [Fact] public void GetSchedule_MitGroupId_FiltertNachGruppe() { var groupId = Guid.NewGuid(); var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = groupId, Weekday = DayOfWeek.Monday, PeriodNumber = 1 }); slots.Add(new TimetableSlot { GroupId = Guid.NewGuid(), Weekday = DayOfWeek.Tuesday, PeriodNumber = 2 }); var tool = new ScheduleTools(slots); var dto = Assert.Single(tool.GetSchedule(groupId)); Assert.Equal(groupId, dto.GroupId); } // ── TimeEntryTools ─────────────────────────────────────────────────────────────────────── [Fact] public void GetTimeEntries_FiltertAufDenAngegebenenZeitraum() { var entries = new FakeTimeEntries(); entries.Add(new TimeEntry { Date = new DateOnly(2026, 1, 5), DurationMinutes = 30 }); entries.Add(new TimeEntry { Date = new DateOnly(2026, 2, 1), DurationMinutes = 45 }); var tool = new TimeEntryTools(entries, new FakeGroups([]), new FakeMcpConfirmation()); var dto = Assert.Single(tool.GetTimeEntries(new DateOnly(2026, 1, 1), new DateOnly(2026, 1, 31))); Assert.Equal(30, dto.DurationMinutes); } [Fact] public async Task CreateTimeEntry_NutzerBestaetigt_SpeichertEintrag() { var entries = new FakeTimeEntries(); var confirmation = new FakeMcpConfirmation { Response = true }; var tool = new TimeEntryTools(entries, new FakeGroups([]), confirmation); var result = await tool.CreateTimeEntry("Korrektur", new DateOnly(2026, 1, 10), 30); Assert.True(result.Applied); Assert.Single(entries.GetByDateRange(new DateOnly(2026, 1, 1), new DateOnly(2026, 1, 31))); } [Fact] public async Task CreateTimeEntry_NutzerLehntAb_SpeichertNichts() { var entries = new FakeTimeEntries(); var confirmation = new FakeMcpConfirmation { Response = false }; var tool = new TimeEntryTools(entries, new FakeGroups([]), confirmation); var result = await tool.CreateTimeEntry("Korrektur", new DateOnly(2026, 1, 10), 30); Assert.False(result.Applied); Assert.Empty(entries.GetByDateRange(new DateOnly(2026, 1, 1), new DateOnly(2026, 1, 31))); } // ── LessonPlanTools ────────────────────────────────────────────────────────────────────── private static LessonPlanTools BuildLessonPlanTools( FakeUnits? units = null, FakeLessons? lessons = null, FakeGroups? groups = null, FakeAttachmentStorage? attachments = null, FakeMcpConfirmation? confirmation = null) => new(units ?? new FakeUnits(), lessons ?? new FakeLessons(), groups ?? new FakeGroups([]), attachments ?? new FakeAttachmentStorage(), confirmation ?? new FakeMcpConfirmation()); [Fact] public void GetLessonPlans_LiefertEinheitenDerGruppeUndStundenImZeitraum() { var groupId = Guid.NewGuid(); var units = new FakeUnits(); units.Add(new Unit { GroupId = groupId, Title = "Optik" }); var lessons = new FakeLessons(); lessons.Add(new Lesson { GroupId = groupId, Date = new DateOnly(2026, 1, 5), Topic = "Brechung" }); lessons.Add(new Lesson { GroupId = groupId, Date = new DateOnly(2026, 3, 1), Topic = "Später" }); var tool = BuildLessonPlanTools(units, lessons); var result = tool.GetLessonPlans(groupId, new DateOnly(2026, 1, 1), new DateOnly(2026, 1, 31)); Assert.Single(result.Units); var lesson = Assert.Single(result.Lessons); Assert.Equal("Brechung", lesson.Topic); } [Fact] public async Task CreateUnit_NutzerBestaetigt_SpeichertEinheit() { var group = new LearningGroup { Name = "7a" }; var units = new FakeUnits(); var tool = BuildLessonPlanTools(units: units, groups: new FakeGroups([group])); var result = await tool.CreateUnit(group.Id, "Optik"); Assert.True(result.Applied); Assert.Single(units.GetByGroup(group.Id)); } [Fact] public async Task UpdateUnit_OhneAenderung_FragtNichtNach() { var unit = new Unit { Title = "Optik", Status = UnitStatus.Planned }; var units = new FakeUnits(); units.Add(unit); var confirmation = new FakeMcpConfirmation(); var tool = BuildLessonPlanTools(units: units, confirmation: confirmation); var result = await tool.UpdateUnit(unit.Id, title: "Optik", status: UnitStatus.Planned); Assert.True(result.Applied); Assert.Equal(0, confirmation.CallCount); } [Fact] public async Task CreateLesson_UnbekannteEinheit_LiefertFehlerOhneNachfrage() { var confirmation = new FakeMcpConfirmation(); var tool = BuildLessonPlanTools(confirmation: confirmation); var result = await tool.CreateLesson(Guid.NewGuid(), new DateOnly(2026, 1, 5), "Brechung"); Assert.False(result.Applied); Assert.Equal(0, confirmation.CallCount); } [Fact] public async Task CreateLesson_NutzerBestaetigt_UebernimmtGruppeVonDerEinheit() { var groupId = Guid.NewGuid(); var unit = new Unit { GroupId = groupId, Title = "Optik" }; var units = new FakeUnits(); units.Add(unit); var lessons = new FakeLessons(); var tool = BuildLessonPlanTools(units, lessons); var result = await tool.CreateLesson(unit.Id, new DateOnly(2026, 1, 5), "Brechung"); Assert.True(result.Applied); var lesson = Assert.Single(lessons.GetByUnit(unit.Id)); Assert.Equal(groupId, lesson.GroupId); } [Fact] public async Task UpdateLesson_AendertNurAngegebeneFelder() { var lesson = new Lesson { Topic = "Brechung", Homework = "S. 12" }; var lessons = new FakeLessons(); lessons.Add(lesson); var tool = BuildLessonPlanTools(lessons: lessons); var result = await tool.UpdateLesson(lesson.Id, topic: "Brechung II"); Assert.True(result.Applied); var updated = lessons.GetById(lesson.Id)!; Assert.Equal("Brechung II", updated.Topic); Assert.Equal("S. 12", updated.Homework); } [Fact] public async Task AddLessonPhase_NutzerBestaetigt_HaengtPhaseAn() { var lesson = new Lesson { Topic = "Brechung" }; var lessons = new FakeLessons(); lessons.Add(lesson); var tool = BuildLessonPlanTools(lessons: lessons); var result = await tool.AddLessonPhase(lesson.Id, "Einstieg", 10); Assert.True(result.Applied); var phase = Assert.Single(lessons.GetById(lesson.Id)!.Phases); Assert.Equal("Einstieg", phase.Name); Assert.Equal(result.Id, phase.Id); } [Fact] public async Task AddLessonPhase_UebergibtEigenenMethodennamenAlsOperationKey() { // Belegt, dass IMcpConfirmationService.ConfirmAsync den [CallerMemberName]-Mechanismus // tatsächlich nutzt (Grundlage für die Sitzungsfreigabe "diese Aktion nicht mehr // nachfragen" in AvaloniaMcpConfirmationService) - ohne echtes UI testbar, weil der Name // vom Compiler an der Aufrufstelle in AddLessonPhase eingesetzt wird, unabhängig von der // konkreten IMcpConfirmationService-Implementierung. var lesson = new Lesson { Topic = "Brechung" }; var lessons = new FakeLessons(); lessons.Add(lesson); var confirmation = new FakeMcpConfirmation(); var tool = BuildLessonPlanTools(lessons: lessons, confirmation: confirmation); await tool.AddLessonPhase(lesson.Id, "Einstieg", 10); Assert.Equal(nameof(LessonPlanTools.AddLessonPhase), confirmation.LastOperationKey); } [Fact] public async Task UpdateLessonPhase_AendertNurAngegebeneFelder() { var phase = new LessonPhaseStep { Name = "Einstieg", DurationMinutes = 10, Material = "Folie" }; var lesson = new Lesson { Topic = "Brechung" }; lesson.Phases.Add(phase); var lessons = new FakeLessons(); lessons.Add(lesson); var tool = BuildLessonPlanTools(lessons: lessons); var result = await tool.UpdateLessonPhase(lesson.Id, phase.Id, durationMinutes: 15); Assert.True(result.Applied); var updated = lessons.GetById(lesson.Id)!.Phases.Single(); Assert.Equal(15, updated.DurationMinutes); Assert.Equal("Folie", updated.Material); } [Fact] public async Task RemoveLessonPhase_NutzerBestaetigt_EntferntPhase() { var phase = new LessonPhaseStep { Name = "Einstieg", DurationMinutes = 10 }; var lesson = new Lesson { Topic = "Brechung" }; lesson.Phases.Add(phase); var lessons = new FakeLessons(); lessons.Add(lesson); var tool = BuildLessonPlanTools(lessons: lessons); var result = await tool.RemoveLessonPhase(lesson.Id, phase.Id); Assert.True(result.Applied); Assert.Empty(lessons.GetById(lesson.Id)!.Phases); } [Fact] public void DownloadLessonAttachment_LiefertBase64Inhalt() { var storage = new FakeAttachmentStorage(); var storageId = storage.Upload("blatt.pdf", new MemoryStream("PDF-Inhalt"u8.ToArray())); var lesson = new Lesson { Topic = "Brechung" }; lesson.Attachments.Add(new DocumentAttachment { StorageId = storageId, FileName = "blatt.pdf", SizeBytes = 10 }); var lessons = new FakeLessons(); lessons.Add(lesson); var tool = BuildLessonPlanTools(lessons: lessons, attachments: storage); var result = tool.DownloadLessonAttachment(lesson.Id, storageId); Assert.Equal("blatt.pdf", result.FileName); Assert.Equal("PDF-Inhalt", System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(result.Base64Content))); } [Fact] public void DownloadLessonAttachment_ZuGross_WirftFehler() { var lesson = new Lesson { Topic = "Brechung" }; lesson.Attachments.Add(new DocumentAttachment { StorageId = "big", FileName = "video.mp4", SizeBytes = LessonPlanTools.MaxInlineAttachmentBytes + 1, }); var lessons = new FakeLessons(); lessons.Add(lesson); var tool = BuildLessonPlanTools(lessons: lessons); Assert.Throws(() => tool.DownloadLessonAttachment(lesson.Id, "big")); } [Fact] public async Task AddLessonAttachment_NutzerBestaetigt_LaedtHochUndHaengtAn() { var lesson = new Lesson { Topic = "Brechung" }; var lessons = new FakeLessons(); lessons.Add(lesson); var storage = new FakeAttachmentStorage(); var tool = BuildLessonPlanTools(lessons: lessons, attachments: storage); var content = Convert.ToBase64String("Arbeitsblatt-Inhalt"u8.ToArray()); var result = await tool.AddLessonAttachment(lesson.Id, "arbeitsblatt.pdf", content); Assert.True(result.Applied); var attachment = Assert.Single(lessons.GetById(lesson.Id)!.Attachments); Assert.Equal("arbeitsblatt.pdf", attachment.FileName); using var stream = storage.OpenRead(attachment.StorageId)!; using var reader = new StreamReader(stream); Assert.Equal("Arbeitsblatt-Inhalt", reader.ReadToEnd()); } [Fact] public async Task AddLessonAttachment_NutzerLehntAb_SpeichertNichts() { var lesson = new Lesson { Topic = "Brechung" }; var lessons = new FakeLessons(); lessons.Add(lesson); var confirmation = new FakeMcpConfirmation { Response = false }; var tool = BuildLessonPlanTools(lessons: lessons, confirmation: confirmation); var content = Convert.ToBase64String("Inhalt"u8.ToArray()); var result = await tool.AddLessonAttachment(lesson.Id, "arbeitsblatt.pdf", content); Assert.False(result.Applied); Assert.Empty(lessons.GetById(lesson.Id)!.Attachments); } [Fact] public async Task AddLessonAttachment_UngueltigesBase64_LiefertFehlerOhneNachfrage() { var lesson = new Lesson { Topic = "Brechung" }; var lessons = new FakeLessons(); lessons.Add(lesson); var confirmation = new FakeMcpConfirmation(); var tool = BuildLessonPlanTools(lessons: lessons, confirmation: confirmation); var result = await tool.AddLessonAttachment(lesson.Id, "arbeitsblatt.pdf", "nicht-base64!!!"); Assert.False(result.Applied); Assert.Equal(0, confirmation.CallCount); } [Fact] public async Task AddLessonAttachment_UnbekannteStunde_LiefertFehlerOhneNachfrage() { var confirmation = new FakeMcpConfirmation(); var tool = BuildLessonPlanTools(confirmation: confirmation); var result = await tool.AddLessonAttachment(Guid.NewGuid(), "x.pdf", Convert.ToBase64String("x"u8.ToArray())); Assert.False(result.Applied); Assert.Equal(0, confirmation.CallCount); } [Fact] public async Task MoveLesson_NutzerBestaetigt_VerschiebtDatum() { var lesson = new Lesson { Topic = "Brechung", Date = new DateOnly(2026, 1, 5) }; var lessons = new FakeLessons(); lessons.Add(lesson); var tool = BuildLessonPlanTools(lessons: lessons); var result = await tool.MoveLesson(lesson.Id, new DateOnly(2026, 1, 12)); Assert.True(result.Applied); Assert.Equal(new DateOnly(2026, 1, 12), lessons.GetById(lesson.Id)!.Date); } [Fact] public async Task MoveLesson_ShiftFollowing_VerschiebtSpaetereStundenDerselbenEinheitMit() { var unitId = Guid.NewGuid(); var moved = new Lesson { UnitId = unitId, Topic = "Brechung", Date = new DateOnly(2026, 1, 5) }; var later = new Lesson { UnitId = unitId, Topic = "Reflexion", Date = new DateOnly(2026, 1, 7) }; var earlier = new Lesson { UnitId = unitId, Topic = "Einstieg", Date = new DateOnly(2026, 1, 1) }; var lessons = new FakeLessons(); lessons.Add(moved); lessons.Add(later); lessons.Add(earlier); var tool = BuildLessonPlanTools(lessons: lessons); // moved: 5.1. -> 12.1. (Delta +7 Tage), later (7.1., danach) muss mitwandern, earlier (1.1., davor) nicht. var result = await tool.MoveLesson(moved.Id, new DateOnly(2026, 1, 12), shiftFollowingLessons: true); Assert.True(result.Applied); Assert.Equal(new DateOnly(2026, 1, 14), lessons.GetById(later.Id)!.Date); Assert.Equal(new DateOnly(2026, 1, 1), lessons.GetById(earlier.Id)!.Date); } [Fact] public async Task MoveLesson_ZielStundeBelegt_LiefertFehlerNachBestaetigung() { var groupId = Guid.NewGuid(); var moved = new Lesson { GroupId = groupId, Topic = "Brechung", Date = new DateOnly(2026, 1, 5), LessonNumber = 1 }; var occupying = new Lesson { GroupId = groupId, Topic = "Anderer Kurs", Date = new DateOnly(2026, 1, 12), LessonNumber = 1 }; var lessons = new FakeLessons(); lessons.Add(moved); lessons.Add(occupying); var confirmation = new FakeMcpConfirmation(); var tool = BuildLessonPlanTools(lessons: lessons, confirmation: confirmation); var result = await tool.MoveLesson(moved.Id, new DateOnly(2026, 1, 12), newPeriod: 1); Assert.False(result.Applied); Assert.Equal(1, confirmation.CallCount); // Bestätigung lief bereits, Konflikt zeigt sich erst beim Speichern. Assert.Equal(new DateOnly(2026, 1, 5), lessons.GetById(moved.Id)!.Date); // unverändert } [Fact] public async Task MoveLesson_UnbekannteStunde_LiefertFehlerOhneNachfrage() { var confirmation = new FakeMcpConfirmation(); var tool = BuildLessonPlanTools(confirmation: confirmation); var result = await tool.MoveLesson(Guid.NewGuid(), new DateOnly(2026, 1, 12)); Assert.False(result.Applied); Assert.Equal(0, confirmation.CallCount); } [Fact] public async Task DeleteLesson_NutzerBestaetigt_LoeschtEndgueltig() { var lesson = new Lesson { Topic = "Brechung" }; var lessons = new FakeLessons(); lessons.Add(lesson); var tool = BuildLessonPlanTools(lessons: lessons); var result = await tool.DeleteLesson(lesson.Id); Assert.True(result.Applied); Assert.Null(lessons.GetById(lesson.Id)); } [Fact] public async Task DeleteLesson_NutzerLehntAb_BleibtErhalten() { var lesson = new Lesson { Topic = "Brechung" }; var lessons = new FakeLessons(); lessons.Add(lesson); var confirmation = new FakeMcpConfirmation { Response = false }; var tool = BuildLessonPlanTools(lessons: lessons, confirmation: confirmation); var result = await tool.DeleteLesson(lesson.Id); Assert.False(result.Applied); Assert.NotNull(lessons.GetById(lesson.Id)); } [Fact] public async Task DeleteLesson_UnbekannteStunde_LiefertFehlerOhneNachfrage() { var confirmation = new FakeMcpConfirmation(); var tool = BuildLessonPlanTools(confirmation: confirmation); var result = await tool.DeleteLesson(Guid.NewGuid()); Assert.False(result.Applied); Assert.Equal(0, confirmation.CallCount); } // ── GroupMembershipTools ───────────────────────────────────────────────────────────────── [Fact] public async Task UpdateStudentGroupAssignment_KeineBestehendeMitgliedschaft_LegtNeueAn() { var student = new Student { FirstName = "Anna", LastName = "Aktiv" }; var group = new LearningGroup { Name = "7a" }; var memberships = new FakeMemberships([]); var confirmation = new FakeMcpConfirmation { Response = true }; var tool = new GroupMembershipTools(memberships, new FakeStudents([student]), new FakeGroups([group]), confirmation); var result = await tool.UpdateStudentGroupAssignment(student.Id, group.Id, niveau: Niveau.E); Assert.True(result.Applied); var membership = Assert.Single(memberships.GetByStudent(student.Id)); Assert.Equal(Niveau.E, membership.Niveau); Assert.Contains("Anna", confirmation.LastMessage); } [Fact] public async Task UpdateStudentGroupAssignment_BestehendeMitgliedschaftUnveraendert_FragtNichtNochmalNach() { var student = new Student { FirstName = "Anna", LastName = "Aktiv" }; var group = new LearningGroup { Name = "7a" }; var existing = new GroupMembership { StudentId = student.Id, GroupId = group.Id, Niveau = Niveau.G }; var memberships = new FakeMemberships([existing]); var confirmation = new FakeMcpConfirmation { Response = true }; var tool = new GroupMembershipTools(memberships, new FakeStudents([student]), new FakeGroups([group]), confirmation); var result = await tool.UpdateStudentGroupAssignment(student.Id, group.Id, niveau: Niveau.G); Assert.True(result.Applied); Assert.Equal(0, confirmation.CallCount); } [Fact] public async Task UpdateStudentGroupAssignment_UnbekannterSchueler_LiefertFehlerOhneNachfrage() { var group = new LearningGroup { Name = "7a" }; var confirmation = new FakeMcpConfirmation { Response = true }; var tool = new GroupMembershipTools(new FakeMemberships([]), new FakeStudents([]), new FakeGroups([group]), confirmation); var result = await tool.UpdateStudentGroupAssignment(Guid.NewGuid(), group.Id); Assert.False(result.Applied); Assert.Equal(0, confirmation.CallCount); } // ── LessonPlanTools — Kompetenzzuordnung ──────────────────────────────────────────────────── [Fact] public async Task AddLessonCompetency_NutzerBestaetigt_OrdnetCodeZu() { var lesson = new Lesson { Topic = "Brechung" }; var lessons = new FakeLessons(); lessons.Add(lesson); var tool = BuildLessonPlanTools(lessons: lessons); var result = await tool.AddLessonCompetency(lesson.Id, "PH.9.1"); Assert.True(result.Applied); Assert.Contains("PH.9.1", lessons.GetById(lesson.Id)!.Competencies); } [Fact] public async Task AddLessonCompetency_BereitsZugeordnet_FragtNichtNochmalNach() { var lesson = new Lesson { Topic = "Brechung" }; lesson.Competencies.Add("PH.9.1"); var lessons = new FakeLessons(); lessons.Add(lesson); var confirmation = new FakeMcpConfirmation(); var tool = BuildLessonPlanTools(lessons: lessons, confirmation: confirmation); var result = await tool.AddLessonCompetency(lesson.Id, "PH.9.1"); Assert.True(result.Applied); Assert.Equal(0, confirmation.CallCount); Assert.Single(lessons.GetById(lesson.Id)!.Competencies); } [Fact] public async Task RemoveLessonCompetency_NutzerBestaetigt_EntferntCode() { var lesson = new Lesson { Topic = "Brechung" }; lesson.Competencies.Add("PH.9.1"); var lessons = new FakeLessons(); lessons.Add(lesson); var tool = BuildLessonPlanTools(lessons: lessons); var result = await tool.RemoveLessonCompetency(lesson.Id, "PH.9.1"); Assert.True(result.Applied); Assert.Empty(lessons.GetById(lesson.Id)!.Competencies); } [Fact] public async Task RemoveLessonCompetency_NichtZugeordnet_LiefertFehlerOhneNachfrage() { var lesson = new Lesson { Topic = "Brechung" }; var lessons = new FakeLessons(); lessons.Add(lesson); var confirmation = new FakeMcpConfirmation(); var tool = BuildLessonPlanTools(lessons: lessons, confirmation: confirmation); var result = await tool.RemoveLessonCompetency(lesson.Id, "PH.9.1"); Assert.False(result.Applied); Assert.Equal(0, confirmation.CallCount); } // ── CompetencyTools ────────────────────────────────────────────────────────────────────── [Fact] public void GetSubjects_LiefertAlleFaecher() { var subject = new Subject { Name = "Mathematik", ShortName = "Ma" }; var tool = new CompetencyTools(new FakeSubjects([subject]), new FakeCompetencyDomains(), new FakeMcpConfirmation()); var dto = Assert.Single(tool.GetSubjects()); Assert.Equal("Mathematik", dto.Name); } [Fact] public async Task CreateSubject_NutzerBestaetigt_SpeichertFach() { var subjects = new FakeSubjects([]); var tool = new CompetencyTools(subjects, new FakeCompetencyDomains(), new FakeMcpConfirmation()); var result = await tool.CreateSubject("Mathematik", "Ma"); Assert.True(result.Applied); var subject = Assert.Single(subjects.GetAll()); Assert.Equal("Mathematik", subject.Name); } [Fact] public async Task CreateSubject_NameBereitsVergeben_LiefertFehler() { var subjects = new FakeSubjects([new Subject { Name = "Mathematik" }]); var tool = new CompetencyTools(subjects, new FakeCompetencyDomains(), new FakeMcpConfirmation()); var result = await tool.CreateSubject("Mathematik"); Assert.False(result.Applied); Assert.Single(subjects.GetAll()); } [Fact] public async Task UpdateSubject_AendertNurAngegebeneFelder() { var subject = new Subject { Name = "Mathematik", ShortName = "Ma" }; var subjects = new FakeSubjects([subject]); var tool = new CompetencyTools(subjects, new FakeCompetencyDomains(), new FakeMcpConfirmation()); var result = await tool.UpdateSubject(subject.Id, shortName: "M"); Assert.True(result.Applied); var updated = subjects.GetById(subject.Id)!; Assert.Equal("Mathematik", updated.Name); Assert.Equal("M", updated.ShortName); } [Fact] public async Task DeleteSubject_NutzerBestaetigt_LoeschtFach() { var subject = new Subject { Name = "Mathematik" }; var subjects = new FakeSubjects([subject]); var tool = new CompetencyTools(subjects, new FakeCompetencyDomains(), new FakeMcpConfirmation()); var result = await tool.DeleteSubject(subject.Id); Assert.True(result.Applied); Assert.Empty(subjects.GetAll()); } [Fact] public async Task DeleteSubject_UnbekannteId_LiefertFehlerOhneNachfrage() { var confirmation = new FakeMcpConfirmation(); var tool = new CompetencyTools(new FakeSubjects([]), new FakeCompetencyDomains(), confirmation); var result = await tool.DeleteSubject(Guid.NewGuid()); Assert.False(result.Applied); Assert.Equal(0, confirmation.CallCount); } [Fact] public void GetCompetencyCatalog_OhneKlassenstufe_LiefertAlleKlassenstufenDesFachs() { var subjectId = Guid.NewGuid(); var domains = new FakeCompetencyDomains(); domains.Add(new CompetencyDomain { SubjectId = subjectId, GradeLevel = 5, Name = "Zahlen" }); domains.Add(new CompetencyDomain { SubjectId = subjectId, GradeLevel = 9, Name = "Funktionen" }); var tool = new CompetencyTools(new FakeSubjects([]), domains, new FakeMcpConfirmation()); var result = tool.GetCompetencyCatalog(subjectId); Assert.Equal(2, result.Count); } [Fact] public async Task CreateCompetencyDomain_NutzerBestaetigt_SpeichertBereich() { var subject = new Subject { Name = "Mathematik" }; var domains = new FakeCompetencyDomains(); var tool = new CompetencyTools(new FakeSubjects([subject]), domains, new FakeMcpConfirmation()); var result = await tool.CreateCompetencyDomain(subject.Id, 9, "Funktionen"); Assert.True(result.Applied); var domain = Assert.Single(domains.GetBySubjectAndGrade(subject.Id, 9)); Assert.Equal("Funktionen", domain.Name); } [Fact] public async Task DeleteCompetencyDomain_NutzerBestaetigt_LoeschtBereichMitItems() { var domain = new CompetencyDomain { Name = "Funktionen" }; domain.Items.Add(new CompetencyItem { Code = "M.9.1", Description = "..." }); var domains = new FakeCompetencyDomains(); domains.Add(domain); var tool = new CompetencyTools(new FakeSubjects([]), domains, new FakeMcpConfirmation()); var result = await tool.DeleteCompetencyDomain(domain.Id); Assert.True(result.Applied); Assert.Null(domains.GetById(domain.Id)); } [Fact] public async Task AddCompetencyItem_NutzerBestaetigt_HaengtItemAn() { var domain = new CompetencyDomain { Name = "Funktionen" }; var domains = new FakeCompetencyDomains(); domains.Add(domain); var tool = new CompetencyTools(new FakeSubjects([]), domains, new FakeMcpConfirmation()); var result = await tool.AddCompetencyItem(domain.Id, "M.9.1", "lineare Funktionen erkennen"); Assert.True(result.Applied); var item = Assert.Single(domains.GetById(domain.Id)!.Items); Assert.Equal("M.9.1", item.Code); } [Fact] public async Task UpdateCompetencyItem_AendertNurAngegebeneFelder() { var item = new CompetencyItem { Code = "M.9.1", Description = "alt" }; var domain = new CompetencyDomain { Name = "Funktionen" }; domain.Items.Add(item); var domains = new FakeCompetencyDomains(); domains.Add(domain); var tool = new CompetencyTools(new FakeSubjects([]), domains, new FakeMcpConfirmation()); var result = await tool.UpdateCompetencyItem(domain.Id, item.Id, description: "neu"); Assert.True(result.Applied); var updated = domains.GetById(domain.Id)!.Items.Single(); Assert.Equal("M.9.1", updated.Code); Assert.Equal("neu", updated.Description); } [Fact] public async Task RemoveCompetencyItem_NutzerBestaetigt_EntferntItem() { var item = new CompetencyItem { Code = "M.9.1", Description = "..." }; var domain = new CompetencyDomain { Name = "Funktionen" }; domain.Items.Add(item); var domains = new FakeCompetencyDomains(); domains.Add(domain); var tool = new CompetencyTools(new FakeSubjects([]), domains, new FakeMcpConfirmation()); var result = await tool.RemoveCompetencyItem(domain.Id, item.Id); Assert.True(result.Applied); Assert.Empty(domains.GetById(domain.Id)!.Items); } }