feat: lokaler MCP-Server, Phase 3 (kleinteilige Unit/Lesson-Tools)
Statt eines "update_lesson", das die ganze Stunde inkl. Verlaufsplan als ein großes JSON-Objekt tauscht, gezielte kleine Tools je Teiloperation (Nutzervorschlag): create/update_unit, create/update_lesson (Metadaten ohne Phasen), add/update/remove_lesson_phase (je eine Phase), download_lesson_attachment (Base64, auf 3 MB gedeckelt). Verkürzt das Lesen-Schreiben-Zeitfenster je Operation und liefert lesbare Diffs für den Bestätigungsdialog statt eines Objekt-Dumps. ILessonRepository um GetById ergänzt (fehlte bisher, war aber Voraussetzung für jedes der neuen Tools). get_lesson_plans liefert jetzt zusätzlich Phase-IDs und Attachment-Metadaten, damit ein Client sie gezielt referenzieren kann. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,18 +10,27 @@ public sealed class McpToolsTests
|
||||
// ── McpToolScope ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void AllowedReadTools_EnthaeltGenauDieSechsReadTools()
|
||||
public void AllowedReadTools_EnthaeltGenauDieErwartetenReadTools()
|
||||
{
|
||||
Assert.Equal(
|
||||
new[] { "get_exams", "get_grades", "get_lesson_plans", "get_schedule", "get_students", "get_time_entries" },
|
||||
new[]
|
||||
{
|
||||
"download_lesson_attachment", "get_exams", "get_grades", "get_lesson_plans",
|
||||
"get_schedule", "get_students", "get_time_entries",
|
||||
},
|
||||
McpToolScope.AllowedReadTools.OrderBy(n => n, StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllowedWriteTools_EnthaeltGenauDieDreiPhase2WriteTools()
|
||||
public void AllowedWriteTools_EnthaeltGenauDieErwartetenWriteTools()
|
||||
{
|
||||
Assert.Equal(
|
||||
new[] { "create_grade_entry", "create_time_entry", "update_student_group_assignment" },
|
||||
new[]
|
||||
{
|
||||
"add_lesson_phase", "create_grade_entry", "create_lesson", "create_time_entry",
|
||||
"create_unit", "remove_lesson_phase", "update_lesson", "update_lesson_phase",
|
||||
"update_student_group_assignment", "update_unit",
|
||||
},
|
||||
McpToolScope.AllowedWriteTools.OrderBy(n => n, StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
@@ -211,6 +220,12 @@ public sealed class McpToolsTests
|
||||
|
||||
// ── 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()
|
||||
{
|
||||
@@ -220,7 +235,7 @@ public sealed class McpToolsTests
|
||||
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 = new LessonPlanTools(units, lessons);
|
||||
var tool = BuildLessonPlanTools(units, lessons);
|
||||
|
||||
var result = tool.GetLessonPlans(groupId, new DateOnly(2026, 1, 1), new DateOnly(2026, 1, 31));
|
||||
|
||||
@@ -229,6 +244,161 @@ public sealed class McpToolsTests
|
||||
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 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"));
|
||||
}
|
||||
|
||||
// ── GroupMembershipTools ─────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user