Skip to content

Add test bench for speed + input-token cost#2

Merged
christopherarter merged 21 commits into
mainfrom
p1-review-epic
Apr 17, 2026
Merged

Add test bench for speed + input-token cost#2
christopherarter merged 21 commits into
mainfrom
p1-review-epic

Conversation

@christopherarter

Copy link
Copy Markdown
Contributor

Summary

Adds a two-mode local bench for watching bully's own speed and input-token cost over time. Runs entirely on the user's machine; no CI integration.

Mode A — fixture suite (bully bench): runs 8 hand-authored fixtures under bench/fixtures/ through run_pipeline, records per-phase timings + cold-start subprocess sample + Anthropic-tokenized payload sizes, appends one JSONL record to bench/history.jsonl per run. --compare diffs the last two runs.

Mode B — config cost analysis (bully bench --config <path>): given any .bully.yml, reports floor tokens per dispatch, per-rule marginal cost (sorted), diff scaling at 1/10/100/1000 added lines, and a scope-grouped breakdown. No history append.

Pipeline stays stdlib-only. anthropic is an optional dep (pip install -e ".[bench]"); without it, both modes fall back to a len(json.dumps(payload)) proxy and tag output method: proxy. The bench never dispatches real model calls — only messages/count_tokens, which is free.

What's in this PR

  • pipeline/pipeline.py: optional phase_timer keyword arg on run_pipeline (zero-cost no-op by default, wraps 7 named phases); subcommand dispatch for bully bench at top of main()
  • pipeline/bench.py (new, ~650 LoC): CLI, fixture loader, token counter (real + proxy fallback), PhaseTimer, run_fixture, run_mode_a, run_mode_b, run_compare
  • bench/fixtures/01-08/: 8 paired-file fixtures covering script/ast/semantic/mixed engines, extends chain, 20-rule payload, 500-line diff, auto-generated-skip short-circuit
  • pipeline/tests/test_bench.py: 24 tests
  • pyproject.toml: new [bench] optional dependencies group
  • README.md: "Test Bench" section
  • docs/superpowers/specs/2026-04-17-test-bench-design.md + docs/superpowers/plans/2026-04-17-test-bench.md: spec and plan

Test Plan

  • pytest pipeline/tests -q — 267/267 pass
  • ruff check . + ruff format --check . — clean
  • python3 -m pipeline.pipeline bench --no-tokens runs all 8 fixtures and appends a line to bench/history.jsonl
  • python3 -m pipeline.pipeline bench --config .bully.yml --no-tokens prints a per-rule cost table for the repo's own config
  • python3 -m pipeline.pipeline bench --compare diffs the last two history entries
  • Smoke test with ANTHROPIC_API_KEY set + pip install -e ".[bench]": confirm method: count_tokens in the output (requires real key; defer to local verification)

🤖 Generated with Claude Code

Chris Arter and others added 21 commits April 17, 2026 12:45
Two-mode bench for watching bully's speed and input-token cost over time:
fixture suite for regression trends (history.jsonl) and per-config cost
analysis for users evaluating rule packs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ten-task TDD plan implementing the test bench spec: phase-timer hook,
fixture loader, token counter, single-fixture runner, Mode A
(history), --compare, Mode B (config cost), 8 fixtures, and README.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bench harness needs to measure per-phase timing without duplicating the
pipeline loop. Default is a zero-cost no-op so normal hook invocations
are unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… test

Ruff UP037 flagged the self-referential return-type quotes on
_NoopPhaseTimer (the file doesn't use `from __future__ import annotations`,
so the annotations aren't actually needed on a private no-op class).
Also tighten the phase-timer test to assert the exact set of 7 phase
names, so a future refactor that drops one will fail the test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`bully bench` now dispatches to pipeline.bench. Flags parsed but not yet
implemented -- subsequent tasks fill in fixture loading, token counting,
and both modes. Also adds module-level __getattr__ to pipeline.py so
`from pipeline import bench` works in the flat-module test context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 2's test attempted `from pipeline import bench`, which required a
module-level __getattr__ in pipeline.py to work with the test's sys.path
setup. That was the wrong fix -- the existing pattern for sibling-module
imports (see test_analyzer.py) is `import bench`, treating bench.py as a
top-level module once pipeline/ is on sys.path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixtures live in bench/fixtures/<name>/{config.yml,fixture.json}.
load_fixture validates both files exist and the metadata has required
fields; discover_fixtures returns all subdirectories sorted by name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
count_tokens uses messages/count_tokens when an API key and the SDK are
available; otherwise returns len(json.dumps(payload)) + len(system) and
tags the result 'proxy'. System prompt is loaded from the real
bully-evaluator agent file so counts match production.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PhaseTimer implements the pipeline hook protocol and records per-phase
elapsed time. run_fixture runs one fixture N times in-process, samples
cold-start once via subprocess, and builds the real semantic payload to
count tokens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously, run_fixture called count_tokens even on fixtures with no
semantic rules -- producing a misleading non-zero token count for a
dispatch that would never happen. Fix to report 0 tokens with method
"n/a-no-semantic-rules" so history entries accurately reflect what real
bully invocations would send.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
\`bully bench\` now enumerates bench/fixtures/*, runs each fixture, and
appends one JSONL line to bench/history.jsonl with per-fixture phase
timings + aggregate totals. Human-readable summary to stdout by
default; --json emits the raw record.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-fixture + aggregate deltas for wall time and input tokens. Fails
clearly when history has fewer than two runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
\`bully bench --config <path>\` parses a .bully.yml and reports floor
tokens per dispatch, per-rule marginal cost (sorted), diff scaling at
1/10/100/1000 added lines, and a scope-grouped breakdown. Script and
ast rules are listed separately as zero model-token cost.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixtures exercise: single-engine smoke (01-03), mixed-engine path (04),
multi-level extends chain (05), payload size scaling (06, 07), and the
auto-generated skip short-circuit (08). Integration test runs all of
them against run_mode_a.

Also fix bench.py import to work in both package context (pipeline.bench)
and direct sys.path context (used by pytest), and fix pre-existing ruff
isort violations in test_bench.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers Mode A (fixture suite + history.jsonl), Mode B (config cost
analysis), and the ANTHROPIC_API_KEY / optional anthropic SDK
requirement for real token counts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously run_fixture set BULLY_TRUST_ALL=1 permanently. Fine for the
CLI path (process exits) but leaks state if bench is consumed as a
library. Save the prior value and restore in a finally block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously returned {returncode, report}; the dict wrapper was unique to
Mode B. Align with the simpler "None = failure, dict = success" shape
used elsewhere.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously `--config ... --compare` silently dropped --compare (config
branch won the if/elif). Use argparse's mutually_exclusive_group so
users get an explicit error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixture targets (.py files under bench/fixtures/*) intentionally model
user code with unused vars, incomplete functions, and style violations
-- they are data, not source. Exclude them from ruff so CI stops
flagging them as real violations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bully can catch style violations at edit time before CI does. The
--force-exclude flag makes ruff respect pyproject's extend-exclude list
even when a single file is passed, so bench fixtures are skipped.

After pulling, run `bully trust --refresh` to re-approve the modified
config (trust gate hashes the .bully.yml contents).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI's `ruff format --check` step flagged formatting differences on both
bench files. Apply the formatter output directly; no semantic changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@christopherarter
christopherarter merged commit d7fa295 into main Apr 17, 2026
2 checks passed
@christopherarter
christopherarter deleted the p1-review-epic branch April 17, 2026 21:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant