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>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@
|
||||
<ProjectReference Include="..\..\src\ClawdDotNet.Tools.FileRW\ClawdDotNet.Tools.FileRW.csproj" />
|
||||
<ProjectReference Include="..\..\src\ClawdDotNet.Tools.Memory\ClawdDotNet.Tools.Memory.csproj" />
|
||||
<ProjectReference Include="..\..\src\ClawdDotNet.Tools.SocialMediaManager\ClawdDotNet.Tools.SocialMediaManager.csproj" />
|
||||
<ProjectReference Include="..\..\src\ClawdDotNet.Tools.Taskboard\ClawdDotNet.Tools.Taskboard.csproj" />
|
||||
<ProjectReference Include="..\..\src\ClawdDotNet.Tools.WebFetch\ClawdDotNet.Tools.WebFetch.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
using System.Text.Json;
|
||||
using ClawdDotNet.Core.Tools;
|
||||
using ClawdDotNet.Tools.FileRW;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Shouldly;
|
||||
|
||||
namespace ClawdDotNet.Tools.Tests.FileRW;
|
||||
|
||||
/// <summary>
|
||||
/// Sortiertes, gedeckeltes Auflisten (#673).
|
||||
///
|
||||
/// Anlass aus dem Testbetrieb: Die Betriebskontrolle meldete zwei Stundendateien als
|
||||
/// fehlend, die es gab. Das Verzeichnis lieferte 105 Eintraege in der Reihenfolge des
|
||||
/// Dateisystems — darin zwei Namen zu finden ist Sucharbeit, und ein guenstiges Modell
|
||||
/// erledigt sie falsch.
|
||||
/// </summary>
|
||||
public sealed class VerzeichnisListeTests : IDisposable
|
||||
{
|
||||
private readonly string _basis;
|
||||
private readonly string _personal;
|
||||
private readonly string _shared;
|
||||
private readonly FileRWTool _tool = new();
|
||||
|
||||
public VerzeichnisListeTests()
|
||||
{
|
||||
_basis = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
|
||||
_personal = Path.Combine(_basis, "Workspace");
|
||||
_shared = Path.Combine(_basis, "SharedWorkspace");
|
||||
Directory.CreateDirectory(_personal);
|
||||
Directory.CreateDirectory(Path.Combine(_shared, "wetter", "stunden"));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(_basis, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
}
|
||||
|
||||
private AgentToolContext Context() => new(
|
||||
"hausmeister", "inst",
|
||||
new Dictionary<string, object?> { ["sharedAccessLevel"] = "ReadWrite" },
|
||||
new NullStateStore(),
|
||||
NullLogger.Instance,
|
||||
CancellationToken.None,
|
||||
WorkspacePath: _personal,
|
||||
SharedWorkspacePath: _shared);
|
||||
|
||||
private Task<ToolResult> Run(string json)
|
||||
=> _tool.ExecuteAsync(JsonDocument.Parse(json).RootElement, Context(), CancellationToken.None);
|
||||
|
||||
/// <summary>Stundendateien, absichtlich in gemischter Reihenfolge geschrieben.</summary>
|
||||
private void Stundendateien(int anzahl, string tag = "2026-09-21")
|
||||
{
|
||||
var verzeichnis = Path.Combine(_shared, "wetter", "stunden");
|
||||
foreach (var stunde in Enumerable.Range(0, anzahl).OrderBy(_ => Guid.NewGuid()))
|
||||
{
|
||||
var pfad = Path.Combine(verzeichnis, $"berlin_{tag}_{stunde:00}-00.txt");
|
||||
File.WriteAllText(pfad, $"{tag} {stunde:00}:00 Temperatur {10 + stunde} C");
|
||||
File.SetLastWriteTime(pfad, DateTime.Parse($"{tag}T{stunde:00}:05:00"));
|
||||
}
|
||||
}
|
||||
|
||||
private static List<string> Namen(string inhalt) => inhalt
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(z => z.Trim())
|
||||
.Where(z => z.StartsWith("20", StringComparison.Ordinal))
|
||||
.Select(z => z.Split(" ", StringSplitOptions.RemoveEmptyEntries).Last())
|
||||
.ToList();
|
||||
|
||||
// ═══════════════════ Reihenfolge ═══════════════════
|
||||
|
||||
[Fact]
|
||||
public async Task Die_juengste_Datei_steht_oben()
|
||||
{
|
||||
Stundendateien(6);
|
||||
|
||||
var result = await Run("""{"action":"list","workspace":"shared","path":"wetter/stunden"}""");
|
||||
|
||||
result.Success.ShouldBeTrue(result.ErrorMessage);
|
||||
result.Content.ShouldContain("neueste zuerst");
|
||||
Namen(result.Content).First().ShouldBe("berlin_2026-09-21_05-00.txt");
|
||||
Namen(result.Content).Last().ShouldBe("berlin_2026-09-21_00-00.txt");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Nach_Namen_sortiert_laeuft_aufsteigend()
|
||||
{
|
||||
Stundendateien(4);
|
||||
|
||||
var result = await Run("""{"action":"list","workspace":"shared","path":"wetter/stunden","sort":"name"}""");
|
||||
|
||||
Namen(result.Content).ShouldBe([
|
||||
"berlin_2026-09-21_00-00.txt", "berlin_2026-09-21_01-00.txt",
|
||||
"berlin_2026-09-21_02-00.txt", "berlin_2026-09-21_03-00.txt"
|
||||
]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Die_Richtung_laesst_sich_umkehren()
|
||||
{
|
||||
Stundendateien(4);
|
||||
|
||||
var result = await Run("""{"action":"list","workspace":"shared","path":"wetter/stunden","sort":"modified","order":"asc"}""");
|
||||
|
||||
result.Content.ShouldContain("aelteste zuerst");
|
||||
Namen(result.Content).First().ShouldBe("berlin_2026-09-21_00-00.txt");
|
||||
}
|
||||
|
||||
// ═══════════════════ Deckel und Gesamtzahl ═══════════════════
|
||||
|
||||
[Fact]
|
||||
public async Task Der_Kopf_nennt_immer_die_Gesamtzahl()
|
||||
{
|
||||
Stundendateien(24);
|
||||
Stundendateien(24, "2026-09-20");
|
||||
Stundendateien(24, "2026-09-19");
|
||||
|
||||
var result = await Run("""{"action":"list","workspace":"shared","path":"wetter/stunden"}""");
|
||||
|
||||
result.Content.ShouldContain($"{FileRWTool.StandardEintraege} von 72 Eintraegen");
|
||||
result.Content.ShouldContain("22 weitere nicht gezeigt");
|
||||
result.Content.ShouldContain("'pattern'", Case.Sensitive, "der Agent soll wissen, wie er gezielt fragt");
|
||||
Namen(result.Content).Count.ShouldBe(FileRWTool.StandardEintraege);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Ein_eigenes_Limit_wird_beachtet_und_gedeckelt()
|
||||
{
|
||||
Stundendateien(10);
|
||||
|
||||
var wenig = await Run("""{"action":"list","workspace":"shared","path":"wetter/stunden","limit":3}""");
|
||||
Namen(wenig.Content).Count.ShouldBe(3);
|
||||
wenig.Content.ShouldContain("3 von 10 Eintraegen");
|
||||
|
||||
var zuviel = await Run("""{"action":"list","workspace":"shared","path":"wetter/stunden","limit":99999}""");
|
||||
Namen(zuviel.Content).Count.ShouldBe(10, "mehr als da ist gibt es nicht");
|
||||
}
|
||||
|
||||
// ═══════════════════ Gezielt fragen ═══════════════════
|
||||
|
||||
[Fact]
|
||||
public async Task Ein_Muster_beantwortet_die_Frage_nach_einer_bestimmten_Datei()
|
||||
{
|
||||
Stundendateien(24);
|
||||
Stundendateien(24, "2026-09-20");
|
||||
|
||||
var result = await Run("""{"action":"list","workspace":"shared","path":"wetter/stunden","pattern":"berlin_2026-09-21_1?-00.txt"}""");
|
||||
|
||||
result.Success.ShouldBeTrue(result.ErrorMessage);
|
||||
result.Content.ShouldContain("10 von 10 Eintraegen");
|
||||
result.Content.ShouldContain("Muster:");
|
||||
Namen(result.Content).ShouldAllBe(n => n.StartsWith("berlin_2026-09-21_1"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Ein_leeres_Verzeichnis_ist_kein_Fehler_sondern_eine_Null()
|
||||
{
|
||||
var result = await Run("""{"action":"list","workspace":"shared","path":"wetter/stunden"}""");
|
||||
|
||||
result.Success.ShouldBeTrue(result.ErrorMessage);
|
||||
result.Content.ShouldContain("0 von 0 Eintraegen");
|
||||
}
|
||||
|
||||
// ═══════════════════ Form ═══════════════════
|
||||
|
||||
[Fact]
|
||||
public async Task Jeder_Eintrag_steht_auf_genau_einer_Zeile()
|
||||
{
|
||||
Stundendateien(3);
|
||||
Directory.CreateDirectory(Path.Combine(_shared, "wetter", "stunden", "alt"));
|
||||
|
||||
var result = await Run("""{"action":"list","workspace":"shared","path":"wetter/stunden"}""");
|
||||
|
||||
result.Content.ShouldNotContain("\"Name\":", Case.Sensitive, "kein eingeruecktes JSON mehr");
|
||||
result.Content.ShouldContain("alt/", Case.Sensitive);
|
||||
result.Content.ShouldContain("<DIR>", Case.Sensitive);
|
||||
Namen(result.Content).Count.ShouldBe(4);
|
||||
}
|
||||
|
||||
// ═══════════════════ Das Muster ist kein zweiter Pfad ═══════════════════
|
||||
|
||||
[Theory]
|
||||
[InlineData("list")]
|
||||
[InlineData("read_many")]
|
||||
public async Task Ein_Muster_darf_nicht_aus_dem_Verzeichnis_herausfuehren(string action)
|
||||
{
|
||||
// Directory.GetFiles wertet Trennzeichen im Muster aus: "../*" listet das
|
||||
// Elternverzeichnis, ohne dass die Pfadpruefung je gefragt wurde.
|
||||
Stundendateien(2);
|
||||
File.WriteAllText(Path.Combine(_shared, "wetter", "geheim.txt"), "nicht fuer den Agenten");
|
||||
|
||||
var result = await Run($$"""{"action":"{{action}}","workspace":"shared","path":"wetter/stunden","pattern":"../*"}""");
|
||||
|
||||
result.Success.ShouldBeFalse("ein Muster ist ein Dateiname, kein Pfad");
|
||||
result.ErrorMessage.ShouldNotBeNull().ShouldContain("Muster");
|
||||
(result.Content ?? "").ShouldNotContain("geheim");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("unter/*.txt")]
|
||||
[InlineData("..\\*")]
|
||||
[InlineData("*/../*")]
|
||||
public async Task Auch_die_anderen_Pfadschreibweisen_werden_abgewiesen(string muster)
|
||||
{
|
||||
Stundendateien(2);
|
||||
|
||||
var input = JsonSerializer.SerializeToElement(
|
||||
new { action = "list", workspace = "shared", path = "wetter/stunden", pattern = muster });
|
||||
var result = await _tool.ExecuteAsync(input, Context(), CancellationToken.None);
|
||||
|
||||
result.Success.ShouldBeFalse();
|
||||
result.ErrorMessage.ShouldNotBeNull().ShouldContain("kein Pfad");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
using System.Text.Json;
|
||||
using ClawdDotNet.Core.State;
|
||||
using ClawdDotNet.Core.Storage;
|
||||
using ClawdDotNet.Core.Tasks;
|
||||
using ClawdDotNet.Core.Tools;
|
||||
using ClawdDotNet.Tools.Taskboard;
|
||||
using ClawdDotNet.Tools.Tests.FileRW;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Shouldly;
|
||||
|
||||
namespace ClawdDotNet.Tools.Tests.Taskboard;
|
||||
|
||||
/// <summary>
|
||||
/// task_find und die Dublettenbremse (#674).
|
||||
///
|
||||
/// Anlass aus dem Testbetrieb: Die Betriebskontrolle laeuft alle drei Stunden und legte
|
||||
/// bei jedem Lauf erneut an, was beim vorigen schon offen war — ueber dreissig Aufgaben
|
||||
/// an @human, darunter sechsmal derselbe Kursbefund.
|
||||
/// </summary>
|
||||
public sealed class DublettenTests : IDisposable
|
||||
{
|
||||
private readonly string _verzeichnis;
|
||||
private readonly SqliteStorage _storage;
|
||||
private readonly SqliteTaskRepository _repo;
|
||||
private readonly TaskboardService _board;
|
||||
private readonly TaskboardTool _tool = new();
|
||||
|
||||
public DublettenTests()
|
||||
{
|
||||
_verzeichnis = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
|
||||
_storage = new SqliteStorage(Path.Combine(_verzeichnis, "state.db"));
|
||||
_repo = new SqliteTaskRepository(_storage);
|
||||
_board = new TaskboardService(_repo, Path.Combine(_verzeichnis, "SharedWorkspace", "tasks"));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
try { Directory.Delete(_verzeichnis, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
}
|
||||
|
||||
private AgentToolContext Context() => new(
|
||||
"hausmeister", "inst",
|
||||
new Dictionary<string, object?>(),
|
||||
new NullStateStore(),
|
||||
NullLogger.Instance,
|
||||
CancellationToken.None,
|
||||
SharedWorkspacePath: Path.Combine(_verzeichnis, "SharedWorkspace"),
|
||||
Tasks: _repo);
|
||||
|
||||
private Task<ToolResult> Run(string json)
|
||||
=> _tool.ExecuteAsync(JsonDocument.Parse(json).RootElement, Context(), CancellationToken.None);
|
||||
|
||||
private Task<TaskItem> Anlegen(string titel, TaskItemStatus status = TaskItemStatus.Todo)
|
||||
=> _board.CreateAsync(new TaskItem { Title = titel, Status = status, Assignee = "@human", Body = "Beobachtet." }, default);
|
||||
|
||||
// ─── task_find ───
|
||||
|
||||
[Fact]
|
||||
public async Task Ohne_Treffer_sagt_die_Antwort_dass_angelegt_werden_darf()
|
||||
{
|
||||
await Anlegen("Betriebsluecke: Wetter Warnungen veraltet");
|
||||
|
||||
var result = await Run("""{"action":"task_find","title":"Betriebsluecke: Eur Kurse nicht aktuell"}""");
|
||||
|
||||
result.Success.ShouldBeTrue(result.ErrorMessage);
|
||||
result.Content.ShouldContain("Keine offene Aufgabe ähnelt");
|
||||
result.Content.ShouldContain("task_create");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Der_gleiche_Befund_wird_mit_Id_Alter_und_Einstufung_gefunden()
|
||||
{
|
||||
var vorhanden = await Anlegen("Betriebsluecke: Eur Kurse nicht aktuell");
|
||||
|
||||
var result = await Run("""{"action":"task_find","title":"Betriebsluecke: Kurse EUR nicht aktuell"}""");
|
||||
|
||||
result.Success.ShouldBeTrue(result.ErrorMessage);
|
||||
result.Content.ShouldContain(vorhanden.Id);
|
||||
result.Content.ShouldContain("derselbe Sachverhalt");
|
||||
result.Content.ShouldContain("offen seit");
|
||||
result.Content.ShouldContain("task_comment", Case.Sensitive, "der Agent soll ergaenzen, nicht neu anlegen");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Ohne_Titel_fragt_das_Werkzeug_nach()
|
||||
{
|
||||
var result = await Run("""{"action":"task_find"}""");
|
||||
|
||||
result.Success.ShouldBeFalse();
|
||||
result.ErrorMessage.ShouldNotBeNull().ShouldContain("'title' ist erforderlich");
|
||||
}
|
||||
|
||||
// ─── die Bremse beim Anlegen ───
|
||||
|
||||
[Fact]
|
||||
public async Task Eine_zweite_gleichlautende_Aufgabe_wird_nicht_angelegt()
|
||||
{
|
||||
var erste = await Anlegen("Betriebsluecke: Eur Kurse nicht aktuell");
|
||||
|
||||
var result = await Run("""{"action":"task_create","title":"Betriebsluecke: Kurse EUR nicht aktuell","assignee":"@human"}""");
|
||||
|
||||
result.Success.ShouldBeFalse();
|
||||
result.ErrorMessage.ShouldNotBeNull().ShouldContain(erste.Id);
|
||||
result.ErrorMessage.ShouldContain("vermerkt");
|
||||
|
||||
(await _board.ListAsync(new TaskQuery { Limit = 50 }, default)).Count
|
||||
.ShouldBe(1, "es bleibt bei der einen Aufgabe");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Das_erneute_Auftreten_steht_danach_an_der_bestehenden_Aufgabe()
|
||||
{
|
||||
var erste = await Anlegen("Betriebsluecke: Nachrichten fehlen");
|
||||
|
||||
await Run("""{"action":"task_create","title":"Betriebsluecke: Nachrichten fehlen","assignee":"@human"}""");
|
||||
|
||||
var datei = await File.ReadAllTextAsync(Path.Combine(_board.TasksDirectory, (await _board.GetAsync(erste.Id, default))!.FileName));
|
||||
datei.ShouldContain("Erneut beobachtet");
|
||||
datei.ShouldContain("hausmeister", Case.Insensitive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Ein_anderer_Sachverhalt_kommt_ganz_normal_durch()
|
||||
{
|
||||
await Anlegen("Betriebsluecke: Eur Kurse nicht aktuell");
|
||||
|
||||
var result = await Run("""{"action":"task_create","title":"Betriebsluecke: Wetter Warnungen stand.md veraltet","assignee":"@human"}""");
|
||||
|
||||
result.Success.ShouldBeTrue(result.ErrorMessage);
|
||||
(await _board.ListAsync(new TaskQuery { Limit = 50 }, default)).Count.ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Mit_force_darf_der_Agent_trotzdem_anlegen()
|
||||
{
|
||||
await Anlegen("Betriebsluecke: Eur Kurse nicht aktuell");
|
||||
|
||||
var result = await Run("""{"action":"task_create","title":"Betriebsluecke: Eur Kurse nicht aktuell","assignee":"@human","force":true}""");
|
||||
|
||||
result.Success.ShouldBeTrue(result.ErrorMessage);
|
||||
(await _board.ListAsync(new TaskQuery { Limit = 50 }, default)).Count.ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Eine_erledigte_Aufgabe_blockiert_den_naechsten_Befund_nicht()
|
||||
{
|
||||
await Anlegen("Betriebsluecke: Eur Kurse nicht aktuell", TaskItemStatus.Done);
|
||||
|
||||
var result = await Run("""{"action":"task_create","title":"Betriebsluecke: Eur Kurse nicht aktuell","assignee":"@human"}""");
|
||||
|
||||
result.Success.ShouldBeTrue(result.ErrorMessage);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user