using System.Collections.Concurrent; using System.Text.Json; using ClawdDotNet.Core.Tools; namespace ClawdDotNet.Core.Tests.Infrastructure; /// /// Konfigurierbares Tool für Engine-Tests: liefert feste Ergebnisse, kann verzögern /// (um Nebenläufigkeit zu provozieren) oder werfen. /// internal sealed class FakeTool : IAgentTool { private readonly Func> _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(); /// Jeder Aufruf wird mitgeschrieben — inklusive der aufrufenden Agent-Id. public ConcurrentBag Invocations { get; } = new(); /// Ob das Werkzeug sich für gleichzeitige Ausführung freigibt (#551). public bool IsParallelSafe { get; private init; } /// /// 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. /// public int MaxConcurrent => _maxConcurrent; private int _current; private int _maxConcurrent; private FakeTool(string name, Func> handler) { Name = name; _handler = handler; } /// /// Ein langsames Werkzeug, das sich für Nebenläufigkeit freigibt und mitzählt, /// wie viele Aufrufe sich überlappt haben. /// 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 }; /// Dasselbe, aber ohne Freigabe — muss seriell bleiben. 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))); /// Verzögert die Ausführung — vergrößert das Zeitfenster für Races. 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); }); /// Scheitert jedes Mal mit derselben Meldung - fuer die Wiederholungswache (#581). public static FakeTool Failing(string error, string name = "TestTool") => new(name, (_, _, _) => Task.FromResult(ToolResult.Fail(error))); /// #720: Refuses with a write conflict. public static FakeTool Conflicting(string error, string name = "TestTool") => new(name, (_, _, _) => Task.FromResult(ToolResult.Conflict(error))); /// Hands the context it was called with to . public static FakeTool Observing(Action 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 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 _values = new(); public Task 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; } }