using ClawdDotNet.Core.Audit; using ClawdDotNet.Core.Storage; using Shouldly; namespace ClawdDotNet.Core.Tests.Audit; /// /// A3: das append-only Audit-Log und die Receipts. Gegen echte SQLite, weil Schema und /// Persistenz der Punkt sind. /// public sealed class AuditRepositoryTests : IDisposable { private readonly string _directory; private readonly SqliteStorage _storage; private readonly SqliteAuditRepository _repo; public AuditRepositoryTests() { _directory = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N")); _storage = new SqliteStorage(Path.Combine(_directory, "state.db")); _repo = new SqliteAuditRepository(_storage); } public void Dispose() { Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools(); try { Directory.Delete(_directory, recursive: true); } catch { /* Aufräumen ist Nebensache */ } } private AuditEntry Entry(string runId, string tool, AuditStatus status = AuditStatus.Ok) => new() { RunId = runId, AgentId = "agent-a", Model = "test/model", Source = "task", Tool = tool, Arguments = "{\"x\":1}", Status = status, Summary = "", DurationMs = 5, OccurredAt = DateTime.UtcNow }; [Fact] public async Task Ein_Eintrag_wird_geschrieben_und_wiedergefunden() { await _repo.AppendAsync(Entry("run-1", "FileRW"), default); var recent = await _repo.ListRecentAsync(10, default); recent.Count.ShouldBe(1); recent[0].Tool.ShouldBe("FileRW"); recent[0].RunId.ShouldBe("run-1"); (await _repo.CountAsync(default)).ShouldBe(1); } [Fact] public async Task Die_Aufrufe_eines_Laufs_kommen_in_Reihenfolge() { await _repo.AppendAsync(Entry("run-1", "Memory"), default); await _repo.AppendAsync(Entry("run-1", "FileRW"), default); await _repo.AppendAsync(Entry("run-2", "Mail"), default); var forRun = await _repo.ListForRunAsync("run-1", default); forRun.Select(e => e.Tool).ShouldBe(["Memory", "FileRW"]); } [Fact] public async Task Die_juengsten_Eintraege_kommen_zuerst() { await _repo.AppendAsync(Entry("run-1", "erst"), default); await _repo.AppendAsync(Entry("run-1", "zuletzt"), default); (await _repo.ListRecentAsync(10, default))[0].Tool.ShouldBe("zuletzt"); } [Fact] public async Task Der_Status_ueberlebt_den_Rundlauf() { await _repo.AppendAsync(Entry("r", "X", AuditStatus.Denied), default); (await _repo.ListRecentAsync(1, default))[0].Status.ShouldBe(AuditStatus.Denied); } // ─── Receipts ─── [Fact] public async Task Ein_Receipt_verknuepft_Lauf_und_Task() { await _repo.RecordReceiptAsync(new RunReceipt { RunId = "run-9", AgentId = "agent-a", Model = "test/model", Source = "task", TaskId = "t-42", Status = "Completed", StepCount = 3, PromptTokens = 100, CompletionTokens = 20, CachedTokens = 10, CostUsd = 0.0123m, CostIsKnown = true, DurationMs = 1200, ResultRef = "fertig", OccurredAt = DateTime.UtcNow }, default); var byRun = await _repo.GetReceiptForRunAsync("run-9", default); byRun.ShouldNotBeNull(); byRun!.TaskId.ShouldBe("t-42"); byRun.CostUsd.ShouldBe(0.0123m); byRun.CostIsKnown.ShouldBeTrue(); var byTask = await _repo.ListReceiptsForTaskAsync("t-42", default); byTask.Count.ShouldBe(1); byTask[0].RunId.ShouldBe("run-9"); } [Fact] public async Task Eintraege_ueberdauern_das_Schliessen_der_Datenbank() { await _repo.AppendAsync(Entry("run-1", "FileRW"), default); var reopened = new SqliteAuditRepository(new SqliteStorage(Path.Combine(_directory, "state.db"))); (await reopened.CountAsync(default)).ShouldBe(1); } }