ADR-0053: The chart-author test helpers ship in lib/ under Statifier.Testing
Copy Markdown
View Source
Status: accepted (2026-08-19) - amends 0006 in part (the harness modules'
home: "feature detection moves to test/support" stops being a placement
rule and becomes a reference-direction rule; the corpus coupling constraint
itself stands unamended)
Context
st-hbdr asks for the chart-author test helpers to become a supported
surface. Statifier.Case gives a declarative chart test - test_scxml/4
takes the XML, the expected initial configuration, and a list of
{event, expected_configuration} steps - and Statifier.FeatureDetector
is what makes an unsupported-feature test fail with the feature named
instead of passing by accident. That pairing is exactly how this engine
tests itself, and exactly what a downstream chart author needs to test
their charts. It is also the engine half of the statifier-ui executable
expectations story: ADR-0006's corpus shape (a document plus expected
configurations per step) is the fixture format, and a published runner is
what makes such a fixture executable outside this repo.
Today none of it is reachable downstream. Both modules live under
test/support/, and mix.exs compiles that directory only in the test
env:
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]A downstream app that depends on statifier gets lib/ only. Writing
declarative chart tests today means copying harness files out of this
repo, which is the unsupported state this record ends.
Two written rules stand in the way, and they are the same rule stated
twice. docs/testing.md:
Unsupported-feature tests fail, not skip (v1's FeatureDetector rule, kept): a test that depends on an unsupported feature flunks with the feature named, so it can never masquerade as passing. Feature detection lives in
test/support, notlib/- it is harness code, not library surface.
And test/support/feature_detector.ex's own moduledoc:
This is harness code, not library surface: nothing in
lib/may reference it.
The rule has two distinguishable halves. The placement half -
"lives in test/support, not lib/" - exists so the harness is not
silently compiled into the library. The reference-direction half -
"nothing in lib/ may reference it" - is the load-bearing one: the
engine must never consult feature detection to decide behavior. Engine
semantics come from the Appendix D port (ADR-0002); an unsupported
feature must surface as a real error or a real conformance failure, never
as a detection-gated branch that lets the engine dodge the test that
would catch it. The fail-not-skip discipline lives on the harness side
for the same reason.
The constraints the decision must fit:
- ADR-0006's closed driving surface: the corpus couples to the library
through
Statifier.Caseand the nine enumerated public functions, and widening that set reopens ADR-0006, not this record. - One package at
2.0.0-dev(mix.exs), unreleased, with a fragment-based changelog (changelog.d/). - The coverage floor measures
lib/only -coveralls.jsonskipstest/support/- and Doctor holds 100% documentation thresholds on everything inlib/. - 281 generated corpus files say
use Statifier.Case, and the corpus generators (tools/corpus/scion/cases.exs,tools/corpus/scxml_w3/cases.exs) callStatifier.FeatureDetector.detect_features/1by that name.
Decision
The helpers are promoted into
lib/under an explicitStatifier.Testingnamespace.Statifier.CasebecomesStatifier.Testing.Case(atlib/statifier/testing/case.ex), keepingtest_scxml/4's shape, andStatifier.FeatureDetectorbecomesStatifier.Testing.FeatureDetector. The namespace is the boundary made self-documenting: everything under it is test-side surface for chart authors, supported and versioned with the engine, and nothing under it is engine.No separate
statifier_testhex package. The package route was the bead's alternative because it sidesteps the placement rule, but it sidesteps a one-paragraph amendment at the price of a second artifact held in permanent lockstep:FeatureDetector's registry is a mirror of exactly what the engine supports at a given commit, so every feature the engine gains would demand a paired release of the helper package, before this repo has shipped even once. Add a second release process, a second changelog, downstream needing two deps, and the cross-repo coordination overhead this project already pays elsewhere (ADR-0025), all to preserve the letter of a rule whose purpose is served more directly by amending it. Rejected.The FeatureDetector rule is amended, not deleted. The placement half is withdrawn: feature detection may live in
lib/, insideStatifier.Testing. The reference-direction half is kept and restated in namespace terms: no module inlib/outsideStatifier.Testing.*may reference anything inside it. The engine still never gates behavior on detected features, unsupported-feature tests still fail with the feature named rather than skip, and the fail-not-skip behavior now ships to downstream authors instead of being private discipline.docs/testing.mdis updated to say this on the implementing branch; ADR-0006 carries the amendment marker.The rest of
test/support/stays private test plumbing. Each for its own reason:Statifier.ContextRecorderandStatifier.TestContentare protocol-poking test doubles for engine internals (the executable-content context contract, stop-on-error, the pending error channel). Theirdefimpls exist precisely so they consolidate only in the test env; publishing them would freeze internal contract details into public API.Statifier.StreamOrderasserts the ADR-0044/0046 subscriber-stream ordering invariants - an internal delivery contract, not a chart author's concern. If downstream ever wants stream assertions, that is its own decision, not a rider on this one.Statifier.TmpDirexists to fix this repo's own concurrent-gate scratch race and carries a repo-internal env-var contract (STATIFIER_TMP_ROOT).Mix.Statifier.AdrJudgeCorpusis repo tooling for the ADR judge corpus tests.
The corpus keeps coupling through
test/supportshims.Statifier.Caseremains intest/supportas a thin wrapper whoseusingdelegates toStatifier.Testing.Case, andStatifier.FeatureDetectorremains as adefdelegatemodule, so the 281 generated files and the corpus generators need no regeneration on the promoting branch. ADR-0006's constraint is untouched: the corpus still drives the library through exactly the nine sanctioned functions, becauseStatifier.Testing.Case's internals are the same internals - promotion moves the module, not the coupling. Whether a later corpus regeneration adopts the new names and retires the shims is a housekeeping call, not a reopen of either record.Downstream consumption is the ordinary dep. A downstream app depends on
statifierand writesuse Statifier.Testing.Case, async: truein its chart tests. Noonly: :testcompanion dep exists or is needed. The helpers compile in every env, including:prod- the ecosystem's standard shape (Plug.Test,Phoenix.ConnTest,Phoenix.LiveViewTestall ship inlib/), and ExUnit ships with Elixir, souse ExUnit.CaseTemplateadds no dependency edge. The dead weight in a prod build is two modules that are never called.
Consequences
mix.exselixirc_pathsis untouched;test/supportremains test-env-only for the modules that stay.- The promoted modules join the
lib/quality regime: the 90% coverage floor and Doctor's 100% documentation thresholds now count them. Both already have direct tests (test/statifier/feature_detector_test.exs,test/statifier/case_test.exs) and moduledocs/specs, so this is pressure to keep, not a gap to open. No gate config file changes, so no ADR-0011 ledger entry is owed. - Dialyzer analyzes the promoted modules; if the PLT lacks
:ex_unit, adding it to the PLT apps is implementation detail, not a gate-config weakening. - Versioning is solved by construction: the helpers ship inside
statifier 2.0.0when it ships, and the feature registry can never drift from the engine it describes because they are one artifact.mix.exsstays2.0.0-dev; the change gets a user-facing fragment atchangelog.d/st-hbdr.md. - The follow-on implementation, in broad strokes (the plan stage owns
the details): move and rename the two modules into
lib/statifier/testing/; leave thetest/supportshims; updatedocs/testing.md's rule paragraph and the moduledoc rule statement to the namespace form; point the existing harness tests at the new names; write the downstream-facing documentation the acceptance criteria require; add the changelog fragment. - statifier-ui's executable expectations gain their runner as public
API: a fixture in the ADR-0006 corpus shape is executable downstream
via
Statifier.Testing.Case.test_scxml/4.
Open questions, recorded rather than resolved here:
- Whether the public entry point keeps the name
test_scxml/4or gains a friendlier alias. ADR-0006 pins the corpus-facing shape, not the downstream-facing name; the default is to keeptest_scxml/4unaliased and let real downstream friction argue for more. - Whether the session path's corpus-tuned timing constants
(
@settle_window_ms 100,@configuration_deadline_ms 4_000) become caller options with today's values as defaults. They were sized to this corpus's delay populations; a downstream chart with a longer load-bearing delay would need the knob. Leaning yes; the plan decides the option names. - Whether
Statifier.StreamOrderever joins the public surface for subscriber-stream assertions. Out of scope here; it would be its own decision record.