(t *testing.T)
| 77 | } |
| 78 | |
| 79 | func TestBuildStatsWindowed(t *testing.T) { |
| 80 | root := t.TempDir() |
| 81 | claudeProj := t.TempDir() |
| 82 | |
| 83 | db, err := flowdb.OpenDB(filepath.Join(root, "flow.db")) |
| 84 | if err != nil { |
| 85 | t.Fatal(err) |
| 86 | } |
| 87 | defer db.Close() |
| 88 | work := "/work/app" |
| 89 | mustExec(t, db, `INSERT INTO tasks (slug,name,status,work_dir,session_id,created_at,updated_at) |
| 90 | VALUES ('t1','T1','done',?, '00000000-0000-4000-8000-000000000001', '2026-05-01T00:00:00Z','2026-05-01T00:00:00Z')`, work) |
| 91 | |
| 92 | jdir := filepath.Join(claudeProj, claude.EncodeCwd(work)) |
| 93 | os.MkdirAll(jdir, 0o755) |
| 94 | // (a) lookup BEFORE the cutoff, (b) lookup AFTER the cutoff, |
| 95 | // (c) lookup with a missing timestamp (zero TS). |
| 96 | before := `{"type":"assistant","timestamp":"2026-05-01T10:00:00.000Z","message":{"role":"assistant","content":[{"type":"tool_use","name":"Bash","input":{"command":"flow show task"}}]}}` |
| 97 | after := `{"type":"assistant","timestamp":"2026-06-10T10:00:00.000Z","message":{"role":"assistant","content":[{"type":"tool_use","name":"Bash","input":{"command":"flow show task"}}]}}` |
| 98 | noTS := `{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","name":"Bash","input":{"command":"flow show task"}}]}}` |
| 99 | os.WriteFile(filepath.Join(jdir, "00000000-0000-4000-8000-000000000001.jsonl"), |
| 100 | []byte(before+"\n"+after+"\n"+noTS+"\n"), 0o644) |
| 101 | |
| 102 | // Brief + updates for t1 — known byte sizes for the windowed token assertion. |
| 103 | // All three lookups are resumes, so each in-window resume contributes |
| 104 | // briefBytes + updatesBytes. Only the after-cutoff one is in window. |
| 105 | briefContent := []byte("# T1 brief\nWindowed test brief.\n") // 32 bytes |
| 106 | updatesContent := []byte("# update\nWindowed update note.\n") // 31 bytes |
| 107 | os.MkdirAll(filepath.Join(root, "tasks", "t1", "updates"), 0o755) |
| 108 | os.WriteFile(filepath.Join(root, "tasks", "t1", "brief.md"), briefContent, 0o644) |
| 109 | os.WriteFile(filepath.Join(root, "tasks", "t1", "updates", "u1.md"), updatesContent, 0o644) |
| 110 | |
| 111 | c := LoadCache(filepath.Join(root, "stats-cache.json")) |
| 112 | since := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC) // between (a) and (b) |
| 113 | s, err := BuildStats(BuildOpts{ |
| 114 | Root: root, ClaudeProjects: claudeProj, DB: db, Cache: c, |
| 115 | Constants: DefaultConstants(), Since: since, Project: "", |
| 116 | }) |
| 117 | if err != nil { |
| 118 | t.Fatal(err) |
| 119 | } |
| 120 | // Only the after-cutoff lookup is counted; zero-TS and before-cutoff excluded. |
| 121 | if s.LookupsTotal != 1 || s.LookupsByKind[LookupResume] != 1 { |
| 122 | t.Errorf("windowed lookups = %d %v, want 1 resume", s.LookupsTotal, s.LookupsByKind) |
| 123 | } |
| 124 | // The window guard must govern byte accumulation too: only the single |
| 125 | // in-window resume contributes; out-of-window and zero-TS contribute ZERO. |
| 126 | wantContextTokens := (int64(len(briefContent)) + int64(len(updatesContent))) / 4 |
| 127 | if s.Savings.ContextTokens != wantContextTokens { |
| 128 | t.Errorf("windowed ContextTokens = %d, want %d (only the 1 in-window resume: (brief=%d + updates=%d)/4)", |
| 129 | s.Savings.ContextTokens, wantContextTokens, len(briefContent), len(updatesContent)) |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | func TestBuildStatsPlaybookRuns(t *testing.T) { |
| 134 | root := t.TempDir() |
nothing calls this directly
no test coverage detected