Files
felixm 25f56b3c71 Add RecentSessions reader over the audit chain
Exposes the last n session summaries (oldest-first) for the reviewer to
read as recent-history context. Missing/empty chain or n<=0 yields nil.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 09:26:57 -04:00

120 lines
3.4 KiB
Go

package store
import (
"os"
"path/filepath"
"strings"
"testing"
)
func auditFixture(t *testing.T) string {
t.Helper()
return filepath.Join(t.TempDir(), "audit.jsonl")
}
func sampleSummary(id string) SessionSummary {
return SessionSummary{
SessionID: id,
NextAction: "write plan",
SuccessCond: "plan committed",
Outcome: "completed",
StartedUnix: 1000,
EndedUnix: 2000,
SwitchCount: 3,
Buckets: []BucketTotal{
{Class: "code", Title: "antidrift", Seconds: 540},
{Class: "firefox", Title: "docs", Seconds: 120},
},
}
}
func TestAppendSessionChainsAndVerifies(t *testing.T) {
path := auditFixture(t)
for i := 0; i < 3; i++ {
if err := AppendSession(path, sampleSummary("session-"+string(rune('a'+i)))); err != nil {
t.Fatalf("append %d: %v", i, err)
}
}
if err := VerifyChain(path); err != nil {
t.Fatalf("chain should verify: %v", err)
}
}
func TestGenesisPrevHashIsZeros(t *testing.T) {
path := auditFixture(t)
if err := AppendSession(path, sampleSummary("session-a")); err != nil {
t.Fatalf("append: %v", err)
}
data, _ := os.ReadFile(path)
if !strings.Contains(string(data), strings.Repeat("0", 64)) {
t.Fatalf("genesis prev_hash should be 64 zeros, got: %s", data)
}
}
func TestVerifyEmptyChain(t *testing.T) {
if err := VerifyChain(auditFixture(t)); err != nil {
t.Fatalf("empty/missing chain should verify, got %v", err)
}
}
func TestTamperDetected(t *testing.T) {
path := auditFixture(t)
_ = AppendSession(path, sampleSummary("session-a"))
_ = AppendSession(path, sampleSummary("session-b"))
_ = AppendSession(path, sampleSummary("session-c"))
// Corrupt the middle line's switch_count.
data, _ := os.ReadFile(path)
lines := strings.Split(strings.TrimRight(string(data), "\n"), "\n")
lines[1] = strings.Replace(lines[1], `"switch_count":3`, `"switch_count":99`, 1)
if err := os.WriteFile(path, []byte(strings.Join(lines, "\n")+"\n"), 0o644); err != nil {
t.Fatalf("rewrite: %v", err)
}
err := VerifyChain(path)
if err == nil || !strings.Contains(err.Error(), "line 2") {
t.Fatalf("tamper on line 2 should be reported, got %v", err)
}
}
func TestRecentSessionsReturnsLastNOldestFirst(t *testing.T) {
path := auditFixture(t)
for _, id := range []string{"a", "b", "c"} {
if err := AppendSession(path, sampleSummary("session-"+id)); err != nil {
t.Fatalf("append %s: %v", id, err)
}
}
got, err := RecentSessions(path, 2)
if err != nil {
t.Fatalf("recent: %v", err)
}
if len(got) != 2 {
t.Fatalf("want 2 summaries, got %d", len(got))
}
if got[0].SessionID != "session-b" || got[1].SessionID != "session-c" {
t.Fatalf("want [b c] oldest-first, got [%s %s]", got[0].SessionID, got[1].SessionID)
}
}
func TestRecentSessionsFewerThanN(t *testing.T) {
path := auditFixture(t)
_ = AppendSession(path, sampleSummary("session-a"))
got, err := RecentSessions(path, 5)
if err != nil {
t.Fatalf("recent: %v", err)
}
if len(got) != 1 || got[0].SessionID != "session-a" {
t.Fatalf("want [a], got %+v", got)
}
}
func TestRecentSessionsMissingOrZero(t *testing.T) {
if got, err := RecentSessions(auditFixture(t), 5); err != nil || got != nil {
t.Fatalf("missing chain: want (nil,nil), got (%+v,%v)", got, err)
}
path := auditFixture(t)
_ = AppendSession(path, sampleSummary("session-a"))
if got, err := RecentSessions(path, 0); err != nil || got != nil {
t.Fatalf("n=0: want (nil,nil), got (%+v,%v)", got, err)
}
}