Skip to content

feat(testing): add SellerTestClient for in-process handler testing#666

Merged
bokelley merged 5 commits into
mainfrom
claude/issue-662-seller-test-client
May 12, 2026
Merged

feat(testing): add SellerTestClient for in-process handler testing#666
bokelley merged 5 commits into
mainfrom
claude/issue-662-seller-test-client

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Refs #662

Adds SellerTestClient — an in-process MCP test harness that eliminates the per-file boilerplate of constructing JSON-RPC envelopes, calling mcp.call_tool(), and extracting adcp_error from structuredContent. Adopters now get a single typed call that handles all three.

from adcp.testing import SellerTestClient

@pytest.fixture
def seller():
    return SellerTestClient(MySeller())

async def test_buy_not_found(seller):
    result = await seller.invoke("update_media_buy", {"media_buy_id": "missing", ...})
    assert not result.passed
    assert result.adcp_error.code == "MEDIA_BUY_NOT_FOUND"

The harness calls mcp.call_tool() in-process — it exercises the full handler dispatch and error-translation stack without HTTP or SSE framing. For HTTP-level tests (auth, CORS, middleware), build_test_client remains the right tool.

What this PR does not include (see #662 for status):

  • run_scenario() — requires bundled compliance scenario playbooks that are not yet in the SDK
  • A2A transport — served via a separate ASGI app; needs its own in-process client primitive

What was tested

  • pytest tests/test_seller_test_client.py — 18 new tests, all passing
  • pytest tests/ -q --ignore=tests/integration — 4447 passed, 1 pre-existing network flake (test_real_tls_handshake_still_validates_hostname)
  • ruff check src/adcp/testing/ — no issues
  • mypy src/adcp/testing/harness.py --ignore-missing-imports — no new errors from this file

New public API

Symbol Module Notes
SellerTestClient adcp.testing In-process MCP harness; takes DecisioningPlatform
ToolInvokeResult adcp.testing Return type with .passed, .data, .adcp_error, .raw
AdcpErrorPayload adcp.testing Typed container for wire adcp_error fields

validation=None (default) disables schema validation so tests target handler behavior. Pass SERVER_DEFAULT_VALIDATION to match production behavior (build_test_client does this by default — see migration note in __init__ docstring).

Nits surfaced by review (not fixed in this PR)

  • Private _mcp attribute accessed in two tests — low-value coupling to implementation detail; safe to remove in a follow-up cleanup
  • docs/testing-guide.md has no mention of SellerTestClient — should be added once the API stabilises
  • result.data may include context echo keys alongside business-payload keys when context-echo is active — documented in raw as the escape hatch

Pre-PR review

  • code-reviewer: approved — fixed both flagged issues (asyncio.Lock on _ensure_mcp; RuntimeError fallthrough guard for unknown FastMCP return shape)
  • dx-expert: approved — fixed DX blocker (removed unused # type: ignore[misc]); nits noted above

Triage-managed PR. This bot does not currently iterate on
review comments or PR conversation threads (only on the source
issue). To unblock:

  • Push fixup commits directly: gh pr checkout <num>
    fix → push.
  • Or re-trigger: comment /triage execute on the source
    issue.

See adcp#3121
for context.

Session: https://claude.ai/code/session_01VZRaadSyoHDm7CU945e3Ba


Generated by Claude Code

claude added 2 commits May 11, 2026 10:26
Eliminates the per-test-file boilerplate of constructing JSON-RPC
envelopes, calling mcp.call_tool(), and extracting adcp_error from
structuredContent. SellerTestClient.invoke() wraps all three steps
into a single typed call that returns ToolInvokeResult with a
.passed property and a typed AdcpErrorPayload.

Refs #662

https://claude.ai/code/session_01VZRaadSyoHDm7CU945e3Ba
- _ensure_mcp now holds asyncio.Lock to prevent double-initialization
  when concurrent coroutines call invoke() before _mcp is set
- FastMCP return-shape handling now guards against unknown shapes with
  an explicit RuntimeError instead of a silent type: ignore unpack
- Removes unused # type: ignore[misc] comment flagged by mypy

https://claude.ai/code/session_01VZRaadSyoHDm7CU945e3Ba
…ntent

ToolInvokeResult is the captured outcome of a single tool call — making it
mutable invited surprising patterns (the deleted test asserted you could
mutate adcp_error to flip passed). Match AdcpErrorPayload's frozen=True.

The raw field is structuredContent from FastMCP's call_tool result, not
the underlying CallToolResult or 2-tuple — rename so adopters reaching
for an escape hatch get an accurate name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Independent dx-expert + code-reviewer pass surfaced four issues:

- Rename .passed -> .ok. `assert result.passed` inside a pytest function
  reads as a tautology with the test runner's own pass/fail vocabulary;
  .ok is unambiguous before the API is publicly importable.
- Raise on missing required adcp_error fields. The harness exists to catch
  server non-conformance — silently substituting "UNKNOWN" for a missing
  code hid exactly the spec-violation cases adopters need surfaced.
- Document the FastMCP-coupling accurately. The 2-tuple success shape is
  produced by this SDK's _AdcpFuncMetadata.convert_result override, not
  the public FastMCP.call_tool contract. Add a defensive plain-dict branch
  so a FastMCP minor-version bump doesn't silently break the harness.
- Drop two tests that asserted on the private _mcp attribute; testing
  implementation, not behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley
Copy link
Copy Markdown
Contributor Author

Fold candidate from #678. Issue #678 proposes SellerA2AClient — a sibling in-process test harness for A2A adopters. It's same-author, same adcp.testing scope, and explicitly defers to this PR as the reference point.

Before this PR merges, consider whether to stub out SellerA2AClient (even just the class skeleton + __all__ entry) so adopters have the surface to import from. A minimal scaffold here would prevent a cold restart in #678 and preserve the design context while the SellerTestClient API is still fresh.

Suggestion only — skip if scope feels right as-is.


Triaged by Claude Code. Session: https://claude.ai/code/session_015nGuiswmggSyTdEinF6SgX


Generated by Claude Code

@bokelley bokelley marked this pull request as ready for review May 12, 2026 00:50
@bokelley bokelley merged commit 965eeda into main May 12, 2026
15 of 16 checks passed
@bokelley bokelley deleted the claude/issue-662-seller-test-client branch May 12, 2026 01:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants