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()); }