Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions planning/changes/2026-06-26.02-retriable-test-hardening/change.md
Original file line number Diff line number Diff line change
@@ -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.
14 changes: 13 additions & 1 deletion tests/test_retriable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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