# `Raxol.Core.Telemetry.InvariantSentinel`
[🔗](https://github.com/DROOdotFOO/raxol/blob/v2.7.0/lib/raxol/core/telemetry/invariant_sentinel.ex#L1)

Arms a registry's `invariant_events/0` for the duration of each test and
fails the test if one of them fired.

    defmodule Raxol.Payments.SettlementTest do
      use ExUnit.Case, async: false
      use Raxol.Core.Telemetry.InvariantSentinel, registry: Raxol.Payments.Telemetry
    end

`registry:` is the only option and it is required: it names a module built
with `use Raxol.Core.Telemetry.Invariants`. An invariant event is one that
can only fire if Raxol itself is wrong. Before this mechanism existed, such
an event was emitted, logged, and ignored -- which is how a turn that
delivered zero content shipped while the library was warning about it on
stderr. A guard that nothing asserts on is not a guard.

## Why this module lives in `lib/` and not `test/support/`

Dependent packages `use` it from THEIR tests, and a dependency's
`test/support` tree is not on the consumer's load path -- only `lib/` is
compiled into the delivered application. ExUnit is therefore touched only
inside functions that this module injects into, or that are called from, a
test module (`ExUnit.Callbacks.on_exit/1`, `ExUnit.Assertions.flunk/1`).
Nothing here requires ExUnit at runtime for a normal library consumer, and
nothing here is executed by simply loading `raxol_core`.

## Declaring an invariant on purpose

A test that DELIBERATELY drives a bad path declares it:

    @tag expect_invariant: [[:raxol, :payments, :xochi, :unchecked_settlement]]

A single event may be given unwrapped (`expect_invariant: [:raxol, :payments,
:xochi, :unchecked_settlement]`). The declaration is an ASSERTION, not a mute
button: such a test also fails when the event does NOT fire. That is the
point -- it converts "this test happens to drive a bad path" into "this test
pins the bad path". Declaring an event the registry does not classify as
`:invariant` is itself a failure, so the tag cannot drift away from the
registry.

## Only sound in `async: false` modules

`:telemetry` handlers are global and run in the EMITTING process, so a
handler cannot tell which concurrently-running test caused an event: an
invariant fired by test A would be reported against test B. Every module that
uses this sentinel MUST therefore be `async: false`. A few hundred
milliseconds of serial runtime is worth a guard that cannot misattribute.

Pid-ancestry filtering is deliberately NOT attempted: `$ancestors` does not
survive the `Task.Supervisor` hops these systems use, so it would silently
drop the events most worth catching.

## How the events survive the test process

The handler runs in whichever process emitted the event, and the verdict is
rendered from `on_exit/1` (the only hook that runs after the test body), by
which time the test process is already gone -- so messaging `self()` from the
handler cannot work. Fired events are parked in a public ETS table owned by
one long-lived collector process, keyed by the per-test handler id, and
dropped when the verdict is rendered. Table and collector names are derived
from the registry module, so two packages' sentinels cannot collide in one
BEAM.

# `arm`

```elixir
@spec arm(map(), module()) :: :ok
```

Attach a per-test handler for every invariant event in `registry` and
register the end-of-test verdict. Called by the `setup` that `__using__/1`
injects.

---

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