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:
@@ -54,6 +54,10 @@ public sealed class UntisDiffServiceTests
|
||||
Assert.Equal(SubstitutionKind.Lesson, entry.Kind);
|
||||
Assert.Equal("1", entry.ExternalId);
|
||||
Assert.Equal(1, entry.PeriodNumber);
|
||||
// Diagnose direkt in der Beschreibung (siehe DeviationReason) - damit der Nutzer im
|
||||
// Stundenplan selbst ablesen kann, welcher Vergleich genau fehlgeschlagen ist, ohne
|
||||
// Rohdaten aus dem Feed teilen zu müssen.
|
||||
Assert.Contains("Fach weicht ab", entry.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -64,7 +68,8 @@ public sealed class UntisDiffServiceTests
|
||||
|
||||
var result = new UntisDiffService().Diff(events, [], [mapping], Monday);
|
||||
|
||||
Assert.Single(result.SubstitutionsToSave);
|
||||
var entry = Assert.Single(result.SubstitutionsToSave);
|
||||
Assert.Contains("Klasse weicht ab", entry.Description);
|
||||
}
|
||||
|
||||
// ── Regression: "Der Mathematik E-Kurs [...] ist ein Kurs aus den Klassen 10a, 10b und 10c
|
||||
@@ -105,6 +110,85 @@ public sealed class UntisDiffServiceTests
|
||||
Assert.Single(result.SubstitutionsToSave);
|
||||
}
|
||||
|
||||
// ── Regression, zweiter Anlauf: der erste Fix (Leerzeichen nur auf evt.Description-Seite
|
||||
// entfernt) reichte nicht, wenn mapping.ClassToken SELBST ein eingebettetes Leerzeichen trägt
|
||||
// — das passiert, wenn die reale DESCRIPTION Klassen nicht mit ";", sondern mit "," trennt:
|
||||
// ExtractClassTokens fand dann kein ";" und speicherte "10a, 10b, 10c" als EIN Token samt
|
||||
// Kommas/Leerzeichen statt drei getrennte Tokens. Ein Vergleich gegen die leerzeichenbereinigte
|
||||
// Description konnte dieses Token dann strukturell NIE finden, unabhängig von der im Dialog
|
||||
// gewählten Gruppe - genau das vom Nutzer beschriebene "immer noch nicht behoben".
|
||||
|
||||
[Fact]
|
||||
public void Diff_KombinierteGruppeMitKommaGetrenntemClassToken_ErzeugtKeineFalscheVertretung()
|
||||
{
|
||||
var mapping = new UntisSlotMapping
|
||||
{
|
||||
// So sieht ClassToken aus, wenn ExtractClassTokens die Klassen mangels ";" nicht
|
||||
// aufsplitten konnte und "10a, 10b, 10c" als ein einziges Token übernommen hat.
|
||||
Weekday = DayOfWeek.Monday, StartTime = SlotStart, Summary = "Mat_E", ClassToken = "10a, 10b, 10c",
|
||||
GroupId = Guid.NewGuid(), PeriodNumber = 1, Confirmed = true,
|
||||
};
|
||||
var events = new List<UntisIcsEvent> { BuildEvent("1", summary: "Mat_E", description: "10a, 10b, 10c HED") };
|
||||
|
||||
var result = new UntisDiffService().Diff(events, [], [mapping], Monday);
|
||||
|
||||
Assert.Empty(result.SubstitutionsToSave);
|
||||
}
|
||||
|
||||
// ── Regression: "Es ist immer noch so [...] Immer die Kurse mit Lerngruppen, die aus
|
||||
// mehreren Klassen zusammengesetzt sind" — eine zuvor erkannte Abweichung, die beim nächsten
|
||||
// Poll nicht mehr besteht, muss die ursprünglich erzeugte Vertretung wieder aufräumen, statt
|
||||
// als Karteileiche stehen zu bleiben.
|
||||
|
||||
[Fact]
|
||||
public void Diff_UnveraendertesEreignis_MeldetDessenExternalIdZumAufraeumen()
|
||||
{
|
||||
var mapping = BuildMapping(Guid.NewGuid());
|
||||
var events = new List<UntisIcsEvent> { BuildEvent("1") };
|
||||
|
||||
var result = new UntisDiffService().Diff(events, [], [mapping], Monday);
|
||||
|
||||
Assert.Contains("1", result.SubstitutionExternalIdsToDelete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Diff_AufsichtWirdZurRegulaerenDuty_MeldetExternalIdZumAufraeumen()
|
||||
{
|
||||
var mapping = BuildSupervisionMapping();
|
||||
var events = new List<UntisIcsEvent>
|
||||
{
|
||||
new() { Uid = "1", Date = Monday.AddDays(1), StartTime = mapping.StartTime, Status = "CONFIRMED" },
|
||||
};
|
||||
var duties = new List<SupervisionDuty> { new() { Weekday = mapping.Weekday, AfterPeriod = mapping.AfterPeriod!.Value } };
|
||||
|
||||
var result = new UntisDiffService().Diff(events, [], [mapping], Monday, existingSupervisionDuties: duties);
|
||||
|
||||
Assert.Empty(result.SubstitutionsToSave);
|
||||
Assert.Contains("1", result.SubstitutionExternalIdsToDelete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Diff_VormalsFehlendeStundeIstWiederImFeed_MeldetMissingExternalIdZumAufraeumen()
|
||||
{
|
||||
var groupId = Guid.NewGuid();
|
||||
var mapping = new UntisSlotMapping
|
||||
{
|
||||
Weekday = DayOfWeek.Thursday, StartTime = new TimeOnly(9, 40), Summary = "NAT", ClassToken = "8c",
|
||||
GroupId = groupId, PeriodNumber = 3, Confirmed = true,
|
||||
};
|
||||
var thisThursday = Monday.AddDays(3);
|
||||
var currentEvents = new List<UntisIcsEvent>
|
||||
{
|
||||
new() { Uid = "regular-1", Date = thisThursday, StartTime = new TimeOnly(9, 40), EndTime = new TimeOnly(11, 10), Summary = "NAT", Description = "8c HED" },
|
||||
};
|
||||
|
||||
var result = new UntisDiffService().Diff(currentEvents, [], [mapping], Monday);
|
||||
|
||||
Assert.Empty(result.SubstitutionsToSave);
|
||||
var expectedMissingId = $"missing-{mapping.Id}-{thisThursday:yyyyMMdd}";
|
||||
Assert.Contains(expectedMissingId, result.SubstitutionExternalIdsToDelete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Diff_StatusCancelled_ErzeugtAusfall()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user