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) } }