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
@@ -85,6 +85,40 @@ public sealed class UntisSyncServiceTests
Assert.Single(substitutions.GetAll());
}
// ── Regression: "Es ist immer noch so [...] Immer die Kurse mit Lerngruppen, die aus mehreren
// Klassen zusammengesetzt sind" — nachdem der eigentliche Vergleichsfehler (ClassToken/
// Trennzeichen) behoben war, blieben bereits erzeugte falsche Vertretungen trotzdem stehen:
// ProcessIcsText schrieb bislang nur NEUE/weiterhin abweichende Kandidaten, räumte aber nie
// eine zuvor automatisch erzeugte Zeile ab, sobald der nächste Poll gar keine Abweichung mehr
// fand. Der Nutzer sah dadurch dieselbe (jetzt stale) Karteileiche, egal wie oft er neu
// zuordnete oder pollte.
[Fact]
public void ProcessIcsText_AbweichungLoestSichSpaeterAuf_EntferntDieZuvorErzeugteVertretung()
{
var mappings = new FakeUntisSlotMappings();
mappings.Add(new UntisSlotMapping
{
Weekday = DayOfWeek.Monday, StartTime = new TimeOnly(7, 50), Summary = "SOL",
ClassToken = "10c", GroupId = Guid.NewGuid(), PeriodNumber = 1, Confirmed = true,
});
var substitutions = new FakeSubstitutionEntries();
var service = BuildService(mappings: mappings, substitutions: substitutions);
// Poll 1: Fach weicht (noch) ab - Vertretung wird angelegt.
var first = service.ProcessIcsText(BuildIcs("1", "20260817T075000", "NAT", "10c HED"));
Assert.Equal(1, first.SubstitutionCount);
Assert.Single(substitutions.GetAll());
// Poll 2: derselbe Termin weicht jetzt nicht mehr ab (z.B. weil sich die WebUntis-Daten
// geändert haben oder ein Vergleichsfehler zwischenzeitlich behoben wurde) - die zuvor
// erzeugte Vertretung darf nicht als Karteileiche stehen bleiben.
var second = service.ProcessIcsText(BuildIcs("1", "20260817T075000", "SOL", "10c HED"));
Assert.Equal(0, second.SubstitutionCount);
Assert.Empty(substitutions.GetAll());
}
// ── Regression: "Am 27.08. fällt eine Stunde NAT in der 8c aus. Dieser Ausfall steht nicht
// im Plan [...] die Klasse ist dort weg" — end-to-end über ProcessIcsText, inkl. der neuen
// Ferien-Ausschluss-Verdrahtung (ISchoolHolidayRepository -> UntisDiffService.freeDates).