DpExchange.Gemini.Fake (DpExchangeGemini v0.2.16)

Copy Markdown View Source

An in-process Gemini, for a consumer's tier-1 tests and for the conformance suite's active-endpoint assertion.

It is not a mock. Nothing is stubbed, no expectation is recorded, and no call is verified. It is a real implementation of DpExchange.Core.Venue that answers from memory instead of from the network, and it runs the same conformance suite as the real adapter.

Two rules

Less capable is allowed. Differently capable is not. Where this cannot answer, it returns an error. It never returns an empty success for something unsupported — a plausible wrong answer is worse than a failure, because only one of the two gets noticed.

It never rewrites a value the caller supplied, and it never stamps the current clock. @at below is fixed, because a fake that stamps utc_now/0 cannot be used to test anything about freshness and is itself the substitution this family refuses.

It models Gemini's refusals, and its two shapes of refusal differ

A fake where everything works proves half the contract. This one refuses:

  • a symbol it does not carry, with the real reason for that endpoint — Gemini does not refuse a bad symbol the same way everywhere, and neither does this fake: :invalid_symbol where Rest's own tests pin a JSON InvalidSymbol body (get_order_book/2, quantization/1, place_order/3), {:unknown_reason, text} where the venue answers with plain text instead (get_price/2, get_top_of_book/2 — measured live, /v1/pubticker — and get_historical_prices/4 — measured live, /v2/candles), or the venue's own unrecognised JSON reason (get_trades/2, measured live as BadRequest). A single invented :not_listed used to stand in for all of these, which is precisely the divergence the 2026-09-06 real/fake parity sweep exists to catch: a consumer's test pattern-matching the real shape passed here for the wrong reason;
  • a timeframe Gemini does not serve, including 2h, 4h and 12h, which the shared vocabulary models and this venue does not;
  • a range reaching before the venue's fixed window, with {:range_unavailable, …} rather than a short answer — because the real endpoint ignores bounds entirely and a truncated result reads as a complete one.

That last one is this venue's distinctive failure mode, and a consumer that has not handled it will find out here rather than in production.

Failure injection and anonymous mode

Every function below that has a real success path (not an unconditional Venue.not_supported()) checks DpExchange.Core.FakeInjection.next_outcome/1 or /2 first — a queued or always-set outcome from FakeInjection.queue_failures/2,3 or fail_always/2,3 short-circuits the fake's normal logic and is returned as-is. authenticated/2 also checks FakeInjection.credentials_bypassed?/1 before running the same scheme-resolution Private's Auth.headers/5 runs — see that function's own comment for the shapes it returns. Neither changes anything for a test that never calls FakeInjection — see that module for the full contract.

subscribe/2, unsubscribe/2 and update_symbols/2 are NOT wired: each takes a list of symbols in one call, and "this one symbol in the batch fails, the rest succeed" is a case whole-call injection cannot express — see FakeInjection's own moduledoc.

Summary

Functions

coverage_by_kind(opts \\ [])

@spec coverage_by_kind(keyword()) :: %{
  required(DpExchange.Core.Capabilities.data_kind()) => %{
    required(DpExchange.Core.Venue.symbol()) => DpExchange.Core.Venue.route()
  }
}

See DpExchange.Gemini.coverage_by_kind/1.

subscribe/2 above only ever pushes a Types.Quote (via get_price/2) — it never builds a Types.TopOfBook, so this fake is honestly :quotes-only. :top_of_book still appears as a key, empty, rather than being omitted: an omitted key here would read as "this fake does not know about that kind", where an empty map reads as what is actually true — the kind is declared, and nothing of it has been observed. Less capable than the real adapter is allowed; answering a different shape is not, so the keys match capabilities().streamable exactly, the same two the real adapter reports.