Stufe 4c (#764): Bezeichner auf Englisch - Typen, Methoden, Felder, Parameter, Locals, Testnamen
Mit Roslyn umbenannt (Verweise folgen, Zeichenketten bleiben unberuehrt). Nicht umbenannt, weil gespeichert oder ueber die API gebunden (Stufe 4e): Properties, Record-Parameter, Enum-Member sowie die Parameter der Endpunkt-Handler (Routen-/Query-Bindung ueber den Namen). Die JSON-Felder anonymer Objekte bleiben ausdruecklich benannt (JsonFilter: pfad). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
837a634a22
commit
77062facc9
@@ -0,0 +1,168 @@
|
||||
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>
|
||||
/// The follow-up check after a crash (#555) and atomic writing (#553).
|
||||
///
|
||||
/// The file system is not abstracted here either — this is precisely about what is on
|
||||
/// disk after a call was interrupted.
|
||||
/// </summary>
|
||||
public sealed class FollowUpCheckTests : IDisposable
|
||||
{
|
||||
private readonly string _root;
|
||||
private readonly FileRWTool _tool = new();
|
||||
|
||||
public FollowUpCheckTests()
|
||||
{
|
||||
_root = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"), "Workspace");
|
||||
Directory.CreateDirectory(_root);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(Path.GetDirectoryName(_root)!, recursive: true); }
|
||||
catch { /* Cleaning up is secondary */ }
|
||||
}
|
||||
|
||||
private AgentToolContext Context() => new(
|
||||
"a1", "inst",
|
||||
new Dictionary<string, object?>(),
|
||||
new NullStateStore(),
|
||||
NullLogger.Instance,
|
||||
CancellationToken.None,
|
||||
WorkspacePath: _root);
|
||||
|
||||
private Task<ProbeFinding> Probe(string json)
|
||||
=> _tool.ProbeAsync(JsonDocument.Parse(json).RootElement, Context(), CancellationToken.None);
|
||||
|
||||
private Task<ToolResult> Run(string json)
|
||||
=> _tool.ExecuteAsync(JsonDocument.Parse(json).RootElement, Context(), CancellationToken.None);
|
||||
|
||||
// ═══════════════════ #553: writing atomically ═══════════════════
|
||||
|
||||
[Fact]
|
||||
public async Task Writing_leaves_no_side_files()
|
||||
{
|
||||
var result = await Run("""{"action":"write","path":"a.txt","content":"Inhalt"}""");
|
||||
result.Success.ShouldBeTrue();
|
||||
|
||||
// Atomic writing goes via a side file — it must be gone afterwards.
|
||||
Directory.GetFiles(_root).ShouldHaveSingleItem();
|
||||
(await File.ReadAllTextAsync(Path.Combine(_root, "a.txt"))).ShouldBe("Inhalt");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Overwriting_replaces_the_content_completely()
|
||||
{
|
||||
await Run("""{"action":"write","path":"a.txt","content":"ein sehr langer alter Inhalt"}""");
|
||||
await Run("""{"action":"write","path":"a.txt","content":"kurz"}""");
|
||||
|
||||
// When replacing, no remnant of the old, longer version may remain.
|
||||
(await File.ReadAllTextAsync(Path.Combine(_root, "a.txt"))).ShouldBe("kurz");
|
||||
}
|
||||
|
||||
// ═══════════════════ #555: follow-up check for write ═══════════════════
|
||||
|
||||
[Fact]
|
||||
public async Task Missing_file_means_nothing_happened()
|
||||
{
|
||||
var finding = await Probe("""{"action":"write","path":"nie.txt","content":"x"}""");
|
||||
|
||||
finding.Outcome.ShouldBe(ProbeOutcome.NotStarted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_intended_content_means_through()
|
||||
{
|
||||
await Run("""{"action":"write","path":"a.txt","content":"fertig"}""");
|
||||
|
||||
var finding = await Probe("""{"action":"write","path":"a.txt","content":"fertig"}""");
|
||||
|
||||
finding.Outcome.ShouldBe(ProbeOutcome.Complete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_old_content_means_not_through()
|
||||
{
|
||||
await Run("""{"action":"write","path":"a.txt","content":"alt"}""");
|
||||
|
||||
// This is what it would look like if the crash came before the replacement.
|
||||
var finding = await Probe("""{"action":"write","path":"a.txt","content":"neu"}""");
|
||||
|
||||
finding.Outcome.ShouldBe(ProbeOutcome.NotStarted);
|
||||
}
|
||||
|
||||
// ═══════════════════ #555: follow-up check for append ═══════════════════
|
||||
|
||||
[Fact]
|
||||
public async Task Fully_appended_text_is_recognised()
|
||||
{
|
||||
await Run("""{"action":"write","path":"log.txt","content":"Zeile 1\n"}""");
|
||||
await Run("""{"action":"append","path":"log.txt","content":"Zeile 2\n"}""");
|
||||
|
||||
var finding = await Probe("""{"action":"append","path":"log.txt","content":"Zeile 2\n"}""");
|
||||
|
||||
finding.Outcome.ShouldBe(ProbeOutcome.Complete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_half_appended_text_is_recognised_as_a_tear()
|
||||
{
|
||||
// Appending is not atomic — exactly this state can really arise.
|
||||
// Here it is produced by hand, because a real tear cannot be reproduced.
|
||||
var path = Path.Combine(_root, "log.txt");
|
||||
await File.WriteAllTextAsync(path, "Zeile 1\nZeile 2 ist nur halb ang");
|
||||
|
||||
var finding = await Probe(
|
||||
"""{"action":"append","path":"log.txt","content":"Zeile 2 ist nur halb angekommen\n"}""");
|
||||
|
||||
finding.Outcome.ShouldBe(ProbeOutcome.Partial);
|
||||
finding.Evidence.ShouldContain("of");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task If_the_text_is_missing_entirely_the_call_counts_as_not_happened()
|
||||
{
|
||||
await Run("""{"action":"write","path":"log.txt","content":"Zeile 1\n"}""");
|
||||
|
||||
var finding = await Probe("""{"action":"append","path":"log.txt","content":"Zeile 2\n"}""");
|
||||
|
||||
finding.Outcome.ShouldBe(ProbeOutcome.NotStarted);
|
||||
}
|
||||
|
||||
// ═══════════════════ The limits of the follow-up check ═══════════════════
|
||||
|
||||
[Theory]
|
||||
[InlineData("read")]
|
||||
[InlineData("delete")]
|
||||
[InlineData("list")]
|
||||
public async Task Nothing_is_claimed_for_other_actions(string action)
|
||||
{
|
||||
var finding = await Probe($$"""{"action":"{{action}}","path":"a.txt"}""");
|
||||
|
||||
finding.Outcome.ShouldBe(ProbeOutcome.Unknown);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task An_unusable_path_gives_unknown_instead_of_an_exception()
|
||||
{
|
||||
// The follow-up check must never throw: a failure of the follow-up check is a statement
|
||||
// about the follow-up check, not about the world.
|
||||
var finding = await Probe("""{"action":"write","path":"../../ausserhalb.txt","content":"x"}""");
|
||||
|
||||
finding.Outcome.ShouldBe(ProbeOutcome.Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class NullStateStore : IStateStore
|
||||
{
|
||||
public Task<string?> GetAsync(string key, CancellationToken ct) => Task.FromResult<string?>(null);
|
||||
public Task SetAsync(string key, string value, CancellationToken ct) => Task.CompletedTask;
|
||||
public Task DeleteAsync(string key, CancellationToken ct) => Task.CompletedTask;
|
||||
}
|
||||
Reference in New Issue
Block a user