[Test] Extract the commerce-client contract from the HTTP client's tests so both transports can run it - #253
Merged
vedanshujain merged 1 commit intoSep 13, 2026
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-capbatch 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), 1stays 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, andleave only the raw HTTP status behind.
http-commerce-client-cart-order-id.test.ts(7 cases) — all 7 stay wire-only (raw-responsecoercion 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.livecounterpart (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 nowire-specific residue at all and is deleted outright.
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
makeClient()for the storefrontCommerceClient, and an optionalmakeAdminClients()for the admin rules/reporting and orders/products slices.arrange.product(spec)andarrange.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 tieris free to implement it for real once that stops being true.
the contract never touches a database directly.
Coverage the contract has today, and what later increments must add
CommerceClientmethods have a contract case today.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
expect(accountingReview
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