Files
ClawdDotNet/tests/ClawdDotNet.Core.Tests/Infrastructure/InMemoryAuditRepository.cs
T
RichardandClaude Opus 5.5 77062facc9 Stufe 4c (#764): Bezeichner auf Englisch - Typen, Methoden, Felder, Parameter, Locals, Testnamen
Mit Roslyn umbenannt (Verweise folgen, Zeichenketten bleiben unberuehrt).
Nicht umbenannt, weil gespeichert oder ueber die API gebunden (Stufe 4e):
Properties, Record-Parameter, Enum-Member sowie die Parameter der
Endpunkt-Handler (Routen-/Query-Bindung ueber den Namen). Die JSON-Felder
anonymer Objekte bleiben ausdruecklich benannt (JsonFilter: pfad).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:18:43 +02:00

103 lines
4.6 KiB
C#

using System.Collections.Concurrent;
using ClawdDotNet.Core.Audit;
namespace ClawdDotNet.Core.Tests.Infrastructure;
/// <summary>
/// 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.
/// </summary>
internal sealed class InMemoryAuditRepository : IAuditRepository
{
public ConcurrentQueue<AuditEntry> Entries { get; } = new();
public ConcurrentQueue<RunReceipt> 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<IReadOnlyList<AuditEntry>> ListRecentAsync(int limit, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<AuditEntry>>(Entries.Reverse().Take(limit).ToList());
public Task<IReadOnlyList<AuditEntry>> ListForRunAsync(string runId, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<AuditEntry>>(Entries.Where(e => e.RunId == runId).ToList());
public Task<RunReceipt?> GetReceiptForRunAsync(string runId, CancellationToken ct)
=> Task.FromResult(Receipts.LastOrDefault(r => r.RunId == runId));
public Task<IReadOnlyList<RunReceipt>> ListReceiptsCalledFromAsync(string runId, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<RunReceipt>>(Receipts.Where(r => r.CallerRunId == runId).ToList());
public Task<IReadOnlyList<RunReceipt>> ListReceiptsForTaskAsync(string taskId, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<RunReceipt>>(Receipts.Where(r => r.TaskId == taskId).ToList());
public Task<IReadOnlyList<RunReceipt>> ListReceiptsAsync(DateTime fromUtc, DateTime toUtc, int limit, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<RunReceipt>>(Receipts
.Where(r => r.OccurredAt >= fromUtc && r.OccurredAt < toUtc)
.OrderBy(r => r.OccurredAt).Take(limit).ToList());
public Task<int> CountAsync(CancellationToken ct) => Task.FromResult(Entries.Count);
/// <summary>Mirrors the SQL rule: Started without a later completion of the same CallId.</summary>
public Task<IReadOnlyList<AuditEntry>> 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<IReadOnlyList<AuditEntry>>(open);
}
public Task<IReadOnlyList<AuditEntry>> ListForAgentAsync(
string agentId, DateTime? since, int limit, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<AuditEntry>>(
Entries.Where(e => e.AgentId == agentId)
.Where(e => since is null || e.OccurredAt >= since)
.Reverse()
.Take(limit)
.ToList());
public Task<IReadOnlyList<ToolUsageStat>> SummarizeToolUsageAsync(
string? agentId, DateTime? since, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<ToolUsageStat>>(
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());
}