Umbau durch Gemini Flash, Durchsicht und Nacharbeit durch Claude. Umfang: - Desktop: LocalizationManager (JSON-Pakete, Rueckfall auf Englisch, Umschalten zur Laufzeit), Localize-Markup-Erweiterung, Sprachwahl in den Einstellungen (AppSettings.Language, Vorgabe "en"); 317 Schluessel je Sprache - WebUI: alle Ansichten auf t()/data-i18n, 656 Schluessel je Sprache - Agenten-Werkzeuge: Beschreibungen, Schemata und Meldungen auf Englisch - Tests an die englischen Meldungen angepasst, keine Pruefung abgeschwaecht Nacharbeit bei der Durchsicht: - ~25 Kommentarbloecke mit Begruendungen waren ersatzlos entfallen, darunter sicherheitsrelevante (NTFS-Datenstrom beim shared:-Praefix, Bremse gegen Agentenschleifen, doppelter POST beim Anlegen einer Instanz). Wieder eingesetzt: auf Englisch, wo die Datei umgestellt ist, sonst im Wortlaut. - Drei WebUI-Schluessel fehlten (logs.paused, instances.instanceOfflineError, instances.noInstanceActiveError). Weil t() bei fehlendem Schluessel den Schluessel selbst liefert, griff der "|| err.message"-Rueckfall nie — die Oberflaeche haette den Schluesselnamen angezeigt. Geprueft: Schluesselgleichheit en/de, Platzhalter je Schluessel, jeder im Code benutzte Schluessel vorhanden, node --check fuer alle Module, 1.220 Tests gruen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
156 lines
6.0 KiB
C#
156 lines
6.0 KiB
C#
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("No open task resembles");
|
|
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("same issue");
|
|
result.Content.ShouldContain("open since");
|
|
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' is required");
|
|
}
|
|
|
|
// ─── 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("recorded");
|
|
|
|
(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("Observed again");
|
|
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);
|
|
}
|
|
}
|