Dance Test Framework -- Design Specification¶
Scope. This specification covers client-visible semantics expressed through composed dance-test steps. Direct evidence of Integrity execution or substrate records belongs in conductor tests. Standalone runtime tests may also exercise client contracts without this DSL; see the Testing Strategy.
The goal of the Dance Test Framework is to make it easier and less error-prone to author and execute test cases. A Test Case is composed of a sequence of Test Steps.
The framework distinguishes two distinct phases:
- Test Case Definition (Fixture Phase)
- Test Case Execution (Execution Phase)
During the Fixture Phase, a test case is created and test steps are added using a set of step adders defined in the Dance Test Language module. These adders are the authoring-time building blocks from which Test Cases are composed.
During the Execution Phase, each Test Step is executed by a corresponding step executor, which performs the actual operation and validates that the observed result matches the expected result defined during the Fixture Phase.
Thus:
- Adders are Fixture Phase building blocks for describing intent and expectations.
- Executors are Execution Phase building blocks for performing operations and validating outcomes.
- The harness provides the shared data structures and orchestration that connect these two phases safely and deterministically.
0. Design Lineage & Rationale¶
0.1 Original Design Intent¶
The original Dance Test design aimed to:
- Separate fixture-time test definition from execution-time behavior
- Allow test steps to be chained safely
- Preserve snapshot immutability
- Make TestCase authoring declarative and low-friction
However, some concepts—particularly around step inputs vs step outputs—were implicit rather than explicit.
0.2 Converged Design Principle (Hybrid Model)¶
The converged design is based on the following principle:
A test step has both a starting point and an expected result, and these two roles have different semantics but must travel together as a single, immutable contract.
Rather than modeling separate “input tokens” and “output tokens”, the design models two roles within a single TestReference, with clear semantics, strong guardrails, and a clean separation between fixture-time intent and execution-time resolution.
This hybrid approach:
- Preserves the critical insight that steps have two snapshots
- Avoids introducing multiple tokens per step
- Keeps TestCase authoring simple and linear
- Encapsulates complexity inside adders and the harness
1. Design Goals¶
The design must:
- Make TestCase definition easier, faster, and less error prone
- Allow TestCase authors to compose test steps declaratively
- Encapsulate complexity inside Dance Test Language adders
- Preserve immutable snapshot semantics
- Cleanly separate fixture-time from execution-time
- Support reuse of prior steps (e.g. delete-after-delete)
- Correctly model commit as a multi-entity operation
- Provide clear guidance for future adder and executor authors
2. Core Concepts¶
2.1 Shared Holon State Vocabulary¶
A single enum defines the lifecycle state of a holon at a point in time, shared across fixture-time modeling and execution-time validation.
pub enum TestHolonState {
Transient,
Staged,
Saved,
SavedLookup,
Abandoned,
Deleted,
}
This enum is descriptive, not behavioral. It records what state a holon is in; it never decides what happens next.
The same enum appears on both the source side and the expected side of a step, but answers a different question on each:
- On the source side, it declares the lifecycle state the executor should assume when resolving the token to a runtime holon.
- On the expected side, it declares the lifecycle state the step should produce, which the executor validates against the observed outcome.
Failure is deliberately not a state here. A step expected to fail declares that through the step's own expected-outcome parameter (§4.2 of the Test Step Authoring Guide), not by placing the holon in an error state.
2.2 TestReference (Step Contract)¶
A TestReference is the immutable contract for a single Test Step.
It has two essential jobs:
- Provide the Test Step executor with a starting point
- Provide the executor with the expected result to validate against
A TestReference is:
- Immutable once created
- Opaque to TestCase authors and adder authors
- Safe to pass and reuse across steps
- The sole artifact executors receive to understand step intent
Structurally, it contains two conceptual halves:
pub struct TestReference {
source: SourceSnapshot,
expected: ExpectedSnapshot,
}
All fields are private; interaction is via controlled accessors only.
2.3 SourceSnapshot (Execution Starting Point)¶
The source side of a TestReference exists to answer the question:
“What holon should this step operate on at execution time?”
pub struct SourceSnapshot {
snapshot: TransientReference,
state: TestHolonState,
}
impl SourceSnapshot {
pub fn id(&self) -> SnapshotId {
self.snapshot.temporary_id().into()
}
}
SnapshotId is an alias for TemporaryId; snapshot lookup maps use it; logical-holon maps use FixtureHolonId.
Semantics¶
- Identifies the holon that execution should target
- Carries the intended lifecycle state used during execution-time resolution
- Never mutated
- Used only by executors and the harness
The source side does not represent mutable state or expected content.
2.4 ExpectedSnapshot (Expected Result)¶
The expected side of a TestReference exists to answer the question:
“What should the result of this step be?”
pub struct ExpectedSnapshot {
snapshot: TransientReference,
state: TestHolonState,
}
Semantics¶
- Used only for validation and chaining
- Its identity may locate a recorded runtime result through
ResolveBy::Expected - Snapshot is immutable
- The snapshot is always present. A
Deletedexpectation still carries one, but it conveys identity only — its content is not meaningful and must not be compared
Executors compare actual outcomes against fixture snapshot content. They may use its identity to look up the corresponding recorded runtime result; the fixture snapshot itself is not that runtime result.
However, fixture-time adders may still interpret a TestReference as a handle to
its owning logical FixtureHolon when constructing a new expected graph. In
particular, relationship adders may resolve a target token to the logical
holon's current expected head snapshot rather than embedding the literal
historical snapshot carried by the token.
2.5 Chaining Semantics¶
Conceptually:
The expected result of step N becomes the starting point for step N+1.
This chaining is expressed in fixture-time logic and enforced by adders and the harness, not by TestCase authors manually wiring references.
ExpectedSnapshot provides a controlled way to derive a new source:
impl ExpectedSnapshot {
pub fn as_source(&self) -> SourceSnapshot {
SourceSnapshot::new(self.snapshot.clone(), self.state)
}
}
This conversion is total. Whether a Deleted expectation may serve as a source is decided by
FixtureHolon, which falls back to its last live snapshot when its head is deleted — see §5 and the
Test Harness Design Spec.
2.6 Test Case Construction and Finalization¶
A test case is not assembled piecemeal. TestCaseInit constructs the four things a fixture needs —
the DancesTestCase, the fixture context, FixtureHolons, and FixtureBindings — together, so
authoring can never begin from a partially initialized state.
Construction is bracketed:
- Open with
TestCaseInit::new(name, description), destructuring all four components. - Add steps through adders, which are the only things that may append to the case.
- Close with
finalize(), exactly once, after all steps are added and before returning the case.
DancesTestCase carries an is_finalized flag that enforces the closing bracket. Steps cannot be
appended after finalization.
finalize() is also what captures fixture-time state for the execution phase, into
TestSessionState:
- the transient holon pool, so
TransientReferences minted during the Fixture Phase resolve at execution time - the fixture head index, so tokens map to the correct logical holon heads
This is the mechanism behind the rule that fixtures may create transient holons but must not stage them: the transient pool crosses into the test runtime, the fixture's staged holons do not.
3. Immutability & Guardrails¶
3.1 Fundamental Invariant¶
Adders must never mutate holons obtained from a TestReference.
All mutation must occur on fresh clones created explicitly inside the adder.
3.2 Structural Enforcement¶
Immutability is enforced by design:
- Adders receive TestReferences, not holons
TestReferencefields are private, and its constructor is crate-internal, andFixtureHolonsowns token minting by harness convention- A snapshot reached through a TestReference is only ever mutated after being explicitly cloned into a fresh working holon
This makes accidental mutation of prior snapshots difficult or impossible.
4. Role of Adders and the Dance Test Language¶
The Dance Test Language exists to encapsulate complexity so TestCases remain simple.
Adders are responsible for:
- Obtaining the correct starting snapshot
- Obtaining the correct expected relationship targets when building expected graphs
- Enforcing lifecycle rules
- Preserving immutability
- Managing fixture-time identity
- Encoding commit, delete, and error semantics
- Minting new TestReferences
TestCase authors compose steps; adders absorb nuance and prevent footguns.
Adders are the primary abstraction boundary of the framework.
5. Fixture-Time Identity: FixtureHolons¶
TestReferences describe step results, but they do not model entity identity across steps.
That role is handled by FixtureHolons.
pub struct FixtureHolon {
head_snapshot: ExpectedSnapshot,
last_live_snapshot: ExpectedSnapshot,
}
pub struct FixtureHolons {
fixture_context: Arc<TransactionContext>,
pub tokens: Vec<TestReference>,
pub holons: BTreeMap<FixtureHolonId, FixtureHolon>,
pub snapshot_to_fixture_holon: BTreeMap<SnapshotId, FixtureHolonId>,
}
Semantics¶
- Every TestReference belongs to exactly one FixtureHolon
- Multiple TestReferences may refer to the same FixtureHolon
- Logical identity is the map key (
FixtureHolonId, aUuidnewtype), not a field - Lifecycle state is derived, not stored:
FixtureHolon::state()returns the head snapshot's state, so the head is authoritative for “current” state by construction last_live_snapshotholds the most recent non-Deletedsnapshot, and is used as the source when the head is deleted. This is what makes delete-after-delete and other post-delete steps expressible; adders must not reimplement the fallback
Full field semantics are in the Test Harness Design Spec.
6. Snapshot Currency and Token Interpretation¶
A key requirement—especially after commit—is that:
The token passed to an adder must be treated as a stable handle to a holon, not as a guarantee of snapshot currency.
Therefore:
- The literal snapshots carried inside a
TestReferenceremain immutable historical facts FixtureHolonsis authoritative for deciding how a token should be interpreted- Different harness APIs may intentionally interpret the same token differently depending on purpose
In the current model, the important interpretations are:
- Source derivation
- adders use
FixtureHolonsto derive the appropriate next source snapshot - when an older token is passed to a later adder after commit, the adder mints a new step token whose source reflects the logical holon's current fixture-time head
- adders use
- Relationship-target expected resolution
- relationship adders resolve target tokens through
FixtureHolons - this embeds the target logical holon's current expected head snapshot in the new expected relationship graph
- relationship adders resolve target tokens through
- Execution-time resolution
- executors use the source side of
TestReference - runtime resolution remains a separate concern from fixture-time graph construction
- executors use the source side of
This behavior is embedded in harness APIs, not exposed as a new token type.
Consequences:
- Passing an older token after commit is safe when the receiving adder uses
FixtureHolonsto interpret it as a logical fixture-holon handle - TestCase authors do not need to update references
- Adders do not need to reason about commit explicitly when deriving sources
- Relationship adders must not assume that embedding a token's literal expected snapshot is the right target behavior
Terminology rule:
- “Head” and “current” are fixture-time concepts
- Prefer “derive” or “select” for fixture-time head decisions, and “resolve” for execution-time runtime-reference lookup. Existing helper names may use “resolve” for fixture-time target selection, but the surrounding text should make the phase explicit.
7. Commit¶
7.1 Why Commit Is Special¶
Commit does not operate on a single TestReference.
At runtime, commit:
- Iterates over the holon pool
- Attempts to commit all staged, non-abandoned holons
At fixture time, the commit adder mirrors that global shape through
FixtureHolons:
Commit reasons over FixtureHolons, not tokens.
7.2 Commit Behavior (Fixture Phase)¶
During the Fixture Phase, an expected Rejected Commit creates no saved tokens
and leaves fixture heads staged. When constructing saved expectations, the
commit adder:
- Iterates over all FixtureHolons
- Selects those in
Stagedstate - Predicts commit outcomes
- Advances the selected FixtureHolon heads to saved expectations;
Erroris not aTestHolonState - Mints new head TestReferences so the post-commit expected state is represented
These new head tokens:
- Are recorded internally
- Are not returned to TestCase authors
- Are consulted later through
FixtureHolonswhen adders derive sources or relationship-target expectations
This allows commit to be global without breaking linear authoring.
8. Execution-Time Resolution¶
At execution time, TestReferences are mapped to runtime holon references through
the execution registry, not through FixtureHolons.
Execution-time resolution:
- Selects the source or expected side according to
ResolveBy - Looks up the recorded execution result for that snapshot identity via
ExecutionHolons - Interprets intended lifecycle state
- Chooses the appropriate runtime representation
- Extracts saved holon IDs when required (e.g. delete)
Execution uses ResolveBy::Source for a step’s input and
ResolveBy::Expected when looking up a recorded output, including relationship
targets and rejected-candidate assertions. Expected snapshot content remains
fixture data; its identity locates the separately recorded runtime result.
Fixture-time interpretation of relationship target tokens is separate from this:
relationship adders may use FixtureHolons to select the current expected head
snapshot for graph expectations, which is distinct from looking up a recorded runtime result by expected
snapshot identity.
9. Saved-Content Comparison Semantics¶
MatchSavedContent does not require exact equality across every persisted edge.
Instead, saved roots are compared by:
- essential holon content
- exact definitional relationship presence/member agreement
Under this rule:
- non-definitional persisted edges, including commit-generated inverse SmartLinks, are intentionally ignored by saved-content equality
- definitional relationship members are compared by saved holon identity where applicable
- each saved fixture holon is still compared independently as its own root, so nested relationship member content is not recursively revalidated from every occurrence of every edge
Implication for test authors:
- use
MatchSavedContentto assert saved structural identity - use targeted traversal/assertion steps when a test needs to verify non-definitional navigational edges explicitly
10. Delete Semantics¶
- Delete requires a saved holon identity
- Saved identity is obtained from execution-time resolution
- Deleted expected holons contain no snapshot
Delete-after-delete scenarios are supported by reusing appropriate source TestReferences and validating expected outcomes.
11. Conceptual Summary¶
- TestReference is the step contract: starting point + expected result
- SourceSnapshot defines what execution operates on
- ExpectedSnapshot defines what should result
- FixtureHolon defines entity identity across steps
- Head token defines snapshot currency
- Adders encapsulate complexity
- Commit advances lifecycle globally
- Execution resolves and validates
This hybrid design preserves implementation insights, keeps authoring simple, and prevents subtle lifecycle and aliasing bugs.
12. Status¶
This document reflects the current converged design of the Dance Test Framework and should be used as the conceptual foundation for implementation, review, and onboarding.