Skip to content

Add EventScenario and ReactorScenario to @cratis/chronicle/testing #129

Closed

Description

@woksin

Chronicle for .NET ships EventScenario (append, appendMany, constraint rejection and queries against an in-process event sequence: Source/Clients/Testing/EventSequences/{EventScenario.cs,InProcessEventSequence.cs,InProcessConstraints.cs}) and ReactorScenario (deliver events to a reactor with injected services and record its side effects: Source/Clients/Testing/Reactors/{ReactorScenario.cs,RecordingReactorSideEffectHandlers.cs}). @cratis/chronicle/testing only has ReadModelScenario.

Add both, with the same kernel-faithfulness rule as the projection evaluator: whatever the in-process harness cannot reproduce faithfully is rejected explicitly and pointed at a kernel-backed test, never approximated silently. Part of #58.

Activity

  1. self-assigned this
    on Sep 27, 2026
  2. woksin commented on Sep 28, 2026

    @woksin
    ContributorAuthor

    Increment 3 (basic ReactorScenario) shipped in #134. Next per the design: constraints (increment 4), then constraint lifecycle/scopes, composition (commands, read models) and replay/redelivery.

  3. cratis-direct commented on Sep 28, 2026

    @cratis-direct
    Contributor

    Handover — outcome of the last review round (round 4, on 3bac434)

    Not passed. Confirmed findings still to fix on feature/scenario-constraints:

    • major — Source/testing/EventScenario.ts:51-52: Default constraint discovery compiles globally registered event types instead of ensuring the explicitly selected constructors are included. I reproduced this by decorating a unique property, calling TypeDiscoverer.default.clear(), then constructing EventScenario with that event in artifacts.eventTypes: two different sources successfully claimed 'Alpha', and both events committed. Registering another constructor under the same event ID produces the same bypass. Selected constraints are silently disabled when their constructor is absent from or replaced in global discovery.
      • Suggested fix: Include selected constructors in compilation while preserving complete discovered constraint groups. Explicitly reject conflicting constructors sharing an event ID rather than silently substituting one. Add regression specs for cleared discovery and overwritten registrations; duplicate claims must reject or construction must throw UnsupportedEventSequenceOperation.
    • major — Source/testing/EventScenario.ts:51-52 (compileConstraints({ eventTypes: discoveredArtifacts?.eventTypes ?? eventTypes, ... })): When a scenario selects event types and leaves constraints to default discovery, compileConstraints receives only the global DefaultClientArtifactsProvider event types. The selected catalog itself is not passed. A selected type that is not the registered class for its event type ID therefore has its decorator constraints dropped with no error. The registry is static and keyed by event type ID, so this happens in two cases: the registry was cleared with the public TypeDiscoverer.default.clear(), documented as being for test isolation, or a later class reused the same event type ID. I reproduced both against the built package. With the registry cleared, class Registered { @field(String) @unique('Email') email = 'Alpha' } gave A: true and B: true, so a cross-source duplicate was accepted. With a shadowing class registered under the same ID, the result was also true and true. The control case, a normally registered type, correctly returned true then false. 2fd4890 compiled the selected eventTypes, so 3bac434 introduced this regression. It breaks the rule to reject, never approximate. It also contradicts constraints.md and testing.md, which describe the scenario as evaluating the selected catalog's constraints.
      • Suggested fix: Compile over the union of the globally discovered event types and the selected eventTypes, deduplicated by constructor. Alternatively, throw UnsupportedEventSequenceOperation when a selected type is not identical to the registered class for its event type ID, because production parity is undefined in that case. Add regression specs for a cleared registry and for a shadowed event type ID, each asserting a violation or a rejection.
    • minor — Source/testing/EventScenario.constraints.spec.ts: 'rejects removal, scoped, composite, case-insensitive and replacement before mutation' (Removal / AnotherOnce assertions), after 'wraps throwing and duplicate fluent definitions...': The earlier test 'wraps throwing and duplicate fluent definitions as unsupported compiler errors' calls constraint('Throws')(Throws), which registers Throws in the static global TypeDiscoverer for the rest of the file. From then on, every default-discovery EventScenario in the file fails first with artifacts.constraints (compiler): Invalid constraint definition: Error: Invalid definition. The later default-discovery assertions for [...types, Removal] and [...types, AnotherOnce] only check for 'artifacts.constraints', so they pass on that compiler error. They do not reach the removal or multi-type unique-event-type rejection they are meant to prove. I confirmed the mechanism with a standalone script: after a throwing @constraint is registered, even a constraint-free selected catalog is rejected with the compiler error. Earlier tests in the file still cover those two rejections, so the loss is test strength, not behavior.
      • Suggested fix: Assert the specific reason in those cases, for example .message.should.include('removal') or 'multi-type unique event cycles'. Alternatively, pass an explicit constraints catalog, or move or isolate the Throws and Duplicate registrations, for example in their own spec file, so they do not leak into later default-discovery tests.

    The list combines both reviewers, so some items describe the same defect from two sides. The round 1–3 fixes hold. The kernel oracle check (including ), 448 focused specs and the snippet validator pass. The remaining regression was introduced by 3bac434: in default discovery, constraints are compiled only from the globally registered event types. The scenario's selected event types must be included too (deduplicated by constructor), or a selected type that is not the registered class for its ID must be rejected.

  4. deleted a comment from cratis-direct on Sep 28, 2026
  5. deleted a comment from cratis-direct on Sep 28, 2026
  6. deleted a comment from cratis-direct on Sep 28, 2026
  7. deleted a comment from cratis-direct on Sep 28, 2026
  8. deleted a comment from cratis-direct on Sep 28, 2026
  9. deleted a comment from cratis-direct on Sep 28, 2026
  10. deleted a comment from cratis-direct on Sep 28, 2026
  11. deleted a comment from cratis-direct on Sep 28, 2026
  12. deleted a comment from cratis-direct on Sep 28, 2026
  13. deleted a comment from cratis-direct on Sep 28, 2026
  14. deleted a comment from cratis-direct on Sep 28, 2026
  15. 32 remaining items

  16. deleted a comment from cratis-direct on Sep 28, 2026
  17. deleted a comment from cratis-direct on Sep 28, 2026
  18. deleted a comment from cratis-direct on Sep 28, 2026
  19. deleted a comment from cratis-direct on Sep 28, 2026
  20. deleted a comment from woksin on Sep 28, 2026
  21. deleted a comment from woksin on Sep 28, 2026
  22. deleted a comment from woksin on Sep 28, 2026
  23. deleted a comment from woksin on Sep 28, 2026
  24. woksin commented on Sep 28, 2026

    @woksin
    ContributorAuthor

    Increment 4 (unscoped constraints in EventScenario/ReactorScenario) shipped in #142. Constraint key values are currently limited to nonempty ASCII letters and spaces (the domain the kernel oracle fixtures prove). Next: increment 5 (constraint lifecycle and scopes, and widening the key-value domain), then 6 (composition: commands, read models) and 7 (replay/redelivery).

  25. woksin commented on Sep 28, 2026

    @woksin
    ContributorAuthor

    Increment 5 parts 5.0 (isolated oracle constraint fixtures with full outcomes) and 5.1 (wider kernel-verified unique-property key domain: digits, proven punctuation, empty/padded strings, é, booleans) shipped in #143. Next per the increment-5 plan: 5.2 unique-property replacement/removal, 5.3 multi-type unique-event constraints and removal cycles, 5.4 composite and case-insensitive keys. Scoped constraints (5.5) are blocked on Cratis/Chronicle#4361 (in-memory testing storage drops constraint scopes).

  26. woksin commented on Sep 28, 2026

    @woksin
    ContributorAuthor

    Increment 5.2 (unique-property replacement and removal, including deferred batch release, matching the pinned kernel) shipped in #144. Next: 5.3 multi-type unique-event constraints and removal cycles, then 5.4 composite and case-insensitive keys. 5.5 scopes remain blocked on Cratis/Chronicle#4361.

  27. woksin commented on Sep 29, 2026

    @woksin
    ContributorAuthor

    Increments 5.3 (unique event-type constraints with removal cycles, #145) and 5.4 (composite and ASCII case-insensitive unique keys, #146) shipped. Remaining: 5.5 scopes (blocked on Cratis/Chronicle#4361), 5.6 Unicode/numeric keys (owner decision on Unicode folding and numeric culture), then increments 6 (composition) and 7 (replay/redelivery).

  28. woksin commented on Sep 29, 2026

    @woksin
    ContributorAuthor

    Increment 6 (ReadModelScenario.observe composition) shipped in #155.

  29. woksin commented on Oct 5, 2026

    @woksin
    ContributorAuthor

    Delivered: EventScenario and ReactorScenario in @cratis/chronicle/testing, with batches, constraints (incl. scoped, #176), composition (#155), routed builders (#213), concept and Guid/numeric/date/object fields (#175, #186). Remaining constraint shapes continue in #232.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions