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; /// /// 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. /// 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 { ["sharedAccessLevel"] = sharedLevel }, new NullStateStore(), NullLogger.Instance, CancellationToken.None, WorkspacePath: _personal, SharedWorkspacePath: _shared); private Task 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 von 24 Datei(en) gelesen"); 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 von 2 Datei(en) gelesen"); result.Content.ShouldContain("Nicht gefunden: 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} von 60 Datei(en) gelesen"); result.Content.ShouldContain("Nicht mehr gelesen"); result.Content.ShouldContain("hole sie mit einem weiteren Aufruf"); } [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("Keine passende Datei"); } [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("Zugriff verweigert"); } [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("Zugriff verweigert"); } [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 von 2 Datei(en) gelesen"); 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("existiert im 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("existiert im 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("existiert im"); } [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("Datei nicht gefunden"); result.ErrorMessage.ShouldNotContain("existiert im"); } }