Asks a venue's fake whether it accepts the order ids its own order-minting calls just returned.
The defect this exists for
A fake that refuses its own ids. dp_exchange_gemini's fake answered place_order/3
with an order, and then answered get_order/3 and cancel_order/3 for that same order
{:error, {:invalid_order_id, _}} — the id it had minted was not one its own read and
cancel would take (gemini issue #5). Every assertion in Core.AdapterContract passed:
group 5 calls get_order/3 with an id it makes up and accepts a refusal as an answer,
group 12 asks only that an active endpoint not answer :not_supported, and nothing placed
an order and then used what came back. A consumer's tier-1 test that places, reads back
and cancels — the first thing anyone writes against a trading fake — could not pass.
The same class is recorded in dp_exchange_webull's fake, whose place_order/3 echoed a
caller-chosen client_order_id that its own get_order/3 and cancel_order/3 then
refused as :not_found because they knew only one fixed id. Different mechanics, one
shape: the fake is a set of independently written functions, and nothing made them agree
about the one value that connects them.
What it checks
Four calls mint order ids. Each is exercised only when capabilities/0 declares it
active, and the ids it mints are collected:
place_order/3must answer{:ok, %Types.Order{}}with a non-empty binary:id.place_orders/3must answer{:ok, list}with one entry per request. An entry that is a%Types.Order{}(bare, or as{:ok, order}) must carry a non-empty binary:id, which is collected. An entry that is a refusal ({:error, _}or{:refused, _}) is the venue refusing that one order — the callback documents the partial batch as the expected shape — and is not a round-trip failure. A bare map (venue JSON, which is all[map()]promises) names an id this check cannot read without guessing the key, so it is not asked about.replace_order/4is exercised on an order the check places for the purpose (needsplace_order/3active), and must answer{:ok, %Types.Order{}}with an id. The id the replace answered is collected; the order that was replaced is not, because a venue may legitimately have retired it.close_position/3answers an order likeplace_order/3does. The suite opens no position, so a refusal ({:error, _}or{:refused, _}) is an answerDpExchange.Core.Venue.close_position/3'sresult/1type allows, and the call is not applicable rather than failed.{:error, :not_supported}is not such an answer for an active endpoint, nor is any other shape.
Every distinct collected id is then offered to the fake, reads first, then cancels:
- If
{:get_order, 3}is active,get_order/3must answer{:ok, %Types.Order{}}. - If
{:cancel_order, 3}is active,cancel_order/3must answer{:ok, %Types.Order{}}or{:ok, :cancelled}— the two shapesDpExchange.Core.Venue.cancel_order/3allows.
All reads run before any cancel, so a fake that forgets an order on cancel is not blamed for a read.
What it deliberately does not do
- It does not require the answered order to carry the placed id. A venue may key an order by a client id on the way in and its own id on the way out, and the contract does not say which. The id is checked for being accepted, not for being echoed.
- It does not fail a venue that declares
get_order/3orcancel_order/3:unsupported. Those are not asked at all; their{:error, :not_supported}is group 12's. - It does not run a minting call that is
:unsupported. There is no id to carry.run/3answers:skippedonly when none of the four ran, and a caller reading that as a pass is right. - It does not exercise
replace_order/4withoutplace_order/3. The suite has no order it could honestly say is open to replace;place_order/3being:unsupportedmakes the replace not applicable here. - It does not read ids out of venue JSON. A
place_orders/3entry that is a bare map is skipped, not guessed at. - It does not prove the real venue behaves this way. It drives a fake. A fake that accepts its own ids says the fake is self-consistent, nothing more.
Fail closed
An active minting call that does not answer as above is not a skip. This check answers
{:error, reason} and the caller fails, because treating a refusal as "nothing to
round-trip" is how three packages once passed assertions that never ran on them (see
docs/reference/core/assertion-coverage.md, "Second axis"). The one exception is
close_position/3's refusal, for the reason above. The remedy is in the message the suite
attaches: either the endpoint is not really active, or the fake needs an option the call
did not carry — declare it in endpoint_opts:.
A raise or an exit from the fake is captured and reported as the answer; it does not escape and take the rest of the test with it.
Summary
Types
The arguments for each call. place_order, place_orders and close_position are full
argument lists; replace_order, get_order and cancel_order are functions from an
order id to the full argument list, so the id lands in whichever position the caller's own
call shape puts it. Every key but place_order is optional; a missing key means that call
is not made.
A follow-up the fake refused: the id, the call that minted it, and what was answered.
Types
@type calls() :: %{ :place_order => [term()], :get_order => (String.t() -> [term()]), :cancel_order => (String.t() -> [term()]), optional(:place_orders) => [term()], optional(:replace_order) => (String.t() -> [term()]), optional(:close_position) => [term()] }
The arguments for each call. place_order, place_orders and close_position are full
argument lists; replace_order, get_order and cancel_order are functions from an
order id to the full argument list, so the id lands in whichever position the caller's own
call shape puts it. Every key but place_order is optional; a missing key means that call
is not made.
@type violation() :: %{ step: :get_order | :cancel_order, id: String.t(), minted_by: :place_order | :place_orders | :replace_order | :close_position, answered: term() }
A follow-up the fake refused: the id, the call that minted it, and what was answered.
Functions
Renders run/3 violations as one line per refused follow-up, for a failure message.
Renders a run/3 error, for a failure message.
@spec run(module(), DpExchange.Core.Capabilities.t(), calls()) :: :skipped | {:ok, [violation()]} | {:error, term()}
Mints order ids on fake and offers each to get_order/3 and cancel_order/3.
caps is the venue's Capabilities, read to decide which calls are active.
Returns :skipped when none of place_order/3, place_orders/3, replace_order/4 and
close_position/3 could run, {:ok, violations} when at least one did ([] is a pass),
and {:error, reason} when an active minting call did not answer as the contract
requires.