- Engine: leere Antworten nachfassen, Aufgaben enden mit Ergebniszeile (EmptyResponse/Incomplete), gleiche Fehlschlaege erkennen (RepeatedFailure), Zeitzeile vor Chatnachrichten, Werkzeugergebnisse gerahmt, Vorpruefung vor dem Staging, Belegkosten wie RunUsage - Staging: gescheiterte Freigaben als Failed; AgentEditor-Aenderungen freigabepflichtig, Selbstbearbeitung gesperrt (allowSelfEdit) - Taskboard: Termine beim Anlegen pruefen, naechster/letzter Lauf, pausieren, Sofortlauf, Kostenprognose, Hinweis auf wachsenden Kontext; Jobs-Reiter und /api/jobs entfernt - Release: Instanzmanager samt Weboberflaeche im Paket, setup.json fragt manager.json ab, Token nicht mehr ins systemd-Journal Version 0.1.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
144 lines
6.0 KiB
C#
144 lines
6.0 KiB
C#
using ClawdDotNet.Core.Audit;
|
|
using ClawdDotNet.Core.Tests.Infrastructure;
|
|
using Shouldly;
|
|
|
|
namespace ClawdDotNet.Core.Tests.Audit;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
///
|
|
/// <para>Seit #554 erzeugt ein ausgefuehrter Aufruf ZWEI Eintraege: erst
|
|
/// <see cref="AuditStatus.Started"/> vor der Wirkung, dann den Ausgang. Die Tests
|
|
/// hier filtern deshalb auf den Abschluss, wo es um den Ausgang geht — dass der
|
|
/// Beginn mitgeschrieben wird, pruefen die WiederaufnahmeTests.</para>
|
|
///
|
|
/// <para>Ein Aufruf, der gar nicht erst laeuft (verweigert, unbekannt, vorgelegt),
|
|
/// hat weiterhin genau einen Eintrag: Der Beginn wird erst nach Rechtepruefung und
|
|
/// Staging geschrieben.</para>
|
|
/// </summary>
|
|
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();
|
|
|
|
// Zwei Aufrufe, je ein Beginn und ein Abschluss.
|
|
entries.Count.ShouldBe(4);
|
|
entries.ShouldAllBe(e => e.Tool == "TestTool");
|
|
entries.Count(e => e.Status == AuditStatus.Started).ShouldBe(2);
|
|
entries.Count(e => e.Status == AuditStatus.Ok).ShouldBe(2);
|
|
|
|
// Jeder Beginn gehoert zu genau einem Abschluss.
|
|
entries.Where(e => e.Status == AuditStatus.Started).Select(e => e.CallId)
|
|
.ShouldBe(entries.Where(e => e.Status == AuditStatus.Ok).Select(e => e.CallId),
|
|
ignoreOrder: true);
|
|
}
|
|
|
|
[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(e => e.Status == AuditStatus.Ok);
|
|
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(e => e.Status != AuditStatus.Started);
|
|
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");
|
|
// Aufgabenlaeufe enden seit #577 erst mit einer Ergebniszeile.
|
|
fixture.Client.RespondsWithToolCall("TestTool").RespondsWithText("ERGEBNIS: ok - 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("ERGEBNIS: ok - 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.ShouldAllBe(e => e.RunId == 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");
|
|
}
|
|
}
|