You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Add EventScenario and ReactorScenario to @cratis/chronicle/testing #129
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.
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.
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.
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).
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).
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.
Activity
woksin commentedon Sep 28, 2026
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.
cratis-direct commentedon Sep 28, 2026
Handover — outcome of the last review round (round 4, on 3bac434)
Not passed. Confirmed findings still to fix on
feature/scenario-constraints: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.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 publicTypeDiscoverer.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 selectedeventTypes, 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.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.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' callsconstraint('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 withartifacts.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..message.should.include('removal')or 'multi-type unique event cycles'. Alternatively, pass an explicitconstraintscatalog, 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.
32 remaining items
woksin commentedon Sep 28, 2026
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).woksin commentedon Sep 28, 2026
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).
woksin commentedon Sep 28, 2026
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.
woksin commentedon Sep 29, 2026
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).
woksin commentedon Sep 29, 2026
Increment 6 (ReadModelScenario.observe composition) shipped in #155.
woksin commentedon Oct 5, 2026
Delivered:
EventScenarioandReactorScenarioin@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.