Skip to content

[Test] Extract the commerce-client contract from the HTTP client's tests so both transports can run it - #253

Merged
vedanshujain merged 1 commit into
feat/in-process-commercefrom
test/commerce-client-contract
Sep 13, 2026
Merged

vedanshujain merged 1 commit into
feat/in-process-commercefrom
test/commerce-client-contract

Conversation

@vedanshujain

Copy link
Copy Markdown
Contributor

What

The eight HTTP-client test files each mix two kinds of assertion: what the commerce client
returns, and how that return traveled over HTTP. This increment lifts the transport-agnostic
assertions out of all eight files into one contract, driven by a small tier interface, so the
same cases can run against the HTTP client today and against an in-process client later. The
wire-specific cases — request shape, headers, path encoding, status-to-error mapping — stay
behind in transport-only files that go away once the service does. Test-only; this is the
eighth and last Phase A increment of work order 02.

The classification rule

An assertion is transport-agnostic, and belongs in the contract, when it is about the client's
method contract: inputs, returned values, typed result tokens, typed rejections, idempotency
replay, money as integer minor units, snapshot semantics. It is HTTP-wire-specific, and stays in
the transport's own file, when it asserts request shape or method, any header (gate tokens
included), base-URL joining or path encoding, status-to-error mapping, retry on 5xx, egress
allowlisting, or a stub server's recorded requests. An assertion is never weakened to move it —
a case may instead split, keeping the client-observable half in the contract and only the wire
half behind. Nothing was dropped: every assertion across the eight files is accounted for as
contract, wire-only, or (for five cases) split between the two.

What moved

  • http-commerce-client.test.ts (12 cases) — 10 to the contract, 2 stay wire-only (an over-cap
    batch lookup and a missing-id rejection, both status-to-error mapping).
  • http-commerce-client-cart.test.ts (17 cases) — 16 to the contract (one whole, five split), 1
    stays wire-only whole plus 5 split wire-status halves. The five split cases hand the computed
    integer totals and every typed refusal reason to the contract through quoteCheckout, and
    leave only the raw HTTP status behind.
  • http-commerce-client-cart-order-id.test.ts (7 cases) — all 7 stay wire-only (raw-response
    coercion cases, unreachable without controlling the wire).
  • http-commerce-client-checkout.test.ts (27 cases) — all 27 stay wire-only (URL/method/body/
    header assertions and status-to-reason tables).
  • http-commerce-client-entitlement.test.ts (3 cases) — all 3 stay wire-only (query shape,
    auth header, status normalization).
  • http-commerce-client-service-token.test.ts (2 cases) and its .live counterpart (3 cases) —
    all 5 stay wire-only (service-token header threading over a stub and over a live service).
  • admin-rules-client.test.ts (4 cases) — moved whole to the contract; the file had no
    wire-specific residue at all and is deleted outright.
  • Three case names that described wire mechanics were reworded to the behaviour they actually
    assert (an idempotency-key case, a batch-lookup case, and an adjust-line case), with no change
    to what either half asserts.

The tier interface

  • A transport supplies makeClient() for the storefront CommerceClient, and an optional
    makeAdminClients() for the admin rules/reporting and orders/products slices.
  • arrange.product(spec) and arrange.cart(currency?) seed only what a case needs pre-existing
    (a commerce row, a cart) — never the method actually under test in that case. The seeding rule:
    a case whose subject is a write method calls that method directly; a case that merely needs a
    product or cart in place uses arrange.
  • reset() may be a no-op tier-side, but only because every case today uses disjoint ids; a tier
    is free to implement it for real once that stops being true.
  • The stub server (and the live-service harness) stand in for the wire — never for a database;
    the contract never touches a database directly.

Coverage the contract has today, and what later increments must add

  • Storefront: 14 of 25 CommerceClient methods have a contract case today.
  • Admin orders + products: 0 of 18 methods — no test file ever exercised these clients.
  • Admin rules: 18 of 25 methods.
  • Reporting: 0 of 6 methods.

The in-process increments that follow this one add contract cases for every gap above, and run
the whole contract against both the HTTP tier and the in-process tier.

Verification

Check Result
lint / typecheck / format clean
expect( accounting 166 before, 166 after — exact match, nothing dropped or weakened
Plugin project, with Postgres 1013 tests passed (contract tier file: 30 passed)
Plugin project, without Postgres 960 passed / 53 skipped — no live service needed
Base-branch comparison delta exactly +5 tests (the five splits), 0 failures on either side

Review

Two independent reviews: round 1 one approve and one request for a misclassification of five
quote cases; round 2 both approve after the lift and a tier-design cleanup; an independent
verification run reconciled the assertion accounting and confirmed behaviour-neutrality against
the base.

🤖 Generated with Claude Code

https://claude.ai/code/session_011NjdC8awspUte5wML6eY2X

…sts so both transports can run it

The HTTP client's test body IS the spec for the commerce client surface, and a
spec only one transport can execute cannot prove a second transport equivalent.
Lift the transport-agnostic cases out of the eight client test files into
`packages/plugin/test/contracts/commerce-client-contract.ts`, exported as the
three slices the in-process client will consume, and bind them to the existing
live-service harness from a new tier file.

The contract knows nothing about how a call travels: every case is arrange
backend state -> call a client method -> assert the returned value. Transports
supply a `CommerceClientTier`: `name`, `setup`/`teardown`, `reset`, `makeClient`,
an OPTIONAL `makeAdminClients` and an `arrange` API with `product` and `cart` —
derived from what the source files actually seed, which is only commerce rows and
carts, through the clients' own writes. A case whose subject is a write method
still calls that method directly; only a case that merely needs a product or cart
to exist goes through `arrange`. No clock, id or hold-expiry hooks: the lifted
cases control time solely through explicit watermark arguments and identity
through explicit idempotency keys, so there is nothing for a hook to do yet. An
admin slice handed a tier with no admin clients fails loudly at collection rather
than running empty, and every case uses disjoint ids — which is the only reason a
tier may implement `reset()` as a no-op, as the README says.

The wire assertions are not weakened and not moved. Request shape, headers
(including both gate tokens), path encoding, status-to-error mapping and the HTTP
statuses the quote route answers on stay in the transport's own files, which are
deleted with the transport. Five cart cases reached past the client to
`POST /checkout/quote`; `quoteCheckout` is on the port, so each is split — the
computed totals in integer minor units and every typed refusal reason are
asserted through the client here, and only the status code stays behind. Three
more cart cases asserted a client half and a wire half together and split the
same way. `admin-rules-client.test.ts` had no wire-specific residue at all and is
deleted outright. Three test names that stated wire mechanics are renamed to the
behaviour they actually assert.

Behaviour-neutral. The `expect(` accounting closes exactly: 166 textual
occurrences before and after, 165 real assertions plus one commented-out future
assertion on both sides. Test count 1008 -> 1013, exactly the five splits, with
the leaf-name diff containing those and the three renames and nothing else.
Live-service gating is unchanged — the same six files skip without a database,
and `pnpm test` still requires no live service. No `src/` diff.

The contract is what survives INC-D3b; the tier file dies with the HTTP client.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011NjdC8awspUte5wML6eY2X
@vedanshujain
vedanshujain merged commit 0321e50 into feat/in-process-commerce Sep 13, 2026
2 checks passed
@vedanshujain
vedanshujain deleted the test/commerce-client-contract branch September 13, 2026 20:55
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