using System.Collections.Concurrent; using ClawdDotNet.Core.Audit; namespace ClawdDotNet.Core.Tests.Infrastructure; /// /// In-memory fake of the audit log for engine tests — keeps entries and receipts in /// lists, so a test can check what the engine stamps, without a file. /// 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> ListReceiptsCalledFromAsync(string runId, CancellationToken ct) => Task.FromResult>(Receipts.Where(r => r.CallerRunId == runId).ToList()); public Task> ListReceiptsForTaskAsync(string taskId, CancellationToken ct) => Task.FromResult>(Receipts.Where(r => r.TaskId == taskId).ToList()); public Task> ListReceiptsAsync(DateTime fromUtc, DateTime toUtc, int limit, CancellationToken ct) => Task.FromResult>(Receipts .Where(r => r.OccurredAt >= fromUtc && r.OccurredAt < toUtc) .OrderBy(r => r.OccurredAt).Take(limit).ToList()); public Task CountAsync(CancellationToken ct) => Task.FromResult(Entries.Count); /// Mirrors the SQL rule: Started without a later completion of the same CallId. public Task> ListOpenCallsAsync(CancellationToken ct) { var all = Entries.ToList(); var completed = all .Where(e => e.Status != AuditStatus.Started && e.CallId.Length > 0) .Select(e => e.CallId) .ToHashSet(); var open = all .Where(e => e.Status == AuditStatus.Started && e.CallId.Length > 0 && !completed.Contains(e.CallId)) .ToList(); return Task.FromResult>(open); } public Task> ListForAgentAsync( string agentId, DateTime? since, int limit, CancellationToken ct) => Task.FromResult>( Entries.Where(e => e.AgentId == agentId) .Where(e => since is null || e.OccurredAt >= since) .Reverse() .Take(limit) .ToList()); public Task> SummarizeToolUsageAsync( string? agentId, DateTime? since, CancellationToken ct) => Task.FromResult>( Entries.Where(e => agentId is null || e.AgentId == agentId) .Where(e => since is null || e.OccurredAt >= since) .GroupBy(e => (e.AgentId, e.Tool)) .Select(g => { var completed = g.Where(e => e.Status != AuditStatus.Started).ToList(); var started = g.Count(e => e.Status == AuditStatus.Started); return new ToolUsageStat { AgentId = g.Key.AgentId, Tool = g.Key.Tool, Calls = completed.Count, Failed = completed.Count(e => e.Status == AuditStatus.Error), Denied = completed.Count(e => e.Status == AuditStatus.Denied), Unfinished = Math.Max(0, started - completed.Count), TotalDurationMs = completed.Sum(e => e.DurationMs), MaxDurationMs = g.Max(e => e.DurationMs) }; }) .OrderByDescending(s => s.Calls) .ToList()); }