using ClawdDotNet.Core.Audit; using ClawdDotNet.Core.Tests.Infrastructure; using Shouldly; namespace ClawdDotNet.Core.Tests.Audit; /// /// A3 an der Engine: Jeder Tool-Aufruf wird protokolliert, jeder Lauf bekommt einen Beleg. /// Entscheidend ist die Provenienz — die Engine stempelt Agent, Modell und Herkunft aus /// ihrem eigenen Wissen, nicht aus dem Tool-Ergebnis. /// public sealed class EngineAuditTests { [Fact] public async Task Jeder_Tool_Aufruf_landet_im_Audit_Log() { var audit = new InMemoryAuditRepository(); var fixture = new EngineFixture(audit).WithTool(FakeTool.Returning("ok")); var agent = fixture.AddAgent("agent-a", "TestTool"); // Zwei Tool-Aufrufe, dann Text. fixture.Client .RespondsWithToolCall("TestTool") .RespondsWithToolCall("TestTool") .RespondsWithText("fertig"); await fixture.Engine.RunAsync(agent, "los", "test-instance", default); var entries = audit.Entries.ToList(); entries.Count.ShouldBe(2); entries.ShouldAllBe(e => e.Tool == "TestTool"); entries.ShouldAllBe(e => e.Status == AuditStatus.Ok); } [Fact] public async Task Die_Herkunft_wird_von_der_Engine_gestempelt() { var audit = new InMemoryAuditRepository(); var fixture = new EngineFixture(audit).WithTool(FakeTool.Returning("ok")); var agent = fixture.AddAgent("agent-a", "TestTool"); fixture.Client.RespondsWithToolCall("TestTool").RespondsWithText("fertig"); await fixture.Engine.RunAsync(agent, "los", "test-instance", default, source: "task", taskId: "t-7"); var entry = audit.Entries.Single(); entry.AgentId.ShouldBe("agent-a"); entry.Model.ShouldBe("test/model"); entry.Source.ShouldBe("task"); entry.RunId.ShouldNotBeNullOrWhiteSpace(); } [Fact] public async Task Ein_verweigerter_Aufruf_wird_als_denied_protokolliert() { var audit = new InMemoryAuditRepository(); // Tool ist registriert, aber dem Agenten NICHT zugewiesen → PermissionGate greift. var fixture = new EngineFixture(audit).WithTool(FakeTool.Returning("ok")); var agent = fixture.AddAgent("agent-a" /* kein Tool zugewiesen */); fixture.Client.RespondsWithToolCall("TestTool").RespondsWithText("fertig"); await fixture.Engine.RunAsync(agent, "los", "test-instance", default); audit.Entries.Single().Status.ShouldBe(AuditStatus.Denied); } [Fact] public async Task Ein_Tool_Fehler_wird_als_error_protokolliert() { var audit = new InMemoryAuditRepository(); var fixture = new EngineFixture(audit) .WithTool(FakeTool.Throwing(new InvalidOperationException("kaputt"))); var agent = fixture.AddAgent("agent-a", "TestTool"); fixture.Client.RespondsWithToolCall("TestTool").RespondsWithText("fertig"); await fixture.Engine.RunAsync(agent, "los", "test-instance", default); var entry = audit.Entries.Single(); entry.Status.ShouldBe(AuditStatus.Error); entry.Summary.ShouldContain("kaputt"); } [Fact] public async Task Ein_Lauf_bekommt_einen_Receipt_mit_Task_Verknuepfung() { var audit = new InMemoryAuditRepository(); var fixture = new EngineFixture(audit).WithTool(FakeTool.Returning("ok")); var agent = fixture.AddAgent("agent-a", "TestTool"); fixture.Client.RespondsWithToolCall("TestTool").RespondsWithText("erledigt"); await fixture.Engine.RunAsync(agent, "los", "test-instance", default, source: "task", taskId: "t-42"); var receipt = audit.Receipts.Single(); receipt.TaskId.ShouldBe("t-42"); receipt.Source.ShouldBe("task"); receipt.Status.ShouldBe("Completed"); receipt.ResultRef.ShouldBe("erledigt"); receipt.StepCount.ShouldBeGreaterThan(0); } [Fact] public async Task Receipt_und_Audit_teilen_dieselbe_RunId() { var audit = new InMemoryAuditRepository(); var fixture = new EngineFixture(audit).WithTool(FakeTool.Returning("ok")); var agent = fixture.AddAgent("agent-a", "TestTool"); fixture.Client.RespondsWithToolCall("TestTool").RespondsWithText("fertig"); await fixture.Engine.RunAsync(agent, "los", "test-instance", default); var runId = audit.Receipts.Single().RunId; audit.Entries.Single().RunId.ShouldBe(runId, "alle Aufrufe eines Laufs tragen dieselbe RunId"); } [Fact] public async Task Ohne_Audit_Repository_laeuft_alles_normal_weiter() { // Gegenprobe: A3 ist optional — die Engine darf ohne Audit-Repo nicht anders laufen. var fixture = new EngineFixture().WithTool(FakeTool.Returning("ok")); var agent = fixture.AddAgent("agent-a", "TestTool"); fixture.Client.RespondsWithToolCall("TestTool").RespondsWithText("fertig"); var result = await fixture.Engine.RunAsync(agent, "los", "test-instance", default); result.Status.ToString().ShouldBe("Completed"); } }