using System.Text.Json; using ClawdDotNet.Core.Tools; using ClawdDotNet.Tools.FileRW; using Microsoft.Extensions.Logging.Abstractions; using Shouldly; namespace ClawdDotNet.Tools.Tests.FileRW; /// /// Found in operation on 0.1.16: an agent wrote shared:betrieb/protokoll.md — the form /// FTP and RocketChat take (#718) — and FileRW refused it every morning. And a pattern that /// matched nowhere sent the agent back and forth between two workspaces. /// public sealed class WorkspaceInPathTests : IDisposable { private readonly string _agents = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"), "Agents"); private readonly FileRWTool _tool = new(); public WorkspaceInPathTests() { Directory.CreateDirectory(Path.Combine(_agents, "SharedWorkspace", "betrieb")); File.WriteAllText(Path.Combine(_agents, "SharedWorkspace", "betrieb", "protokoll.md"), "shared log"); Directory.CreateDirectory(Path.Combine(_agents, "redaktion", "Workspace")); File.WriteAllText(Path.Combine(_agents, "redaktion", "Workspace", "notiz.md"), "personal note"); } public void Dispose() { try { Directory.Delete(Path.GetDirectoryName(_agents)!, recursive: true); } catch { /* cleaning up is secondary */ } } private Task Run(object input) { var context = new AgentToolContext("redaktion", "inst", new Dictionary { ["sharedAccessLevel"] = "ReadWrite" }, new NullStateStore(), NullLogger.Instance, CancellationToken.None, WorkspacePath: Path.Combine(_agents, "redaktion", "Workspace"), SharedWorkspacePath: Path.Combine(_agents, "SharedWorkspace")); return _tool.ExecuteAsync(JsonSerializer.SerializeToElement(input), context, CancellationToken.None); } [Fact] public async Task A_path_that_names_its_workspace_is_read_from_there() { var result = await Run(new { action = "read", path = "shared:betrieb/protokoll.md" }); result.Success.ShouldBeTrue(result.ErrorMessage); result.Content.ShouldContain("shared log"); } [Fact] public async Task A_list_of_paths_from_one_workspace_is_read_from_there() { var result = await Run(new { action = "read_many", paths = new[] { "shared:betrieb/protokoll.md" } }); result.Success.ShouldBeTrue(result.ErrorMessage); result.Content.ShouldContain("shared log"); } [Fact] public async Task Two_workspaces_in_one_list_or_a_contradiction_are_refused_plainly() { (await Run(new { action = "read_many", paths = new[] { "shared:betrieb/protokoll.md", "personal:notiz.md" } })) .ErrorMessage!.ShouldContain("more than one workspace"); (await Run(new { action = "read", workspace = "personal", path = "shared:betrieb/protokoll.md" })) .ErrorMessage!.ShouldContain("'workspace' says 'personal'"); } [Fact] public async Task A_colon_that_names_no_workspace_is_still_refused_and_only_once_denied() { var result = await Run(new { action = "read", path = "notiz.md:versteckt" }); result.Success.ShouldBeFalse(); result.ErrorMessage!.ShouldStartWith("Access denied: Path contains"); result.ErrorMessage.ShouldNotContain("Access denied: Access denied"); } [Fact] public async Task A_pattern_that_matches_nowhere_points_nowhere() { var result = await Run(new { action = "read_many", workspace = "shared", pattern = "berlin_*.txt" }); result.Success.ShouldBeFalse(); result.ErrorMessage!.ShouldNotContain("workspace:", customMessage: "nothing matches in personal either — no hint"); } [Fact] public async Task A_pattern_that_matches_elsewhere_says_where() { File.WriteAllText(Path.Combine(_agents, "redaktion", "Workspace", "berlin_1.txt"), "x"); var result = await Run(new { action = "read_many", workspace = "shared", pattern = "berlin_*.txt" }); result.ErrorMessage!.ShouldContain("Matching files exist in the personal workspace"); } }