Wiederaufnahme nach Absturz, Nachschau und Auswertung (#553-#556)
#553 FileRW schreibt atomar. Die Vorkehrung lag seit #241 im selben Repository und wurde ueberall benutzt - Einstellungen, Sicherungen, Chat-Zustand, Identity.md - nur der Weg, ueber den Agenten ihre Arbeitsergebnisse ablegen, ging daran vorbei. Damit stand das Verhaeltnis auf dem Kopf: geschuetzt war das Eigene, ungeschuetzt das Ergebnis. append bleibt bewusst nicht atomar; das ginge nur, indem die ganze Datei neu geschrieben wird, und bei einer grossen Protokolldatei waere das der falsche Handel. #554 Wiederaufnahme. Neuer Eintrag AuditStatus.Started, geschrieben VOR der Wirkung und als einziger nicht "best effort" - auf ihn stuetzt sich alles Weitere, also laeuft der Aufruf gar nicht erst, wenn er nicht durchkommt. Neue Kennzeichnung IsRepeatable, getrennt von IsParallelSafe: Das eine heisst "stoert keinen gleichzeitigen Aufruf", das andere "richtet zweimal keinen Schaden an". CrashRecovery sichtet beim Start, was offen blieb, und stuft ein - fuehrt aber nichts aus. Es sichtet und berichtet; entscheiden soll der Aufrufer. Dabei kippte eine alte Regel: Bisher wurde ein Abbruch bewusst NICHT protokolliert. Das war richtig, solange es keinen Eintrag vor dem Aufruf gab. Jetzt saehe sonst jeder Benutzerabbruch beim naechsten Start aus wie ein Absturz - bis niemand mehr hinsieht. Deshalb AuditStatus.Cancelled. #555 Optionale Zustandspruefung IToolStateProbe, umgesetzt in FileRW. Unknown ist eine vollwertige Antwort, kein Fehlschlag: Ob ein SMTP-Server angenommen hat, laesst sich nicht feststellen, und ein Werkzeug, das hier raet, waere schlimmer als eines, das schweigt. Partial gilt ausdruecklich nicht als geklaert - einen halb angehaengten Text sollte niemand automatisch geraderuecken. #556 Auswertung. ListForAgentAsync und SummarizeToolUsageAsync. Die Dauer wird nur ueber abgeschlossene Aufrufe gemittelt; zaehlten die Started-Eintraege mit ihrer Dauer 0 mit, saehe jedes Werkzeug doppelt so schnell aus, wie es ist. Nebenbei: SqliteStorage kann jetzt Spalten nachruesten. CREATE TABLE IF NOT EXISTS allein genuegte nicht - bei einer bestehenden Datenbank tut die Anweisung nichts, und erst das naechste INSERT waere gescheitert, im Betrieb beim Anwender. Vier Audit-Tests angepasst: Ein ausgefuehrter Aufruf erzeugt jetzt zwei Eintraege statt einem. Ein Aufruf, der gar nicht erst laeuft - verweigert, unbekannt, vorgelegt - hat weiterhin genau einen; der Beginn wird erst nach Rechtepruefung und Staging geschrieben. 23 neue Tests, 912 gesamt, keine Regression. Build ohne Warnungen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,15 @@ 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.
|
||||
///
|
||||
/// <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
|
||||
{
|
||||
@@ -27,9 +36,17 @@ public sealed class EngineAuditTests
|
||||
await fixture.Engine.RunAsync(agent, "los", "test-instance", default);
|
||||
|
||||
var entries = audit.Entries.ToList();
|
||||
entries.Count.ShouldBe(2);
|
||||
|
||||
// Zwei Aufrufe, je ein Beginn und ein Abschluss.
|
||||
entries.Count.ShouldBe(4);
|
||||
entries.ShouldAllBe(e => e.Tool == "TestTool");
|
||||
entries.ShouldAllBe(e => e.Status == AuditStatus.Ok);
|
||||
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]
|
||||
@@ -42,7 +59,7 @@ public sealed class EngineAuditTests
|
||||
|
||||
await fixture.Engine.RunAsync(agent, "los", "test-instance", default, source: "task", taskId: "t-7");
|
||||
|
||||
var entry = audit.Entries.Single();
|
||||
var entry = audit.Entries.Single(e => e.Status == AuditStatus.Ok);
|
||||
entry.AgentId.ShouldBe("agent-a");
|
||||
entry.Model.ShouldBe("test/model");
|
||||
entry.Source.ShouldBe("task");
|
||||
@@ -74,7 +91,7 @@ public sealed class EngineAuditTests
|
||||
|
||||
await fixture.Engine.RunAsync(agent, "los", "test-instance", default);
|
||||
|
||||
var entry = audit.Entries.Single();
|
||||
var entry = audit.Entries.Single(e => e.Status != AuditStatus.Started);
|
||||
entry.Status.ShouldBe(AuditStatus.Error);
|
||||
entry.Summary.ShouldContain("kaputt");
|
||||
}
|
||||
@@ -108,7 +125,7 @@ public sealed class EngineAuditTests
|
||||
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");
|
||||
audit.Entries.ShouldAllBe(e => e.RunId == runId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user