using ClawdDotNet.Core.Tasks; using Shouldly; namespace ClawdDotNet.Core.Tests.Tasks; /// /// Title comparison (#674). The cases are the real titles the test operation produced: /// over thirty open tasks for @human, six of them the same exchange-rate complaint. /// public sealed class TaskSimilarityTests { // ─── the duplicates that actually piled up ─── [Theory] [InlineData("Betriebsluecke: Eur Kurse nicht aktuell", "Betriebsluecke: Kurse EUR nicht aktuell")] [InlineData("Betriebsluecke: Eur Kurse", "Betriebsluecke: Eur Kurse nicht aktuell")] [InlineData("Betriebsluecke: Nachrichten", "Betriebsluecke: Nachrichten heute fehlend")] [InlineData("Betriebsluecke: Wetter Stunden", "Betriebsluecke: Wetter Stunden (17:00, 18:00 fehlen)")] [InlineData("Betriebslücke: Währungskurse eur.csv", "Betriebsluecke: Waehrungskurse eur.csv")] [InlineData("Morgenbericht", "morgenbericht")] public void The_same_finding_told_twice_is_a_duplicate(string left, string right) { TaskSimilarity.Compare(left, right).ShouldBe(TaskTitleMatch.Duplicate); TaskSimilarity.Compare(right, left).ShouldBe(TaskTitleMatch.Duplicate, "the comparison is symmetric"); } // ─── and the ones that must stay apart ─── [Theory] [InlineData("Betriebsluecke: Wetter Stunden", "Betriebsluecke: Wetter Warnungen stand.md veraltet")] [InlineData("Betriebsluecke: Nachrichten fehlen", "Betriebsluecke: Eur Kurse nicht aktuell")] [InlineData("Wetter", "Wetter Warnungen stand.md veraltet")] [InlineData("Morgenbericht", "Tagesabschluss Wetter")] [InlineData("DB-Pruefung: Wetterwerte uebernehmen", "Web-Pruefung: Grenzen der Whitelist")] public void Different_subjects_are_not_duplicates(string left, string right) => TaskSimilarity.Compare(left, right).ShouldNotBe(TaskTitleMatch.Duplicate); [Fact] public void A_single_shared_word_does_not_swallow_a_whole_topic() { // "Wetter" is one word of many weather tasks - containment must not apply here. TaskSimilarity.Compare("Wetter", "Wetter Stunden fehlen seit gestern").ShouldBe(TaskTitleMatch.None); } [Fact] public void Related_but_distinguishable_titles_come_back_as_similar() => TaskSimilarity.Compare( "Betriebsluecke: Wetter Stunden fehlen", "Betriebsluecke: Wetter Stunden veraltet").ShouldBe(TaskTitleMatch.Similar); // ─── normalisation ─── [Theory] [InlineData("Betriebslücke: Wetter", "betriebsluecke wetter")] [InlineData(" Doppelte Leerzeichen ", "doppelte leerzeichen")] [InlineData("Größe/Maß (17:00)", "groesse mass 17 00")] [InlineData("Café", "cafe")] [InlineData(null, "")] public void Normalisation_folds_case_umlauts_and_punctuation(string? input, string expected) => TaskSimilarity.Normalize(input).ShouldBe(expected); // ─── only open tasks can be duplicated ─── [Fact] public void A_closed_task_does_not_block_a_new_one() { var tasks = new[] { new TaskItem { Id = "t-alt", Title = "Betriebsluecke: Eur Kurse", Status = TaskItemStatus.Done }, new TaskItem { Id = "t-weg", Title = "Betriebsluecke: Eur Kurse", Status = TaskItemStatus.Canceled }, new TaskItem { Id = "t-offen", Title = "Betriebsluecke: Eur Kurse nicht aktuell", Status = TaskItemStatus.Todo } }; var hits = TaskSimilarity.FindOpen(tasks, "Betriebsluecke: Kurse EUR nicht aktuell"); hits.Count.ShouldBe(1, "erledigte Aufgaben sind erledigt - derselbe Befund darf wiederkommen"); hits[0].Task.Id.ShouldBe("t-offen"); hits[0].Match.ShouldBe(TaskTitleMatch.Duplicate); } [Fact] public void The_closest_match_comes_first_and_the_task_itself_is_skipped() { var tasks = new[] { new TaskItem { Id = "t-verwandt", Title = "Betriebsluecke: Wetter Stunden veraltet", Status = TaskItemStatus.Todo }, new TaskItem { Id = "t-gleich", Title = "Betriebsluecke: Wetter Stunden fehlen", Status = TaskItemStatus.Todo }, new TaskItem { Id = "t-selbst", Title = "Betriebsluecke: Wetter Stunden fehlen", Status = TaskItemStatus.Todo } }; var hits = TaskSimilarity.FindOpen(tasks, "Betriebsluecke: Wetter Stunden fehlen", exceptId: "t-selbst"); hits.Select(h => h.Task.Id).ShouldBe(["t-gleich", "t-verwandt"]); hits[0].Match.ShouldBe(TaskTitleMatch.Duplicate); hits[1].Match.ShouldBe(TaskTitleMatch.Similar); } }