tests: live integration smoke for sync + async guards#5
Merged
Conversation
Adds `tests/integration/test_live_ap2_guard.py` — five tests
(four sync + one async) exercising cycles_guard_payment and
cycles_guard_payment_async end-to-end against a real Cycles
server. Pattern mirrors cycles-client-python/tests/integration/
test_live_server.py: module-level `pytest.mark.skipif(not
CYCLES_BASE_URL)` so the whole file is skipped at collection
time when env vars are unset. Default `pytest` runs and CI
ignore it; verified locally — 147 passed, 5 skipped.
Why: every existing test uses MagicMock or AsyncMock for the
CyclesClient surface. A server-side rename of a `Subject.dimensions`
key, a field that flips from string to enum, or a change to
how response.is_success classifies status codes — none of those
would surface in mock-based tests. The integration suite catches
that class of regression when run against a real dev server.
Cost: zero in CI (skipped by env-var gate). When run, each test
uses a fresh UUID-based transaction_id and a $0.00000001 (1
micro-cent) amount, so the suite is idempotent across runs and
costs essentially nothing in budget per execution.
Covers:
- Sync clean commit (lifecycle + receipt fields)
- Sync exception → release (no deadlock, propagates)
- Sync dry-run → AP2DryRunResult with decision payload
- Sync idempotent replay (same mandate → same reserve key,
server collapses or surfaces finalized status — both OK)
- Async clean commit with open_mandate_hash scope (exercises the
AP2 §6 consume-once path through async)
To run locally:
CYCLES_BASE_URL=http://localhost:7878 \
CYCLES_API_KEY=cyc_dev_xxx \
CYCLES_TENANT=ap2-integration \
pytest tests/integration -v
Tenant needs a budget with `payment.charge` permitted.
README Development section + AUDIT entry both updated. No public
API change. No wire change. No version bump.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
tests/integration/test_live_ap2_guard.py— five tests (four sync + one async) exercisingcycles_guard_paymentandcycles_guard_payment_asyncend-to-end against a real Cycles server. Pattern mirrorscycles-client-python/tests/integration/test_live_server.py: module-levelpytest.mark.skipif(not CYCLES_BASE_URL)so the whole file is skipped at collection time when env vars are unset.Why
Every existing test uses
MagicMock/AsyncMockfor theCyclesClientsurface. A server-side rename of aSubject.dimensionskey, a field that flips from string to enum, or a change to howresponse.is_successclassifies status codes — none of those would surface in mock-based tests. This integration suite catches that class of regression when run against a real dev server.Cost
Zero in CI (skipped by env-var gate). When run locally, each test uses a fresh UUID-based
transaction_idand a$0.00000001amount, so the suite is idempotent across runs and costs essentially nothing in budget per execution.Coverage
test_clean_commit_against_live_server(sync)decisionreflects server response,reservation_idnon-nulltest_exception_inside_with_releases(sync)withpropagates and release is attempted; no deadlocktest_dry_run_raises_result_no_reservation_created(sync)AP2DryRunResultraised with decision payload, body never executestest_idempotent_replay_returns_same_reservation_id(sync)test_async_clean_commit_against_live_server(async)open_mandate_hashset (exercises the AP2 §6 consume-once scope through the async path)How to run
CYCLES_BASE_URL=http://localhost:7878 \ CYCLES_API_KEY=cyc_dev_xxx \ CYCLES_TENANT=ap2-integration \ pytest tests/integration -vTenant needs a budget with
payment.chargepermitted.Test plan
pytest --cov=runcycles_ap2 --cov-fail-under=95— 147 passed, 5 skipped (integration suite skipped via env-var gate)ruff check . && ruff format --check .— cleanmypy --strict runcycles_ap2— 0 errors (8 files)python -m build— sdist + wheel build cleanlyWhat this PR is NOT