Files
RichardandClaude Opus 5.5 ad71682c2d i18n: Englisch als Hauptsprache, Sprachpakete fuer Desktop und WebUI (#661)
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>
2026-09-24 09:43:51 +02:00

208 lines
7.9 KiB
C#

using System.Text.Json;
using ClawdDotNet.Core.State;
using ClawdDotNet.Core.Tools;
using ClawdDotNet.Tools.FileRW;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
namespace ClawdDotNet.Tools.Tests.FileRW;
/// <summary>
/// Sammel-Lesen (#663) und der Hinweis auf den anderen Workspace (#662).
///
/// Anlass aus dem Testbetrieb: Der Tagesabschluss las 24 Stundendateien einzeln und starb
/// an der Schrittgrenze, und derselbe Agent rief Dateien wiederholt im falschen Workspace
/// ab, weil die Fehlermeldung nicht sagte, wo sie liegen.
/// </summary>
public sealed class SammelLesenTests : IDisposable
{
private readonly string _basis;
private readonly string _personal;
private readonly string _shared;
private readonly FileRWTool _tool = new();
public SammelLesenTests()
{
_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(string sharedLevel = "ReadWrite") => new(
"hausmeister", "inst",
new Dictionary<string, object?> { ["sharedAccessLevel"] = sharedLevel },
new NullStateStore(),
NullLogger.Instance,
CancellationToken.None,
WorkspacePath: _personal,
SharedWorkspacePath: _shared);
private Task<ToolResult> Run(string json, string sharedLevel = "ReadWrite")
=> _tool.ExecuteAsync(JsonDocument.Parse(json).RootElement, Context(sharedLevel), CancellationToken.None);
private void Stundendateien(int anzahl, string tag = "2026-09-20")
{
for (var stunde = 0; stunde < anzahl; stunde++)
{
File.WriteAllText(
Path.Combine(_shared, "wetter", "stunden", $"berlin_{tag}_{stunde:00}-00.txt"),
$"{tag} {stunde:00}:00 Temperatur {10 + stunde} C");
}
}
// ═══════════════════ #663: ein Aufruf statt 24 ═══════════════════
[Fact]
public async Task Ein_Verzeichnis_mit_Muster_kommt_in_einem_Aufruf()
{
Stundendateien(24);
Stundendateien(3, "2026-09-19");
var result = await Run("""{"action":"read_many","workspace":"shared","path":"wetter/stunden","pattern":"berlin_2026-09-20_*.txt"}""");
result.Success.ShouldBeTrue(result.ErrorMessage);
result.Content.ShouldContain("24 of 24 file(s) read");
result.Content.ShouldContain("Temperatur 10 C");
result.Content.ShouldContain("Temperatur 33 C");
result.Content.ShouldNotContain("2026-09-19", Case.Sensitive);
}
[Fact]
public async Task Die_Dateien_kommen_benannt_und_in_fester_Reihenfolge()
{
Stundendateien(3);
var result = await Run("""{"action":"read_many","workspace":"shared","path":"wetter/stunden"}""");
var reihenfolge = new[] { "00-00.txt", "01-00.txt", "02-00.txt" }
.Select(n => result.Content.IndexOf(n, StringComparison.Ordinal)).ToList();
reihenfolge.ShouldAllBe(i => i > 0);
reihenfolge.ShouldBe(reihenfolge.OrderBy(i => i).ToList(), "sortiert, damit der Agent den Tag zusammensetzen kann");
result.Content.ShouldContain("--- shared:/wetter/stunden/berlin_2026-09-20_01-00.txt ---");
}
[Fact]
public async Task Eine_ausdrueckliche_Liste_wird_gelesen_und_Fehlendes_benannt()
{
Stundendateien(2);
var result = await Run("""
{"action":"read_many","workspace":"shared","paths":[
"wetter/stunden/berlin_2026-09-20_00-00.txt",
"wetter/stunden/berlin_2026-09-20_09-00.txt"]}
""");
result.Success.ShouldBeTrue(result.ErrorMessage);
result.Content.ShouldContain("1 of 2 file(s) read");
result.Content.ShouldContain("Not found: wetter/stunden/berlin_2026-09-20_09-00.txt");
}
[Fact]
public async Task Zu_viele_Dateien_werden_gedeckelt_und_der_Rest_genannt()
{
Stundendateien(60);
var result = await Run("""{"action":"read_many","workspace":"shared","path":"wetter/stunden"}""");
result.Success.ShouldBeTrue(result.ErrorMessage);
result.Content.ShouldContain($"{FileRWTool.MaxDateienJeAufruf} of 60 file(s) read");
result.Content.ShouldContain("Skipped reading");
result.Content.ShouldContain("fetch with an additional call");
}
[Fact]
public async Task Ein_leeres_Verzeichnis_meldet_sich_als_Fehler()
{
var result = await Run("""{"action":"read_many","workspace":"shared","path":"wetter/stunden"}""");
result.Success.ShouldBeFalse();
result.ErrorMessage.ShouldNotBeNull().ShouldContain("No matching file");
}
[Fact]
public async Task Ohne_Leserecht_am_gemeinsamen_Bereich_kommt_nichts_heraus()
{
Stundendateien(2);
var result = await Run("""{"action":"read_many","workspace":"shared","path":"wetter/stunden"}""", sharedLevel: "Denied");
result.Success.ShouldBeFalse();
result.ErrorMessage.ShouldNotBeNull().ShouldContain("Access denied");
}
[Fact]
public async Task Der_Ausbruch_aus_dem_Workspace_wird_auch_hier_abgewiesen()
{
var result = await Run("""{"action":"read_many","workspace":"shared","paths":["../../geheim.txt"]}""");
result.Success.ShouldBeFalse();
result.ErrorMessage.ShouldNotBeNull().ShouldContain("Access denied");
}
[Fact]
public async Task Eine_verbotene_Dateiendung_wird_uebergangen_statt_den_Lauf_zu_kippen()
{
Stundendateien(2);
File.WriteAllText(Path.Combine(_shared, "wetter", "stunden", "skript.exe"), "MZ");
var result = await Run("""{"action":"read_many","workspace":"shared","path":"wetter/stunden"}""");
result.Success.ShouldBeTrue(result.ErrorMessage);
result.Content.ShouldContain("2 of 2 file(s) read");
result.Content.ShouldNotContain("skript.exe");
}
// ═══════════════════ #662: Hinweis auf den anderen Workspace ═══════════════════
[Fact]
public async Task Liegt_die_Datei_im_anderen_Workspace_sagt_die_Meldung_das()
{
Stundendateien(1);
var result = await Run("""{"action":"read","path":"wetter/stunden/berlin_2026-09-20_00-00.txt"}""");
result.Success.ShouldBeFalse();
result.ErrorMessage.ShouldNotBeNull().ShouldContain("exists in the shared workspace");
result.ErrorMessage.ShouldContain("workspace: \"shared\"", Case.Sensitive);
}
[Fact]
public async Task Auch_ein_Verzeichnis_bekommt_den_Hinweis()
{
var result = await Run("""{"action":"list","path":"wetter/stunden"}""");
result.Success.ShouldBeFalse();
result.ErrorMessage.ShouldNotBeNull().ShouldContain("exists in the shared workspace");
}
[Fact]
public async Task Ohne_Recht_am_anderen_Workspace_gibt_es_keinen_Hinweis()
{
// Ein Hinweis auf etwas, das der Agent ohnehin nicht darf, schickt ihn ins Leere.
Stundendateien(1);
var result = await Run("""{"action":"read","path":"wetter/stunden/berlin_2026-09-20_00-00.txt"}""", sharedLevel: "Denied");
result.Success.ShouldBeFalse();
result.ErrorMessage.ShouldNotBeNull().ShouldNotContain("exists in");
}
[Fact]
public async Task Gibt_es_den_Pfad_nirgends_bleibt_die_Meldung_schlicht()
{
var result = await Run("""{"action":"read","workspace":"shared","path":"wetter/stunden/fehlt.txt"}""");
result.Success.ShouldBeFalse();
result.ErrorMessage.ShouldNotBeNull().ShouldContain("File not found");
result.ErrorMessage.ShouldNotContain("exists in");
}
}