Files
LehrerApp/LehrerApp.Desktop.Tests/McpToolsTests.cs
T
adminandClaude Sonnet 5 3f8813df51 feat: MCP-Tools für Fach-/Kompetenzverwaltung + Kompetenzzuordnung zu Lessons (Nutzer-Nachtrag)
Neue CompetencyTools.cs: create/update/delete_subject, create/update/delete_competency_domain,
add/update/remove_competency_item, get_subjects, get_competency_catalog. delete_subject und
delete_competency_domain als weitere Destructive-Ausnahmen neben delete_lesson (kein Papierkorb).

Lesson bekommt ein neues Competencies-Feld (analog zu Unit.Competencies) sowie
add_lesson_competency/remove_lesson_competency in LessonPlanTools.cs.

ICompetencyDomainRepository um GetBySubject() erweitert, um den Katalog eines Fachs ohne
bekannte Klassenstufe abzufragen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 20:01:23 +02:00

881 lines
36 KiB
C#

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_lesson_plans", "get_schedule", "get_students", "get_subjects", "get_time_entries",
"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",
"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));
}
// ── 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<InvalidOperationException>(() => 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);
}
}