v0.6.0
Opt-in RSpec matchers over the whole introspection surface. One require
in your spec helper — require "statecraft/rspec" — includes seven
matchers into every example group; RSpec never becomes a runtime
dependency of the gem, and an isolated-process probe keeps it that way.
The question matchers consult the guards with the metadata your
production call will carry:
allow_event(:pay).with_metadata(...)overcan_fire?refuse_event(:cancel).because_of(:guard)— the refusal with its
reason, named fromrefusals_forallow_transition_to(:paid).via(:pay)/.directlyover
available_transitionshave_transitioned_to(:paid)— strictly log-based
The class-level pair answers the graph's shape, guards untouched:
have_edge(:pending, :cancelled).via(:cancel) and
have_initial_state(:pending).
The block matcher asserts the transition itself — the state move AND
the appended log row in one expression:
expect { order.fire!(:pay, metadata: { "amount" => 100 }) }
.to transition(order).from(:pending).to(:paid)
.via_event(:pay).with_metadata("amount" => 100)
A failing matcher explains itself from the same introspection the
pipeline consults: the current state, the reachable edges, the refusing
guard with its layer. Two honest limits, stated in the README: bang-form
exceptions fly through like with change, and because_of names
record-layer guards only — an input-reading guard: has no name there,
and the failure message says so instead of guessing.
The example app's suite now exercises the matchers in a real Rails
application on every CI run.