fix: WebUntis-Vertretungen für kombinierte Gruppen und Karteileichen

- ClassToken-Vergleich normalisiert jetzt symmetrisch (beide Seiten) und
  ExtractClassTokens akzeptiert ";" und "," als Trennzeichen zwischen
  Klassen, da nie an einem echten kombinierten Termin verifiziert wurde,
  welches WebUntis tatsächlich verwendet.
- Abweichende Vertretungen tragen jetzt den genauen Vergleichsgrund
  (Fach/Klasse, roh vs. erwartet) in ihrer Beschreibung.
- Automatisch erzeugte Vertretungen, die bei einem späteren Poll nicht
  mehr abweichen, werden jetzt aktiv wieder entfernt statt als
  Karteileichen stehen zu bleiben (UntisDiffResult.SubstitutionExternalIdsToDelete).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 18:43:36 +02:00
co-authored by Claude Sonnet 5
parent 4eb4d0a946
commit 4b9bcfe85d
7 changed files with 240 additions and 14 deletions
@@ -102,6 +102,29 @@ public sealed class UntisMatchingServiceTests
Assert.True(match.IsConfident);
}
[Fact]
public void BuildMatches_KombinierteKlassenKommaGetrennt_WerdenEbenfallsAufgesplittet()
{
// Nicht empirisch belegt, ob WebUntis mehrere Klassen mit ";" oder "," trennt (das einzige
// real verifizierte Beispiel hatte nur eine Klasse) - ExtractClassTokens darf sich deshalb
// nicht auf ";" verlassen, sonst landet "10a, 10b, 10c" als EIN Token mit Leerzeichen statt
// drei getrennten (siehe UntisDiffServiceTests: Diff_KombinierteGruppeMitKommaGetrenntemClassToken...).
var groupA = new LearningGroup { Name = "10a" };
var groupB = new LearningGroup { Name = "10b" };
var events = new List<UntisIcsEvent>
{
Event(DayOfWeek.Wednesday, new TimeOnly(9, 40), new TimeOnly(11, 10), "Mat_E", "10a, 10b HED"),
};
var existingSlot = new TimetableSlot { GroupId = groupB.Id, Weekday = DayOfWeek.Wednesday, PeriodNumber = 3 };
var result = new UntisMatchingService().BuildMatches(events, [groupA, groupB], [existingSlot], BuildPeriodSchedule());
var match = Assert.Single(result.Matches);
Assert.Equal(["10a", "10b"], match.Pattern.ClassTokens);
Assert.Equal(groupB.Id, match.SuggestedGroupId);
Assert.True(match.IsConfident);
}
[Fact]
public void BuildMatches_KeinePassendeGruppe_BleibtUnbestaetigt()
{