Test Step Authoring Guide (Adder + Executor)¶
This guide is for authors of new test steps and maintainers updating existing ones.
A “test step” always has two halves:
- a Fixture-phase adder (a method on
DancesTestCase) that specifies and registers the step, and - an Execution-phase executor (in
execution_steps) that runs the step and records its outcome.
Before adding either, confirm a new step type is actually the right answer — see §7.
The goal is to make TestCase authoring easy, fast, and safe by encapsulating subtle correctness requirements inside adders and harness helpers.
1. Core Concept: A Step Is a Contract¶
It is a contract composed during the Fixture Phase.
That contract specifies:
- Source intent: what holon the executor should operate on
- Expected outcome: what holon state the step should produce
- Step parameters: any additional data needed to perform the operation
This contract is materialized as:
- a
TestReference(source + expected), and - a concrete
TestStepstruct holding the TestReference plus step-specific parameters.
2. Adder Responsibilities (Fixture Phase)¶
2.1 Purpose of an Adder¶
An adder’s job is to fully define and register one test step.
Specifically, an adder must:
- derive the step’s source specification from an input
TestReference - compute the expected holon state produced by the step
- resolve expected relationship targets correctly when constructing expected graphs
- mint a new
TestReferencedescribing that step - decide whether the step continues an existing logical holon or creates a new one
- construct the concrete
TestStep - append that TestStep to the
TestCase
Adders exist so TestCase authors never need to reason about snapshot lifetimes, commit semantics, or token redirection.
2.2 Adder Inputs and Outputs¶
Conceptually, every adder follows this pattern:
Inputs
- a source
TestReference(unless the step is global, e.g.commit) - step-specific parameters (PropertyMap, relationship ops, flags, etc.)
- mutable access to
FixtureHolons - access to the fixture context (for cloning snapshots)
Outputs
- a newly minted
TestReference(except for some global steps likecommit) - a fully-constructed
TestStepappended to theTestCase
The adder is the only place where TestSteps are created and registered.
2.3 Canonical Adder Sequence (Critical)¶
For any step that produces an expected holon snapshot (most steps), the adder must follow this exact sequence.
Canonical Adder Sequence¶
- Derive the fixture-time source snapshot
- Derive the source snapshot through
FixtureHolonshelpers. - Treat the input token as a logical-holon handle, not as a guarantee that its embedded snapshot is the authoritative current source.
-
Never mutate this snapshot.
-
Clone the source snapshot into a working holon
- Use
FixtureHolons::copy_fixture_snapshotfor a transient fixture snapshot. - This is the only holon the adder may mutate.
-
If the source is Deleted, error unless the step explicitly supports Deleted input.
-
Apply the step’s effects
- Apply property changes, relationship edits, lifecycle transitions, etc.
- This produces the expected post-step holon content.
Relationship-step nuance:
- when embedding relationship targets into the new expected graph, do not
blindly use the literal historical snapshot carried by the target token
- instead, resolve the target token through FixtureHolons so the expected
graph embeds the logical holon's current expected head snapshot
- Freeze the expected output snapshot
- The working holon produced by applying the step’s effects becomes the ExpectedSnapshot snapshot and, due to "tight chaining" may become the SourceSnapshot snapshot in later TestSteps.
- Therefore, it should not be mutated after this point.
-
If there is any possibility the working holon could be mutated after this point, the adder must clone it and use the clone as the expected snapshot.
-
Mint the new TestReference
- Construct:
SourceSnapshot: derived from the input TestReferenceExpectedSnapshot: frozen snapshot + expected lifecycle state
-
Mint the TestReference via
FixtureHolonshelpers only. -
Register logical holon identity
- Decide whether this step:
- continues the same logical holon, or
- creates a new logical holon
-
Call the appropriate
FixtureHolonsregistration API. -
Construct and append the TestStep
- Create the concrete TestStep struct containing:
- the minted TestReference
- all step-specific parameters
-
Append it to the
TestCase. -
Return the minted TestReference
- Return the newly created TestReference to the caller.
- This returned reference is what TestCase authors use to chain subsequent steps.
It is very important that this exact ordering be followed. Doing the right operations in the wrong order leads to aliasing bugs, broken chaining, and unpredictable results.
Source Derivation vs Relationship-Target Expected Resolution¶
Adder authors need to keep two distinct harness behaviors separate:
- Source derivation
- determines which snapshot should become the next step's source
- supports head advancement across commit boundaries
- Relationship-target expected resolution
- determines which snapshot should be embedded as the target in a newly constructed expected relationship graph
- uses the target token as a handle to the logical holon's current expected head
These behaviors are related, but they are not interchangeable.
2.4 Deciding Whether a Step Creates a New Logical Holon¶
The adder author must explicitly decide whether the step yields:
Same logical holon (continue existing FixtureHolon)¶
Examples:
- with_properties
- add_relationship
- remove_relationship
- abandon_staged
- delete_saved
- many “modify in place” lifecycle steps
Rule of thumb: If the step conceptually modifies or transitions the same entity, reuse the FixtureHolon.
New logical holon (allocate new FixtureHolon)¶
Examples:
- stage_new_holon
- stage_new_from_clone (when treated as creating a new entity)
- explicit “create new” steps
Rule of thumb: If the step creates an independently identifiable entity, allocate a new FixtureHolon.
This decision is enforced through FixtureHolons; adders should not manage identity manually.
2.5 Special Step: Commit Adder¶
commit is special because it does not operate on a single source TestReference.
Validate the attempt's candidate dispositions and relationship-retry participants through
FixtureHolons, then append the Commit step with its prepared expectations. Only candidates
that advance receive result tokens; NoAction under Incomplete stays staged, and retry
participants retain their existing saved mappings. Expected rejection or command-level failure
mints no result tokens and leaves heads unchanged.
Follow the Commit outcome table for head advancement; do not advance every staged head unconditionally. TestCase authors keep using their existing TestReferences and never capture internal Commit result tokens.
3. Executor Responsibilities (Execution Phase)¶
3.1 Purpose of an Executor¶
An executor must:
- Resolve the correct execution-time source holon
- Perform the operation
- Validate actual vs expected outcome
- Record the execution result for chaining
Executors never mint tokens and never consult FixtureHolons.
3.2 Canonical Executor Sequence (Critical)¶
Source-bearing mutation executors follow this sequence. Global assertions, bootstrap operations, Commit, and rejection checks use their own outcome contracts and need not resolve one source or record a new holon.
- Resolve execution-time source holon
- Use the harness helper
ExecutionHolons::resolve_execution_reference - Provide the step’s
TestReference -
Relationship targets resolve through
resolve_relationship_targets; batches throughresolve_execution_references -
Execute the operation
- Perform create / update / stage / delete using real holon APIs
-
Capture the resulting runtime reference (or Deleted)
-
Validate the outcome
- Compare lifecycle state vs
ExpectedSnapshot.state - If live, compare content vs ExpectedSnapshot snapshot
- If deleted, ensure deletion semantics match expectation
Saved-content nuance:
- MatchSavedContent compares saved roots by definitional equivalence:
property values plus definitional relationships
- non-definitional persisted edges, including commit-generated inverse
SmartLinks, are intentionally ignored by that equality check
- use targeted traversal/assertion steps when a test needs to verify those
navigational edges explicitly
- Record the result
- Record the outcome against the ExpectedSnapshot token
- Use the harness helper
ExecutionHolons::record - Do not record against source tokens
This recording step is what enables subsequent steps to resolve correctly.
3.3 Special Executor: Commit¶
Retain candidate handles before dispatch, then compare observed dispositions and newly appended
operational errors with the attempt's declarations. Match saved results by committed identity,
and bind only the result tokens prepared by the adder. Under Complete, NoAction binds to
its saved source without consuming a saved result; under Incomplete, it receives no binding.
Relationship-retry participants retain their existing mappings and produce no further saved results.
For expected rejection, record no saved outcomes; staged candidates remain available to
VerifyCommitRejection. See execution correspondence
and the worked retry example for the shared contract.
4. Step Parameters and Expected Outcomes¶
4.0 Step shapes¶
Not every step operates on one holon. Choose the appropriate source and outcome
contract for each DanceTestStep:
Token-bearing steps carry a step_token: TestReference and follow the canonical adder sequence
in §2.3 — StageHolon, WithProperties, AddRelatedHolons, DeleteHolon,
AbandonStagedChanges, StageNewVersion, StageNewFromClone, LookupSavedHolonByKey, and so on.
Commit steps have no single source holon but carry tokens minted for the
expected saved outcomes. Their adders reason over FixtureHolons (§2.5).
VerifyCommitRejection separately carries retained staged-candidate tokens and
expected findings. Rejected Commit attempts must not advance those candidates
to saved expectations.
Global and assertion steps need no source token. Examples — EnsureDatabaseCount, MatchSavedContent, BeginTransaction,
LoadCoreSchema, and LoadHolonsInternal — assert against database or schema
state rather than against a holon the fixture is tracking. Their adders return no TestReference
and skip steps 1–6 of the canonical sequence entirely; they construct the step and append it.
Variants carry a description; operation steps expose their applicable expected
error or outcome contract. Consult test_steps.rs for each complete variant.
4.1 Step-specific parameters¶
TestReference contains only source and expected holon specifications.
All step-specific parameters belong in the TestStep struct, for example:
- PropertyMap
- relationship references
- flags and modes
4.2 Expected test outcome vs expected holon outcome¶
Some steps assert failure or error conditions.
These expectations:
- are not part of TestReference
- belong in the TestStep struct (e.g.,
ExpectedStepOutcome)
This is required because:
- some steps have no source TestReference (commit)
- some steps fail without producing a meaningful holon
5. Common Footguns (Explicitly Forbidden)¶
Adders must not:¶
- mutate snapshots from input TestReferences
- mint tokens without
FixtureHolons - infer commit effects from token history
- append incomplete TestSteps
Executors must not:¶
- mint fixture tokens
- consult
FixtureHolons - record results against source snapshot tokens
- bypass harness resolution helpers
One important boundary:
- executors may consult
ExecutionHolons - executors must not depend on fixture-time head tracking or author-side token interpretation rules directly
6. Summary Invariants¶
- Every adder produces exactly one complete TestStep
- Every token-bearing TestStep has exactly one TestReference; Commit carries outcome-token collections; other global steps need no source token; rejection assertions carry candidate tokens (§4.0)
- Adders follow clone → apply → freeze → mint → register → append
- Source-bearing mutation executors follow resolve → execute → validate → record
- Saved Commit expectations advance heads internally; rejection retains staged heads, and authors reuse old references
- Relationship adders must resolve target tokens through
FixtureHolonswhen building expected graphs MatchSavedContentis for saved structural equality, not for asserting every persisted navigational edge
These rules are foundational.
New test steps must follow them to remain compatible with commit semantics, fixture-time head selection, and tight chaining.
7. When a New Step Type Is Not the Answer¶
Adding a step type is the right move when a client-side operation has no adder yet. It is the wrong move when the thing you actually want to assert lives below the dance layer.
Do not add a step type in order to:
- observe which substrate action a write authored (
Createvs root-addressedUpdate) - prove that an Integrity callback fired, as distinct from a coordinator rejecting the input earlier
- pin an exact consensus-visible rejection message
- author an operation the production APIs deliberately cannot construct
- assert what a conductor will or will not dispatch
A client-visible error alone does not prove which internal enforcement path ran.
A step that checks only that error would provide weaker evidence. They belong in a conductor test: a plain
#[tokio::test] that calls the zome extern directly. See the
Conductor Test Framework.
Choose by the evidence required: client-visible outcomes can use the DSL or a standalone runtime test; claims about substrate actions or Integrity execution need direct conductor evidence.