Erlaubt einem lokalen KI-Client (z.B. Claude Desktop) strukturierten Lesezugriff auf Schüler, Klausuren, Noten, Stundenplan und Zeiterfassung. Neuer LehrerApp.McpBridge-Prozess reicht stdio-JSON-RPC über eine Named Pipe an einen In-Process-MCP-Server im Avalonia-Hauptprozess durch (ModelContextProtocol.Core, StreamServerTransport direkt auf der Pipe). Standardmäßig deaktiviert, Opt-in über neuen Einstellungen-Tab. Dokumentationstypen (Gesprächsnotizen/Vorfälle/Förderpläne) sind auf Code-Ebene nie erreichbar (McpToolScope, analog PlainEventStore.Allowed). Write-Tools, Bestätigungsdialog-UI, Worksheets/Lesson-Plans-Tools und macOS-Packaging folgen in späteren Phasen (siehe TODO.md 4.5.25). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
139 lines
6.1 KiB
C#
139 lines
6.1 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_EnthaeltGenauDieFuenfPhase1Tools()
|
|
{
|
|
Assert.Equal(
|
|
new[] { "get_exams", "get_grades", "get_schedule", "get_students", "get_time_entries" },
|
|
McpToolScope.AllowedReadTools.OrderBy(n => n, StringComparer.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void AllowedReadTools_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.
|
|
Assert.DoesNotContain(McpToolScope.AllowedReadTools, n =>
|
|
n.Contains("documentation", StringComparison.OrdinalIgnoreCase) ||
|
|
n.Contains("vorgang", 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);
|
|
|
|
var dto = Assert.Single(tool.GetGrades(groupId, studentA));
|
|
|
|
Assert.Equal(studentA, dto.StudentId);
|
|
}
|
|
|
|
// ── 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);
|
|
|
|
var dto = Assert.Single(tool.GetTimeEntries(new DateOnly(2026, 1, 1), new DateOnly(2026, 1, 31)));
|
|
|
|
Assert.Equal(30, dto.DurationMinutes);
|
|
}
|
|
}
|