Testing Strategy¶
Test plans are scenario catalogs, not code, per the "do not implement"
constraint. Each class gets Positive / Negative / Boundary / Validation
/ Serialization / Performance scenarios. These map directly onto
tests/unit/foundation/ (see tests/README.md at the repo root).
General policy¶
- Positive tests confirm documented Preconditions → Postconditions hold for valid input.
- Negative tests confirm every documented Exception is raised under the condition that names it.
- Boundary tests probe limits from
constants.py(e.g.MAX_HISTORY_LENGTH) and empty/singleton/maximal collection sizes. - Validation tests exercise every layer in
08-validation-and-serialization-strategy.mdindependently. - Serialization tests round-trip every class through every
SerializationFormat, and specifically test envelope version-mismatch handling. - Performance considerations are documented as budgets/expectations
here, enforced later by
tests/performance/foundation/, not implemented in this specification.
Entity¶
- Positive:
create()yieldsCREATEDstage with empty state; full lifecycle walkCREATED → INITIALIZED → ACTIVE → INACTIVE → ACTIVE → SUSPENDED → ACTIVE → DESTROYEDsucceeds;update_state()appends tohistoryand incrementsversion;clone()produces a distinctidwith equal Properties/Attributes;observe()does not mutatestate. - Negative:
activate()beforeinitialize()raisesInvalidLifecycleTransitionError;update_state()whileSUSPENDEDraises;update_state()violating aHARDConstraintraisesConstraintViolationErrorand leavesstateunchanged;add_property()with a duplicate name raisesDuplicatePropertyError;remove_property()afterACTIVEraisesImmutablePropertyError;compare()against a differenttyperaisesInvalidComparisonError. - Boundary:
historyat exactlyMAX_HISTORY_LENGTHevicts the oldest entry on the nextsnapshot();restore()withindexbeyondhistorybounds raises; Entity with zero Properties/ Attributes still validates and serializes successfully;nameat maximum grammar length fromID_PATTERNaccepted, one character over rejected. - Validation: every registered
Validatoron aProperty/Attributeis invoked exactly once perinitialize()/set_value()call;SOFTConstraint violation does not raise but appears invalidate()'sValidationResultand emitsConstraintViolated. - Serialization: round-trip through JSON, YAML, MessagePack,
Binary, Protobuf preserves
id,properties,attributes,state,version; deserializing an envelope with a newersdk_versionthan the running SDK surfaces a version-mismatch warning, not a hard failure, for additive-only formats. - Performance:
update_state()on an Entity with ahistoryatMAX_HISTORY_LENGTHmust not incur O(n) cost proportional to full history on every call (amortized O(1) eviction expected);clone()cost should scale linearly with Property/Attribute count, not history length (history is not cloned).
System¶
- Positive:
add_entity()/remove_entity(cascade=True)keeprelationshipsconsistent;run_interaction()across two members updates both and the aggregate Systemstate;find_entity()by tag returns all matches;clone()preserves internal topology with fresh Identities for both the System and every member. - Negative:
add_relationship()referencing a non-member Entity raisesEntityNotFoundError;remove_entity(cascade=False)with dangling Relationships raisesRelationshipError;run_interaction()with a participant not inentitiesraisesEntityNotFoundError;run_interaction()where one participant'sBehaviourforbids the type raisesInteractionErrorbefore any State mutation occurs (fail-fast, verified via unchangedversionon all participants). - Boundary: System with zero members validates and serializes
successfully (empty aggregate
state); System-of-Systems nesting at exactlyMAX_SYSTEM_NESTING_DEPTHsucceeds, one level deeper raises. - Validation:
System.validate()aggregates System-level Constraint results and every member's ownvalidate()result into oneValidationResult. - Serialization: serialized System document contains each member Entity exactly once even when referenced by multiple Relationships (no duplication); deserializing reconstructs the same topology (Relationship participant ids resolve to the same reconstructed Entity instances).
- Performance:
find_entity()byidshould be O(1) (dict lookup); bytype/tagpredicate is allowed O(n) but must be documented as such so callers avoid it in hot loops.
State¶
- Positive:
create()freezesvalues;diff()between two States of the same owner correctly lists added/removed/changed keys;merge()with disjoint keys unions successfully. - Negative:
diff()/merge()/strictcompare()across differentowner_idraiseIncompatibleStateError;merge()with overlapping keys and nooverwrite=TrueraisesStateMergeConflictError; attempting to mutatevaluesdirectly raises (immutability enforcement, mapped to a language-appropriate "frozen" error). - Boundary:
Statewith zerovaluesentries is valid;merge()of two States each with the maximum representable key count still succeeds (or documents an explicit cap, TBD at implementation). - Validation:
validate(constraints)against an emptyconstraintslist always returnsis_valid=True,errors=[]. - Serialization: round-trip preserves
valuestypes exactly (numeric vs. string vs. bool distinctions survive JSON round-trip, the one format most prone to type coercion bugs — explicit test required). - Performance:
diff()cost should be O(k) in the number of distinct keys across both States, not O(n) in unrelated history.
Interaction¶
- Positive:
apply()thencommit()on a valid Interaction updates every participant and transitionsstatustoCOMPLETED;cancel()fromPENDINGtransitions toCANCELLEDwithout touching any participantstate. - Negative:
commit()beforeapply()raisesInvalidInteractionStateError; acommit()where the second of two participants'update_state()raisesConstraintViolationErrorresults in the first participant's State being rolled back (_rollback_partial_commit) and overallstatus = FAILED— this atomicity guarantee is a mandatory negative-test case, not optional;create()with a participant whoseBehaviourforbidstype_raisesInteractionErrorat construction, beforeapply()is ever called. - Boundary:
Interactionwith exactly one participant (minimum valid count) succeeds; with the maximum practically-tested participant count (e.g. 100) still completes commit atomicity correctly. - Validation:
Transformation.input_schemamismatch (when declared) raisesSchemaMismatchErrorfromapply(), without changingstatusaway fromPENDING/RUNNINGinconsistently (transitions toFAILEDcleanly). - Serialization:
resultsserializes as anEntityId → Statemap correctly; aPENDINGInteraction (emptyresults) serializes and deserializes to an equivalentPENDINGInteraction. - Performance:
apply()(pure compute) should be safely parallelizable across independent Interactions in a benchmark scenario with disjoint participant sets — documented expectation forruntimeto validate once implemented, not testable infoundationalone.
Supporting classes (Identity, Property, Attribute, Behaviour,¶
Relationship, Constraint, Lifecycle, Event, Observation,
Knowledge, Transformation, Space, Time)
Each follows the same six-category pattern at a scope proportional to
its size; representative highlights not already covered under
Entity/System/State/Interaction above:
Identity: Negative —matches()against a mismatchedscopereturnsFalse, never raises. Boundary —idat the exactID_PATTERNlength limit.Property/Attribute: Validation — everyValidatortype (TypeValidator,RangeValidator,RegexValidator,RequiredValidator,PredicateValidator,ValidatorChainin bothFAIL_FASTandCOLLECT_ALLmodes) gets its own scenario.Behaviour: Negative —permits()for an unregistered Interaction type returnsFalse(never raises; onlycheck_preconditions()participates in a raising path via its caller).Relationship: Boundary —cardinality=ONE_TO_ONEwith three participants raisesInvalidCardinalityErrorat construction.Constraint: Positive —HARDandSOFTseverity both evaluate correctly; Negative — malformedexpression(referencing a nonexistent operator) raisesInvalidConstraintExpressionErrorat construction, not atevaluate()time (fail-fast).Lifecycle: Negative — every disallowed transition pair in the canonical table is exhaustively tested (DESTROYED → *always raises); Positive —can_transition_to()never raises, matchestransition_to()'s accept/reject decision exactly (contract- consistency test).Event/EventBus: Boundary — publishing pastEVENT_BUS_DEFAULT_QUEUE_DEPTHraisesEventBusUnavailableErrorrather than blocking; Positive —subscribe_all()receives every publishedEventType.Observation: Validation —uncertainty < 0raises at construction.Knowledge: Negative — emptycontributing_observationswithoutmetadata["a_priori"] = TrueraisesInsufficientEvidenceError; defaultpredict()raisesPredictionNotSupportedError.Transformation: Negative —inverse()on a non-reversible Transformation raisesTransformationNotReversibleError.Space: Boundary —contains()at exactly a declared bound (inclusive) vs. one unit beyond (exclusive).Time: Negative —compare()/delta()across mismatchedmoderaiseIncompatibleTimeModeError; Boundary —DISCRETE_STEPwith a negativevalueraises at construction.
Cross-cutting test suites (not per-class)¶
- Event catalog completeness test: every
EventTypeenumerated in07-events.mdis actually published by at least one integration test path (prevents silently-dead event definitions). - Exception hierarchy completeness test: every leaf exception in
06-exceptions-and-utilities.mdis raised by at least one negative test somewhere in the suite (same rationale). - Interface conformance test: every class claiming to implement a
nominal interface from
04-interfaces-protocols.mdis checked viaisinstance()against both the ABC and its structural Protocol mirror. - Round-trip determinism test: serialize → deserialize → serialize again must be byte-identical (or structurally identical for non-deterministic formats like unordered JSON keys) for every class, across every format.