using System.Collections.Concurrent;
using ClawdDotNet.Core.Audit;
namespace ClawdDotNet.Core.Tests.Infrastructure;
///
/// In-Memory-Attrappe des Audit-Logs für Engine-Tests — hält Einträge und Belege in
/// Listen, damit ein Test prüfen kann, was die Engine stempelt, ohne eine Datei.
///
internal sealed class InMemoryAuditRepository : IAuditRepository
{
public ConcurrentQueue Entries { get; } = new();
public ConcurrentQueue Receipts { get; } = new();
public Task AppendAsync(AuditEntry entry, CancellationToken ct)
{
Entries.Enqueue(entry);
return Task.CompletedTask;
}
public Task RecordReceiptAsync(RunReceipt receipt, CancellationToken ct)
{
Receipts.Enqueue(receipt);
return Task.CompletedTask;
}
public Task> ListRecentAsync(int limit, CancellationToken ct)
=> Task.FromResult>(Entries.Reverse().Take(limit).ToList());
public Task> ListForRunAsync(string runId, CancellationToken ct)
=> Task.FromResult>(Entries.Where(e => e.RunId == runId).ToList());
public Task GetReceiptForRunAsync(string runId, CancellationToken ct)
=> Task.FromResult(Receipts.LastOrDefault(r => r.RunId == runId));
public Task> ListReceiptsForTaskAsync(string taskId, CancellationToken ct)
=> Task.FromResult>(Receipts.Where(r => r.TaskId == taskId).ToList());
public Task CountAsync(CancellationToken ct) => Task.FromResult(Entries.Count);
/// Spiegelt die SQL-Regel: Started ohne späteren Abschluss derselben CallId.
public Task> ListOpenCallsAsync(CancellationToken ct)
{
var alle = Entries.ToList();
var abgeschlossen = alle
.Where(e => e.Status != AuditStatus.Started && e.CallId.Length > 0)
.Select(e => e.CallId)
.ToHashSet();
var offen = alle
.Where(e => e.Status == AuditStatus.Started
&& e.CallId.Length > 0
&& !abgeschlossen.Contains(e.CallId))
.ToList();
return Task.FromResult>(offen);
}
public Task> ListForAgentAsync(
string agentId, DateTime? seit, int limit, CancellationToken ct)
=> Task.FromResult>(
Entries.Where(e => e.AgentId == agentId)
.Where(e => seit is null || e.OccurredAt >= seit)
.Reverse()
.Take(limit)
.ToList());
public Task> SummarizeToolUsageAsync(
string? agentId, DateTime? seit, CancellationToken ct)
=> Task.FromResult>(
Entries.Where(e => agentId is null || e.AgentId == agentId)
.Where(e => seit is null || e.OccurredAt >= seit)
.GroupBy(e => (e.AgentId, e.Tool))
.Select(g =>
{
var abgeschlossen = g.Where(e => e.Status != AuditStatus.Started).ToList();
var begonnen = g.Count(e => e.Status == AuditStatus.Started);
return new ToolUsageStat
{
AgentId = g.Key.AgentId,
Tool = g.Key.Tool,
Calls = abgeschlossen.Count,
Failed = abgeschlossen.Count(e => e.Status == AuditStatus.Error),
Denied = abgeschlossen.Count(e => e.Status == AuditStatus.Denied),
Unfinished = Math.Max(0, begonnen - abgeschlossen.Count),
TotalDurationMs = abgeschlossen.Sum(e => e.DurationMs),
MaxDurationMs = g.Max(e => e.DurationMs)
};
})
.OrderByDescending(s => s.Calls)
.ToList());
}