Skip to content

feat(story-1.8): RecordedTransport — built-in Transport test double#13

Merged
lesnik512 merged 7 commits into
mainfrom
story/1-8-recordedtransport
May 31, 2026
Merged

feat(story-1.8): RecordedTransport — built-in Transport test double#13
lesnik512 merged 7 commits into
mainfrom
story/1-8-recordedtransport

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Summary

  • Adds `src/httpware/transports/recorded.py` with `RecordedTransport`, a built-in `Transport` test double. Route table maps `(method, url)` to `Response | BaseException`; configurable `default` for the no-match case (`None` → `RuntimeError` per archive AC; `Response` → returned; `BaseException` → raised). Method names are uppercased on insert and lookup. Routes fire indefinitely on repeat matches.
  • Exposes `transport.requests: list[Request]`, `transport.last_request` (property), and `transport.aclose_calls: int` for assertion patterns. `add_route(method, url, response_or_exception)` for incremental setup. `stream()` raises `NotImplementedError` — streaming lands in Epic 4 (Story 4-1).
  • Re-exported as `httpware.RecordedTransport`.
  • Consolidation: the five in-tree test stubs accumulated through Stories 2-1 and 1-7 (`_FakeTransport`, `_OkTransport`, `_FailingTransport`, two `_RecordingTransport` variants, `_TrackingTransport`) are replaced with one canonical class. Mechanical refactor; no test behavior changes.
  • 16 new tests (15 behavioral + 1 reexport) in `tests/test_transports_recorded.py`; 100% line coverage on the new module.

This closes Epic 1.

Out of scope (subsequent stories): URL pattern matching / globs, cassette files loaded from JSON, streaming responses (Epic 4).

Spec + plan: `docs/superpowers/specs/2026-05-31-recordedtransport-design.md`, `docs/superpowers/plans/2026-05-31-recordedtransport-plan.md`.

Test plan

  • `just test` — 273 passed, 1 deselected, 100% line coverage on the new module.
  • `just lint-ci` clean.
  • `tests/test_no_httpx2_leakage.py` still passes.
  • `tests/test_optional_extras_isolation.py` still passes.
  • All six existing test files pass after the stub-replacement commit (test_middleware.py, test_client_construction.py, test_client_methods.py, test_client_response_model.py, test_client_lifecycle.py, test_client_middleware_wiring.py).
  • CI green on all matrix entries (3.11/3.12/3.13/3.14 + lint).

🤖 Generated with Claude Code

lesnik512 and others added 7 commits May 31, 2026 22:49
Pragmatic Transport test double consolidating the five drifting in-tree
stubs (_OkTransport, _FailingTransport, _FakeTransport, two
_RecordingTransport variants, _TrackingTransport). Routes keyed by
(method.upper(), url) → Response | BaseException with a configurable
default for the no-match case (None → archive's RuntimeError). Routes
fire indefinitely on repeat matches. requests: list[Request] +
last_request property + aclose_calls counter cover the observability
patterns spread across the existing stubs.

In-tree stub replacement bundled as a single follow-up commit on the
same branch.

Out of scope: URL pattern matching, cassette files, streaming (Epic 4).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds src/httpware/transports/recorded.py with RecordedTransport:
- routes: Mapping[(method, url), Response | BaseException] with method
  uppercased on insert
- default: Response | BaseException | None — None raises RuntimeError per
  archive AC; otherwise the default is returned or raised
- requests: list[Request] populated on every __call__
- last_request property reading requests[-1]
- aclose_calls counter
- add_route(method, url, response_or_exception) for incremental setup
- stream() raises NotImplementedError (lands in Epic 4)

Five tests cover: route match returns Response, route raises Exception,
no-match raises RuntimeError, default Response returned on no-match,
default Exception raised on no-match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ll suite

Ten additional tests bring RecordedTransport to 15 total:
- method normalization in both directions (route key vs request)
- requests list captures every call in order; last_request property
- aclose counter; idempotent close that doesn't block subsequent calls
- stream() raises NotImplementedError pointing to Epic 4
- isinstance(RecordedTransport(), Transport) — protocol satisfaction
- add_route adds and replaces entries
- routes fire indefinitely on repeat matching calls

100% line coverage on src/httpware/transports/recorded.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds RecordedTransport to httpware/__init__.py imports and __all__ so
consumers can `from httpware import RecordedTransport` in addition to
the subpackage path. CHANGELOG records the Story 1.8 surface and notes
the in-tree stub consolidation (Task 4).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…edTransport

Replaces the file-local _FakeTransport (test_client_construction.py),
_OkTransport and _FailingTransport (test_middleware.py), two distinct
_RecordingTransport variants (test_client_methods.py,
test_client_response_model.py, test_client_middleware_wiring.py), and
_TrackingTransport (test_client_lifecycle.py) with one shared
RecordedTransport class.

Each replacement is mechanical:
- _FakeTransport() → RecordedTransport()
- _OkTransport() → RecordedTransport(default=Response(...))
- _FailingTransport(exc) → RecordedTransport(default=exc)
- _RecordingTransport (last_request flavor) → RecordedTransport(default=Response(...))
- _RecordingTransport (calls counter) → RecordedTransport(default=Response(...))
  with .calls → len(transport.requests)
- _TrackingTransport → RecordedTransport()  (aclose_calls attribute preserved)

Also fixes a pre-existing ruff format violation in recorded.py (ternary
expression on three lines instead of one).

No test behavior changes. Total test count unchanged from this commit's
edits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…onale

Final review concern: the docstring named the route/default type
(`Response | BaseException`) without explaining why BaseException
(not Exception) is the chosen union. Adds a one-paragraph note: the
choice lets test code express `asyncio.CancelledError`, `SystemExit`,
etc.; surfaces that these bypass `except Exception:`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lesnik512 lesnik512 merged commit 204d463 into main May 31, 2026
5 checks passed
@lesnik512 lesnik512 deleted the story/1-8-recordedtransport branch May 31, 2026 20:18
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.

1 participant