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;
///
/// Die Nachschau nach einem Absturz (#555) und das atomare Schreiben (#553).
///
/// Das Dateisystem wird auch hier nicht abstrahiert — es geht gerade darum, was auf der
/// Platte steht, nachdem ein Aufruf unterbrochen wurde.
///
public sealed class NachschauTests : IDisposable
{
private readonly string _root;
private readonly FileRWTool _tool = new();
public NachschauTests()
{
_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 { /* Aufräumen ist Nebensache */ }
}
private AgentToolContext Context() => new(
"a1", "inst",
new Dictionary(),
new NullStateStore(),
NullLogger.Instance,
CancellationToken.None,
WorkspacePath: _root);
private Task Probe(string json)
=> _tool.ProbeAsync(JsonDocument.Parse(json).RootElement, Context(), CancellationToken.None);
private Task Run(string json)
=> _tool.ExecuteAsync(JsonDocument.Parse(json).RootElement, Context(), CancellationToken.None);
// ═══════════════════ #553: atomar schreiben ═══════════════════
[Fact]
public async Task Schreiben_hinterlaesst_keine_Nebendateien()
{
var result = await Run("""{"action":"write","path":"a.txt","content":"Inhalt"}""");
result.Success.ShouldBeTrue();
// Das atomare Schreiben geht über eine Nebendatei — die muss danach weg sein.
Directory.GetFiles(_root).ShouldHaveSingleItem();
(await File.ReadAllTextAsync(Path.Combine(_root, "a.txt"))).ShouldBe("Inhalt");
}
[Fact]
public async Task Ueberschreiben_ersetzt_den_Inhalt_vollstaendig()
{
await Run("""{"action":"write","path":"a.txt","content":"ein sehr langer alter Inhalt"}""");
await Run("""{"action":"write","path":"a.txt","content":"kurz"}""");
// Beim Ersetzen darf kein Rest der alten, längeren Fassung stehenbleiben.
(await File.ReadAllTextAsync(Path.Combine(_root, "a.txt"))).ShouldBe("kurz");
}
// ═══════════════════ #555: Nachschau bei write ═══════════════════
[Fact]
public async Task Fehlende_Datei_heisst_nichts_geschehen()
{
var befund = await Probe("""{"action":"write","path":"nie.txt","content":"x"}""");
befund.Outcome.ShouldBe(ProbeOutcome.NotStarted);
}
[Fact]
public async Task Der_beabsichtigte_Inhalt_heisst_durchgekommen()
{
await Run("""{"action":"write","path":"a.txt","content":"fertig"}""");
var befund = await Probe("""{"action":"write","path":"a.txt","content":"fertig"}""");
befund.Outcome.ShouldBe(ProbeOutcome.Complete);
}
[Fact]
public async Task Der_alte_Inhalt_heisst_nicht_durchgekommen()
{
await Run("""{"action":"write","path":"a.txt","content":"alt"}""");
// So sähe es aus, wenn der Absturz vor dem Ersetzen kam.
var befund = await Probe("""{"action":"write","path":"a.txt","content":"neu"}""");
befund.Outcome.ShouldBe(ProbeOutcome.NotStarted);
}
// ═══════════════════ #555: Nachschau bei append ═══════════════════
[Fact]
public async Task Vollstaendig_angehaengter_Text_wird_erkannt()
{
await Run("""{"action":"write","path":"log.txt","content":"Zeile 1\n"}""");
await Run("""{"action":"append","path":"log.txt","content":"Zeile 2\n"}""");
var befund = await Probe("""{"action":"append","path":"log.txt","content":"Zeile 2\n"}""");
befund.Outcome.ShouldBe(ProbeOutcome.Complete);
}
[Fact]
public async Task Ein_halb_angehaengter_Text_wird_als_Riss_erkannt()
{
// Anhängen ist nicht atomar — genau dieser Zustand kann wirklich entstehen.
// Hier wird er von Hand hergestellt, weil ein echter Riss nicht reproduzierbar ist.
var pfad = Path.Combine(_root, "log.txt");
await File.WriteAllTextAsync(pfad, "Zeile 1\nZeile 2 ist nur halb ang");
var befund = await Probe(
"""{"action":"append","path":"log.txt","content":"Zeile 2 ist nur halb angekommen\n"}""");
befund.Outcome.ShouldBe(ProbeOutcome.Partial);
befund.Evidence.ShouldContain("of");
}
[Fact]
public async Task Fehlt_der_Text_ganz_gilt_der_Aufruf_als_nicht_geschehen()
{
await Run("""{"action":"write","path":"log.txt","content":"Zeile 1\n"}""");
var befund = await Probe("""{"action":"append","path":"log.txt","content":"Zeile 2\n"}""");
befund.Outcome.ShouldBe(ProbeOutcome.NotStarted);
}
// ═══════════════════ Die Grenzen der Nachschau ═══════════════════
[Theory]
[InlineData("read")]
[InlineData("delete")]
[InlineData("list")]
public async Task Fuer_andere_Aktionen_wird_nichts_behauptet(string aktion)
{
var befund = await Probe($$"""{"action":"{{aktion}}","path":"a.txt"}""");
befund.Outcome.ShouldBe(ProbeOutcome.Unknown);
}
[Fact]
public async Task Ein_unbrauchbarer_Pfad_ergibt_Unbekannt_statt_einer_Ausnahme()
{
// Die Nachschau darf nie werfen: Ein Fehlschlag der Nachschau ist eine Aussage
// über die Nachschau, nicht über die Welt.
var befund = await Probe("""{"action":"write","path":"../../ausserhalb.txt","content":"x"}""");
befund.Outcome.ShouldBe(ProbeOutcome.Unknown);
}
}
internal sealed class NullStateStore : IStateStore
{
public Task GetAsync(string key, CancellationToken ct) => Task.FromResult(null);
public Task SetAsync(string key, string value, CancellationToken ct) => Task.CompletedTask;
public Task DeleteAsync(string key, CancellationToken ct) => Task.CompletedTask;
}