Files
wehub-resource-sync f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

920 lines
28 KiB
Go

package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.kenn.io/agentsview/internal/db"
)
// renderStatsHuman renders stats through printStatsHuman and returns the
// captured output, asserting the render itself does not error.
func renderStatsHuman(t *testing.T, stats *db.SessionStats) string {
t.Helper()
var buf bytes.Buffer
require.NoError(t, printStatsHuman(&buf, stats), "printStatsHuman")
return buf.String()
}
// assertContainsAll asserts every want substring is present in out.
func assertContainsAll(t *testing.T, out string, wants ...string) {
t.Helper()
for _, w := range wants {
assert.Contains(t, out, w, "missing %q in output", w)
}
}
// assertContainsNone asserts none of the banned substrings appear in out.
func assertContainsNone(t *testing.T, out string, banned ...string) {
t.Helper()
for _, b := range banned {
assert.NotContains(t, out, b, "unexpected %q in output", b)
}
}
// setupGoldenStatsDataDir creates a temp data dir, points
// AGENTSVIEW_DATA_DIR at it, pins TZ to UTC for deterministic time
// formatting, seeds the golden fixture DB, and returns the data dir.
// Shared by the stats, usage, and activity command tests that exercise
// the offline/read-only archive query path.
func setupGoldenStatsDataDir(t *testing.T) string {
t.Helper()
dataDir := newAgentDataDir(t)
t.Setenv("AGENTSVIEW_CURSOR_ATTRIBUTION_DB",
filepath.Join(dataDir, "missing-cursor-attribution.db"))
// TZ is normally pinned by --timezone=UTC, but the environment can
// still leak into date parsing on some platforms; pin it too.
t.Setenv("TZ", "UTC")
copyGoldenFixtureDB(t, sessionsDBPath(dataDir))
return dataDir
}
// runDefaultStatsJSON runs the `stats --format json` CLI path over the
// golden fixture window and returns the raw JSON output. Callers
// unmarshal into whichever shape they need.
func runDefaultStatsJSON(t *testing.T) string {
t.Helper()
registerSQLiteDaemonRuntime(t, os.Getenv("AGENTSVIEW_DATA_DIR"))
out, err := executeCommand(newRootCommand(),
"stats",
"--format", "json",
"--since", "2026-04-01",
"--until", "2026-04-15",
"--timezone", "UTC",
)
require.NoError(t, err, "stats output:\n%s", out)
return out
}
// writeCustomModelPricingConfig writes a config.toml under dataDir that sets
// custom per-million-token pricing well above the built-in defaults, so tests
// can assert that custom pricing is applied.
func writeCustomModelPricingConfig(t *testing.T, dataDir string) {
t.Helper()
require.NoError(t, os.WriteFile(
filepath.Join(dataDir, "config.toml"),
[]byte(`
[custom_model_pricing."claude-sonnet-4-20250514"]
input = 300.0
output = 1500.0
cache_creation = 375.0
cache_read = 30.0
[custom_model_pricing."claude-opus-4-20250514"]
input = 1500.0
output = 7500.0
cache_creation = 1875.0
cache_read = 150.0
`),
0o600,
))
}
// TestPrintStatsHuman_Populated exercises the happy path with
// every optional section present. It does not pin exact text — the
// golden-file test in Task 20 owns that — but it guards the sections
// and nil-pointer branches that are hardest to eyeball in the stub.
func TestPrintStatsHuman_Populated(t *testing.T) {
prsOpened := 12
prsMerged := 9
stats := &db.SessionStats{
SchemaVersion: 1,
Window: db.StatsWindow{
Since: "2026-03-21T00:00:00Z",
Until: "2026-04-18T00:00:00Z",
Days: 28,
},
Filters: db.StatsFilters{
Agent: "all",
ProjectsExcluded: []string{},
Timezone: "America/New_York",
},
Totals: db.StatsTotals{
SessionsAll: 11905,
SessionsHuman: 322,
SessionsAutomation: 11583,
MessagesTotal: 109324,
UserMessagesTotal: 3012,
},
Archetypes: db.StatsArchetypes{
Automation: 11583,
Quick: 125,
Standard: 101,
Deep: 79,
Marathon: 17,
Primary: "automation",
PrimaryHuman: "quick",
},
Distributions: db.StatsDistributions{
DurationMinutes: db.ScopedDistributionPair{
ScopeAll: db.ScopedDistribution{Mean: 14.7},
ScopeHuman: db.ScopedDistribution{Mean: 22.0},
},
UserMessages: db.ScopedDistributionPair{
ScopeAll: db.ScopedDistribution{Mean: 11.2},
ScopeHuman: db.ScopedDistribution{Mean: 7.2},
},
PeakContextTokens: db.PeakContextDistribution{
ScopeAll: db.ScopedDistribution{Mean: 48000},
NullCount: 0,
},
ToolsPerTurn: db.ScopedDistributionPair{
ScopeAll: db.ScopedDistribution{Mean: 2.3},
},
},
Velocity: db.StatsVelocity{
TurnCycleSeconds: db.StatsPercentiles{
P50: 20, P90: 90, Mean: 45,
},
FirstResponseSeconds: db.StatsPercentiles{
P50: 5, P90: 15, Mean: 8,
},
MessagesPerActiveHour: 120.0,
},
ToolMix: db.StatsToolMix{
ByCategory: map[string]int{
"Bash": 1234, "Edit": 876, "Read": 543,
"Grep": 321, "Glob": 210, "Write": 50,
},
TotalCalls: 3234,
},
ModelMix: db.StatsModelMix{
ByTokens: map[string]int64{
"claude-opus-4-7": 5600000,
"claude-sonnet-4-6": 1200000,
},
},
AgentPortfolio: db.StatsAgentPortfolio{
BySessions: map[string]int{"claude": 11905, "codex": 234},
ByTokens: map[string]int64{"claude": 6800000, "codex": 120000},
ByMessages: map[string]int{"claude": 109000, "codex": 2100},
Primary: "claude",
},
CacheEconomics: &db.StatsCacheEconomics{
ClaudeOnly: true,
CacheHitRatio: db.CacheHitRatioDistribution{
Overall: 0.78,
},
DollarsSavedVsUncached: 88.54,
DollarsSpent: 42.13,
},
Adoption: &db.StatsAdoption{
ClaudeOnly: true,
PlanModeRate: 0.12,
SubagentsPerSession: 0.3,
DistinctSkills: 8,
},
Temporal: db.StatsTemporal{
HourlyUTC: []db.TemporalHourlyUTCEntry{
{TS: "2026-04-01T00:00:00Z", Sessions: 3, UserMessages: 12},
{TS: "2026-04-01T01:00:00Z", Sessions: 2, UserMessages: 8},
},
ReporterTimezone: "America/New_York",
},
OutcomeStats: &db.StatsOutcomeStats{
ReposActive: 3,
Commits: 84,
LOCAdded: 5421,
LOCRemoved: 1823,
FilesChanged: 127,
PRsOpened: &prsOpened,
PRsMerged: &prsMerged,
},
Outcomes: &db.StatsOutcomes{
ClaudeOnly: true,
Success: 280,
Failure: 14,
Unknown: 28,
GradeDistribution: map[string]int{"A": 120, "B": 95, "C": 52, "D": 13, "F": 0},
ToolRetryRate: 0.064,
CompactionsPerSession: 0.1,
AvgEditChurn: 1.2,
},
CodeAttribution: &db.CodeAttribution{
Sources: []db.CodeAttributionSource{{
Provider: "cursor",
Scope: "machine_local",
Status: "available",
Metrics: &db.CursorAttributionMetrics{
ScoredCommits: 2,
LinesAdded: 30,
LinesDeleted: 12,
TabLinesAdded: 8,
TabLinesDeleted: 2,
ComposerLinesAdded: 3,
ComposerLinesDeleted: 1,
HumanLinesAdded: 19,
HumanLinesDeleted: 9,
BlankLinesAdded: 4,
BlankLinesDeleted: 0,
AIAuthoredPct: 11.0 / 30.0,
ConversationCounts: []db.CursorConversationCount{
{Model: "claude-3.5-sonnet", Mode: "composer", Count: 2},
{Model: "claude-3.5-sonnet", Mode: "tab", Count: 1},
},
}},
},
},
GeneratedAt: "2026-04-18T00:00:00Z",
}
out := renderStatsHuman(t, stats)
require.GreaterOrEqual(t, len(out), 200,
"output suspiciously short (%d bytes):\n%s", len(out), out)
// Guard every major section header so accidental drops are caught.
assertContainsAll(t, out,
"Session window:",
"Totals",
"Archetypes",
"Session shape",
"Velocity",
"Tool mix",
"Model mix",
"Agent portfolio",
"Cache economics",
"Adoption",
"Temporal",
"Outcome stats",
"Outcomes",
"Code attribution",
)
// Thousands separators must be applied to large counts.
assert.Contains(t, out, "11,905",
"expected thousands separator for 11,905")
assert.Contains(t, out, "claude-3.5-sonnet / composer",
"expected cursor conversation row")
}
// TestPrintStatsHuman_Empty guards the zero-session short
// circuit: no optional sections, just the header + "no sessions".
func TestPrintStatsHuman_Empty(t *testing.T) {
stats := &db.SessionStats{
SchemaVersion: 1,
Window: db.StatsWindow{
Since: "2026-04-11T00:00:00Z",
Until: "2026-04-18T00:00:00Z",
Days: 7,
},
Filters: db.StatsFilters{
Agent: "all",
ProjectsExcluded: []string{},
Timezone: "UTC",
},
}
out := renderStatsHuman(t, stats)
assert.Contains(t, out, "no sessions",
"expected zero-session placeholder in output")
// No optional section headers should appear.
assertContainsNone(t, out,
"Archetypes", "Velocity", "Cache economics", "Outcomes",
"Code attribution")
}
// TestFmtInt64 covers the thousands-separator helper.
func TestFmtInt64(t *testing.T) {
cases := []struct {
in int64
want string
}{
{0, "0"},
{5, "5"},
{999, "999"},
{1000, "1,000"},
{12345, "12,345"},
{123456, "123,456"},
{1234567, "1,234,567"},
{-1234, "-1,234"},
}
for _, c := range cases {
assert.Equal(t, c.want, fmtInt64(c.in),
"fmtInt64(%d)", c.in)
}
}
func TestStatsCommand_OutcomeFlagsRegistered(t *testing.T) {
cmd := newStatsCommand()
for _, name := range []string{
"include-git-outcomes",
"include-github-outcomes",
} {
assert.NotNil(t, cmd.Flags().Lookup(name),
"missing --%s flag", name)
}
}
func TestStatsCommandUsesDiscoveredDaemon(t *testing.T) {
dataDir := newAgentDataDir(t)
var gotQuery url.Values
ts := daemonRouteTestServer(t, map[string]http.HandlerFunc{
"/api/v1/session-stats": func(w http.ResponseWriter, r *http.Request) {
gotQuery = r.URL.Query()
writeJSONResponse(w, `{
"schema_version": 1,
"window": {
"since": "2026-04-01T00:00:00Z",
"until": "2026-04-15T00:00:00Z",
"days": 14
},
"filters": {
"agent": "codex",
"projects_excluded": [],
"timezone": "UTC"
},
"totals": {"sessions_all": 5}
}`)
},
})
registerSyncRouteTestRuntime(t, dataDir, ts.URL)
out, err := executeCommand(newRootCommand(),
"stats",
"--format", "json",
"--since", "2026-04-01",
"--until", "2026-04-15",
"--agent", "codex",
"--timezone", "UTC",
)
require.NoError(t, err, "stats output:\n%s", out)
assert.Equal(t, "2026-04-01", gotQuery.Get("since"))
assert.Equal(t, "2026-04-15", gotQuery.Get("until"))
assert.Equal(t, "codex", gotQuery.Get("agent"))
assert.Equal(t, "UTC", gotQuery.Get("timezone"))
var got db.SessionStats
require.NoError(t, json.Unmarshal([]byte(out), &got))
assert.Equal(t, 5, got.Totals.SessionsAll)
}
func TestStatsCommandReportsDaemonValidationError(t *testing.T) {
dataDir := newAgentDataDir(t)
var called bool
ts := daemonRouteTestServer(t, map[string]http.HandlerFunc{
"/api/v1/session-stats": func(w http.ResponseWriter, r *http.Request) {
called = true
w.WriteHeader(http.StatusBadRequest)
writeJSONResponse(w, `{"error":"invalid timezone: Fake/Zone"}`)
},
})
registerSyncRouteTestRuntime(t, dataDir, ts.URL)
out, err := executeCommand(newRootCommand(),
"stats",
"--format", "json",
"--timezone", "Fake/Zone",
)
require.Error(t, err, "stats output:\n%s", out)
assert.True(t, called, "stats should use the discovered daemon")
assert.Contains(t, err.Error(), "HTTP 400")
assert.Contains(t, err.Error(), "invalid timezone: Fake/Zone")
}
func TestStatsCommandSkipsReadOnlyDaemon(t *testing.T) {
dataDir := setupGoldenStatsDataDir(t)
var called bool
ts := daemonRouteTestServer(t, map[string]http.HandlerFunc{
"/api/v1/session-stats": func(w http.ResponseWriter, r *http.Request) {
called = true
http.Error(w, "pg session stats unavailable", http.StatusNotImplemented)
},
})
registerTestRuntime(t, dataDir, ts.URL, true)
out, err := executeCommand(newRootCommand(),
"stats",
"--format", "json",
"--since", "2026-04-01",
"--until", "2026-04-15",
"--timezone", "UTC",
)
require.NoError(t, err, "stats output:\n%s", out)
assert.False(t, called, "read-only daemon stats endpoint should be skipped")
var got db.SessionStats
require.NoError(t, json.Unmarshal([]byte(out), &got))
assert.Equal(t, 9, got.Totals.SessionsAll)
}
// updateGolden toggles regeneration of stats_golden.json.
// Pass `go test ./cmd/agentsview -run TestStatsGolden -update`
// after intentionally changing the fixture or the stats pipeline.
var updateGolden = flag.Bool(
"update", false,
"rewrite golden files under testdata/ instead of comparing",
)
// TestStatsGolden is the end-to-end guard for the v1 JSON schema: it
// seeds a deterministic fixture DB, runs the full `stats --format
// json` CLI path through the root command, and compares the parsed
// output to testdata/stats_golden.json.
//
// Determinism comes from four levers:
// 1. Absolute --since/--until dates so windowBounds never reads
// time.Now for the window boundary.
// 2. --timezone=UTC so Temporal.ReporterTimezone is a fixed string
// independent of the host's TZ env.
// 3. Session and message timestamps are absolute RFC3339 strings
// inside that window, so temporal.hourly_utc keys are stable.
// 4. GeneratedAt is stripped from both sides before comparison
// because GetSessionStats stamps it from time.Now().
//
// Regenerate after an intentional change with:
//
// go test ./cmd/agentsview -run TestStatsGolden -update
func TestStatsGolden(t *testing.T) {
setupGoldenStatsDataDir(t)
out := runDefaultStatsJSON(t)
var got map[string]any
require.NoError(t, json.Unmarshal([]byte(out), &got),
"unmarshal stats output, output:\n%s", out)
delete(got, "generated_at")
goldenPath := filepath.Join(
"testdata", "stats_golden.json",
)
if *updateGolden {
buf, err := json.MarshalIndent(got, "", " ")
require.NoError(t, err, "marshal golden")
buf = append(buf, '\n')
require.NoError(t, os.MkdirAll(
filepath.Dir(goldenPath), 0o755,
), "mkdir testdata")
require.NoError(t, os.WriteFile(
goldenPath, buf, 0o644,
), "write golden")
t.Logf("rewrote %s (%d bytes)", goldenPath, len(buf))
return
}
raw, err := os.ReadFile(goldenPath)
require.NoError(t, err, "read golden (run with -update to generate)")
var want map[string]any
require.NoError(t, json.Unmarshal(raw, &want), "unmarshal golden")
delete(want, "generated_at")
if !assert.Equal(t, want, got) {
gotBuf, _ := json.MarshalIndent(got, "", " ")
wantBuf, _ := json.MarshalIndent(want, "", " ")
t.Fatalf(
"stats JSON mismatch — regenerate with "+
"`go test ./cmd/agentsview -run "+
"TestStatsGolden -update` if intentional.\n"+
"--- got ---\n%s\n--- want ---\n%s",
gotBuf, wantBuf,
)
}
}
func TestStatsReadOnlyOpenAppliesCustomPricing(t *testing.T) {
dataDir := setupGoldenStatsDataDir(t)
writeCustomModelPricingConfig(t, dataDir)
out := runDefaultStatsJSON(t)
var got db.SessionStats
require.NoError(t, json.Unmarshal([]byte(out), &got))
require.NotNil(t, got.CacheEconomics)
assert.Greater(t, got.CacheEconomics.DollarsSpent, 600.0,
"custom pricing should be applied to the read-only stats DB handle")
}
var (
goldenFixtureTemplateOnce sync.Once
goldenFixtureTemplateFiles map[string][]byte
goldenFixtureTemplateErr error
)
func copyGoldenFixtureDB(t *testing.T, dbPath string) {
t.Helper()
goldenFixtureTemplateOnce.Do(func() {
goldenFixtureTemplateFiles, goldenFixtureTemplateErr =
buildGoldenFixtureTemplateFiles(t)
})
require.NoError(t, goldenFixtureTemplateErr, "build golden fixture template")
require.NoError(t, os.MkdirAll(filepath.Dir(dbPath), 0o755),
"create golden fixture dir")
for _, suffix := range []string{"", "-wal", "-shm"} {
data, ok := goldenFixtureTemplateFiles[suffix]
if !ok {
continue
}
require.NoError(t, os.WriteFile(dbPath+suffix, data, 0o600),
"copy golden fixture db%s", suffix)
}
}
func buildGoldenFixtureTemplateFiles(t *testing.T) (map[string][]byte, error) {
t.Helper()
dir, err := os.MkdirTemp("", "agentsview-golden-stats-*")
if err != nil {
return nil, fmt.Errorf("create golden fixture template dir: %w", err)
}
defer os.RemoveAll(dir)
dbPath := filepath.Join(dir, "sessions.db")
d, err := db.Open(dbPath)
if err != nil {
return nil, fmt.Errorf("open golden fixture template db: %w", err)
}
closed := false
defer func() {
if !closed {
_ = d.Close()
}
}()
seedGoldenFixtureDB(t, d)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := d.CheckpointWALTruncate(ctx); err != nil {
return nil, fmt.Errorf("checkpoint golden fixture template: %w", err)
}
if err := d.Close(); err != nil {
return nil, fmt.Errorf("close golden fixture template: %w", err)
}
closed = true
files := make(map[string][]byte, 3)
for _, suffix := range []string{"", "-wal", "-shm"} {
data, err := os.ReadFile(dbPath + suffix)
if err != nil {
if suffix != "" && errors.Is(err, os.ErrNotExist) {
continue
}
return nil, fmt.Errorf(
"read golden fixture template %s: %w",
dbPath+suffix, err,
)
}
files[suffix] = data
}
return files, nil
}
// seedGoldenFixtureDB seeds a deterministic session set into a fresh
// SQLite database at dbPath. The fixture exercises the full v1 schema:
//
// - Three agents (claude, codex, cursor) so agent_portfolio has variety.
// - User-message counts spanning every archetype bucket so archetypes,
// distributions.user_messages, and primary/primary_human all resolve.
// - Two assistant models (sonnet, opus) so model_mix has >1 row.
// - A handful of peak_context_tokens values so peak_context has a
// non-zero mean and a non-zero null_count.
// - Tool calls across three categories plus the three adoption
// tool_names (ExitPlanMode, Task, Skill) so tool_mix and adoption
// are both populated.
// - A couple of sessions with outcome + health_grade set so
// outcomes.success / failure / grade_distribution are non-trivial.
//
// No cwd is set on any session so outcome_stats stays nil (git
// integration is out of scope for this test). No GH_TOKEN env is
// propagated, so PRsOpened/PRsMerged stay nil regardless.
func seedGoldenFixtureDB(t *testing.T, d *db.DB) {
t.Helper()
require.NoError(t, d.UpsertModelPricing([]db.ModelPricing{
{
ModelPattern: "claude-sonnet-4-20250514",
InputPerMTok: 3.0,
OutputPerMTok: 15.0,
CacheCreationPerMTok: 3.75,
CacheReadPerMTok: 0.30,
},
{
ModelPattern: "claude-opus-4-20250514",
InputPerMTok: 15.0,
OutputPerMTok: 75.0,
CacheCreationPerMTok: 18.75,
CacheReadPerMTok: 1.50,
},
}), "seed pricing")
for _, spec := range goldenFixtureSessions {
seedGoldenSession(t, d, spec)
}
}
// goldenSessionSpec fully describes a fixture session. The slice
// goldenFixtureSessions is the single source of truth for the golden
// file; edit carefully, then regenerate with -update.
type goldenSessionSpec struct {
id string
project string
agent string
model string // empty -> no assistant model/token_usage seeded
startedAt string // RFC3339 UTC
durationMin int // minutes; adds to startedAt to compute ended_at
userMsgs int // user-message rows seeded under this session
peakContext int // peak_context_tokens (0 -> not set)
outcome string // sessions.outcome column
healthGrade string // sessions.health_grade column
toolCategory string // tool_calls.category (empty -> no tool calls)
toolName string // tool_calls.tool_name (empty -> "Read")
toolCount int // number of tool_calls rows to insert
skillName string // populated for Skill tool_calls
retryCount int // sessions.tool_retry_count
editChurn int // sessions.edit_churn_count
compactions int // sessions.compaction_count
}
// goldenFixtureSessions is deliberately small (11 rows) to keep the
// golden JSON under ~5 KB while still covering every section. Session
// IDs are deterministic and grouped by agent for readability.
var goldenFixtureSessions = []goldenSessionSpec{
{
id: "c-auto-01", project: "proj-alpha", agent: "claude",
model: "claude-sonnet-4-20250514",
startedAt: "2026-04-05T10:00:00Z", durationMin: 5,
userMsgs: 1, outcome: "completed", healthGrade: "A",
},
{
id: "c-auto-02", project: "proj-alpha", agent: "claude",
model: "claude-sonnet-4-20250514",
startedAt: "2026-04-05T11:00:00Z", durationMin: 4,
userMsgs: 1, outcome: "completed", healthGrade: "A",
},
{
id: "c-quick-01", project: "proj-alpha", agent: "claude",
model: "claude-sonnet-4-20250514",
startedAt: "2026-04-06T10:00:00Z", durationMin: 15,
userMsgs: 3, peakContext: 20000,
outcome: "completed", healthGrade: "A",
toolCategory: "file", toolName: "Read", toolCount: 4,
},
{
id: "c-quick-02", project: "proj-beta", agent: "claude",
model: "claude-opus-4-20250514",
startedAt: "2026-04-07T10:00:00Z", durationMin: 20,
userMsgs: 3, outcome: "completed", healthGrade: "B",
toolCategory: "shell", toolName: "Bash", toolCount: 3,
},
{
id: "c-std-01", project: "proj-beta", agent: "claude",
model: "claude-sonnet-4-20250514",
startedAt: "2026-04-08T10:00:00Z", durationMin: 45,
userMsgs: 10, peakContext: 55000,
outcome: "completed", healthGrade: "B",
toolCategory: "file", toolName: "Edit", toolCount: 6,
retryCount: 1, editChurn: 2,
},
{
id: "c-deep-01", project: "proj-beta", agent: "claude",
model: "claude-opus-4-20250514",
startedAt: "2026-04-09T10:00:00Z", durationMin: 120,
userMsgs: 30, peakContext: 95000,
outcome: "completed", healthGrade: "A",
toolCategory: "search", toolName: "Grep", toolCount: 8,
retryCount: 2, compactions: 1,
},
{
id: "c-deep-02", project: "proj-gamma", agent: "claude",
model: "claude-opus-4-20250514",
startedAt: "2026-04-10T10:00:00Z", durationMin: 150,
userMsgs: 25, peakContext: 75000,
outcome: "errored", healthGrade: "C",
toolCategory: "other", toolName: "ExitPlanMode", toolCount: 1,
},
{
id: "c-marathon-01", project: "proj-gamma", agent: "claude",
model: "claude-sonnet-4-20250514",
startedAt: "2026-04-11T10:00:00Z", durationMin: 240,
userMsgs: 80, peakContext: 130000,
outcome: "completed", healthGrade: "A",
toolCategory: "other", toolName: "Task", toolCount: 2,
retryCount: 3, compactions: 2, editChurn: 5,
},
{
id: "cx-std-01", project: "proj-alpha", agent: "codex",
startedAt: "2026-04-08T14:00:00Z", durationMin: 30,
userMsgs: 10,
},
{
id: "cu-quick-01", project: "proj-beta", agent: "cursor",
startedAt: "2026-04-09T14:00:00Z", durationMin: 10,
userMsgs: 3,
},
{
id: "c-skill-01", project: "proj-alpha", agent: "claude",
model: "claude-sonnet-4-20250514",
startedAt: "2026-04-12T10:00:00Z", durationMin: 25,
userMsgs: 8, outcome: "completed", healthGrade: "B",
toolCategory: "other", toolName: "Skill", toolCount: 2,
skillName: "summarize",
},
}
// goldenOutputTokens returns the assistant output_tokens for the i-th
// (zero-based) user/assistant turn in a fixture session. seedGoldenSession
// and buildGoldenMessages both call this so the precomputed session total
// and the per-message token_usage never drift apart.
func goldenOutputTokens(i int) int {
return 200 + 30*i
}
// seedGoldenSession persists one goldenSessionSpec: the session row,
// N user messages + N assistant messages at 1-minute spacing, optional
// token_usage on assistant messages, and optional tool_calls rows.
// All timestamps are derived from spec.startedAt so the fixture is
// trivially regenerable.
func seedGoldenSession(
t *testing.T, d *db.DB, spec goldenSessionSpec,
) {
t.Helper()
startedAt := spec.startedAt
endedAt := addMinutes(startedAt, spec.durationMin)
// Pre-compute total_output_tokens by summing the per-assistant
// output_tokens the message builder will stamp. Kept in sync with
// buildGoldenMessages via goldenOutputTokens so agent_portfolio.by_tokens
// has meaningful non-zero values.
totalOutput := 0
if spec.model != "" {
for i := 0; i < spec.userMsgs; i++ {
totalOutput += goldenOutputTokens(i)
}
}
session := db.Session{
ID: spec.id,
Project: spec.project,
Machine: "golden-host",
Agent: spec.agent,
StartedAt: &startedAt,
EndedAt: &endedAt,
UserMessageCount: spec.userMsgs,
// UserMsgs x 2 gives one assistant per user; ensures
// message_count > 0 even for 1-user "automation" rows.
MessageCount: spec.userMsgs * 2,
PeakContextTokens: spec.peakContext,
HasPeakContextTokens: spec.peakContext > 0,
TotalOutputTokens: totalOutput,
HasTotalOutputTokens: totalOutput > 0,
}
require.NoError(t, d.UpsertSession(session), "upsert %s", spec.id)
if spec.outcome != "" || spec.healthGrade != "" ||
spec.retryCount > 0 || spec.editChurn > 0 ||
spec.compactions > 0 {
var grade *string
if spec.healthGrade != "" {
g := spec.healthGrade
grade = &g
}
require.NoError(t, d.UpdateSessionSignals(spec.id, db.SessionSignalUpdate{
Outcome: spec.outcome,
HealthGrade: grade,
ToolRetryCount: spec.retryCount,
EditChurnCount: spec.editChurn,
CompactionCount: spec.compactions,
}), "update signals %s", spec.id)
}
msgs := buildGoldenMessages(spec)
if len(msgs) > 0 {
require.NoError(t, d.InsertMessages(msgs),
"insert messages %s", spec.id)
}
}
// buildGoldenMessages returns interleaved user/assistant messages.
// Assistant messages carry model + token_usage when spec.model is
// non-empty so cache_economics + model_mix pick them up. When the
// spec requests tool calls, they attach to the first N assistant
// messages (round-robin when toolCount exceeds userMsgs) so every
// tool_call resolves to a real message_id via InsertMessages.
func buildGoldenMessages(spec goldenSessionSpec) []db.Message {
out := make([]db.Message, 0, spec.userMsgs*2)
toolsBuilt := 0
toolName := spec.toolName
if toolName == "" && spec.toolCount > 0 {
toolName = "Read"
}
for i := 0; i < spec.userMsgs; i++ {
ts := addMinutes(spec.startedAt, i)
out = append(out, db.Message{
SessionID: spec.id,
Ordinal: i * 2,
Role: "user",
Content: "u",
ContentLength: 1,
Timestamp: ts,
})
// Offset the assistant reply by a fixed 10 seconds so
// velocity.first_response_seconds and turn_cycle_seconds
// have non-zero distributions in the golden output.
asst := db.Message{
SessionID: spec.id,
Ordinal: i*2 + 1,
Role: "assistant",
Content: "a",
ContentLength: 1,
Timestamp: addSeconds(ts, 10),
}
if spec.model != "" {
asst.Model = spec.model
// Stable, small token counts so cache_economics numbers
// are deterministic. Vary per-message by ordinal so
// different sessions accumulate differently.
input := 400 + 50*i
output := goldenOutputTokens(i)
cacheCr := 100 + 20*i
cacheRd := 600 + 100*i
asst.TokenUsage = fmt.Appendf(nil,
`{"input_tokens":%d,"output_tokens":%d,`+
`"cache_creation_input_tokens":%d,`+
`"cache_read_input_tokens":%d}`,
input, output, cacheCr, cacheRd,
)
asst.OutputTokens = output
asst.HasOutputTokens = true
}
out = append(out, asst)
}
// Distribute tool_calls round-robin across the assistant
// messages just appended so each call has a valid host, even
// when toolCount > userMsgs.
for toolsBuilt < spec.toolCount {
asstIdx := 1 + 2*(toolsBuilt%spec.userMsgs) // ordinal of nth asst
for j := range out {
if out[j].Ordinal != asstIdx {
continue
}
out[j].HasToolUse = true
out[j].ToolCalls = append(out[j].ToolCalls, db.ToolCall{
ToolName: toolName,
Category: spec.toolCategory,
SkillName: spec.skillName,
ToolUseID: fmt.Sprintf("%s-tc-%d",
spec.id, toolsBuilt),
})
break
}
toolsBuilt++
}
return out
}
// addMinutes parses an RFC3339 timestamp and returns it + n minutes,
// formatted back to RFC3339 UTC. All fixture timestamps are UTC with
// a "Z" suffix, so time.Parse is exact round-trip.
func addMinutes(ts string, n int) string {
return addDuration(ts, time.Duration(n)*time.Minute)
}
// addSeconds is the second-granularity sibling of addMinutes, used by
// the message builder to offset assistant replies from their user
// prompts so velocity percentiles exercise non-zero values.
func addSeconds(ts string, n int) string {
return addDuration(ts, time.Duration(n)*time.Second)
}
func addDuration(ts string, d time.Duration) string {
parsed, err := time.Parse(time.RFC3339, ts)
if err != nil {
// Only called with literals we control, so this is a
// programmer error; surface it in the test diff instead of
// a hidden panic.
return "INVALID:" + ts
}
return parsed.Add(d).UTC().Format(time.RFC3339)
}