diff --git a/CLAUDE.md b/CLAUDE.md index bfccb564d0..459c8c3460 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,15 +22,16 @@ This repo contains the CLI for Entire. ### Command Layout -The CLI is organized around five noun groups plus a small set of top-level +The CLI is organized around six noun groups plus a small set of top-level verbs. The groups are the canonical home for each verb; legacy top-level shortcuts remain functional but hidden, and emit a deprecation hint pointing at the canonical group form. -- `session` (alias: `sessions`): `list`, `info`, `stop`, `attach`, `resume`, `current` -- `checkpoint` (aliases: `cp`, `checkpoints`): `list`, `explain`, `search`, plus +- `session` (alias: `sessions`): `list`, `info`, `tokens`, `stop`, `attach`, `resume`, `current` +- `checkpoint` (aliases: `cp`, `checkpoints`): `list`, `explain`, `tokens`, `search`, plus the deprecated `rewind` (functional, prints a cobra deprecation message, will be removed in a future release) +- `tokens`: `profile` - `agent`: bare opens the interactive agent selector, plus `list`, `add`, `remove` - `configure`: bare prints help and a hint pointing at `entire agent`; flags manage non-agent settings (telemetry, git-hook installation mode, strategy diff --git a/cmd/entire/cli/root.go b/cmd/entire/cli/root.go index b4c26447b5..7145ae14e9 100644 --- a/cmd/entire/cli/root.go +++ b/cmd/entire/cli/root.go @@ -84,6 +84,7 @@ func NewRootCmd() *cobra.Command { // Noun groups (canonical homes for subcommands). cmd.AddCommand(newSessionsCmd()) // 'session' (with 'sessions' as Cobra alias) cmd.AddCommand(newCheckpointGroupCmd()) // 'checkpoint' / 'cp' / 'checkpoints' + cmd.AddCommand(newTokensGroupCmd()) // 'tokens' cmd.AddCommand(newAgentGroupCmd()) // 'agent' cmd.AddCommand(newAuthCmd()) // 'auth' cmd.AddCommand(newDoctorCmd()) // 'doctor' (group: trace/logs/bundle) diff --git a/cmd/entire/cli/session_tokens.go b/cmd/entire/cli/session_tokens.go index c79e6c7437..0b43bfd63c 100644 --- a/cmd/entire/cli/session_tokens.go +++ b/cmd/entire/cli/session_tokens.go @@ -489,8 +489,12 @@ func writeTokenRecommendations(w io.Writer, recs []sessionTokensRecommendation) } func writeTokenUsageSection(w io.Writer, tokens *sessionTokensUsage) { + writeTokenUsageSectionWithTitle(w, "Token usage", tokens) +} + +func writeTokenUsageSectionWithTitle(w io.Writer, title string, tokens *sessionTokensUsage) { fmt.Fprintln(w) - fmt.Fprintln(w, "Token usage") + fmt.Fprintln(w, title) if tokens != nil { fmt.Fprintf(w, "Total: %s tokens\n", formatTokenCount(tokens.Total)) parts := []string{ diff --git a/cmd/entire/cli/tokens_profile.go b/cmd/entire/cli/tokens_profile.go new file mode 100644 index 0000000000..16813458c0 --- /dev/null +++ b/cmd/entire/cli/tokens_profile.go @@ -0,0 +1,422 @@ +package cli + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + + "github.com/entireio/cli/cmd/entire/cli/agent" + "github.com/entireio/cli/cmd/entire/cli/checkpoint" + "github.com/entireio/cli/cmd/entire/cli/checkpoint/id" + "github.com/spf13/cobra" +) + +type tokensProfileReport struct { + Source string `json:"source"` + UsageScope string `json:"usage_scope"` + CheckpointsAvailable int `json:"checkpoints_available"` + CheckpointsAnalyzed int `json:"checkpoints_analyzed"` + CheckpointsWithTokenData int `json:"checkpoints_with_token_data"` + MissingTokenData int `json:"missing_token_data"` + MetadataReadWarnings int `json:"metadata_read_warnings,omitempty"` + Tokens *sessionTokensUsage `json:"tokens,omitempty"` + Signals []tokensProfileSignal `json:"signals,omitempty"` + Recommendations []sessionTokensRecommendation `json:"recommendations,omitempty"` + Limitations []string `json:"limitations,omitempty"` +} + +type tokensProfileSignal struct { + ID string `json:"id"` + Label string `json:"label"` + Count int `json:"count"` + Percent int `json:"percent"` + CheckpointIDs []string `json:"checkpoint_ids,omitempty"` +} + +type tokensProfileSignalDefinition struct { + id string + label string +} + +var tokensProfileSignalDefinitions = []tokensProfileSignalDefinition{ + {id: "context-replay-hotspot", label: "Cache/context replay hotspot"}, + {id: "api-call-amplification", label: "API call amplification"}, + {id: "subagent-heavy", label: "Subagent-heavy sessions"}, + {id: "missing-token-data", label: "Missing token data"}, +} + +const tokensProfileUsageScopeCheckpointObserved = "checkpoint_observed" + +func newTokensGroupCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "tokens", + Short: "Analyze token usage across sessions and checkpoints", + Long: `Analyze token usage across sessions and checkpoints. + +Commands: + profile Aggregate token usage across committed checkpoints + +Examples: + entire tokens profile + entire tokens profile --json`, + RunE: func(cmd *cobra.Command, _ []string) error { + return cmd.Help() + }, + } + + cmd.AddCommand(newTokensProfileCmd()) + return cmd +} + +func newTokensProfileCmd() *cobra.Command { + var jsonFlag bool + var limitFlag int + var allFlag bool + + cmd := &cobra.Command{ + Use: "profile", + Short: "Aggregate token usage and recommendations across checkpoint history", + Long: `Aggregate token usage and recommendations across committed checkpoint history. + +The profile reads committed checkpoint metadata only. It does not inspect +transcripts or source files, so it is deterministic and avoids adding token +cost while diagnosing token usage. By default it scans the latest 50 committed +checkpoints; use --limit or --all to change the scope.`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + limit := limitFlag + if allFlag { + limit = 0 + } else if limit <= 0 { + return errors.New("--limit must be positive unless --all is used") + } + return runTokensProfile(cmd.Context(), cmd, jsonFlag, limit) + }, + } + + cmd.Flags().BoolVar(&jsonFlag, "json", false, "Output as JSON") + cmd.Flags().IntVar(&limitFlag, "limit", 50, "Maximum committed checkpoints to analyze") + cmd.Flags().BoolVar(&allFlag, "all", false, "Analyze all committed checkpoints") + cmd.MarkFlagsMutuallyExclusive("limit", "all") + return cmd +} + +func runTokensProfile(ctx context.Context, cmd *cobra.Command, jsonOutput bool, limit int) error { + repo, err := openRepository(ctx) + if err != nil { + cmd.SilenceUsage = true + fmt.Fprintln(cmd.ErrOrStderr(), "Not a git repository.") + return NewSilentError(err) + } + defer repo.Close() + + store := checkpoint.NewGitStore(repo, checkpoint.ResolveCommittedRefs(ctx)) + store.SetBlobFetcher(FetchBlobsByHash) + infos, err := store.ListCommitted(ctx) + if err != nil { + return fmt.Errorf("failed to list checkpoints: %w", err) + } + + report, err := buildTokensProfileReport(ctx, store, infos, limit) + if err != nil { + return err + } + + if jsonOutput { + return writeTokensProfileJSON(cmd.OutOrStdout(), report) + } + writeTokensProfileText(cmd.OutOrStdout(), report) + return nil +} + +func buildTokensProfileReport(ctx context.Context, store *checkpoint.GitStore, infos []checkpoint.CommittedInfo, limit int) (tokensProfileReport, error) { + checkpointsAvailable := len(infos) + infos = limitTokensProfileCheckpoints(infos, limit) + report := tokensProfileReport{ + Source: "committed_checkpoints", + UsageScope: tokensProfileUsageScopeCheckpointObserved, + CheckpointsAvailable: checkpointsAvailable, + CheckpointsAnalyzed: len(infos), + } + signals := make(map[string]*tokensProfileSignal, len(tokensProfileSignalDefinitions)) + var aggregate *agent.TokenUsage + + for _, info := range infos { + if err := ctx.Err(); err != nil { + return tokensProfileReport{}, err //nolint:wrapcheck // Propagating context cancellation. + } + + summary, err := store.ReadCommitted(ctx, info.CheckpointID) + if err != nil { + return tokensProfileReport{}, fmt.Errorf("failed to read checkpoint %s: %w", info.CheckpointID, err) + } + if summary == nil { + report.MissingTokenData++ + addTokensProfileSignal(signals, "missing-token-data", info.CheckpointID, report.CheckpointsAnalyzed) + continue + } + + usage, metadataReadWarning, err := tokensProfileCheckpointUsage(ctx, store, info.CheckpointID, summary) + if err != nil { + return tokensProfileReport{}, err + } + if metadataReadWarning { + report.MetadataReadWarnings++ + } + tokens := buildSessionTokensUsage(usage) + if tokens == nil { + report.MissingTokenData++ + addTokensProfileSignal(signals, "missing-token-data", info.CheckpointID, report.CheckpointsAnalyzed) + continue + } + + report.CheckpointsWithTokenData++ + aggregate = addCheckpointTokenUsage(aggregate, usage) + addTokensProfileTokenSignals(signals, info.CheckpointID, tokens, report.CheckpointsAnalyzed) + } + + report.Tokens = buildSessionTokensUsage(aggregate) + report.Signals = orderedTokensProfileSignals(signals) + report.Recommendations = tokensProfileRecommendations(report) + report.Limitations = tokensProfileLimitations(report) + return report, nil +} + +func limitTokensProfileCheckpoints(infos []checkpoint.CommittedInfo, limit int) []checkpoint.CommittedInfo { + if limit <= 0 || len(infos) <= limit { + return infos + } + return infos[:limit] +} + +func tokensProfileCheckpointUsage(ctx context.Context, store *checkpoint.GitStore, checkpointID id.CheckpointID, summary *checkpoint.CheckpointSummary) (*agent.TokenUsage, bool, error) { + if summary == nil { + return nil, false, nil + } + + metas := make([]*checkpoint.CommittedMetadata, 0, len(summary.Sessions)) + metadataReadWarning := false + for i := range len(summary.Sessions) { + meta, err := store.ReadSessionMetadata(ctx, checkpointID, i) + if err != nil { + if ctxErr := ctx.Err(); ctxErr != nil { + return nil, false, ctxErr //nolint:wrapcheck // Propagating context cancellation. + } + metadataReadWarning = true + continue + } + metas = append(metas, meta) + } + sessionUsage := aggregateCheckpointTokenUsage(metas) + if !metadataReadWarning && sessionUsage != nil { + return sessionUsage, false, nil + } + if summary.TokenUsage != nil { + return summary.TokenUsage, metadataReadWarning, nil + } + return sessionUsage, metadataReadWarning, nil +} + +func addTokensProfileTokenSignals(signals map[string]*tokensProfileSignal, checkpointID id.CheckpointID, tokens *sessionTokensUsage, denominator int) { + if tokens == nil { + return + } + if tokens.Total > 0 && tokenPercent(tokens.CacheRead, tokens.Total) >= 80 { + addTokensProfileSignal(signals, "context-replay-hotspot", checkpointID, denominator) + } + if tokens.APICalls >= 20 { + addTokensProfileSignal(signals, "api-call-amplification", checkpointID, denominator) + } + if tokens.Total > 0 && tokens.SubagentTotal*100 >= tokens.Total*10 { + addTokensProfileSignal(signals, "subagent-heavy", checkpointID, denominator) + } +} + +func addTokensProfileSignal(signals map[string]*tokensProfileSignal, signalID string, checkpointID id.CheckpointID, denominator int) { + signal := signals[signalID] + if signal == nil { + definition := tokensProfileSignalDefinitionFor(signalID) + signal = &tokensProfileSignal{ + ID: definition.id, + Label: definition.label, + } + signals[signalID] = signal + } + signal.Count++ + if denominator > 0 { + signal.Percent = roundedPercent(signal.Count, denominator) + } + if checkpointID != "" { + signal.CheckpointIDs = append(signal.CheckpointIDs, checkpointID.String()) + } +} + +func tokensProfileSignalDefinitionFor(signalID string) tokensProfileSignalDefinition { + for _, definition := range tokensProfileSignalDefinitions { + if definition.id == signalID { + return definition + } + } + return tokensProfileSignalDefinition{id: signalID, label: signalID} +} + +func orderedTokensProfileSignals(signals map[string]*tokensProfileSignal) []tokensProfileSignal { + ordered := make([]tokensProfileSignal, 0, len(signals)) + for _, definition := range tokensProfileSignalDefinitions { + if signal := signals[definition.id]; signal != nil { + ordered = append(ordered, *signal) + } + } + return ordered +} + +func tokensProfileRecommendations(report tokensProfileReport) []sessionTokensRecommendation { + var recs []sessionTokensRecommendation + + if report.CheckpointsAnalyzed == 0 { + return []sessionTokensRecommendation{{ + ID: "no-checkpoints", + Severity: "low", + Message: "Create checkpoints first; token profiling needs committed checkpoint metadata to identify patterns.", + Signals: []string{"empty_checkpoint_history"}, + }} + } + + if tokensProfileSignalCount(report.Signals, "context-replay-hotspot") > 0 || + tokensProfileSignalCount(report.Signals, "api-call-amplification") > 0 { + recs = append(recs, sessionTokensRecommendation{ + ID: "search-before-reinvestigation", + Severity: "high", + Message: "Use `entire search` for prior decisions/checkpoints before broad re-investigation.", + Signals: []string{"cache_read_tokens", "api_call_count"}, + }) + } + if tokensProfileSignalCount(report.Signals, "api-call-amplification") > 0 { + recs = append(recs, sessionTokensRecommendation{ + ID: "batch-diagnostics", + Severity: "medium", + Message: "Batch diagnostic reads around one narrowed hypothesis when API call amplification repeats.", + Signals: []string{"api_call_count"}, + }) + } + if tokensProfileSignalCount(report.Signals, "context-replay-hotspot") > 0 { + recs = append(recs, sessionTokensRecommendation{ + ID: "preserve-then-compact", + Severity: "medium", + Message: "Summarize useful findings before continuing large-context work; compact or restart only after preserving relevant context.", + Signals: []string{"cache_read_tokens"}, + }) + } + if tokensProfileSignalCount(report.Signals, "subagent-heavy") > 0 { + recs = append(recs, sessionTokensRecommendation{ + ID: "scope-subagents", + Severity: "medium", + Message: "Scope subagent tasks tightly with a narrow objective and expected output.", + Signals: []string{"subagent_tokens"}, + }) + } + if report.MissingTokenData > 0 { + recs = append(recs, sessionTokensRecommendation{ + ID: "improve-token-coverage", + Severity: "low", + Message: "Increase token coverage by using agents and checkpoints that report token usage.", + Signals: []string{"missing_token_usage"}, + }) + } + + if len(recs) == 0 { + recs = append(recs, sessionTokensRecommendation{ + ID: "no-repeated-hotspots", + Severity: "low", + Message: "No repeated token hotspots were visible in committed checkpoint metadata.", + Signals: []string{"checkpoint_token_metadata"}, + }) + } + return recs +} + +func tokensProfileSignalCount(signals []tokensProfileSignal, signalID string) int { + for _, signal := range signals { + if signal.ID == signalID { + return signal.Count + } + } + return 0 +} + +func tokensProfileLimitations(report tokensProfileReport) []string { + var limitations []string + if report.CheckpointsAvailable > report.CheckpointsAnalyzed { + limitations = append(limitations, fmt.Sprintf("Limited to latest %d of %d committed checkpoints; use --limit or --all to change scope.", report.CheckpointsAnalyzed, report.CheckpointsAvailable)) + } + if report.CheckpointsAnalyzed == 0 { + limitations = append(limitations, "No committed checkpoints found.") + } + if report.MissingTokenData > 0 { + limitations = append(limitations, fmt.Sprintf("%d checkpoint%s did not include token usage.", report.MissingTokenData, pluralSuffix(report.MissingTokenData))) + } + if report.MetadataReadWarnings > 0 { + limitations = append(limitations, fmt.Sprintf("%d checkpoint%s had incomplete session metadata; profile used root token summaries or readable sessions where available.", report.MetadataReadWarnings, pluralSuffix(report.MetadataReadWarnings))) + } + if report.Tokens != nil { + limitations = append(limitations, "Token totals are summed from analyzed checkpoints and may include overlapping checkpoint history; treat them as checkpoint-observed volume, not guaranteed unique session spend.") + } + if report.CheckpointsAnalyzed > 0 { + limitations = append(limitations, "Tool-level search/read spend is not captured yet; this profile infers patterns from token totals, cache/context replay, API call counts, and subagent totals.") + } + return limitations +} + +func writeTokensProfileJSON(w io.Writer, report tokensProfileReport) error { + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + if err := enc.Encode(report); err != nil { + return fmt.Errorf("failed to encode token profile report: %w", err) + } + return nil +} + +func writeTokensProfileText(w io.Writer, report tokensProfileReport) { + fmt.Fprintln(w, "Token profile") + fmt.Fprintln(w) + fmt.Fprintf(w, "Source: %s\n", report.Source) + fmt.Fprintf(w, "Checkpoints available: %d\n", report.CheckpointsAvailable) + fmt.Fprintf(w, "Checkpoints analyzed: %d\n", report.CheckpointsAnalyzed) + fmt.Fprintf(w, "With token data: %d\n", report.CheckpointsWithTokenData) + fmt.Fprintf(w, "Missing token data: %d\n", report.MissingTokenData) + if report.MetadataReadWarnings > 0 { + fmt.Fprintf(w, "Metadata warnings: %d\n", report.MetadataReadWarnings) + } + + writeTokenUsageSectionWithTitle(w, "Checkpoint-observed token usage", report.Tokens) + writeTokensProfileSignals(w, report.Signals) + if len(report.Recommendations) > 0 { + writeTokenRecommendations(w, report.Recommendations) + } + writeTokenLimitations(w, report.Limitations) +} + +func writeTokensProfileSignals(w io.Writer, signals []tokensProfileSignal) { + if len(signals) == 0 { + return + } + + fmt.Fprintln(w) + fmt.Fprintln(w, "Repeated signals") + for _, signal := range signals { + fmt.Fprintf(w, "- %s: %d checkpoint%s", signal.Label, signal.Count, pluralSuffix(signal.Count)) + if signal.Percent > 0 { + fmt.Fprintf(w, " (%d%%)", signal.Percent) + } + fmt.Fprintln(w) + } +} + +func pluralSuffix(count int) string { + if count == 1 { + return "" + } + return "s" +} diff --git a/cmd/entire/cli/tokens_profile_test.go b/cmd/entire/cli/tokens_profile_test.go new file mode 100644 index 0000000000..83d29e45e6 --- /dev/null +++ b/cmd/entire/cli/tokens_profile_test.go @@ -0,0 +1,290 @@ +package cli + +import ( + "bytes" + "context" + "encoding/json" + "strings" + "testing" + + "github.com/entireio/cli/cmd/entire/cli/agent" + "github.com/entireio/cli/cmd/entire/cli/checkpoint" + "github.com/entireio/cli/cmd/entire/cli/checkpoint/id" + "github.com/entireio/cli/cmd/entire/cli/strategy" + "github.com/entireio/cli/redact" +) + +func TestTokensProfileCmd_TextOutputAggregatesCommittedCheckpoints(t *testing.T) { + repo, _ := runExplainAutoTestRepo(t) + ctx := context.Background() + store := checkpoint.NewGitStore(repo, checkpoint.DefaultV1Refs()) + + writeProfileTokenCheckpoint(ctx, t, store, "100aaa000001", "profile-cache-hotspot", &agent.TokenUsage{ + InputTokens: 100, + CacheCreationTokens: 100, + CacheReadTokens: 800, + APICallCount: 5, + }) + writeProfileTokenCheckpoint(ctx, t, store, "100aaa000002", "profile-api-heavy", &agent.TokenUsage{ + InputTokens: 400, + OutputTokens: 100, + APICallCount: 25, + }) + writeProfileTokenCheckpoint(ctx, t, store, "100aaa000003", "profile-subagent-heavy", &agent.TokenUsage{ + InputTokens: 500, + OutputTokens: 500, + APICallCount: 3, + SubagentTokens: &agent.TokenUsage{ + InputTokens: 1_000, + }, + }) + writeProfileTokenCheckpoint(ctx, t, store, "100aaa000004", "profile-missing", nil) + + cmd := newTokensGroupCmd() + var stdout bytes.Buffer + cmd.SetOut(&stdout) + cmd.SetArgs([]string{"profile"}) + + if err := cmd.ExecuteContext(ctx); err != nil { + t.Fatalf("expected no error, got: %v", err) + } + + out := stdout.String() + checks := []string{ + "Token profile", + "Checkpoints analyzed: 4", + "With token data: 3", + "Missing token data: 1", + "Checkpoint-observed token usage", + "Total: 3.5k tokens", + "Cache read: 800", + "API calls: 33", + "Repeated signals", + "Cache/context replay hotspot: 1 checkpoint", + "API call amplification: 1 checkpoint", + "Subagent-heavy sessions: 1 checkpoint", + "Missing token data: 1 checkpoint", + "Recommendations", + "Use `entire search` for prior decisions/checkpoints before broad re-investigation.", + "Token totals are summed from analyzed checkpoints and may include overlapping checkpoint history", + "Tool-level search/read spend is not captured yet", + } + for _, check := range checks { + if !strings.Contains(out, check) { + t.Errorf("expected %q in output, got:\n%s", check, out) + } + } + + tokenUsageIndex := strings.Index(out, "Checkpoint-observed token usage") + recommendationsIndex := strings.Index(out, "Recommendations") + if tokenUsageIndex == -1 || recommendationsIndex == -1 { + t.Fatalf("expected token usage and recommendations sections, got:\n%s", out) + } + if tokenUsageIndex > recommendationsIndex { + t.Fatalf("expected token usage before recommendations, got:\n%s", out) + } +} + +func TestTokensProfileCmd_JSONOutput(t *testing.T) { + repo, _ := runExplainAutoTestRepo(t) + ctx := context.Background() + store := checkpoint.NewGitStore(repo, checkpoint.DefaultV1Refs()) + + writeProfileTokenCheckpoint(ctx, t, store, "200bbb000001", "profile-json-cache", &agent.TokenUsage{ + InputTokens: 100, + CacheReadTokens: 900, + APICallCount: 2, + }) + writeProfileTokenCheckpoint(ctx, t, store, "200bbb000002", "profile-json-api", &agent.TokenUsage{ + InputTokens: 200, + OutputTokens: 100, + APICallCount: 22, + }) + + cmd := newTokensGroupCmd() + var stdout bytes.Buffer + cmd.SetOut(&stdout) + cmd.SetArgs([]string{"profile", "--json"}) + + if err := cmd.ExecuteContext(ctx); err != nil { + t.Fatalf("expected no error, got: %v", err) + } + + var result tokensProfileReport + if err := json.Unmarshal(stdout.Bytes(), &result); err != nil { + t.Fatalf("expected valid JSON, got parse error: %v\noutput: %s", err, stdout.String()) + } + var raw map[string]interface{} + if err := json.Unmarshal(stdout.Bytes(), &raw); err != nil { + t.Fatalf("expected valid JSON object, got parse error: %v\noutput: %s", err, stdout.String()) + } + if raw["usage_scope"] != "checkpoint_observed" { + t.Fatalf("usage_scope = %v, want checkpoint_observed", raw["usage_scope"]) + } + if result.CheckpointsAnalyzed != 2 { + t.Fatalf("checkpoints_analyzed = %d, want 2", result.CheckpointsAnalyzed) + } + if result.CheckpointsWithTokenData != 2 { + t.Fatalf("checkpoints_with_token_data = %d, want 2", result.CheckpointsWithTokenData) + } + if result.Tokens == nil || result.Tokens.Total != 1300 { + t.Fatalf("unexpected token total: %+v", result.Tokens) + } + if got := signalCount(result.Signals, "context-replay-hotspot"); got != 1 { + t.Fatalf("context-replay-hotspot signal count = %d, want 1", got) + } + if got := signalCount(result.Signals, "api-call-amplification"); got != 1 { + t.Fatalf("api-call-amplification signal count = %d, want 1", got) + } + if len(result.Recommendations) == 0 { + t.Fatalf("expected recommendations, got none") + } +} + +func TestTokensProfileCmd_JSONOutputReportsAPICallOnlyCheckpoints(t *testing.T) { + repo, _ := runExplainAutoTestRepo(t) + ctx := context.Background() + store := checkpoint.NewGitStore(repo, checkpoint.DefaultV1Refs()) + + writeProfileTokenCheckpoint(ctx, t, store, "250bbb000001", "profile-json-api-only", &agent.TokenUsage{ + APICallCount: 25, + }) + + cmd := newTokensGroupCmd() + var stdout bytes.Buffer + cmd.SetOut(&stdout) + cmd.SetArgs([]string{"profile", "--json"}) + + if err := cmd.ExecuteContext(ctx); err != nil { + t.Fatalf("expected no error, got: %v", err) + } + + var result tokensProfileReport + if err := json.Unmarshal(stdout.Bytes(), &result); err != nil { + t.Fatalf("expected valid JSON, got parse error: %v\noutput: %s", err, stdout.String()) + } + if result.CheckpointsWithTokenData != 1 { + t.Fatalf("checkpoints_with_token_data = %d, want 1", result.CheckpointsWithTokenData) + } + if result.MissingTokenData != 0 { + t.Fatalf("missing_token_data = %d, want 0", result.MissingTokenData) + } + if result.Tokens == nil || result.Tokens.Total != 0 || result.Tokens.APICalls != 25 { + t.Fatalf("unexpected token usage: %+v", result.Tokens) + } + if got := signalCount(result.Signals, "api-call-amplification"); got != 1 { + t.Fatalf("api-call-amplification signal count = %d, want 1", got) + } +} + +func TestTokensProfileCmd_LimitScopesAnalyzedCheckpoints(t *testing.T) { + repo, _ := runExplainAutoTestRepo(t) + ctx := context.Background() + store := checkpoint.NewGitStore(repo, checkpoint.DefaultV1Refs()) + + writeProfileTokenCheckpoint(ctx, t, store, "300ccc000001", "profile-limit-one", &agent.TokenUsage{ + InputTokens: 100, + OutputTokens: 100, + APICallCount: 1, + }) + writeProfileTokenCheckpoint(ctx, t, store, "300ccc000002", "profile-limit-two", &agent.TokenUsage{ + InputTokens: 100, + OutputTokens: 100, + APICallCount: 1, + }) + writeProfileTokenCheckpoint(ctx, t, store, "300ccc000003", "profile-limit-three", &agent.TokenUsage{ + InputTokens: 100, + OutputTokens: 100, + APICallCount: 1, + }) + + cmd := newTokensGroupCmd() + var stdout bytes.Buffer + cmd.SetOut(&stdout) + cmd.SetArgs([]string{"profile", "--limit", "2"}) + + if err := cmd.ExecuteContext(ctx); err != nil { + t.Fatalf("expected no error, got: %v", err) + } + + out := stdout.String() + checks := []string{ + "Checkpoints available: 3", + "Checkpoints analyzed: 2", + "Total: 400 tokens", + "Limited to latest 2 of 3 committed checkpoints", + } + for _, check := range checks { + if !strings.Contains(out, check) { + t.Errorf("expected %q in output, got:\n%s", check, out) + } + } +} + +func TestTokensProfileCmd_LimitAndAllAreMutuallyExclusive(t *testing.T) { + runExplainAutoTestRepo(t) + + cmd := newTokensGroupCmd() + cmd.SetArgs([]string{"profile", "--limit", "2", "--all"}) + + err := cmd.ExecuteContext(context.Background()) + if err == nil { + t.Fatal("expected error for --limit with --all") + } + if !strings.Contains(err.Error(), "limit") || !strings.Contains(err.Error(), "all") { + t.Fatalf("expected error to mention limit and all, got: %v", err) + } +} + +func TestTokensProfileCmd_EmptyHistory(t *testing.T) { + runExplainAutoTestRepo(t) + + cmd := newTokensGroupCmd() + var stdout bytes.Buffer + cmd.SetOut(&stdout) + cmd.SetArgs([]string{"profile"}) + + if err := cmd.ExecuteContext(context.Background()); err != nil { + t.Fatalf("expected no error, got: %v", err) + } + + out := stdout.String() + checks := []string{ + "Token profile", + "Checkpoints analyzed: 0", + "Token data: unavailable", + "No committed checkpoints found.", + } + for _, check := range checks { + if !strings.Contains(out, check) { + t.Errorf("expected %q in output, got:\n%s", check, out) + } + } +} + +func signalCount(signals []tokensProfileSignal, id string) int { + for _, signal := range signals { + if signal.ID == id { + return signal.Count + } + } + return 0 +} + +func writeProfileTokenCheckpoint(ctx context.Context, t *testing.T, store *checkpoint.GitStore, checkpointID string, sessionID string, usage *agent.TokenUsage) { + t.Helper() + + if err := store.WriteCommitted(ctx, checkpoint.WriteCommittedOptions{ + CheckpointID: id.MustCheckpointID(checkpointID), + SessionID: sessionID, + Strategy: strategy.StrategyNameManualCommit, + Branch: "tokens-profile", + Agent: testAgentClaude, + Transcript: redact.AlreadyRedacted([]byte(`{"type":"user","message":{"content":[{"type":"text","text":"profile"}]}}` + "\n")), + AuthorName: "Test", + AuthorEmail: "test@example.com", + TokenUsage: usage, + }); err != nil { + t.Fatalf("WriteCommitted(%s) error = %v", checkpointID, err) + } +}