using System.Collections.Concurrent;
using System.Text.Json;
using ClawdDotNet.Core.Tools;
namespace ClawdDotNet.Core.Tests.Infrastructure;
///
/// Configurable tool for engine tests: delivers fixed results, can delay
/// (to provoke concurrency) or throw.
///
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();
/// Every call is recorded — including the calling agent ID.
public ConcurrentBag Invocations { get; } = new();
/// Whether the tool declares itself fit for simultaneous execution (#551).
public bool IsParallelSafe { get; private init; }
///
/// Highest number of simultaneously running calls this tool has seen.
///
/// That is the only proof that counts: a shorter total runtime could also
/// come from a fast machine, but a peak of 2 can only
/// arise if two calls actually overlapped each other.
///
public int MaxConcurrent => _maxConcurrent;
private int _current;
private int _maxConcurrent;
private FakeTool(string name, Func> handler)
{
Name = name;
_handler = handler;
}
///
/// A slow tool that declares itself fit for concurrency and counts
/// how many calls overlapped.
///
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 };
/// The same, but without declaring itself fit — must stay serial.
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)));
/// Delays execution — enlarges the time window for 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);
});
/// Fails every time with the same message - for the repetition guard (#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);
// Advance the peak without ever lowering it: compare and swap
// until the stored value is no longer smaller.
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;
}
}