diff --git a/planning/changes/2026-06-26.02-retriable-test-hardening/change.md b/planning/changes/2026-06-26.02-retriable-test-hardening/change.md new file mode 100644 index 0000000..7dfdfed --- /dev/null +++ b/planning/changes/2026-06-26.02-retriable-test-hardening/change.md @@ -0,0 +1,37 @@ +--- +summary: Hardened tests/test_retriable.py — made the `.orig` invariant explicit in the helper and added a cycle case whose loop contains a retriable DBAPIError link. +--- + +# Change: Harden the retriable predicate tests + +**Lane:** lightweight — ≲30 LOC net, ≤2 files, no new file, no public-API +change, a single straightforward test. + +## Goal + +Fold the two deferred Minor findings from the [retriable-error-seam +review](../2026-06-26.01-retriable-error-seam/design.md)'s final whole-branch +pass into the test suite, so the predicate's invariants are explicit rather than +implicit. + +## Approach + +Test-only hardening of the in-memory predicate suite. No production code or +capability contract changes, so no `architecture/` promotion. + +- The `_make_dbapi_error` helper now asserts `err.orig is not None`. The whole + matrix hinges on `.orig` being set (the predicate reads `.orig.__cause__`); + the assertion makes that load-bearing invariant explicit instead of relying on + the True-expected cases to fail if it ever broke. +- A new case complements `test_is_retriable_cause_cycle_terminates`: a cyclic + chain that *contains* a retriable `DBAPIError` link returns `True` — the walk + finds the link before the cycle guard would re-visit a node. + +## Files + +- `tests/test_retriable.py` — assertion in the helper; one new cycle-with-link test. + +## Verification + +- [x] `just test` — full suite green (25 passed, 100% coverage). +- [x] `just lint-ci` — clean. diff --git a/tests/test_retriable.py b/tests/test_retriable.py index afbc0d2..93b4743 100644 --- a/tests/test_retriable.py +++ b/tests/test_retriable.py @@ -8,7 +8,9 @@ def _make_dbapi_error(cause: BaseException) -> DBAPIError: orig = Exception("db error") orig.__cause__ = cause - return DBAPIError("SELECT 1", None, orig) + err = DBAPIError("SELECT 1", None, orig) + assert err.orig is not None # the predicate hinges on .orig being set; make the invariant explicit + return err def test_retriable_asyncpg_errors_contains_expected_classes() -> None: @@ -55,3 +57,13 @@ def test_is_retriable_cause_cycle_terminates() -> None: a.__cause__ = b b.__cause__ = a assert is_retriable(a) is False + + +def test_is_retriable_cycle_containing_retriable_link() -> None: + # A cyclic chain that *contains* a retriable DBAPIError still returns True: + # the walk finds the retriable link before the cycle guard would re-visit a node. + outer = ValueError("outer") + dbapi_err = _make_dbapi_error(asyncpg.SerializationError()) + outer.__cause__ = dbapi_err + dbapi_err.__cause__ = outer + assert is_retriable(outer) is True