# `DpExchange.Core.FakeInjection`
[🔗](https://github.com/DistortionPoint/dp-exchange-core/blob/v0.3.120/lib/dp_exchange/core/fake_injection.ex#L1)

Deterministic failure injection and a credential-free wiring mode, for a venue's `Fake`
to consult at the entry of its own public functions.

## Why this exists

A consumer migrating fully onto the real per-venue `Fake` modules for testing (rather
than keeping a parallel hand-rolled test double indefinitely) needs two things none of
the four `Fake`s exposed on their own: a way to make a `Fake` fail on demand, to test a
consumer's own retry/circuit-breaker/alerting code without reaching a real venue; and a
way to skip a `Fake`'s venue-faithful credential check, to test pure dispatch/decode
logic without constructing valid-looking credentials for every call.

## Deterministic, never a rate

Every outcome here is queued explicitly and consumed in order. There is no
`error_rate: 0.5`-shaped knob and there will not be one: a test that fails 30% of the
time on its own schedule is not more useful than one that never fails, and "dialable"
was always asking for exact, reproducible control, not simulated flakiness.

## Two independent axes

**Which functions one override reaches** is not configurable — every public function a
`Fake` gates through `next_outcome/1` or `next_outcome/2` is gated the same way. This
was a real design question (see `docs/design/2026-09-04_webull-sharding-and-fake-injection.md`
§3.6.1 in `dp_exchange_core`) and the answer is uniform gating: the feature this
replaces asked for exactly one knob, and building per-function targeting nobody asked
for is the same speculative surface this family's own conventions rule out on a bug fix.

**Which symbol one override reaches**, for a function that takes one, IS configurable —
a symbol-targeted override can never be satisfied by, or interfere with, a call for a
different symbol (§3.6.2). `queue_failures/2` and `fail_always/2` are whole-call;
`queue_failures/3` and `fail_always/3` target one symbol. A symbol-specific queue is
always checked before the whole-call one.

## Built on `Config`, not a new mechanism

Every value here lives behind `DpExchange.Core.Config.put_override/2` — process-scoped,
`$callers`-aware, safe under `async: true`, for the exact reason `Config`'s own
moduledoc gives: a node-wide seam configured by one test configures it for every other
async test running beside it. **This means injection only reaches a `Fake` function
called from the configuring test's own process (or a `Task` it spawned)** — a `Fake`
called from inside a separately-supervised `GenServer` (as some streaming paths are)
will not see it, the same limitation every other `Config`-based seam in this family
already has.

**A `Task` consumes the test's queue, not a copy of it.** The state lives in an `Agent` the
configuring process starts and links to itself; only its pid sits in the process
dictionary, where a `Task` finds it through `$callers`. Each pop is one
`Agent.get_and_update/2`, so an outcome popped in a task is gone for the test's next call,
and two tasks cannot both receive the head of one queue. The agent ends with the process
that started it.

## Scope

Whole-call injection is exactly that: it picks the outcome one `Fake` function call
returns. It does not simulate a partially-failing *batch* — a function that takes a
list of symbols in one call (a venue's own bulk subscribe, say) and needs one symbol
within that batch to fail while the rest succeed needs the fake response itself to
carry partial success, which this does not attempt. Flagged as an open question in the
design doc, not resolved here.

# `outcome`

```elixir
@type outcome() :: term()
```

Whatever a `Venue` callback may return: a value to hand back as-is.

# `state`

```elixir
@type state() :: %{
  global: [outcome()],
  global_always: outcome() | :none,
  by_symbol: %{optional(String.t()) =&gt; [outcome()]},
  symbol_always: %{optional(String.t()) =&gt; outcome()},
  bypass_credentials?: boolean()
}
```

Injection state for one venue, held behind one `Config` override.

# `bypass_credentials`

```elixir
@spec bypass_credentials(atom()) :: :ok
```

Makes `venue`'s `Fake` skip its normal credential check for the rest of this process
(or a `Task` it spawns) — the default, venue-faithful refusal is untouched for any test
that does not call this.

# `credentials_bypassed?`

```elixir
@spec credentials_bypassed?(atom()) :: boolean()
```

Whether `bypass_credentials/1` is in effect for `venue` in the calling process.

# `fail_always`

```elixir
@spec fail_always(atom(), outcome()) :: :ok
```

Every whole-call `next_outcome/1` (and unmatched `next_outcome/2`) returns `outcome` until `reset/1`.

# `fail_always`

```elixir
@spec fail_always(atom(), String.t(), outcome()) :: :ok
```

Every `next_outcome/2` for `symbol` returns `outcome` until `reset/1`.

# `next_outcome`

```elixir
@spec next_outcome(atom()) :: {:override, outcome()} | :none
```

For a `Fake` function with no symbol argument.

# `next_outcome`

```elixir
@spec next_outcome(atom(), String.t() | nil) :: {:override, outcome()} | :none
```

For a `Fake` function whose call names `symbol`. Checks, in order: an always-fail set
for this exact symbol, a queued outcome for this exact symbol, an always-fail set for
every call, a queued whole-call outcome — the first of these that has something wins.

# `queue_failures`

```elixir
@spec queue_failures(atom(), [outcome()]) :: :ok
```

Queues `outcomes` to be returned, in order, by the next matching whole-call
`next_outcome/1` reads (and `next_outcome/2` reads for a symbol with no
symbol-specific queue of its own). Each call to `next_outcome/1` or `2` pops one entry;
once exhausted, normal `Fake` behaviour resumes.

# `queue_failures`

```elixir
@spec queue_failures(atom(), String.t(), [outcome()]) :: :ok
```

Queues `outcomes` for `symbol` only. A call naming a different symbol — or no symbol at
all — never sees these and is never affected by them.

# `reset`

```elixir
@spec reset(atom()) :: :ok
```

Clears every override (queued failures, always-fail, credential bypass) for `venue`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
