Skip to content

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:

  1. Test Case Definition (Fixture Phase)
  2. 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:

  1. Provide the Test Step executor with a starting point
  2. 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 Deleted expectation 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
  • TestReference fields are private, and its constructor is crate-internal, and FixtureHolons owns 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, a Uuid newtype), 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_snapshot holds the most recent non-Deleted snapshot, 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 TestReference remain immutable historical facts
  • FixtureHolons is 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 FixtureHolons to 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
  • 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
  • Execution-time resolution
    • executors use the source side of TestReference
    • runtime resolution remains a separate concern from fixture-time graph construction

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 FixtureHolons to 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:

  1. Iterates over all FixtureHolons
  2. Selects those in Staged state
  3. Predicts commit outcomes
  4. Advances the selected FixtureHolon heads to saved expectations; Error is not a TestHolonState
  5. 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 FixtureHolons when 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:

  1. essential holon content
  2. 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 MatchSavedContent to 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.