- "shared:betrieb/protokoll.md" wird zu workspace + Pfad (wie FTP/RocketChat seit #718); muss zu einem angegebenen workspace passen, eine Pfadliste nennt nur einen Bereich - read_many mit Muster: Hinweis auf einen anderen Bereich nur, wenn dort Dateien passen; ohne Pfad und Muster kein Hinweis (die Wurzel gibt es ueberall) - verweigerter Pfad: kein doppeltes "Access denied", Warnung statt ERR mit Stacktrace - Tests: WorkspaceInPathTests (6) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
4.0 KiB
C#
98 lines
4.0 KiB
C#
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>
|
|
/// Found in operation on 0.1.16: an agent wrote <c>shared:betrieb/protokoll.md</c> — 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.
|
|
/// </summary>
|
|
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<ToolResult> Run(object input)
|
|
{
|
|
var context = new AgentToolContext("redaktion", "inst",
|
|
new Dictionary<string, object?> { ["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");
|
|
}
|
|
}
|