Die Betriebskontrolle im Testbetrieb meldete Stundendateien als fehlend, die es gab: 105 Eintraege in der Reihenfolge des Dateisystems, vierzeilig als eingeruecktes JSON. list liefert jetzt die 50 zuletzt geaenderten Eintraege, neueste zuerst, eine Zeile je Eintrag, mit Gesamtzahl im Kopf; pattern, sort, order und limit steuern das. Dabei aufgefallen (#675): Directory.GetFiles wertet Trennzeichen im Muster aus - pattern "../*" fuehrte an der Pfadpruefung vorbei aus dem Verzeichnis heraus, in list wie in read_many. Muster mit '/', '\' oder '..' werden jetzt abgewiesen. Und derselbe Befund ist keine zweite Aufgabe (#674): task_find beantwortet die Frage vor dem Anlegen, task_create legt bei gleichbedeutendem Titel einer offenen Aufgabe keine zweite an, sondern vermerkt das erneute Auftreten an der bestehenden. force=true geht trotzdem, Erledigtes blockiert nichts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
98 lines
4.5 KiB
C#
98 lines
4.5 KiB
C#
using ClawdDotNet.Core.Tasks;
|
|
using Shouldly;
|
|
|
|
namespace ClawdDotNet.Core.Tests.Tasks;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|