Skip to content

Make it impossible for a test to spend inference tokens - #79

Merged
trevhud merged 1 commit into
mainfrom
chore/test-eval-taxonomy
Jul 30, 2026
Merged

Make it impossible for a test to spend inference tokens#79
trevhud merged 1 commit into
mainfrom
chore/test-eval-taxonomy

Conversation

@trevhud

@trevhud trevhud commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Adopts the dividing line as a mechanism rather than a convention:

  • test — needs no inference. Automated, fast, every push.
  • eval — costs tokens. Deliberate, periodic, never in CI.

The suite already complied — by convention

The live e2e suites use fake keys and point ROTE_BASE_URL_<NODE_ID> at a local Messages stub. But complying and being unable to do otherwise are different things. Two DBOS e2e suites previously burned real subscription inference on every run, and that was caught by wall-clock timing, not by any assertion — because the failure mode of this bug is silent spending.

Two leaks, both closed in conftest

  1. Credentials in the environment. A developer with real keys exported would have any SDK built without an explicit key bill them. All vendor credential vars are now scrubbed per-test.
  2. A subscription CLI on PATH. The one with nothing to scrub: claude -p authenticates from an OAuth session, so having no API key is not protection — not finding the binary is. Bare-name shutil.which("claude" | "codex") now returns None.

Verified adversarially

finds claude sees credentials
with guard no no
without guard yes yes

There is a claude binary on PATH on this machine, so this closes a live hole rather than a theoretical one.

Deliberately narrow

An over-broad guard gets disabled, so it isn't one:

  • Only bare-name lookups are blocked. Several driver tests write a fake claude stub and pass its absolute path as executable= — that still resolves. My first attempt blocked those too and broke 9 tests.
  • Every other which delegates, so the e2e suites' node/npm/docker probes are untouched. Blocking those would trade a spending bug for a silent-skip coverage bug — the exact trade this repo keeps having to undo.

tests/test_no_inference_in_tests.py verifies the guard itself, including that an opt-in test can patch the lookup back. The guard is a default, not a cage.

Docs

CLAUDE.md gains a Tests vs. evals section with the rule, what enforces it, and the two consequences that are easy to get wrong:

  • An e2e test is still a test — real npm, wrangler, Postgres, no inference → automated.
  • @pytest.mark.slow says what toolchain a suite needs, never whether it should run.

1175 fast + 27 slow pass; ruff, strict mypy, sanity-check clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SBuSDL7Svm7iDzgmhTpDSW

@trevhud trevhud closed this Jul 30, 2026
@trevhud
trevhud deleted the chore/test-eval-taxonomy branch July 30, 2026 23:49
@trevhud
trevhud restored the chore/test-eval-taxonomy branch July 30, 2026 23:50
@trevhud trevhud reopened this Jul 30, 2026
Adopts the project's dividing line as a mechanism rather than a
convention: anything that does not require inference is a TEST
(automated, fast, every push); anything that costs tokens is an EVAL
(deliberate, periodic, never in CI).

The suite already complied — the live e2e suites use fake keys and point
ROTE_BASE_URL_<NODE_ID> at a local Messages stub. It complied by
convention, which is not the same as being unable to do otherwise. Two
DBOS e2e suites previously burned real subscription inference on every
run, and that was caught by wall-clock timing rather than by any
assertion, because the failure mode of this bug is silent spending.

Two independent leaks, both closed in conftest:

1. **Credentials in the environment.** A developer running the suite
   with real keys exported would have any SDK constructed without an
   explicit key bill them. All vendor credential vars are now scrubbed
   per-test.
2. **A subscription CLI on PATH.** This is the one with nothing to
   scrub: `claude -p` authenticates from an OAuth session, so having no
   API key is not protection — not finding the binary is. Bare-name
   `shutil.which("claude" | "codex")` now returns None.

Verified adversarially rather than by inspection. With the guard, a
probe test cannot find the CLI and sees no credentials; without it, both
succeed — there IS a claude binary on PATH on this machine, so this
closes a live hole, not a theoretical one.

Deliberately narrow in two directions, because an over-broad guard gets
disabled:

- Only BARE-NAME lookups are blocked. Several driver tests write a fake
  `claude` script under tmp_path and pass its absolute path as
  `executable=`; that is a stub, not a subscription, and it still
  resolves. The first attempt blocked those too and broke 9 tests.
- Every other `which` delegates, so the e2e suites' node/npm/docker
  probes are untouched — blocking those would trade a spending bug for a
  silent-skip coverage bug, which is the trade this project keeps having
  to undo.

`tests/test_no_inference_in_tests.py` verifies the guard itself,
including that an opt-in test can still patch the lookup back: the guard
is a default, not a cage.

CLAUDE.md gains a "Tests vs. evals" section stating the rule, what
enforces it, and the two consequences that are easy to get wrong — an
e2e test is still a test (real npm, wrangler, Postgres, no inference →
automated), and `@pytest.mark.slow` says what toolchain a suite needs,
never whether it should run.

1175 fast + 27 slow pass; ruff, strict mypy, sanity-check clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBuSDL7Svm7iDzgmhTpDSW
@trevhud
trevhud force-pushed the chore/test-eval-taxonomy branch from de8eacd to 92e7cd6 Compare July 30, 2026 23:50
@trevhud
trevhud merged commit 57f331d into main Jul 30, 2026
7 checks passed
@trevhud
trevhud deleted the chore/test-eval-taxonomy branch July 30, 2026 23:54
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