Skip to content

fix: bring e2e tests into default CI, fix 3 stale against attribution hardening - #130

Merged
susheem-k merged 1 commit into
mainfrom
fix/e2e-regression-suite
Sep 12, 2026
Merged

susheem-k merged 1 commit into
mainfrom
fix/e2e-regression-suite

Conversation

@susheem-k

Copy link
Copy Markdown
Collaborator

Summary

Establishes a 100%-passing regression baseline (including the example-bench e2e suite) before starting the remote-only "swap" work in #118. tests/examples/ was excluded from CI via addopts, so two tests silently drifted out of sync with the intentional attribution-hardening commit 28337d1 and nobody noticed.

Closes #127.

  • test_triad_e2e.py::test_triad_pipeline_completes_with_ledger: asserted the old (pre-hardening) precedence where a payload-supplied intent wins. The actual/intended contract is that the agent's own instrument_app(intent=...) always wins over a payload value. Fixed the assertion.
  • test_triad_e2e.py::test_triad_cost_not_double_counted_without_parent_rollup: monkeypatched build_price_book on the example server module, which never imports that name — pricing is resolved lazily in tokenops.control.run. Fixed to patch the correct module.
  • test_bench_e2e.py::test_run_dims_persisted_for_segmentation → renamed test_run_dims_only_allowlisted_payload_keys_persist and rewritten to assert the intended, hardened contract: only allow-listed payload keys (user_id/user) reach RunRecord.dims; an arbitrary tag (team) must not leak into segmentation (attribution._PAYLOAD_USER_DIM_ALLOWLIST).
  • pyproject.toml: narrowed addopts from -m 'not e2e and not live' to -m 'not live'e2e (real servers, real Governor/policy HALT/MUTATE, no network/API key needed) is now part of the default suite and CI, so this class of regression can't go unnoticed again. live (needs real keys/vendored frameworks) stays excluded.
  • Updated docs/testing.md and CHANGELOG.md to match.

No production code changed — this is test-suite and CI-config only.

Test plan

  • python -m pytest -v -m "e2e or live" → 10 passed, 1 skipped (only the key-gated test_live_baseline_smoke skips)
  • python -m pytest -q (full default suite) → 247 passed, 16 skipped, 1 deselected
  • ruff check src tests examples clean
  • ruff format --check src tests examples clean

🤖 Generated with Claude Code

… hardening (#127)

Two example-bench e2e tests had gone stale against the intentional
attribution-hardening commit 28337d1 (agent-declared intent/mode beat
payload-supplied values; only user_id/user are trusted from an untrusted
payload) without anyone noticing, because tests/examples/ was excluded
from CI via addopts. Narrows addopts from "-m 'not e2e and not live'" to
"-m 'not live'" so this class of regression can't go unnoticed again;
none of the 10 non-live e2e tests need a network call or API key.

- test_triad_e2e.py::test_triad_pipeline_completes_with_ledger: assert
  the agent's own instrument_app(intent=...) wins, not the payload value
- test_triad_e2e.py::test_triad_cost_not_double_counted_without_parent_rollup:
  patch build_price_book on tokenops.control.run (where it's resolved),
  not on the example server module
- test_bench_e2e.py: renamed test_run_dims_persisted_for_segmentation to
  test_run_dims_only_allowlisted_payload_keys_persist and rewrote it to
  assert the intended, hardened contract

Full suite: 247 passed, 16 skipped, 1 deselected. ruff check/format clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
susheem-k added a commit that referenced this pull request Sep 12, 2026
…cope (#118)

First increment of tokenops#118 Phase 2 — two small, independent, mechanical
pieces from the locked decisions, landed ahead of the larger Ledger/LedgerView
backend rewire:

- Ledger.RunState -> LocalRunState (decision #9): makes the two-tier model
  explicit — this is the per-process Tier-1 cache, not the plane's
  authoritative run_state. Pure rename, no behavior change.
- PolicyInstance.data_scope: local | global, default local (decision #5).
  Persisted in the local Store (additive migration, mirrors the pattern
  already used for dims/parent_span/governance_events) and round-tripped
  through governance_config_for. Not yet consumed by build_governor/Governor
  — grouping detectors by data_scope is part of the LedgerBackend rewire.

Full suite: 237 passed, 16 skipped, 11 deselected (matches main's current
baseline pre-#130). ruff check/format and mypy clean on touched files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@susheem-k
susheem-k added this pull request to stack #135 September 12, 2026 12:24
@susheem-k
susheem-k merged commit 8dda5dc into main Sep 12, 2026
3 checks passed
susheem-k added a commit that referenced this pull request Sep 12, 2026
…cope (#118)

First increment of tokenops#118 Phase 2 — two small, independent, mechanical
pieces from the locked decisions, landed ahead of the larger Ledger/LedgerView
backend rewire:

- Ledger.RunState -> LocalRunState (decision #9): makes the two-tier model
  explicit — this is the per-process Tier-1 cache, not the plane's
  authoritative run_state. Pure rename, no behavior change.
- PolicyInstance.data_scope: local | global, default local (decision #5).
  Persisted in the local Store (additive migration, mirrors the pattern
  already used for dims/parent_span/governance_events) and round-tripped
  through governance_config_for. Not yet consumed by build_governor/Governor
  — grouping detectors by data_scope is part of the LedgerBackend rewire.

Full suite: 237 passed, 16 skipped, 11 deselected (matches main's current
baseline pre-#130). ruff check/format and mypy clean on touched files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
susheem-k added a commit that referenced this pull request Sep 12, 2026
…cope (#131)

* refactor: rename RunState to LocalRunState; add PolicyInstance.data_scope (#118)

First increment of tokenops#118 Phase 2 — two small, independent, mechanical
pieces from the locked decisions, landed ahead of the larger Ledger/LedgerView
backend rewire:

- Ledger.RunState -> LocalRunState (decision #9): makes the two-tier model
  explicit — this is the per-process Tier-1 cache, not the plane's
  authoritative run_state. Pure rename, no behavior change.
- PolicyInstance.data_scope: local | global, default local (decision #5).
  Persisted in the local Store (additive migration, mirrors the pattern
  already used for dims/parent_span/governance_events) and round-tripped
  through governance_config_for. Not yet consumed by build_governor/Governor
  — grouping detectors by data_scope is part of the LedgerBackend rewire.

Full suite: 237 passed, 16 skipped, 11 deselected (matches main's current
baseline pre-#130). ruff check/format and mypy clean on touched files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat: wire Ledger onto LedgerBackend (precheck/apply_events) alongside Store (#118)

Adds a backend= mode to Ledger, additive alongside the existing store=/in-memory
modes (mutually exclusive with store=) — the core Ledger/LedgerView piece of the
remote-only rewrite's Phase 2 checklist. build_governor/ControlPlaneClient don't
construct it yet; that swap, and migrating the ~18 Store-dependent tests, are the
next increments (staged deliberately, per the epic's own Phase 2/3 split).

- record(): batches the step + spent_add (when cost > 0) into one apply_events
  call sharing an idempotency seq, so the ack's totals cover the run-total cum
  spend without a separate read_state round trip for the common case.
- admit/complete/mark_halted/clear_halt: one-shot apply_events writes. Their
  idempotency keys are random (uuid4), not the contract's deterministic
  {run_id}:...:{seq} recipe — that recipe needs a real run_id to stay
  collision-free across processes for a non-run-scoped segment key (e.g. an
  agent-dimension concurrency cap shared across runs), which isn't always
  available at these call sites. A random key is correct for a one-shot,
  non-retried write either way; the deterministic recipe only earns its keep
  once a buffered backend needs a retried flush to regenerate the same key
  (the existing # TODO(buffering) seam in ledger_backend.py).
- cost_micros/budget_left/is_halted/inflight: read via backend.read_state
  (precheck). No per-call batching across detectors yet (one read per method
  call) — Governor grouping detectors by data_scope into a single precheck per
  governed moment is a separate, later optimization, not required for
  correctness given there's no client-side write buffer yet either (decision #6).
- velocity/recent/window/step_count are unchanged: always Tier-1 LocalRunState,
  never a backend round trip — those are inherently per-process reads.

tests/test_ledger_backend_mode.py: 9 new tests against FakeLedgerBackend,
including two Ledger instances sharing one backend (the actual cross-process
scenario this whole rewire is for) for halt visibility and inflight counting.

Full suite: 246 passed, 16 skipped, 11 deselected. ruff check/format and mypy
clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat: eliminate the local ledger — tokenops is now hard-dependent on the control plane (#118)

The core swap for tokenops#118: ControlPlaneClient.from_env() now requires
CONTROL_PLANE_URL/TOKENOPS_URL and raises if neither is set. There is no more
TOKENOPS_EMBEDDED and no code path left that silently falls back to a local
SQLite ledger. build_governor/tokenops_run construct Ledger(backend=...) (an
HttpLedgerBackend, i.e. real precheck/events:batch traffic) for every live run.

- client.py: from_env() requires a URL; new .backend property (lazy
  HttpLedgerBackend); should_mount_run_registration() always False (no embedded
  mode left to self-host POST /v1/runs under). The store= constructor kwarg
  remains as an explicit, visible test-only escape hatch — never reachable from
  from_env(), so no env misconfiguration can select it.
- config.py/run.py: build_governor/build_governance_stack take backend=; run.py
  picks store vs backend off client_obj.embedded (not the local store= param,
  which was a real bug in an earlier draft of this change — a client=
  constructed with store= wasn't routing to its own Store).
- http_store.py: HttpStore accepts an injectable httpx.Client (mirrors
  HttpLedgerBackend) so tests can point it at an in-process app.
- dev_plane.py (new): launches a real agentplane-control-plane on a real
  localhost TCP port, in-process. Used by tokenops.demo (still zero-setup: it
  launches its own throwaway plane and configures it over HTTP) and by
  tests/conftest.py::live_plane_url for tests that must exercise
  ControlPlaneClient.from_env() itself (an in-process ASGI app isn't reachable
  that way since from_env() builds its own plain httpx.Client).
- CI now installs agentplane-control-plane from the control-plane repo
  (git+.../control-plane@main) alongside .[dev,contract] — the [contract] tests
  and tests/examples/ e2e suite now actually run a real control plane in CI
  instead of importorskip-ing past it. They're load-bearing coverage now.
- Migrated every TOKENOPS_EMBEDDED-dependent test (test_tokenops_run.py,
  test_control_plane_client.py, test_control_plane_app.py,
  test_governance_config_cache.py, test_demo.py, and the two tests/examples/
  e2e files) onto either the explicit store= escape hatch or live_plane_url.
- tests/examples/test_bench_e2e.py and test_triad_e2e.py now configure their
  policies/budgets on a real, in-process control plane over its own HTTP API
  (PUT /v1/budgets, PUT /v1/policies) instead of a local Store — this is the
  concrete demonstration that governance policies configured on the plane
  reach a live, multi-agent run and HALT/steer it (step_cap, cost_budget,
  output_runaway CANCEL+RETRY, tool_output_cap deep swap).
- Removed the now-dead TOKENOPS_EMBEDDED convenience shims from the three
  example CLI clients (a2a, brief, triad) — nothing reads that var anymore.
- README/onboarding/control-plane-deploy docs: removed the stale
  TOKENOPS_EMBEDDED documentation and precedence warnings.

Full suite: 258 passed, 16 skipped, 1 deselected. ruff check/format clean.
mypy clean on every touched file.

Not in this change (separate, later Phase 2/3 checklist items): removing the
bundled src/tokenops/server + src/tokenops/ui/streamlit, dropping span_id,
Governor batching detectors by data_scope into one precheck per call, and
migrating the ~18 policy-logic unit tests that construct Ledger()/Store()
directly for white-box testing (not a production bypass — never reachable from
ControlPlaneClient.from_env()).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: fix Quickstart to reflect the hard dependency on a running control plane

Two active falsehoods left over from the previous pass on this file (docs/env-var
table only, not the Quickstart copy itself):
- Section 1's manual snippet called ControlPlaneClient.from_env() with no mention
  a control plane must already be running — it now raises immediately without one.
- The Quickdeploy tip claimed "a single-process agent doesn't need it running at
  all", which was true under the old embedded-ledger fallback and is false now
  that tokenops has no ledger of its own (#118).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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.

e2e/live tests excluded from CI, and 3 of 11 are currently broken on main

2 participants