FileRW: Dateiaktion ohne path abweisen, Hostpfad nicht in Fehlermeldungen

Testinstanz 2026-10-02: write mit destinationPath statt path loeste auf die
Wurzel des Arbeitsbereichs auf; die Ausnahme nannte den absoluten Ordner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-10-02 09:18:05 +02:00
co-authored by Claude Opus 5.5
parent 0a75afa51b
commit 2699b98781
2 changed files with 81 additions and 1 deletions
@@ -0,0 +1,58 @@
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>
/// Test instance, 2026-10-02: a model sent <c>destinationPath</c> instead of <c>path</c> to write.
/// The path resolved to the workspace root, the move onto the folder failed, and the error named
/// the host's absolute folder. A missing path is the model's mistake and says so plainly.
/// </summary>
public sealed class MissingPathTests : IDisposable
{
private readonly string _workspace = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"), "Workspace");
public MissingPathTests() => Directory.CreateDirectory(_workspace);
public void Dispose()
{
try { Directory.Delete(Path.GetDirectoryName(_workspace)!, recursive: true); }
catch { /* cleanup is secondary */ }
}
private AgentToolContext Context() => new("desk", "inst", new Dictionary<string, object?>(), new NullStateStore(), NullLogger.Instance,
CancellationToken.None, WorkspacePath: _workspace);
[Theory]
[InlineData("""{"action":"write","destinationPath":"wetter/x.txt","content":"x"}""", "destinationPath")]
[InlineData("""{"action":"write","content":"x"}""", "'path' is required for write")]
[InlineData("""{"action":"append","path":" ","content":"x"}""", "'path' is required for append")]
[InlineData("""{"action":"read"}""", "'path' is required for read")]
public async Task A_file_action_without_a_path_is_refused_without_naming_the_host_folder(string json, string expected)
{
var input = JsonDocument.Parse(json).RootElement;
var tool = new FileRWTool();
var result = await tool.ExecuteAsync(input, Context(), CancellationToken.None);
result.Success.ShouldBeFalse();
result.ErrorMessage.ShouldContain(expected);
result.ErrorMessage!.ShouldNotContain(_workspace);
tool.Preflight(input, Context()).ShouldNotBeNull("the check runs before staging as well");
Directory.EnumerateFileSystemEntries(_workspace).ShouldBeEmpty();
}
[Fact]
public async Task Listing_the_root_still_needs_no_path()
{
File.WriteAllText(Path.Combine(_workspace, "a.txt"), "x");
var result = await new FileRWTool().ExecuteAsync(JsonDocument.Parse("""{"action":"list"}""").RootElement, Context(), CancellationToken.None);
result.Success.ShouldBeTrue(result.ErrorMessage);
result.Content.ShouldContain("a.txt");
}
}