Skip to content

fix(engine): keep the token ledger out of test suites and out of the model CLI's environment - #2311

Merged
don-petry merged 4 commits into
mainfrom
claude/issue-2304-ledger-isolation
Oct 12, 2026
Merged

don-petry merged 4 commits into
mainfrom
claude/issue-2304-ledger-isolation

Conversation

@don-petry

@don-petry don-petry commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The org-wide Gemini usage ledger is almost entirely test fixture data: about 8 of 1,819 request-counting Gemini records over four days were real calls. The cause is not a test job. The dispatch job of dev-lead-reusable.yml sets TOKEN_LOG_FILE for the whole job; the agent session inherits it; when the agent runs this repository's tests as part of its work, suites that do not isolate the variable append their fixture calls, rejections and cooldowns to the run's real ledger; the run then uploads it.

Measured for this change, by running every bats file on its own with the variable exported: 16 of 247 files wrote 221 records (the largest, test_fix_reviews, 122). That is more than the seven suites the issue names, and includes three files outside tests/dev-lead/unit/ and two that mention the variable.

Design

Two independent halves; either alone stops the leak, and they fail in different ways.

1. Tests never see an inherited ledger. setup_suite() in tests/dev-lead/unit/setup_suite.bash unsets TOKEN_LOG_FILE, GEMINI_LEDGER_FILE (the Gemini library's override for the same file) and _ENGINE_USAGE_OUT (a per-call usage sidecar). A test that needs a ledger sets its own.

  • bats runs setup_suite in the shell that starts every test file, so an unset there reaches the tests (verified with a probe suite on bats 1.13).
  • bats loads one setup_suite.bash per run, the one beside the first path it is given, and never looks in subdirectories. So tests/ and tests/dev-lead/integration/ each gain a one-line setup_suite.bash that sources the unit one. Every directory holding bats files now has the same hooks.
  • Side effect, intended: a run that starts in tests/ now also gets the git-identity guard the unit directory already had. All 157 top-level files give the same counts with it.

2. The model CLI is started without the ledger. New _engine_child in scripts/engine.sh runs its command under env -u for the same three variables, and all five CLI starts go through it (Copilot, Gemini's two paths, Claude's two paths). The parent shell keeps the variable and still records every call; the child never needed it. This stops whatever the session runs from writing to the ledger, tests or not.

No workflow file changes.

Risk

  • The engine.sh half reaches production only when the dev-lead channel moves. This repository pins dev-lead/v139-ring0 for both the workflow and its scripts. The suite-hook half needs no tag: the agent runs tests from the branch it is working on, so any branch containing this commit stops leaking; branches cut earlier keep leaking until they pick it up.
  • model-ab.yml also sets the variable and starts two claude --print probes directly, not through engine.sh. They grant no tools and are left as they are.
  • The nested bats runs in the new guard add about 25 seconds to the unit job (measured on macOS). The guard file is not in lint.yml's list, so the time-limited bats job is unaffected. A change that touches only top-level tests/ does not trigger the workflow that runs the guard; it fires on the next change that does.
  • Whether one full pass of the tests explains the volume seen in production was not confirmed: one pass yields 12 Gemini records, so the 1,811 fixture records over four days imply many passes or repeated runs of the Gemini suites by agent sessions.

Test plan

  • Before and after, every bats file run alone in a clean environment with the three variables exported: 16 files and 221 records before; no file and no record after (249 files).
  • tests/dev-lead/unit/test_token_ledger_isolation.bats (new, 3 tests): every directory with bats files has suite hooks, and a probe run through each sees no ledger variable; one real engine suite and three top-level suites, run nested with the variables exported, leave the ledger directory empty.
  • tests/dev-lead/unit/test_engine_child_env.bats (new, 6 tests): for each launch site a stub CLI dumps its environment and finds none of the three variables, while the parent's ledger still gets the call's record; in one, the stub itself calls the token writers and the ledger still holds only the parent's record.
  • Existing suites: per-file counts identical before and after across the engine, Gemini, fix-ci, fix-issue, fix-reviews and token suites. test_engine_writer test 15, which failed only when a ledger was inherited, now passes in that case too.
  • Mutation-checked: 14 mutations, each turning named tests red. I re-ran two after rebasing: with the suite-level unset removed, all 3 isolation tests fail; with the child keeping the variables, all 6 child-environment tests fail.
  • shellcheck --severity=warning -x clean; the fixed-path check exits 0; no negated assertion that cannot fail.
  • I replaced the subagent's nested run of seven engine suites (139 seconds) with one suite: the isolation is suite-wide, so more suites added time and no coverage.
  • Not run: CI; the live e2e scenarios.

Rollback

Revert the squash commit. Tests and agent sessions go back to inheriting the ledger path.

Monitoring

  • After this merges: the next agent session that runs tests on a branch containing it should upload a ledger with no fixture model names (gemini-same, gemini-test, gemini-u1, …).
  • After dev-lead/v139-ring0 moves: the "Gemini ledger provenance" section added by fix(gemini-ledger): hand the tier report only records traceable to trusted runs #2305 should report zero records for models or keys with no caps row. Until both land, the warning there is expected.

Closes #2304

🤖 Generated with Claude Code

View guided diff Turn on auto-fix

don-petry and others added 2 commits October 11, 2026 15:50
The dev-lead and pr-review jobs export TOKEN_LOG_FILE for the whole job. The
agent session inherited it and ran this repository's bats suites, whose fixture
calls, rejections and cooldowns were appended to the run's real ledger and
uploaded with it.

Two independent fixes:

- scripts/engine.sh starts every model CLI (claude, gemini, gh copilot; five
  launch sites) through _engine_child, which removes TOKEN_LOG_FILE,
  GEMINI_LEDGER_FILE and _ENGINE_USAGE_OUT from the child's environment. The
  parent shell still records each call around the CLI.
- tests/dev-lead/unit/setup_suite.bash unsets the same three variables before
  any test file starts. tests/ and tests/dev-lead/integration/ gain a
  setup_suite.bash that loads it: bats reads one such file per run, the one
  beside the first path it is given, and never looks in subdirectories.

Measured with TOKEN_LOG_FILE exported and every bats file run on its own: 16
files wrote 221 records (plus usage sidecars and locks) before, none after.

Tests: test_engine_child_env.bats (each launch site, and the parent's record)
and test_token_ledger_isolation.bats (suite hooks in every directory of bats
files, and nested runs of the suites that wrote).

Refs #2304

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, not seven

The isolation is suite-wide, so six more suites added about two minutes to the
unit job and no coverage.

Refs #2304

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@don-petry don-petry added the dev-lead:hands-off Exclude from dev-lead agent automation label Oct 11, 2026
@don-petry
don-petry marked this pull request as ready for review October 11, 2026 20:52
@don-petry
don-petry requested a review from a team as a code owner October 11, 2026 20:52
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 24 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 203d39a2-7e65-4cef-a58a-5fc53f052fbf

📥 Commits

Reviewing files that changed from the base of the PR and between 5797da5 and 7a128c5.


📒 Files selected for processing (6)
  • scripts/engine.sh
  • tests/dev-lead/integration/setup_suite.bash
  • tests/dev-lead/unit/setup_suite.bash
  • tests/dev-lead/unit/test_engine_child_env.bats
  • tests/dev-lead/unit/test_token_ledger_isolation.bats
  • tests/setup_suite.bash

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@don-petry

Copy link
Copy Markdown
Collaborator Author

dev-lead is withholding action on this item.

It is labeled dev-lead:hands-off (deliberately kept off-limits to dev-lead (e.g. changes to the dev-lead automation itself)), so dev-lead will not pick it up while that label is present. This notice is posted once so the withhold is visible rather than looking like a stalled run.

To re-enable automated pickup: remove the dev-lead:hands-off label.

@donpetry-bot

Copy link
Copy Markdown
Contributor

Advisory bots were rate-limited; auto-approval is withheld until they recover. pr-review-sweep will re-review this PR after 2026-10-11T21:54:03Z.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements token ledger isolation by introducing an _engine_child wrapper in scripts/engine.sh that unsets ledger-related environment variables before invoking model CLIs, preventing child processes from polluting the real token ledger. It also unsets these variables in BATS suite-level hooks and adds comprehensive unit tests to verify the isolation. The review feedback suggests optimizing a find command in the tests to avoid spawning multiple dirname processes, and replacing a non-idiomatic return 1 assertion in BATS with false.

Comment thread tests/dev-lead/unit/test_token_ledger_isolation.bats Outdated
Comment thread tests/dev-lead/unit/test_token_ledger_isolation.bats Outdated
@don-petry

Copy link
Copy Markdown
Collaborator Author

QA Lead — test-risk advisory

Risk tier: LOW. The production change is one env -u wrapper on five CLI launch sites. Stub tests cover every site, and each half of the fix was mutation-checked. Rollback is a single revert, and the engine.sh half doesn't reach production until dev-lead/v139-ring0 moves.

What I'd shore up (highest leverage first):

  • Guard against the next launch site. The isolation test enforces "every bats directory has hooks", but nothing makes a new timeout … claude|gemini|gh copilot call in engine.sh go through _engine_child. Add a cheap static check that fails on any direct launch. A sixth site would leak again with every test still green.
  • Run CI before merge. The PR says CI was not run, and the 25 s timing was measured on macOS. _fresh_bats uses env -i and strips BATS_LIBEXEC from PATH, which depends on how bats installs its launcher. Confirm it is deterministic on the Linux runner's bats version, so it doesn't become a flaky gate.
  • Make the post-merge monitoring a check. "No fixture model names in the uploaded ledger" is currently something someone has to remember to look at. A deny-list assertion (gemini-test, gemini-u1, gemini-fixture-*) in the fix(gemini-ledger): hand the tier report only records traceable to trusted runs #2305 provenance report would turn it into a check. Fixture cooldowns in the real ledger can throttle real Gemini keys, so this is worth guarding.
  • Minor: the nested-writer case (test 6) only drives the Claude path. One Gemini variant would close the symmetry cheaply.

Escalate? no

Advisory only — I comment, I do not change code. Ground: BMAD Test Architecture
(bmad-tea v1.19.0). Opt out on this item with the qa-lead:hands-off label.

@codeant-ai

codeant-ai Bot commented Oct 11, 2026

Copy link
Copy Markdown

CodeAnt PR Risk: Low Risk

  • The PR appears safe to merge; model CLI subprocesses no longer inherit the token-ledger paths, while the parent shell retains them for recording.
  • Suite hooks and focused tests cover inherited-ledger isolation across test directories and the Claude, Gemini, and Copilot call paths.

Assessed commit: ff15b1b583bd

don-petry and others added 2 commits October 11, 2026 16:27
From the QA advisory: nothing stopped a sixth launch site from being added
without _engine_child, and it would have leaked with every behavioural test
still green. A static test now fails on any model CLI started in engine.sh
outside the wrapper. Also the two small points from review of the isolation
test (no per-file dirname fork; fail with false, not return).

Refs #2304

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@don-petry

Copy link
Copy Markdown
Collaborator Author

Review round 1, head 7a128c5b. From the QA advisory:

  • Guard against the next launch site: added. A static test fails on any model CLI started in engine.sh outside _engine_child, whether under timeout or bare. Unwrapping one existing site, or appending a bare claude --print, each turns it red and names the line.
  • CI before merge: the first CI run on this pull request passed unit on the Linux runner, including the nested runs, so the env -i launcher works with that bats version. It reruns on this head.
  • Make the monitoring a check: the detector already exists in fix(gemini-ledger): hand the tier report only records traceable to trusted runs #2305 rather than as a deny-list: it counts any Gemini record whose model or key index has no row in the caps file, warns, and makes the dispatch-decision reader answer unknown. That catches every fixture model name without listing them. It cannot catch fixture records that use real model names, which is why the fix is here at the source.
  • Not done: a Gemini variant of the nested-writer case. The three ledger variables are removed by one helper for every CLI, and the per-site tests cover Gemini's two paths.

@sonarqubecloud

Copy link
Copy Markdown

@donpetry-bot

Copy link
Copy Markdown
Contributor

Advisory bots were rate-limited; auto-approval is withheld until they recover. pr-review-sweep will re-review this PR after 2026-10-11T22:30:33Z.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dev-lead:hands-off Exclude from dev-lead agent automation

Projects

None yet

2 participants