package herdr import ( "os" "path/filepath" "strings" "testing" "time" ) func TestClaudeStopHookUsage(t *testing.T) { // The hook parser's path validation is independent from transcript IO. _, path, err := ClaudeStopHookUsage(strings.NewReader(`{"transcript_path":"/tmp/transcript.jsonl"}`)) if path != "/tmp/transcript.jsonl" || err == nil { t.Fatalf("path=%q err=%v", path, err) } } func TestCodexRolloutFallback(t *testing.T) { paths, err := CodexRolloutPaths(t.TempDir()) if err == nil || len(paths) != 0 { t.Fatalf("paths=%v err=%v", paths, err) } } // TestClaudeUsageIsLastTurnNotCumulative guards the exact trap the spec // (ยง5.2.1) calls out by name: occupancy must reflect the current context // window (the last turn's usage snapshot), not a running total across many // turns. A transcript with one huge early turn and a small final turn must // report low occupancy, because Claude Code's own usage lines are already // cumulative-per-turn snapshots, not deltas to be summed. func TestClaudeUsageIsLastTurnNotCumulative(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "transcript.jsonl") lines := []string{ `{"message":{"usage":{"input_tokens":180000,"cache_read_input_tokens":0,"cache_creation_input_tokens":0,"output_tokens":500}}}`, `{"message":{"usage":{"input_tokens":2000,"cache_read_input_tokens":500,"cache_creation_input_tokens":0,"output_tokens":100}}}`, } if err := os.WriteFile(path, []byte(strings.Join(lines, "\n")+"\n"), 0644); err != nil { t.Fatal(err) } u, err := ClaudeUsage(path) if err != nil { t.Fatal(err) } frac := Fraction(u, 200000) if frac > 0.05 { t.Fatalf("occupancy=%v, want low (last-turn usage, not the 180000-token first turn)", frac) } } func TestClaudeSessionFileNewestByMtime(t *testing.T) { worktree := t.TempDir() home := t.TempDir() t.Setenv("HOME", home) abs, err := filepath.Abs(worktree) if err != nil { t.Fatal(err) } enc := strings.ReplaceAll(abs, "/", "-") dir := filepath.Join(home, ".claude", "projects", enc) if err := os.MkdirAll(dir, 0755); err != nil { t.Fatal(err) } old := filepath.Join(dir, "old.jsonl") newer := filepath.Join(dir, "new.jsonl") if err := os.WriteFile(old, []byte("{}\n"), 0644); err != nil { t.Fatal(err) } if err := os.WriteFile(newer, []byte("{}\n"), 0644); err != nil { t.Fatal(err) } now := time.Now() if err := os.Chtimes(old, now.Add(-time.Hour), now.Add(-time.Hour)); err != nil { t.Fatal(err) } if err := os.Chtimes(newer, now, now); err != nil { t.Fatal(err) } got, err := ClaudeSessionFile(worktree) if err != nil { t.Fatal(err) } if got != newer { t.Fatalf("got %s, want newest transcript %s", got, newer) } } func TestClaudeSessionFileMissingIsHardError(t *testing.T) { worktree := t.TempDir() t.Setenv("HOME", t.TempDir()) if _, err := ClaudeSessionFile(worktree); err == nil { t.Fatal("expected error for missing session transcript directory, got nil") } }