@cratis/chronicle 6.22.1 → 6.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  2. package/dist/eventSequences/EventSequence.js +2 -27
  3. package/dist/eventSequences/EventSequence.js.map +1 -1
  4. package/dist/eventSequences/createAppendResult.d.ts +13 -0
  5. package/dist/eventSequences/createAppendResult.d.ts.map +1 -0
  6. package/dist/eventSequences/createAppendResult.js +31 -0
  7. package/dist/eventSequences/createAppendResult.js.map +1 -0
  8. package/dist/events/constraints/Constraints.d.ts +8 -3
  9. package/dist/events/constraints/Constraints.d.ts.map +1 -1
  10. package/dist/events/constraints/Constraints.js +135 -125
  11. package/dist/events/constraints/Constraints.js.map +1 -1
  12. package/dist/testing/EventScenario.d.ts.map +1 -1
  13. package/dist/testing/EventScenario.js +70 -10
  14. package/dist/testing/EventScenario.js.map +1 -1
  15. package/dist/testing/InProcessConstraints.d.ts +17 -0
  16. package/dist/testing/InProcessConstraints.d.ts.map +1 -0
  17. package/dist/testing/InProcessConstraints.js +142 -0
  18. package/dist/testing/InProcessConstraints.js.map +1 -0
  19. package/dist/testing/InProcessEventSequence.d.ts +5 -1
  20. package/dist/testing/InProcessEventSequence.d.ts.map +1 -1
  21. package/dist/testing/InProcessEventSequence.js +43 -26
  22. package/dist/testing/InProcessEventSequence.js.map +1 -1
  23. package/dist/testing/ReactorScenario.d.ts.map +1 -1
  24. package/dist/testing/ReactorScenario.js +3 -2
  25. package/dist/testing/ReactorScenario.js.map +1 -1
  26. package/dist/tsconfig.tsbuildinfo +1 -1
  27. package/eventSequences/EventSequence.ts +3 -32
  28. package/eventSequences/createAppendResult.ts +44 -0
  29. package/events/constraints/Constraints.ts +57 -47
  30. package/events/constraints/for_Constraints/when_resolving_violation_messages.spec.ts +28 -0
  31. package/package.json +1 -1
  32. package/testing/EventScenario.constraint-discovery.spec.ts +69 -0
  33. package/testing/EventScenario.constraint-values.spec.ts +199 -0
  34. package/testing/EventScenario.constraints.spec.ts +415 -0
  35. package/testing/EventScenario.fixtures.spec.ts +5 -3
  36. package/testing/EventScenario.rejections.spec.ts +10 -3
  37. package/testing/EventScenario.ts +72 -11
  38. package/testing/InProcessConstraints.ts +148 -0
  39. package/testing/InProcessEventSequence.ts +42 -25
  40. package/testing/ReactorScenario.ts +3 -1
@@ -3,8 +3,8 @@
3
3
 
4
4
  import { ConstraintType } from '@cratis/chronicle.contracts';
5
5
  import type { ConstraintViolation } from '../../eventSequences/ConstraintViolation.js';
6
- import { IClientArtifactsProvider } from '../../artifacts/index.js';
7
- import { ChronicleConnection } from '../../connection/index.js';
6
+ import type { IClientArtifactsProvider } from '../../artifacts/index.js';
7
+ import type { ChronicleConnection } from '../../connection/index.js';
8
8
  import { ConstraintId } from './ConstraintId.js';
9
9
  import { IConstraint } from './IConstraint.js';
10
10
  import { IConstraints } from './IConstraints.js';
@@ -35,38 +35,22 @@ function assertMatchingScope(name: string, left: ConstraintScopeCapture, right:
35
35
  }
36
36
  }
37
37
 
38
- /** Manages discovery and registration of constraints with the Chronicle Kernel. */
39
- export class Constraints implements IConstraints {
40
- private readonly _captures = new Map<string, ConstraintCapture>();
41
-
42
- /**
43
- * Creates a new {@link Constraints} instance.
44
- * @param _eventStore - The name of the event store these constraints belong to.
45
- * @param _connection - The connection used to communicate with the Kernel.
46
- * @param _clientArtifacts - Provider for discovered client artifact types.
47
- */
48
- constructor(
49
- private readonly _eventStore: string,
50
- private readonly _connection: ChronicleConnection,
51
- private readonly _clientArtifacts: IClientArtifactsProvider
52
- ) {}
53
-
54
- /** @inheritdoc */
55
- async discover(): Promise<void> {
56
- this._captures.clear();
57
- const fluentIds = new Set<string>();
58
- for (const type of this._clientArtifacts.constraints) {
59
- const metadata = getConstraintMetadata(type);
38
+ /** Compile decorator and fluent constraints using the production registration rules. */
39
+ export function compileConstraints(provider: Pick<IClientArtifactsProvider, 'eventTypes' | 'constraints'>): Map<string, ConstraintCapture> {
40
+ const captures = new Map<string, ConstraintCapture>();
41
+ const fluentIds = new Set<string>();
42
+ for (const definition of provider.constraints) {
43
+ const metadata = getConstraintMetadata(definition);
60
44
  if (!metadata) continue;
61
- if (fluentIds.has(metadata.id.value)) throw new Error(`Duplicate constraint id '${metadata.id.value}'.`);
62
- fluentIds.add(metadata.id.value);
63
-
64
- const builder = new ConstraintBuilder(metadata.id.value);
65
- const instance = new (type as new () => IConstraint)();
45
+ const id = metadata.id.value;
46
+ if (fluentIds.has(id)) throw new Error(`Duplicate constraint id '${id}'.`);
47
+ fluentIds.add(id);
48
+ const builder = new ConstraintBuilder(id);
49
+ const instance = new (definition as new () => IConstraint)();
66
50
  instance.define(builder);
67
51
  const capture = builder.capture;
68
52
  const name = wireNameOf(capture);
69
- const existing = this._captures.get(name);
53
+ const existing = captures.get(name);
70
54
  if (existing?.uniqueEventType && capture.uniqueEventType) {
71
55
  assertMatchingScope(name, existing.scope, capture.scope);
72
56
  const merged = existing.uniqueEventType;
@@ -82,12 +66,12 @@ export class Constraints implements IConstraints {
82
66
  } else if (existing) {
83
67
  throw new Error(`Duplicate constraint name '${name}'.`);
84
68
  } else {
85
- this._captures.set(name, capture);
69
+ captures.set(name, capture);
86
70
  }
87
71
  }
88
72
 
89
73
  const removalEvents = new Map<string, Function[]>();
90
- for (const eventType of this._clientArtifacts.eventTypes) {
74
+ for (const eventType of provider.eventTypes) {
91
75
  for (const name of getRemovedConstraintNames(eventType)) {
92
76
  const events = removalEvents.get(name) ?? [];
93
77
  events.push(eventType);
@@ -95,16 +79,16 @@ export class Constraints implements IConstraints {
95
79
  }
96
80
  }
97
81
 
98
- for (const eventType of this._clientArtifacts.eventTypes) {
82
+ for (const eventType of provider.eventTypes) {
99
83
  const eventMetadata = getUniqueEventMetadata(eventType);
100
84
  if (eventMetadata) {
101
85
  const name = eventMetadata.name ?? eventType.name;
102
- let capture = this._captures.get(name);
86
+ let capture = captures.get(name);
103
87
  if (!capture) {
104
88
  const builder = new ConstraintBuilder(name);
105
89
  builder.uniqueFor(eventType, eventMetadata.message, name);
106
90
  capture = builder.capture;
107
- this._captures.set(name, capture);
91
+ captures.set(name, capture);
108
92
  } else if (!capture.uniqueEventType) {
109
93
  throw new Error(`Constraint '${name}' is not a unique event type constraint.`);
110
94
  } else {
@@ -120,12 +104,12 @@ export class Constraints implements IConstraints {
120
104
  const metadata = getUniquePropertyMetadata(eventType, property);
121
105
  if (!metadata) continue;
122
106
  const name = metadata.name ?? property;
123
- let capture = this._captures.get(name);
107
+ let capture = captures.get(name);
124
108
  if (!capture) {
125
109
  const builder = new ConstraintBuilder(name);
126
110
  builder.unique(() => {});
127
111
  capture = builder.capture;
128
- this._captures.set(name, capture);
112
+ captures.set(name, capture);
129
113
  }
130
114
  if (!capture.uniqueConstraint) throw new Error(`Constraint '${name}' is not a unique property constraint.`);
131
115
  assertMatchingScope(name, capture.scope, decoratorScope);
@@ -142,7 +126,7 @@ export class Constraints implements IConstraints {
142
126
  }
143
127
 
144
128
  for (const [name, eventTypes] of removalEvents) {
145
- const capture = this._captures.get(name);
129
+ const capture = captures.get(name);
146
130
  if (capture) {
147
131
  if (capture.uniqueConstraint) {
148
132
  const unique = new UniqueConstraintBuilder(capture.uniqueConstraint);
@@ -156,6 +140,40 @@ export class Constraints implements IConstraints {
156
140
  }
157
141
  }
158
142
  }
143
+ return captures;
144
+ }
145
+
146
+ /** Resolve the registered message and detail substitutions without a connection. */
147
+ export function resolveConstraintMessage(captures: ReadonlyMap<string, ConstraintCapture>, violation: ConstraintViolation): ConstraintViolation {
148
+ const capture = captures.get(violation.constraintId);
149
+ const message = capture?.uniqueConstraint?.message ?? capture?.uniqueEventType?.message;
150
+ if (!message) return violation;
151
+ let resolved = message;
152
+ for (const [key, value] of Object.entries(violation.details)) {
153
+ resolved = resolved.replaceAll(`{${key}}`, () => value);
154
+ }
155
+ return { ...violation, message: resolved };
156
+ }
157
+
158
+ /** Manages discovery and registration of constraints with the Chronicle Kernel. */
159
+ export class Constraints implements IConstraints {
160
+ private _captures = new Map<string, ConstraintCapture>();
161
+
162
+ /**
163
+ * Creates a new {@link Constraints} instance.
164
+ * @param _eventStore - The name of the event store these constraints belong to.
165
+ * @param _connection - The connection used to communicate with the Kernel.
166
+ * @param _clientArtifacts - Provider for discovered client artifact types.
167
+ */
168
+ constructor(
169
+ private readonly _eventStore: string,
170
+ private readonly _connection: ChronicleConnection,
171
+ private readonly _clientArtifacts: IClientArtifactsProvider
172
+ ) {}
173
+
174
+ /** @inheritdoc */
175
+ async discover(): Promise<void> {
176
+ this._captures = compileConstraints(this._clientArtifacts);
159
177
  }
160
178
 
161
179
  /** @inheritdoc */
@@ -242,14 +260,6 @@ export class Constraints implements IConstraints {
242
260
  * @returns The violation with its configured message and substituted details, if available.
243
261
  */
244
262
  resolveMessageFor(violation: ConstraintViolation): ConstraintViolation {
245
- const capture = this._captures.get(violation.constraintId);
246
- const message = capture?.uniqueConstraint?.message ?? capture?.uniqueEventType?.message;
247
- if (!message) return violation;
248
-
249
- let resolved = message;
250
- for (const [key, value] of Object.entries(violation.details)) {
251
- resolved = resolved.replaceAll(`{${key}}`, () => value);
252
- }
253
- return { ...violation, message: resolved };
263
+ return resolveConstraintMessage(this._captures, violation);
254
264
  }
255
265
  }
@@ -1,6 +1,8 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
+ import { readFileSync } from 'node:fs';
5
+ import { field } from '@cratis/fundamentals';
4
6
  import { ConstraintType, type ConstraintViolation as ContractsConstraintViolation } from '@cratis/chronicle.contracts';
5
7
  import { chai, describe, it, vi } from 'vitest';
6
8
  import type { ChronicleConnection } from '../../../connection/ChronicleConnection.js';
@@ -70,6 +72,32 @@ describe('when resolving violation messages', () => {
70
72
  result.constraintViolations[0].message.should.equal('Address a$b already registered');
71
73
  });
72
74
 
75
+ it('should map a kernel wire violation name and resolve its configured message on a connected append', async () => {
76
+ class OracleKeyClaimed { @field(String) @unique('OracleKey', 'Already claimed: {PropertyName}={PropertyValue}') key = 'Alpha'; }
77
+ eventType('OracleKeyClaimed')(OracleKeyClaimed);
78
+ const discovered = constraints([OracleKeyClaimed]);
79
+ await discovered.discover();
80
+ const fixture = JSON.parse(readFileSync(new URL('../../../testing/fixtures/constraints.json', import.meta.url), 'utf8')) as {
81
+ expected: { outcomes: Array<{ wireViolations: Array<{ SequenceNumber: string; ConstraintName: string;
82
+ EventTypeId: string; ConstraintType: number; Message: string; Details: Record<string, string> }>;
83
+ violations: Array<{ message: string }> }> }
84
+ };
85
+ const wire = fixture.expected.outcomes[2].wireViolations[0];
86
+ const connection = {
87
+ eventSequences: { append: vi.fn().mockResolvedValue({ Response: {
88
+ SequenceNumber: 18446744073709551615n, Errors: [],
89
+ ConstraintViolations: [{ ...wire, SequenceNumber: BigInt(wire.SequenceNumber) }]
90
+ } }) }
91
+ } as unknown as ChronicleConnection;
92
+ const sequence = new EventSequence(EventSequenceId.eventLog, 'store', 'namespace', connection,
93
+ {} as IUnitOfWorkManager, discovered.resolveMessageFor.bind(discovered));
94
+ const result = await sequence.append('B', new OracleKeyClaimed());
95
+ result.isSuccess.should.be.false;
96
+ result.constraintViolations[0].constraintId.should.equal(wire.ConstraintName);
97
+ result.constraintViolations[0].message.should.equal(fixture.expected.outcomes[2].violations[0].message);
98
+ result.constraintViolations[0].message.should.not.equal(wire.Message);
99
+ });
100
+
73
101
  it('should resolve a named fluent event type with a different class id', async () => {
74
102
  const discovered = constraints([], [FluentRegistration]);
75
103
  await discovered.discover();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/chronicle",
3
- "version": "6.22.1",
3
+ "version": "6.24.0",
4
4
  "description": "Event sourcing for TypeScript and Node.js — the idiomatic client for Cratis Chronicle, the open-source event-sourcing database",
5
5
  "author": "Cratis",
6
6
  "license": "MIT",
@@ -0,0 +1,69 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import 'reflect-metadata';
5
+ import { chai, describe, it } from 'vitest';
6
+ import { field } from '@cratis/fundamentals';
7
+ import { eventType } from '../events/eventTypeDecorator.js';
8
+ import { unique } from '../events/constraints/unique.js';
9
+ import { TypeDiscoverer } from '../types/TypeDiscoverer.js';
10
+ import { EventScenario, UnsupportedEventSequenceOperation } from './index.js';
11
+
12
+ chai.should();
13
+
14
+ describe('selected constraint discovery', () => {
15
+ it('enforces selected decorators when global discovery has been cleared', async () => {
16
+ class SelectedClaim {
17
+ @field(String) @unique('ClearedRegistryKey') key: string;
18
+ constructor(key: string) { this.key = key; }
19
+ }
20
+ eventType('ClearedRegistryClaim')(SelectedClaim);
21
+ TypeDiscoverer.default.clear();
22
+
23
+ const subject = new EventScenario({ artifacts: { eventTypes: [SelectedClaim] } });
24
+ (await subject.append('A', new SelectedClaim('Alpha'))).isSuccess.should.be.true;
25
+ const result = await subject.append('B', new SelectedClaim('Alpha'));
26
+ result.isSuccess.should.be.false;
27
+ result.constraintViolations[0].constraintId.should.equal('ClearedRegistryKey');
28
+ subject.appendedEvents.length.should.equal(1);
29
+ });
30
+
31
+ it('appends a constraint-free selected class shadowed by a later registration with the same ID', async () => {
32
+ class SelectedItemAdded {
33
+ @field(String) label: string;
34
+ constructor(label: string) { this.label = label; }
35
+ }
36
+ eventType('ShadowedPlainItemAdded')(SelectedItemAdded);
37
+ class ShadowItemAdded {
38
+ @field(String) name: string;
39
+ constructor(name: string) { this.name = name; }
40
+ }
41
+ eventType('ShadowedPlainItemAdded')(ShadowItemAdded);
42
+
43
+ const subject = new EventScenario({ artifacts: { eventTypes: [SelectedItemAdded] } });
44
+ (await subject.append('A', new SelectedItemAdded('Alpha'))).isSuccess.should.be.true;
45
+ subject.appendedEvents.length.should.equal(1);
46
+ subject.appendedEvents[0].content.should.deep.equal({ label: 'Alpha' });
47
+ });
48
+
49
+ it('rejects a globally registered constructor shadowing the selected event type ID', () => {
50
+ class SelectedClaim {
51
+ @field(String) @unique('ShadowedRegistryKey') key = 'Alpha';
52
+ }
53
+ eventType('ShadowedRegistryClaim')(SelectedClaim);
54
+ class ShadowClaim {
55
+ @field(String) key = 'Alpha';
56
+ }
57
+ eventType('ShadowedRegistryClaim')(ShadowClaim);
58
+
59
+ try {
60
+ new EventScenario({ artifacts: { eventTypes: [SelectedClaim] } });
61
+ throw new Error('Expected conflicting constructors to be rejected');
62
+ } catch (error) {
63
+ (error instanceof UnsupportedEventSequenceOperation).should.be.true;
64
+ (error as Error).message.should.include('artifacts.eventTypes');
65
+ (error as Error).message.should.include('ShadowedRegistryClaim');
66
+ (error as Error).message.should.include('Conflicting constructors share an event type ID');
67
+ }
68
+ });
69
+ });
@@ -0,0 +1,199 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import 'reflect-metadata';
5
+ import { readFileSync } from 'node:fs';
6
+ import { chai, describe, it, vi } from 'vitest';
7
+ import { ConceptAs, field } from '@cratis/fundamentals';
8
+ import { eventType, getEventTypeMetadata } from '../events/eventTypeDecorator.js';
9
+ import { compileConstraints } from '../events/constraints/Constraints.js';
10
+ import { unique } from '../events/constraints/unique.js';
11
+ import { constraint } from '../events/constraints/constraint.js';
12
+ import type { IConstraint } from '../events/constraints/IConstraint.js';
13
+ import type { IConstraintBuilder } from '../events/constraints/IConstraintBuilder.js';
14
+ import { EventScenario, UnsupportedEventSequenceOperation } from './index.js';
15
+ import { InProcessConstraints } from './InProcessConstraints.js';
16
+
17
+ chai.should();
18
+ class OracleDomainText {
19
+ @field(String) key: string;
20
+ constructor(key: string) { this.key = key; }
21
+ }
22
+ eventType('OracleDomainText')(OracleDomainText);
23
+ class OracleDomainShared {
24
+ @field(String) key: string;
25
+ constructor(key: string) { this.key = key; }
26
+ }
27
+ eventType('OracleDomainShared')(OracleDomainShared);
28
+ class OracleDomainFlag {
29
+ @field(Boolean) key: boolean;
30
+ constructor(key: boolean) { this.key = key; }
31
+ }
32
+ eventType('OracleDomainFlag')(OracleDomainFlag);
33
+ class TextKey implements IConstraint {
34
+ define(builder: IConstraintBuilder) {
35
+ builder.unique(key => key.on(OracleDomainText, event => event.key).on(OracleDomainShared, event => event.key)
36
+ .withMessage('Taken: {PropertyValue}'));
37
+ }
38
+ }
39
+ constraint('OracleDomainKey')(TextKey);
40
+ class ScalarKey implements IConstraint {
41
+ define(builder: IConstraintBuilder) {
42
+ builder.unique(key => key.on(OracleDomainText, event => event.key).on(OracleDomainShared, event => event.key)
43
+ .on(OracleDomainFlag, event => event.key).withMessage('Taken: {PropertyValue}'));
44
+ }
45
+ }
46
+ constraint('OracleDomainKey')(ScalarKey);
47
+
48
+ type Input = { source: string; type: 'string' | 'shared' | 'boolean'; value: string | boolean;
49
+ sourceType?: string; streamType?: string; streamId?: string };
50
+ type Routing = { sourceType?: string; streamType?: string; streamId?: string };
51
+ type History = { sequence: string; source: string; sourceType: string; streamType: string; streamId: string;
52
+ type: string; content: { key: string | boolean }; hash: string };
53
+ type Outcome = { success: boolean; sequences: string[]; violations: Array<{ id: string; message: string; details: Record<string, string> }>;
54
+ wireViolations: Array<{ EventTypeId: string; SequenceNumber: string; ConstraintType: number;
55
+ ConstraintName: string; Message: string; Details: Record<string, string> }>;
56
+ errors: string[]; history: History[]; next: string };
57
+ type Fixture = { isolatedConstraintOperations: Array<{ mode: 'single' | 'batch'; events: Input[]; options?: Routing }>;
58
+ eventSchemas: Record<string, { eventTypeId: string; properties: Record<string, string> }>;
59
+ constraintDefinitions: Array<{ kind: string; name: string; events: Array<{ type: string; properties: string[] }>;
60
+ ignoreCasing: boolean; removedWithEventTypeIds: string[]; message: string }>;
61
+ expected: { outcomes: Outcome[] } };
62
+ const eventTypes = [OracleDomainText, OracleDomainShared, OracleDomainFlag];
63
+ const aliases = { string: OracleDomainText, shared: OracleDomainShared, boolean: OracleDomainFlag };
64
+ const aliasesById = Object.fromEntries(Object.entries(aliases).map(([alias, type]) => [getEventTypeMetadata(type)!.eventType.id.value, alias]));
65
+ const load = (name: string): Fixture => JSON.parse(readFileSync(new URL(`./fixtures/${name}.json`, import.meta.url), 'utf8')) as Fixture;
66
+ const event = (entry: Input) => entry.type === 'string' ? new OracleDomainText(entry.value as string) :
67
+ entry.type === 'shared' ? new OracleDomainShared(entry.value as string) : new OracleDomainFlag(entry.value as boolean);
68
+ const historyOf = (scenario: EventScenario): History[] => scenario.appendedEvents.map(entry => ({
69
+ sequence: entry.context.sequenceNumber.toString(), source: entry.context.eventSourceId,
70
+ sourceType: entry.context.eventSourceType, streamType: entry.context.eventStreamType, streamId: entry.context.eventStreamId,
71
+ type: entry.eventType.id.value, content: entry.content as { key: string | boolean }, hash: entry.context.hash
72
+ }));
73
+
74
+ describe('fixture-backed unique scalar values', () => {
75
+ for (const name of ['constraints-isolation', 'constraints-key-domain']) {
76
+ it(`matches the packaged kernel ${name} after every operation`, async () => {
77
+ const fixture = load(name);
78
+ const constraints = [name === 'constraints-isolation' ? TextKey : ScalarKey];
79
+ const compiled = compileConstraints({ eventTypes, constraints });
80
+ Object.fromEntries(Object.entries(aliases).map(([alias, type]) => {
81
+ const metadata = getEventTypeMetadata(type)!;
82
+ return [alias, { eventTypeId: metadata.eventType.id.value,
83
+ properties: Object.fromEntries(Object.entries(metadata.schema.properties!).map(([key, property]) => [key, property.type])) }];
84
+ })).should.deep.equal(fixture.eventSchemas);
85
+ [...compiled].map(([key, capture]) => ({
86
+ kind: 'uniqueProperty', name: key,
87
+ events: capture.uniqueConstraint!.eventDefinitions.map(entry => ({ type: aliasesById[entry.eventTypeId], properties: entry.properties })),
88
+ ignoreCasing: capture.uniqueConstraint!.ignoreCasing,
89
+ removedWithEventTypeIds: capture.uniqueConstraint!.removedWithEventTypeIds ?? [],
90
+ message: capture.uniqueConstraint!.message ?? ''
91
+ })).should.deep.equal(fixture.constraintDefinitions);
92
+ fixture.isolatedConstraintOperations.length.should.be.greaterThan(0);
93
+ fixture.expected.outcomes.length.should.equal(fixture.isolatedConstraintOperations.length);
94
+ const scenario = new EventScenario({ artifacts: { eventTypes, constraints } });
95
+ const notifications = scenario.eventSequence.appendOperations[Symbol.asyncIterator]();
96
+ const validate = vi.spyOn(InProcessConstraints.prototype, 'validate');
97
+ let count = 0;
98
+ try {
99
+ for (const [index, operation] of fixture.isolatedConstraintOperations.entries()) {
100
+ const expected = fixture.expected.outcomes[index];
101
+ const before = historyOf(scenario);
102
+ const resultCount = scenario.results.length;
103
+ const nextNotification = notifications.next();
104
+ const results = operation.mode === 'single'
105
+ ? [await scenario.append(operation.events[0].source, event(operation.events[0]))]
106
+ : await scenario.appendMany(operation.events.map(entry => ({ eventSourceId: entry.source, event: event(entry),
107
+ eventSourceType: entry.sourceType ?? operation.options?.sourceType,
108
+ eventStreamType: entry.streamType ?? operation.options?.streamType,
109
+ eventStreamId: entry.streamId ?? operation.options?.streamId })));
110
+ const notification = (await nextNotification).value!;
111
+ const raw = validate.mock.results.at(-1)?.value as ReturnType<InProcessConstraints['validate']>;
112
+ raw.map(({ SequenceNumber, ...violation }) => ({ ...violation, SequenceNumber: SequenceNumber.toString() }))
113
+ .should.deep.equal(expected.wireViolations);
114
+ notification.length.should.equal(operation.events.length);
115
+ notification.every(item => item.result.isSuccess === expected.success).should.be.true;
116
+ count += operation.events.length;
117
+ scenario.results.length.should.equal(count);
118
+ results.length.should.equal(operation.events.length);
119
+ for (const [position, result] of results.entries()) {
120
+ result.isSuccess.should.equal(expected.success);
121
+ result.sequenceNumber.value.toString().should.equal(expected.success ? expected.sequences[position] : '0');
122
+ result.constraintViolations.map(item => ({ id: item.constraintId, message: item.message, details: item.details }))
123
+ .should.deep.equal(expected.violations);
124
+ result.errors.map(item => item.message).should.deep.equal(expected.errors);
125
+ }
126
+ if (!expected.success) {
127
+ historyOf(scenario).should.deep.equal(before);
128
+ scenario.results.length.should.equal(resultCount + operation.events.length);
129
+ }
130
+ historyOf(scenario).should.deep.equal(expected.history);
131
+ (await scenario.eventSequence.getNextSequenceNumber()).value.toString().should.equal(expected.next);
132
+ }
133
+ } finally {
134
+ validate.mockRestore();
135
+ await notifications.return?.();
136
+ }
137
+ });
138
+ }
139
+
140
+ it('accepts serialized unconstrained concepts and dates but rejects a constrained date', async () => {
141
+ class ScenarioLabel extends ConceptAs<string> { static readonly valueType = String; }
142
+ class UnconstrainedValue {
143
+ @field(ScenarioLabel) concept = new ScenarioLabel('example');
144
+ @field(String) date = new Date('2025-01-02T03:04:05.000Z');
145
+ }
146
+ eventType('OracleUnconstrainedValue')(UnconstrainedValue);
147
+ class ConstrainedValue {
148
+ @field(String) @unique('OracleConstrainedValue') key = new Date('2025-01-02T03:04:05.000Z');
149
+ }
150
+ eventType('OracleConstrainedValue')(ConstrainedValue);
151
+ const scenario = new EventScenario({ artifacts: { eventTypes: [UnconstrainedValue, ConstrainedValue] } });
152
+ const date = new Date('2025-01-02T03:04:05.000Z');
153
+ (await scenario.append('A', new UnconstrainedValue())).isSuccess.should.be.true;
154
+ scenario.appendedEvents[0].content.should.deep.equal({ concept: 'example', date: date.toISOString() });
155
+ await scenario.append('B', new ConstrainedValue()).then(() => { throw new Error('Constrained date accepted'); }, error => {
156
+ (error instanceof UnsupportedEventSequenceOperation).should.be.true;
157
+ (error as Error).message.should.include('append.content');
158
+ (error as Error).message.should.include('Use a kernel-backed test.');
159
+ });
160
+ scenario.appendedEvents.length.should.equal(1);
161
+ scenario.results.length.should.equal(1);
162
+ });
163
+
164
+ it('rejects conversions and strings outside the captured domain without changing state', async () => {
165
+ const scenario = new EventScenario({ artifacts: { eventTypes: [OracleDomainText, OracleDomainShared, OracleDomainFlag], constraints: [ScalarKey] } });
166
+ (await scenario.append('A', new OracleDomainText('0'))).isSuccess.should.be.true;
167
+ const before = historyOf(scenario);
168
+ const results = scenario.results;
169
+ const unprovenKeys = ['!', '#', '%', '&', "'", '(', ')', '*', '+', ',', '/', ';', '<', '=', '>', '?', '[', ']', '^', '`', '~'];
170
+ const invalidContent: unknown[] = [1, 0, 1.5, undefined, [], {}, new Date(), 'É', 'e\u0301', '"', '\\', '\n'];
171
+ // The production serializer throws on null before checkedContent can run.
172
+ for (const [value, operation] of [
173
+ ...unprovenKeys.map(value => [value, 'artifacts.constraints']),
174
+ ...invalidContent.map(value => [value, 'append.content']),
175
+ [null, 'append.serialization']
176
+ ] as Array<[unknown, string]>) {
177
+ const bad = new OracleDomainText('valid');
178
+ Object.assign(bad, { key: value });
179
+ await scenario.append('B', bad).then(() => { throw new Error('Unsupported value accepted'); }, error => {
180
+ (error instanceof UnsupportedEventSequenceOperation).should.be.true;
181
+ (error as Error).message.should.include(operation, `value: ${JSON.stringify(value)}; error: ${(error as Error).message}`);
182
+ (error as Error).message.should.include('Use a kernel-backed test.');
183
+ });
184
+ historyOf(scenario).should.deep.equal(before);
185
+ scenario.results.should.deep.equal(results);
186
+ (await scenario.eventSequence.getNextSequenceNumber()).value.should.equal(1n);
187
+ }
188
+ const wrong = new OracleDomainFlag(true);
189
+ Object.assign(wrong, { key: 'true' });
190
+ await scenario.appendMany([{ eventSourceId: 'C', event: new OracleDomainText('new') },
191
+ { eventSourceId: 'D', event: wrong }]).then(() => { throw new Error('Mismatched batch accepted'); }, error => {
192
+ (error instanceof UnsupportedEventSequenceOperation).should.be.true;
193
+ (error as Error).message.should.include('appendMany.content');
194
+ (error as Error).message.should.include('Use a kernel-backed test.');
195
+ });
196
+ historyOf(scenario).should.deep.equal(before);
197
+ scenario.results.should.deep.equal(results);
198
+ });
199
+ });