addf3a19d1
First Tier-2 battery, plugging into run.Ports.Audit: - storage.go/writer.go: skillaudit's Storage interface + per-run Writer moved clean (only utils->fmt); the Writer already matches run.RunRecorder's shape. - sink.go: Sink adapts a Storage to run.Audit (StartRun -> a run row + a Writer wrapped as run.RunRecorder, converting run.RunStats on Close). NewSink(nil) is equivalent to no audit. Compile-time proofs: Sink is run.Audit, recorder is run.RunRecorder. - memory.go: NewMemory() — a zero-dependency, queryable in-process Storage (retains runs + logs; all 17 read/filter/purge/walk methods) so a light host gets run history with no setup. Mort keeps its GORM Storage; contrib/store adds durable SQLite at P4. End-to-end test: wire audit.NewSink(audit.NewMemory()) into the executor, run an agent, and the run is recorded with terminal status/output and queryable by caller. CI invariant verified: core imports ZERO from the audit battery (proper battery direction; battery imports core, never the reverse). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
79 lines
2.3 KiB
Go
79 lines
2.3 KiB
Go
package audit_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"gitea.stevedudenhoeffer.com/steve/majordomo/llm"
|
|
"gitea.stevedudenhoeffer.com/steve/majordomo/provider/fake"
|
|
|
|
"gitea.stevedudenhoeffer.com/steve/executus/audit"
|
|
"gitea.stevedudenhoeffer.com/steve/executus/run"
|
|
"gitea.stevedudenhoeffer.com/steve/executus/tool"
|
|
)
|
|
|
|
// TestAuditBatteryEndToEnd wires the audit battery (Memory storage) into
|
|
// run.Ports.Audit, runs an agent, and verifies the run was recorded and is
|
|
// queryable — proving Sink/Writer/Memory satisfy the core seams end to end.
|
|
func TestAuditBatteryEndToEnd(t *testing.T) {
|
|
mem := audit.NewMemory()
|
|
|
|
fp := fake.New("fake")
|
|
fp.Enqueue("m", fake.Reply("the answer"))
|
|
m, err := fp.Model("m")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ex := run.New(run.Config{
|
|
Registry: tool.NewRegistry(),
|
|
Models: func(ctx context.Context, _ string) (context.Context, llm.Model, error) {
|
|
return ctx, m, nil
|
|
},
|
|
Ports: run.Ports{Audit: audit.NewSink(mem)},
|
|
})
|
|
|
|
res := ex.Run(context.Background(),
|
|
run.RunnableAgent{ID: "agent-1", Name: "a", ModelTier: "m"},
|
|
tool.Invocation{RunID: "run-xyz", CallerID: "caller-1"},
|
|
"question")
|
|
if res.Err != nil {
|
|
t.Fatalf("run error: %v", res.Err)
|
|
}
|
|
|
|
// The run was recorded with a terminal status + output.
|
|
got, err := mem.GetRun(context.Background(), "run-xyz")
|
|
if err != nil {
|
|
t.Fatalf("GetRun: %v", err)
|
|
}
|
|
if got.Status != "ok" {
|
|
t.Errorf("status = %q, want ok", got.Status)
|
|
}
|
|
if got.Output != "the answer" {
|
|
t.Errorf("output = %q, want %q", got.Output, "the answer")
|
|
}
|
|
if got.FinishedAt == nil {
|
|
t.Error("FinishedAt should be set after the run")
|
|
}
|
|
if got.SkillID != "agent-1" {
|
|
t.Errorf("SkillID = %q, want agent-1 (the subject id)", got.SkillID)
|
|
}
|
|
|
|
// And it is queryable by caller.
|
|
runs, err := mem.ListRunsByCaller(context.Background(), "caller-1", 10)
|
|
if err != nil {
|
|
t.Fatalf("ListRunsByCaller: %v", err)
|
|
}
|
|
if len(runs) != 1 || runs[0].ID != "run-xyz" {
|
|
t.Errorf("ListRunsByCaller = %+v, want [run-xyz]", runs)
|
|
}
|
|
}
|
|
|
|
// TestNilSinkRecordsNothing: NewSink(nil) is equivalent to no audit.
|
|
func TestNilSinkRecordsNothing(t *testing.T) {
|
|
s := audit.NewSink(nil)
|
|
if rec := s.StartRun(context.Background(), run.RunInfo{RunID: "r"}); rec != nil {
|
|
t.Error("NewSink(nil).StartRun should return a nil recorder")
|
|
}
|
|
}
|