Files
ClawdDotNet/tests/ClawdDotNet.Core.Tests/Infrastructure/FakeTool.cs
T
RichardandClaude Opus 5.5 79a3a7854f Verlorene Updates erkennen statt warten: optimistische Pruefung beim Ersetzen, Konflikte im Audit und im Lagebild (#720)
FileLedger merkt je Agent die gelesene Fassung einer Datei in gemeinsamen
Bereichen. write, copy und Downloads (FTP, RocketChat) ersetzen eine Datei nur,
wenn sie seit dem Lesen unveraendert ist; eine fremde Datei nicht ungelesen.
append bleibt ungeprueft, macht aeltere Lesestaende aber ungueltig.
Abgelehnte Schreibvorgaenge: eigener Audit-Status Conflict, Tabelle
FileConflicts, Hotspots (>= 3 in 7 Tagen) in der Teamuebersicht.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 10:45:46 +02:00

134 lines
5.1 KiB
C#

using System.Collections.Concurrent;
using System.Text.Json;
using ClawdDotNet.Core.Tools;
namespace ClawdDotNet.Core.Tests.Infrastructure;
/// <summary>
/// Konfigurierbares Tool für Engine-Tests: liefert feste Ergebnisse, kann verzögern
/// (um Nebenläufigkeit zu provozieren) oder werfen.
/// </summary>
internal sealed class FakeTool : IAgentTool
{
private readonly Func<JsonElement, AgentToolContext, CancellationToken, Task<ToolResult>> _handler;
public string Name { get; }
public string Description => $"Test-Tool {Name}";
public JsonElement InputSchema { get; } = JsonDocument.Parse("""
{ "type": "object", "properties": { "action": { "type": "string" } } }
""").RootElement.Clone();
/// <summary>Jeder Aufruf wird mitgeschrieben — inklusive der aufrufenden Agent-Id.</summary>
public ConcurrentBag<string> Invocations { get; } = new();
/// <summary>Ob das Werkzeug sich für gleichzeitige Ausführung freigibt (#551).</summary>
public bool IsParallelSafe { get; private init; }
/// <summary>
/// Höchste Zahl gleichzeitig laufender Aufrufe, die dieses Werkzeug gesehen hat.
///
/// Das ist der einzige Nachweis, der zählt: Eine kürzere Gesamtlaufzeit könnte auch
/// von einer schnellen Maschine kommen, aber ein Höchststand von 2 kann nur
/// entstehen, wenn zwei Aufrufe einander tatsächlich überlappt haben.
/// </summary>
public int MaxConcurrent => _maxConcurrent;
private int _current;
private int _maxConcurrent;
private FakeTool(string name, Func<JsonElement, AgentToolContext, CancellationToken, Task<ToolResult>> handler)
{
Name = name;
_handler = handler;
}
/// <summary>
/// Ein langsames Werkzeug, das sich für Nebenläufigkeit freigibt und mitzählt,
/// wie viele Aufrufe sich überlappt haben.
/// </summary>
public static FakeTool SlowParallelSafe(TimeSpan delay, string result = "ok", string name = "ParallelTool")
=> new(name, async (_, _, ct) =>
{
await Task.Delay(delay, ct);
return ToolResult.Ok(result);
})
{ IsParallelSafe = true };
/// <summary>Dasselbe, aber ohne Freigabe — muss seriell bleiben.</summary>
public static FakeTool SlowSerial(TimeSpan delay, string result = "ok", string name = "SerialTool")
=> new(name, async (_, _, ct) =>
{
await Task.Delay(delay, ct);
return ToolResult.Ok(result);
});
public static FakeTool Returning(string result, string name = "TestTool")
=> new(name, (_, _, _) => Task.FromResult(ToolResult.Ok(result)));
/// <summary>Verzögert die Ausführung — vergrößert das Zeitfenster für Races.</summary>
public static FakeTool Slow(TimeSpan delay, string result = "ok", string name = "TestTool")
=> new(name, async (_, _, ct) =>
{
await Task.Delay(delay, ct);
return ToolResult.Ok(result);
});
/// <summary>Scheitert jedes Mal mit derselben Meldung - fuer die Wiederholungswache (#581).</summary>
public static FakeTool Failing(string error, string name = "TestTool")
=> new(name, (_, _, _) => Task.FromResult(ToolResult.Fail(error)));
/// <summary>#720: Refuses with a write conflict.</summary>
public static FakeTool Conflicting(string error, string name = "TestTool")
=> new(name, (_, _, _) => Task.FromResult(ToolResult.Conflict(error)));
/// <summary>Hands the context it was called with to <paramref name="seen"/>.</summary>
public static FakeTool Observing(Action<AgentToolContext> seen, string name = "TestTool")
=> new(name, (_, context, _) => { seen(context); return Task.FromResult(ToolResult.Ok("ok")); });
public static FakeTool Throwing(Exception ex, string name = "TestTool")
=> new(name, (_, _, _) => throw ex);
public async Task<ToolResult> ExecuteAsync(JsonElement input, AgentToolContext context, CancellationToken ct)
{
Invocations.Add(context.AgentId);
var now = Interlocked.Increment(ref _current);
// Höchststand fortschreiben, ohne ihn je zu senken: Vergleichen und Tauschen,
// bis der gespeicherte Wert nicht mehr kleiner ist.
int seen;
while (now > (seen = Volatile.Read(ref _maxConcurrent)))
Interlocked.CompareExchange(ref _maxConcurrent, now, seen);
try
{
return await _handler(input, context, ct);
}
finally
{
Interlocked.Decrement(ref _current);
}
}
}
internal sealed class InMemoryStateStore : ClawdDotNet.Core.State.IStateStore
{
private readonly ConcurrentDictionary<string, string> _values = new();
public Task<string?> GetAsync(string key, CancellationToken ct)
=> Task.FromResult(_values.GetValueOrDefault(key));
public Task SetAsync(string key, string value, CancellationToken ct)
{
_values[key] = value;
return Task.CompletedTask;
}
public Task DeleteAsync(string key, CancellationToken ct)
{
_values.TryRemove(key, out _);
return Task.CompletedTask;
}
}