From bd6b9269d909b78d9bf5b520c7da2181022e3148 Mon Sep 17 00:00:00 2001 From: Albert Mavashev Date: Fri, 3 Jul 2026 13:34:04 -0400 Subject: [PATCH 1/3] test(integration): probe public readiness endpoint instead of guarded aggregate health MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aggregate /actuator/health requires X-Admin-API-Key since cycles-server v0.1.25.45 and fails closed with 500 when the server has no admin key configured — this assertion has been the visible failure in the org nightly Full-Stack Integration since 2026-06-28. The liveness/readiness probes are the public, Redis-aware health contract a credential-less client can rely on; the health-check test now probes /actuator/health/readiness. Integration-test-only; no library code change, no version bump. AUDIT.md updated. Companion PRs: runcycles/.github (workflow readiness polling + event-server management port) and the sibling client repo. --- AUDIT.md | 3 ++- tests/integration/test_live_server.py | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/AUDIT.md b/AUDIT.md index 33cf0ab..e6b67df 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -1,6 +1,7 @@ # Cycles Protocol v0.1.25 — Client (Python) Audit -**Date:** 2026-05-22 (v0.4.3 — `expires_from`/`expires_to` and `finalized_from`/`finalized_to` ISO-8601 window-filter passthrough on `list_reservations` per `cycles-protocol-v0.yaml` revision 2026-05-22; closes the Python-client side of runcycles/cycles-server#162. No code change — `**query_params` already forwards arbitrary kwargs. Added sync + async regression tests; unlike `from`/`to` the new param names are plain kwargs (no Python-reserved-word workaround needed). 393 tests pass at 100% coverage.), +**Date:** 2026-07-03 (integration-test-only, no version bump — `test_health_check` now probes the public `/actuator/health/readiness` endpoint instead of aggregate `/actuator/health`, which requires `X-Admin-API-Key` since cycles-server v0.1.25.45 and fails closed with 500 when the server has no admin key configured. The old assertion had failed the org nightly Full-Stack Integration every night since 2026-06-28. No library code change.), +2026-05-22 (v0.4.3 — `expires_from`/`expires_to` and `finalized_from`/`finalized_to` ISO-8601 window-filter passthrough on `list_reservations` per `cycles-protocol-v0.yaml` revision 2026-05-22; closes the Python-client side of runcycles/cycles-server#162. No code change — `**query_params` already forwards arbitrary kwargs. Added sync + async regression tests; unlike `from`/`to` the new param names are plain kwargs (no Python-reserved-word workaround needed). 393 tests pass at 100% coverage.), 2026-05-21 (v0.4.2 — `from` / `to` ISO-8601 window-filter passthrough on `list_reservations` per `cycles-protocol-v0.yaml` revision 2026-05-21; closes the client side of runcycles/cycles-server#159. No code change — the existing `**query_params` signature already forwards arbitrary kwargs to the URL query string. Added sync + async regression tests that lock the passthrough in (using the `**{"from": ..., "to": ...}` dict-unpack form because `from` is a Python reserved keyword). 391 tests pass at 100% coverage.), 2026-03-14 **Spec:** `cycles-protocol-v0.yaml` (OpenAPI 3.1.0, v0.1.25) diff --git a/tests/integration/test_live_server.py b/tests/integration/test_live_server.py index 8ac3777..1b2c737 100644 --- a/tests/integration/test_live_server.py +++ b/tests/integration/test_live_server.py @@ -26,8 +26,14 @@ def test_health_check(): - """Server responds to health endpoint.""" - res = requests.get(f"{BASE}/actuator/health", timeout=5) + """Server responds to its public readiness probe. + + Aggregate /actuator/health requires X-Admin-API-Key since cycles-server + v0.1.25.45 (500 when the server has no admin key configured); the + liveness/readiness probes are the public, Redis-aware health contract + a credential-less client can rely on. + """ + res = requests.get(f"{BASE}/actuator/health/readiness", timeout=5) assert res.status_code == 200 From b886532b57f640788a86574524352cfe53dec0c1 Mon Sep 17 00:00:00 2001 From: Albert Mavashev Date: Thu, 9 Jul 2026 09:49:06 -0400 Subject: [PATCH 2/3] docs: fix README transport-error claims README documented CyclesTransportError as raised on network failure; nothing in the package raises it. Rewrote the exception-table row and added a Transport errors subsection describing actual behavior: the decorator raises CyclesProtocolError with status == -1 (error_code None) at reserve time; the programmatic client returns CyclesResponse with is_transport_error / status -1. The class remains exported for user code. AUDIT.md updated (repo convention: no CHANGELOG entry for doc-only changes). Surfaced by the cycles-docs integration audit (runcycles/cycles-docs#785). --- AUDIT.md | 14 ++++++++++++++ README.md | 21 ++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/AUDIT.md b/AUDIT.md index e6b67df..e0b16f3 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -271,3 +271,17 @@ Cross-cutting hardening landed in response to org-wide tracking issues filed in Not included in this change: bumping the reusable-workflow ref `runcycles/.github/.github/workflows/ci-python.yml@main` to `@v1` (`runcycles/.github#60`). That bump is intentionally split into a separate follow-up PR — it depends on the `v1` tag existing in `runcycles/.github`, which is being cut after the canonical-script PR (`runcycles/.github#64`) merges. Protocol conformance: No protocol or wire-format changes. No SDK source touched. Test suite unaffected. + +## README Transport-Error Documentation Fix (added 2026-07-09) + +**Files:** `README.md` +**Version:** unreleased (docs-only, no version bump, no CHANGELOG entry per repo convention) + +Documentation-only correction. The README's exception-hierarchy table described `CyclesTransportError` as "Network-level failure (connection, DNS, timeout)", implying the SDK raises it — but nothing in the package ever raises it. Actual behavior: + +- **`@cycles` decorator / HOF paths:** a transport failure at reserve time raises `CyclesProtocolError` with `status == -1` and `error_code=None` (via `_build_protocol_exception` in `lifecycle.py`); commit-time transport failures are retried in the background by the commit retry engine, not raised. +- **Programmatic client:** never raises for transport failures — returns `CyclesResponse` with `is_transport_error == True` and `status == -1` (`CyclesResponse.transport_error` constructor in `response.py`). + +The table row now states the class is exported for user code but never raised by the SDK, and a new "Transport errors" subsection documents the `status == -1` behavior for both API surfaces with a detection example. Wording matches the docs site (`cycles-docs/how-to/error-handling-patterns-in-python.md`). `CyclesTransportError` remains exported from `runcycles/__init__.py` for use in user code — no API change. + +Protocol conformance: No protocol or wire-format changes. No SDK source touched. Test suite unaffected. diff --git a/README.md b/README.md index 2c290ec..768b981 100644 --- a/README.md +++ b/README.md @@ -304,7 +304,26 @@ Exception hierarchy: | `DebtOutstandingError` | Outstanding debt blocks new reservations | | `ReservationExpiredError` | Operating on an expired reservation | | `ReservationFinalizedError` | Operating on an already-committed/released reservation | -| `CyclesTransportError` | Network-level failure (connection, DNS, timeout) | +| `CyclesTransportError` | Exported for use in your own code; never raised by the SDK — transport failures surface as `status == -1` (see below) | + +### Transport errors + +When the HTTP request itself fails (DNS resolution, connection refused, timeout), the SDK never raises `CyclesTransportError` — the class is exported for use in your own code (e.g. wrapping transport-level failures in higher-level integrations). Instead: + +- **`@cycles` decorator:** a transport failure at reserve time raises `CyclesProtocolError` with `status == -1` and `error_code=None`. Transport failures at commit time are retried in the background by the commit retry engine, not raised. +- **Programmatic client:** calls never raise for transport failures — they return a `CyclesResponse` with `is_transport_error == True` and `status == -1` (shown above). + +```python +from runcycles import CyclesProtocolError + +try: + guarded_func() +except CyclesProtocolError as e: + if e.status == -1: + print(f"Network error reaching Cycles: {e}") # retry or degrade + else: + raise +``` ## Preflight checks (decide) From 5df11f1cf5b62457e486cd24bedd00266dfe133f Mon Sep 17 00:00:00 2001 From: Albert Mavashev Date: Thu, 9 Jul 2026 10:01:50 -0400 Subject: [PATCH 3/3] docs: apply codex review to transport-error fix Apply 4 / modify 0 / skip 0: - README CyclesProtocolError row now covers SDK-wrapped reserve-time transport failures (status == -1) - transport-errors bullet broadened to lifecycle-managed surfaces (@cycles and stream_reservation()) - CyclesTransportError docstring no longer claims it is raised; documents the actual status == -1 surfacing - AUDIT.md rolling date line updated to 2026-07-09 --- AUDIT.md | 3 ++- README.md | 4 ++-- runcycles/exceptions.py | 8 +++++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/AUDIT.md b/AUDIT.md index e0b16f3..d95741a 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -1,6 +1,7 @@ # Cycles Protocol v0.1.25 — Client (Python) Audit -**Date:** 2026-07-03 (integration-test-only, no version bump — `test_health_check` now probes the public `/actuator/health/readiness` endpoint instead of aggregate `/actuator/health`, which requires `X-Admin-API-Key` since cycles-server v0.1.25.45 and fails closed with 500 when the server has no admin key configured. The old assertion had failed the org nightly Full-Stack Integration every night since 2026-06-28. No library code change.), +**Date:** 2026-07-09 (README + docstring transport-error documentation fix, no version bump — see the dated entry at the end of this file. `CyclesTransportError` is exported but never raised by the SDK; README and its docstring now describe the actual `status == -1` surfacing.), +2026-07-03 (integration-test-only, no version bump — `test_health_check` now probes the public `/actuator/health/readiness` endpoint instead of aggregate `/actuator/health`, which requires `X-Admin-API-Key` since cycles-server v0.1.25.45 and fails closed with 500 when the server has no admin key configured. The old assertion had failed the org nightly Full-Stack Integration every night since 2026-06-28. No library code change.), 2026-05-22 (v0.4.3 — `expires_from`/`expires_to` and `finalized_from`/`finalized_to` ISO-8601 window-filter passthrough on `list_reservations` per `cycles-protocol-v0.yaml` revision 2026-05-22; closes the Python-client side of runcycles/cycles-server#162. No code change — `**query_params` already forwards arbitrary kwargs. Added sync + async regression tests; unlike `from`/`to` the new param names are plain kwargs (no Python-reserved-word workaround needed). 393 tests pass at 100% coverage.), 2026-05-21 (v0.4.2 — `from` / `to` ISO-8601 window-filter passthrough on `list_reservations` per `cycles-protocol-v0.yaml` revision 2026-05-21; closes the client side of runcycles/cycles-server#159. No code change — the existing `**query_params` signature already forwards arbitrary kwargs to the URL query string. Added sync + async regression tests that lock the passthrough in (using the `**{"from": ..., "to": ...}` dict-unpack form because `from` is a Python reserved keyword). 391 tests pass at 100% coverage.), 2026-03-14 diff --git a/README.md b/README.md index 768b981..63ce081 100644 --- a/README.md +++ b/README.md @@ -298,7 +298,7 @@ Exception hierarchy: | Exception | When | |---|---| | `CyclesError` | Base for all Cycles errors | -| `CyclesProtocolError` | Server returned a protocol-level error | +| `CyclesProtocolError` | Server returned a protocol-level error; also raised with `status == -1` when the SDK wraps a reserve-time transport failure | | `BudgetExceededError` | Budget insufficient for the reservation | | `OverdraftLimitExceededError` | Debt exceeds the overdraft limit | | `DebtOutstandingError` | Outstanding debt blocks new reservations | @@ -310,7 +310,7 @@ Exception hierarchy: When the HTTP request itself fails (DNS resolution, connection refused, timeout), the SDK never raises `CyclesTransportError` — the class is exported for use in your own code (e.g. wrapping transport-level failures in higher-level integrations). Instead: -- **`@cycles` decorator:** a transport failure at reserve time raises `CyclesProtocolError` with `status == -1` and `error_code=None`. Transport failures at commit time are retried in the background by the commit retry engine, not raised. +- **Lifecycle-managed surfaces (`@cycles` and `stream_reservation()`):** a transport failure at reserve time raises `CyclesProtocolError` with `status == -1` and `error_code=None`. Transport failures at commit time are retried in the background by the commit retry engine, not raised. - **Programmatic client:** calls never raise for transport failures — they return a `CyclesResponse` with `is_transport_error == True` and `status == -1` (shown above). ```python diff --git a/runcycles/exceptions.py b/runcycles/exceptions.py index 6d372e1..d434ed6 100644 --- a/runcycles/exceptions.py +++ b/runcycles/exceptions.py @@ -77,7 +77,13 @@ class ReservationFinalizedError(CyclesProtocolError): class CyclesTransportError(CyclesError): - """Raised when a transport-level error occurs (connection timeout, DNS failure, etc.).""" + """Transport-level error (connection timeout, DNS failure, etc.). + + Exported for use in application code; the SDK itself surfaces transport + failures as ``CyclesProtocolError`` with ``status == -1`` (decorator and + ``stream_reservation`` paths) or ``CyclesResponse.transport_error`` + (programmatic client) rather than raising this class. + """ def __init__(self, message: str, *, cause: Exception | None = None) -> None: super().__init__(message)