feat: Untis-Hub-Kadenzkopplung, anpassbarer Statusvorschlag + KI-/MCP-Unterstuetzung beim Fehlzeitenabgleich (Nutzer-Feedback)
CI / build-and-test (push) Canceled after 0s
CI / build-and-test (push) Canceled after 0s
- UntisHubService.RecordRun: ein abgeschlossener Langzeit-Fehlzeitenabgleich schliesst die kurzfristige Kadenz derselben Gruppe automatisch mit ab (nicht umgekehrt). - Fehlzeitenabgleich-Dialog: neue "Uebernahme als"-ComboBox statt starrem Zielstatus, vorbelegt mit dem berechneten Vorschlag, aber frei aenderbar. - Neuer ai-backend-Endpunkt untis-status.php + AiPlanningService.RequestUntisStatusSuggestionsAsync: gebuendelter, anonymisierter KI-Statusvorschlag (nur Positions-Id + Rohsignale, nie Name/Klasse/ Datum), mit hartem Id-Mengen-Abgleich gegen Verwechslung. - Neue MCP-Tools (UntisComparisonTools): get_untis_hub_status, get_untis_absence_rows/ apply_untis_absence_status (anonymer Weg ueber ENr-Zuordnung) sowie get_named_untis_absence_pattern als bewusste, eng begrenzte Ausnahme (Name+Fehlzeiten fuer explizit angegebene Schueler-IDs, mit Bestaetigung ohne Sitzungsfreigabe - dafuer IMcpConfirmationService.ConfirmAsync um allowSessionTrust erweitert). - MapStatus/ENr-Zuordnung aus dem ViewModel in das neue, geteilte UntisLessonAbsenceHelper gezogen, damit Dialog und MCP-Tool nie unterschiedliche Statusvorschlaege berechnen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.Services.Mcp.Tools;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
/// Aus Konsistenz mit den drei bestehenden WebUntis-Abgleichs-ViewModels bewusst ohne Tests für
|
||||
/// GetUntisAbsenceRows/GetNamedUntisAbsencePattern gelassen (siehe TODO.md) - beide rufen
|
||||
/// WebUntisIntegrationService.GetLessonAbsencesAsync auf, was einen echten WebUntis-JSON-RPC-
|
||||
/// Handshake voraussetzt. Getestet werden die Teile, die keinen WebUntis-Zugriff brauchen:
|
||||
/// GetUntisHubStatus (reine Delegation an UntisHubService) und die Validierungspfade von
|
||||
/// ApplyUntisAbsenceStatus, die vor jedem WebUntis-/Datenbankzugriff greifen.
|
||||
public sealed class UntisComparisonToolsTests
|
||||
{
|
||||
private static UntisComparisonTools Build(
|
||||
List<LearningGroup>? groups = null, List<Student>? students = null,
|
||||
List<ParticipationSession>? sessions = null, FakeMcpConfirmation? confirmation = null) =>
|
||||
new(new FakeGroups(groups ?? []), new FakeStudents(students ?? []), new FakeSessions(sessions ?? []),
|
||||
new FakeEntries(), TestSupport.BuildWebUntisIntegrationService(),
|
||||
TestSupport.BuildUntisHubService(groups ?? []), confirmation ?? new FakeMcpConfirmation());
|
||||
|
||||
[Fact]
|
||||
public void GetUntisHubStatus_DelegiertAnUntisHubServiceUndMapptKindUndDueStateAlsText()
|
||||
{
|
||||
var group = new LearningGroup { Name = "9a", WebUntisLessonId = 42 };
|
||||
var tool = Build([group]);
|
||||
|
||||
var rows = tool.GetUntisHubStatus();
|
||||
|
||||
Assert.Contains(rows, r => r.Kind == nameof(UntisHubJobKind.FehlzeitenKurz) && r.GroupName == "9a"
|
||||
&& r.DueState == nameof(UntisHubDueState.Overdue));
|
||||
Assert.Contains(rows, r => r.Kind == nameof(UntisHubJobKind.OffenePeriods) && r.GroupName == "Offene Stunden");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyUntisAbsenceStatus_UnbekannterStatus_WirdOhneRowIdPruefungAbgelehnt()
|
||||
{
|
||||
var tool = Build();
|
||||
|
||||
var result = await tool.ApplyUntisAbsenceStatus("irgendeine-id", "Suspendiert");
|
||||
|
||||
Assert.False(result.Applied);
|
||||
Assert.Contains("Status", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyUntisAbsenceStatus_NichtSelektierbarerAberGueltigerEnumWert_WirdAbgelehnt()
|
||||
{
|
||||
// "Truant"/"Suspended" etc. sind gültige AttendanceStatus-Werte, aber keine, die WebUntis
|
||||
// hier je melden würde (siehe SelectableStatuses) - müssen trotzdem abgelehnt werden, damit
|
||||
// ein KI-Client nicht versehentlich einen fachlich unpassenden Status setzen kann.
|
||||
var tool = Build();
|
||||
|
||||
var result = await tool.ApplyUntisAbsenceStatus("irgendeine-id", nameof(AttendanceStatus.Truant));
|
||||
|
||||
Assert.False(result.Applied);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyUntisAbsenceStatus_UnbekannteRowId_WirdAbgelehntOhneBestaetigung()
|
||||
{
|
||||
var confirmation = new FakeMcpConfirmation();
|
||||
var tool = Build(confirmation: confirmation);
|
||||
|
||||
var result = await tool.ApplyUntisAbsenceStatus("nie-vergeben", nameof(AttendanceStatus.Excused));
|
||||
|
||||
Assert.False(result.Applied);
|
||||
Assert.Contains("row-id", result.Message);
|
||||
Assert.Equal(0, confirmation.CallCount); // erst gar nicht bis zur Bestätigung vorgedrungen
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user