128 lines
4.0 KiB
C#
128 lines
4.0 KiB
C#
using ClawdDotNet.Core.Audit;
|
|
using ClawdDotNet.Core.Storage;
|
|
using Shouldly;
|
|
|
|
namespace ClawdDotNet.Core.Tests.Audit;
|
|
|
|
/// <summary>
|
|
/// A3: das append-only Audit-Log und die Receipts. Gegen echte SQLite, weil Schema und
|
|
/// Persistenz der Punkt sind.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|