Files
RichardandClaude Opus 5 060dc6a6ad FileRW list sortiert und gedeckelt (#673), Taskboard task_find (#674)
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>
2026-09-21 21:19:19 +02:00

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);
}
}