@cratis/chronicle 6.19.0 → 6.20.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 (63) hide show
  1. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  2. package/dist/eventSequences/EventSequence.js +2 -9
  3. package/dist/eventSequences/EventSequence.js.map +1 -1
  4. package/dist/eventSequences/prepareSingleAppend.d.ts +12 -0
  5. package/dist/eventSequences/prepareSingleAppend.d.ts.map +1 -0
  6. package/dist/eventSequences/prepareSingleAppend.js +22 -0
  7. package/dist/eventSequences/prepareSingleAppend.js.map +1 -0
  8. package/dist/testing/EventScenario.d.ts +29 -0
  9. package/dist/testing/EventScenario.d.ts.map +1 -0
  10. package/dist/testing/EventScenario.js +60 -0
  11. package/dist/testing/EventScenario.js.map +1 -0
  12. package/dist/testing/EventScenarioGiven.d.ts +9 -0
  13. package/dist/testing/EventScenarioGiven.d.ts.map +1 -0
  14. package/dist/testing/EventScenarioGiven.js +19 -0
  15. package/dist/testing/EventScenarioGiven.js.map +1 -0
  16. package/dist/testing/EventScenarioGivenBuilder.d.ts +9 -0
  17. package/dist/testing/EventScenarioGivenBuilder.d.ts.map +1 -0
  18. package/dist/testing/EventScenarioGivenBuilder.js +12 -0
  19. package/dist/testing/EventScenarioGivenBuilder.js.map +1 -0
  20. package/dist/testing/EventScenarioOptions.d.ts +21 -0
  21. package/dist/testing/EventScenarioOptions.d.ts.map +1 -0
  22. package/dist/testing/EventScenarioOptions.js +4 -0
  23. package/dist/testing/EventScenarioOptions.js.map +1 -0
  24. package/dist/testing/EventScenarioSeed.d.ts +5 -0
  25. package/dist/testing/EventScenarioSeed.d.ts.map +1 -0
  26. package/dist/testing/EventScenarioSeed.js +14 -0
  27. package/dist/testing/EventScenarioSeed.js.map +1 -0
  28. package/dist/testing/EventScenarioWhen.d.ts +11 -0
  29. package/dist/testing/EventScenarioWhen.d.ts.map +1 -0
  30. package/dist/testing/EventScenarioWhen.js +17 -0
  31. package/dist/testing/EventScenarioWhen.js.map +1 -0
  32. package/dist/testing/EventScenarioWhenBuilder.d.ts +9 -0
  33. package/dist/testing/EventScenarioWhenBuilder.d.ts.map +1 -0
  34. package/dist/testing/EventScenarioWhenBuilder.js +12 -0
  35. package/dist/testing/EventScenarioWhenBuilder.js.map +1 -0
  36. package/dist/testing/InProcessEventSequence.d.ts +49 -0
  37. package/dist/testing/InProcessEventSequence.d.ts.map +1 -0
  38. package/dist/testing/InProcessEventSequence.js +265 -0
  39. package/dist/testing/InProcessEventSequence.js.map +1 -0
  40. package/dist/testing/UnsupportedEventSequenceOperation.d.ts +5 -0
  41. package/dist/testing/UnsupportedEventSequenceOperation.d.ts.map +1 -0
  42. package/dist/testing/UnsupportedEventSequenceOperation.js +10 -0
  43. package/dist/testing/UnsupportedEventSequenceOperation.js.map +1 -0
  44. package/dist/testing/index.d.ts +7 -0
  45. package/dist/testing/index.d.ts.map +1 -1
  46. package/dist/testing/index.js +6 -0
  47. package/dist/testing/index.js.map +1 -1
  48. package/dist/tsconfig.tsbuildinfo +1 -1
  49. package/eventSequences/EventSequence.ts +2 -12
  50. package/eventSequences/prepareSingleAppend.ts +25 -0
  51. package/package.json +1 -1
  52. package/testing/EventScenario.fixtures.spec.ts +271 -0
  53. package/testing/EventScenario.rejections.spec.ts +307 -0
  54. package/testing/EventScenario.ts +77 -0
  55. package/testing/EventScenarioGiven.ts +16 -0
  56. package/testing/EventScenarioGivenBuilder.ts +11 -0
  57. package/testing/EventScenarioOptions.ts +20 -0
  58. package/testing/EventScenarioSeed.ts +17 -0
  59. package/testing/EventScenarioWhen.ts +16 -0
  60. package/testing/EventScenarioWhenBuilder.ts +11 -0
  61. package/testing/InProcessEventSequence.ts +275 -0
  62. package/testing/UnsupportedEventSequenceOperation.ts +10 -0
  63. package/testing/index.ts +7 -0
@@ -0,0 +1,77 @@
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 { DefaultClientArtifactsProvider } from '../artifacts/DefaultClientArtifactsProvider.js';
5
+ import { TypeDiscoverer } from '../types/TypeDiscoverer.js';
6
+ import type { AppendOptions } from '../eventSequences/AppendOptions.js';
7
+ import type { AppendResult } from '../eventSequences/AppendResult.js';
8
+ import type { EventForEventSourceId } from '../eventSequences/EventForEventSourceId.js';
9
+ import type { AppendedEvent } from '../events/AppendedEvent.js';
10
+ import type { IEventSequence } from '../eventSequences/IEventSequence.js';
11
+ import type { IEventLog } from '../eventSequences/IEventLog.js';
12
+ import { EventSequenceId } from '../eventSequences/EventSequenceId.js';
13
+ import { getEventTypeMetadata } from '../events/eventTypeDecorator.js';
14
+ import { getUniqueEventMetadata, getUniquePropertyMetadata } from '../events/constraints/unique.js';
15
+ import type { EventScenarioOptions } from './EventScenarioOptions.js';
16
+ import { EventScenarioGivenBuilder } from './EventScenarioGivenBuilder.js';
17
+ import { EventScenarioWhenBuilder } from './EventScenarioWhenBuilder.js';
18
+ import { InProcessEventSequence } from './InProcessEventSequence.js';
19
+ import { registerScenarioSeed } from './EventScenarioSeed.js';
20
+ import { UnsupportedEventSequenceOperation } from './UnsupportedEventSequenceOperation.js';
21
+
22
+ /** A kernel-free, fixture-bounded single-event append scenario. No observer runs automatically. */
23
+ export class EventScenario {
24
+ readonly eventSequence: IEventSequence;
25
+ readonly given: EventScenarioGivenBuilder;
26
+ readonly when: EventScenarioWhenBuilder;
27
+
28
+ constructor(options: EventScenarioOptions = {}) {
29
+ const artifacts = options.artifacts ?? new DefaultClientArtifactsProvider(TypeDiscoverer.default);
30
+ const eventTypes = [...artifacts.eventTypes];
31
+ if (options.artifacts?.constraints?.length === 0 && options.constraints !== 'disabled') {
32
+ throw new UnsupportedEventSequenceOperation('artifacts.constraints', 'empty constraint catalog',
33
+ 'An empty selected catalog cannot silently disable constraint discovery; set constraints: disabled explicitly.');
34
+ }
35
+ const discovered = options.artifacts?.constraints ?? new DefaultClientArtifactsProvider(TypeDiscoverer.default).constraints;
36
+ if (options.constraints !== 'disabled' && (discovered.length || eventTypes.some(type => {
37
+ const metadata = getEventTypeMetadata(type);
38
+ return getUniqueEventMetadata(type) || [...(metadata?.members.keys() ?? [])].some(key => getUniquePropertyMetadata(type, key));
39
+ }))) {
40
+ throw new UnsupportedEventSequenceOperation('artifacts.constraints', discovered.map(type => type.name).join(', ') || 'event metadata',
41
+ 'Constraint definitions are rejected until kernel-backed constraint fixtures are available.');
42
+ }
43
+ if (artifacts.eventTypeMigrations?.length) {
44
+ throw new UnsupportedEventSequenceOperation('artifacts.eventTypeMigrations', artifacts.eventTypeMigrations.map(type => type.name).join(', '),
45
+ 'Migrations are not fixture-backed.');
46
+ }
47
+ const sequence = new InProcessEventSequence(options, eventTypes);
48
+ this.eventSequence = sequence;
49
+ registerScenarioSeed(this, (source, event) => sequence.seed(source, event));
50
+ this.given = new EventScenarioGivenBuilder(this);
51
+ this.when = new EventScenarioWhenBuilder(this);
52
+ }
53
+
54
+ /** Alias for the default event-log sequence only. */
55
+ get eventLog(): IEventLog {
56
+ if (this.eventSequence.id.value !== EventSequenceId.eventLog.value) {
57
+ throw new UnsupportedEventSequenceOperation('eventLog', this.eventSequence.id.value, 'Only the default sequence is an event log.');
58
+ }
59
+ return this.eventSequence;
60
+ }
61
+ async append(source: string, event: object, options?: AppendOptions): Promise<AppendResult> {
62
+ return this.eventSequence.append(source, event, options);
63
+ }
64
+ appendMany(source: string, events: object[], options?: AppendOptions): Promise<AppendResult[]>;
65
+ appendMany(events: EventForEventSourceId[], options?: AppendOptions): Promise<AppendResult[]>;
66
+ async appendMany(_sourceOrEvents: string | EventForEventSourceId[], _eventsOrOptions?: object[] | AppendOptions, _options?: AppendOptions): Promise<AppendResult[]> {
67
+ throw new UnsupportedEventSequenceOperation('appendMany', this.eventSequence.id.value,
68
+ 'Both batch overloads require kernel-backed atomicity fixtures.');
69
+ }
70
+ get results(): readonly AppendResult[] { return (this.eventSequence as InProcessEventSequence).results; }
71
+ get appendedEvents(): readonly AppendedEvent[] { return (this.eventSequence as InProcessEventSequence).appendedEvents; }
72
+
73
+ /** Assertion view, not a callable Promise-like then method. */
74
+ get then(): { readonly results: readonly AppendResult[]; readonly appendedEvents: readonly AppendedEvent[] } {
75
+ return { results: this.results, appendedEvents: this.appendedEvents };
76
+ }
77
+ }
@@ -0,0 +1,16 @@
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 type { EventScenario } from './EventScenario.js';
5
+ import { seedScenario } from './EventScenarioSeed.js';
6
+ import { UnsupportedEventSequenceOperation } from './UnsupportedEventSequenceOperation.js';
7
+
8
+ /** Seeds exactly one event through the ordinary single-append pipeline. */
9
+ export class EventScenarioGiven {
10
+ constructor(private readonly _scenario: EventScenario, private readonly _id: string) {}
11
+ async events(...events: object[]): Promise<void> {
12
+ if (events.length !== 1) throw new UnsupportedEventSequenceOperation('given.events', this._id,
13
+ 'Zero or multiple setup events are not supported in this increment; call events once per event.');
14
+ await seedScenario(this._scenario, this._id, events[0]);
15
+ }
16
+ }
@@ -0,0 +1,11 @@
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 type { EventScenario } from './EventScenario.js';
5
+ import { EventScenarioGiven } from './EventScenarioGiven.js';
6
+
7
+ /** Selects the source for setup events. */
8
+ export class EventScenarioGivenBuilder {
9
+ constructor(private readonly _scenario: EventScenario) {}
10
+ forEventSource(id: string): EventScenarioGiven { return new EventScenarioGiven(this._scenario, id); }
11
+ }
@@ -0,0 +1,20 @@
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 type { Constructor } from '@cratis/fundamentals';
5
+ import type { EventSequenceId } from '../eventSequences/EventSequenceId.js';
6
+
7
+ /** Configuration captured once when constructing an event scenario. */
8
+ export interface EventScenarioOptions {
9
+ /** An isolated catalog; omitting it discovers process-wide event and constraint artifacts. */
10
+ artifacts?: { eventTypes: Constructor[]; constraints?: Constructor[]; eventTypeMigrations?: Constructor[] };
11
+ /** Constraint discovery is the default. Explicitly disable it only for tests without constraints. */
12
+ constraints?: 'disabled';
13
+ /** Scenario-local identifiers; only the fixture-backed defaults are supported in this increment. */
14
+ eventStore?: string;
15
+ namespace?: string;
16
+ eventSequenceId?: EventSequenceId;
17
+ /** Deterministic sources for metadata otherwise provided by the environment. */
18
+ clock?: () => Date;
19
+ correlationId?: () => string;
20
+ }
@@ -0,0 +1,17 @@
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 type { EventScenario } from './EventScenario.js';
5
+
6
+ const seeders = new WeakMap<EventScenario, (source: string, event: object) => Promise<void>>();
7
+
8
+ /** Internal builder wiring; not exported by the testing package. */
9
+ export function registerScenarioSeed(scenario: EventScenario, seed: (source: string, event: object) => Promise<void>): void {
10
+ seeders.set(scenario, seed);
11
+ }
12
+
13
+ export function seedScenario(scenario: EventScenario, source: string, event: object): Promise<void> {
14
+ const seed = seeders.get(scenario);
15
+ if (!seed) throw new Error('EventScenario setup requires a constructed scenario.');
16
+ return seed(source, event);
17
+ }
@@ -0,0 +1,16 @@
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 type { AppendResult } from '../eventSequences/AppendResult.js';
5
+ import type { EventScenario } from './EventScenario.js';
6
+ import { UnsupportedEventSequenceOperation } from './UnsupportedEventSequenceOperation.js';
7
+
8
+ /** A single act-phase append; the plural form is reserved for atomic batches. */
9
+ export class EventScenarioWhen {
10
+ constructor(private readonly _scenario: EventScenario, private readonly _id: string) {}
11
+ async event(event: object): Promise<AppendResult> { return this._scenario.append(this._id, event); }
12
+ async events(..._events: object[]): Promise<readonly AppendResult[]> {
13
+ throw new UnsupportedEventSequenceOperation('when.events/appendMany', this._id,
14
+ 'Even a one-event plural action uses kernel batch semantics; batch fixtures are not included.');
15
+ }
16
+ }
@@ -0,0 +1,11 @@
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 type { EventScenario } from './EventScenario.js';
5
+ import { EventScenarioWhen } from './EventScenarioWhen.js';
6
+
7
+ /** Selects the source for an act-phase append. */
8
+ export class EventScenarioWhenBuilder {
9
+ constructor(private readonly _scenario: EventScenario) {}
10
+ forEventSource(id: string): EventScenarioWhen { return new EventScenarioWhen(this._scenario, id); }
11
+ }
@@ -0,0 +1,275 @@
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 { createHash } from 'node:crypto';
5
+ import { EventObservationState } from '@cratis/chronicle.contracts';
6
+ import type { Constructor } from '@cratis/fundamentals';
7
+ import { getEventTypeMetadata } from '../events/eventTypeDecorator.js';
8
+ import { getTagsFor } from '../events/tagDecorator.js';
9
+ import type { AppendedEvent } from '../events/AppendedEvent.js';
10
+ import { Tag } from '../events/Tag.js';
11
+ import { EventType } from '../events/EventType.js';
12
+ import { EventTypeId } from '../events/EventTypeId.js';
13
+ import { EventTypeGeneration } from '../events/EventTypeGeneration.js';
14
+ import { Identity } from '../identity/Identity.js';
15
+ import type { AppendOptions } from '../eventSequences/AppendOptions.js';
16
+ import type { AppendResult } from '../eventSequences/AppendResult.js';
17
+ import type { AppendedEventWithResult } from '../eventSequences/AppendedEventWithResult.js';
18
+ import type { EventForEventSourceId } from '../eventSequences/EventForEventSourceId.js';
19
+ import { EventSequenceId } from '../eventSequences/EventSequenceId.js';
20
+ import { EventSequenceNumber } from '../eventSequences/EventSequenceNumber.js';
21
+ import type { IEventSequence } from '../eventSequences/IEventSequence.js';
22
+ import type { ITransactionalEventSequence } from '../eventSequences/ITransactionalEventSequence.js';
23
+ import type { CompleteStreamResult } from '../eventSequences/CompleteStreamResult.js';
24
+ import { prepareSingleAppend } from '../eventSequences/prepareSingleAppend.js';
25
+ import { toContractsGuid } from '../connection/Guid.js';
26
+ import { getUniqueEventMetadata, getUniquePropertyMetadata } from '../events/constraints/unique.js';
27
+ import { getRemovedConstraintNames } from '../events/constraints/removeConstraint.js';
28
+ import type { EventScenarioOptions } from './EventScenarioOptions.js';
29
+ import { UnsupportedEventSequenceOperation } from './UnsupportedEventSequenceOperation.js';
30
+
31
+ // JS trim() omits U+0085, which the kernel trims. Reject unproven non-ASCII whitespace and controls.
32
+ const unprovenFilterCharacters = /[\u007f-\u009f]|(?=[^\x00-\x7f])\p{White_Space}/u;
33
+
34
+ /** Fixture-backed, scenario-local single-append sequence; no kernel or observer scheduler is started. */
35
+ export class InProcessEventSequence implements IEventSequence {
36
+ readonly id: EventSequenceId;
37
+ private readonly _catalog = new Map<Function, ReturnType<typeof getEventTypeMetadata>>();
38
+ private readonly _history: AppendedEvent[] = [];
39
+ private readonly _store: string;
40
+ private readonly _namespace: string;
41
+ private readonly _clock: () => Date;
42
+ private readonly _correlationId?: () => string;
43
+ private _setup = false;
44
+ private _allowSeedAppend = false;
45
+ private _busy = false;
46
+ private readonly _results: AppendResult[] = [];
47
+
48
+ constructor(options: EventScenarioOptions, eventTypes: Constructor[]) {
49
+ if (options.eventSequenceId && options.eventSequenceId.value !== EventSequenceId.eventLog.value) {
50
+ throw this.unsupported('options.eventSequenceId', options.eventSequenceId.value, 'Custom sequences are not fixture-backed.');
51
+ }
52
+ this.id = options.eventSequenceId ?? EventSequenceId.eventLog;
53
+ if (options.eventStore !== undefined && options.eventStore !== 'test-event-store') {
54
+ throw this.unsupported('options.eventStore', options.eventStore, 'Custom event stores are not fixture-backed.');
55
+ }
56
+ if (options.namespace !== undefined && options.namespace !== 'default') {
57
+ throw this.unsupported('options.namespace', options.namespace, 'Custom namespaces are not fixture-backed.');
58
+ }
59
+ this._store = options.eventStore ?? 'test-event-store';
60
+ this._namespace = options.namespace ?? 'default';
61
+ this._clock = options.clock ?? (() => new Date());
62
+ this._correlationId = options.correlationId;
63
+ const ids = new Set<string>();
64
+ for (const type of eventTypes) {
65
+ const metadata = getEventTypeMetadata(type);
66
+ if (!metadata) throw this.unsupported('artifacts.eventTypes', type.name, 'Event type has no @eventType metadata.');
67
+ const eventType = metadata.eventType;
68
+ if (!eventType.id.value.trim() || eventType.id.value !== eventType.id.value.trim() ||
69
+ unprovenFilterCharacters.test(eventType.id.value) || eventType.id.value.includes(',')) {
70
+ throw this.unsupported('artifacts.eventTypes.id', eventType.id.value, 'Comma-separated or padded event IDs have unproven filter behavior.');
71
+ }
72
+ if (ids.has(eventType.id.value)) throw this.unsupported('artifacts.eventTypes', type.name, 'Duplicate event ID or generation history.');
73
+ ids.add(eventType.id.value);
74
+ if (eventType.generation.value !== 1 || eventType.tombstone) {
75
+ throw this.unsupported('artifacts.eventTypes', type.name, 'Only generation 1, non-tombstone events are proven.');
76
+ }
77
+ if (options.constraints !== 'disabled' && (getUniqueEventMetadata(type) || getRemovedConstraintNames(type).length ||
78
+ [...metadata.members.keys()].some(key => getUniquePropertyMetadata(type, key)))) {
79
+ throw this.unsupported('artifacts.eventTypes.constraints', type.name, 'Constraint metadata is not supported in this increment.');
80
+ }
81
+ if (getTagsFor(type).length) throw this.unsupported('artifacts.eventTypes.tags', type.name, 'Tagged events are not fixture-backed.');
82
+ // Validate the complete schema, not only properties present on a particular event.
83
+ const schema = metadata.schema;
84
+ if (schema.type !== 'object' || schema.compliance?.length || schema.security?.length ||
85
+ schema.enum !== undefined || schema.items !== undefined || schema.format !== undefined ||
86
+ schema.additionalProperties !== false ||
87
+ schema.required?.length !== Object.keys(schema.properties ?? {}).length ||
88
+ !schema.properties || Object.keys(schema.properties).length === 0 ||
89
+ Object.entries(schema.properties).some(([key, property]) =>
90
+ !/^[a-z][a-zA-Z0-9]*$/.test(key) || !['string', 'boolean'].includes(property.type ?? '') ||
91
+ property.format !== undefined || property.compliance?.length || property.security?.length ||
92
+ property.items !== undefined || property.properties !== undefined || property.enum !== undefined ||
93
+ property.additionalProperties !== undefined)) {
94
+ throw this.unsupported('artifacts.eventTypes.schema', type.name, 'Only flat, unclassified string/boolean fields are fixture-backed.');
95
+ }
96
+ this._catalog.set(type, metadata);
97
+ }
98
+ }
99
+
100
+ get results(): readonly AppendResult[] { return Object.freeze([...this._results]); }
101
+ get appendedEvents(): readonly AppendedEvent[] { return Object.freeze(this._history.map(event => this.snapshot(event))); }
102
+
103
+ /** Seed through the same append path, without recording act-phase results. */
104
+ async seed(source: string, event: object): Promise<void> {
105
+ if (this._setup) throw this.unsupported('given.events', this.id.value, 'Overlapping setup calls are not fixture-backed.');
106
+ this._setup = true;
107
+ this._allowSeedAppend = true;
108
+ let pending: Promise<AppendResult>;
109
+ try { pending = this.append(source, event); }
110
+ finally { this._allowSeedAppend = false; }
111
+ try {
112
+ const result = await pending;
113
+ if (!result.isSuccess) throw new Error(`EventScenario given setup failed: ${JSON.stringify(result)}`);
114
+ } finally {
115
+ this._setup = false;
116
+ }
117
+ }
118
+
119
+ get transactional(): ITransactionalEventSequence { throw this.unsupported('transactional', this.id.value, 'Transactions require a kernel.'); }
120
+ get appendOperations(): AsyncIterable<AppendedEventWithResult[]> { throw this.unsupported('appendOperations', this.id.value, 'Append notifications are not fixture-backed.'); }
121
+
122
+ async append(eventSourceId: string, event: object, options?: AppendOptions): Promise<AppendResult> {
123
+ if (this._busy || (this._setup && !this._allowSeedAppend)) {
124
+ throw this.unsupported('append', this.id.value, 'Overlapping calls do not have proven ordering.');
125
+ }
126
+ this._busy = true;
127
+ try {
128
+ if (!event || typeof event !== 'object') throw this.unsupported('append.event', this.id.value, 'Only registered event instances are supported.');
129
+ if (!/^[A-Za-z0-9_-]+$/.test(eventSourceId)) throw this.unsupported('append.source', eventSourceId, 'Only simple source identifiers are fixture-backed.');
130
+ if (options && (Reflect.ownKeys(options).length || options.correlationId !== undefined || options.sourceType !== undefined ||
131
+ options.streamType !== undefined || options.streamId !== undefined || options.subject !== undefined ||
132
+ options.occurred !== undefined || options.eventSourceId !== undefined || options.concurrencyScope !== undefined ||
133
+ options.tags !== undefined || options.concurrencyScopes !== undefined)) {
134
+ throw this.unsupported('append.options', event.constructor.name, 'Append metadata, routing and concurrency are not fixture-backed.');
135
+ }
136
+ const metadata = this._catalog.get(event.constructor);
137
+ if (!metadata) throw this.unsupported('append.event', event.constructor.name, 'Event is not in the selected, validated catalog.');
138
+ let prepared: ReturnType<typeof prepareSingleAppend>;
139
+ let content: Record<string, unknown>;
140
+ try {
141
+ prepared = prepareSingleAppend(event, this._correlationId ? { correlationId: this._correlationId() } : undefined);
142
+ content = JSON.parse(prepared.content) as Record<string, unknown>;
143
+ } catch (error) {
144
+ throw this.unsupported('append.serialization', event.constructor.name, `Payload or metadata could not be serialized: ${String(error)}.`);
145
+ }
146
+ try {
147
+ toContractsGuid(prepared.correlationId);
148
+ if (!/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(prepared.correlationId.toString())) throw new Error('Invalid correlation ID.');
149
+ } catch {
150
+ throw this.unsupported('append.correlationId', event.constructor.name, 'Correlation ID must be a valid GUID.');
151
+ }
152
+ if (prepared.identity !== Identity.system || prepared.causationChain.length !== 2 ||
153
+ Object.keys(prepared.causationChain[0].properties).length !== 0) {
154
+ throw this.unsupported('append.context', event.constructor.name, 'Ambient identity or causation metadata is not fixture-backed.');
155
+ }
156
+ const properties = metadata.schema.properties!;
157
+ if (!content || typeof content !== 'object' || Array.isArray(content) ||
158
+ Object.keys(content).length !== Object.keys(properties).length ||
159
+ Object.entries(properties).some(([key, property]) => typeof content[key] !== property.type ||
160
+ (property.type === 'string' && !/^[\x20-\x21\x23-\x5b\x5d-\x7e\u00e9]*$/.test(content[key] as string)))) {
161
+ throw this.unsupported('append.content', event.constructor.name, 'Content differs from the flat scalar schema; null, missing and extra values are not proven.');
162
+ }
163
+ const occurred = this._clock();
164
+ // Date has millisecond precision; only the UTC year range representable by DateTimeOffset is supported.
165
+ if (!(occurred instanceof Date) || Number.isNaN(occurred.getTime()) ||
166
+ occurred.getUTCFullYear() < 1 || occurred.getUTCFullYear() > 9999) {
167
+ throw this.unsupported('append.clock', event.constructor.name, 'Clock must return a valid Date within UTC years 1–9999 (millisecond precision).');
168
+ }
169
+ const canonical = Object.fromEntries(Object.entries(content).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
170
+ const hash = createHash('sha256').update(`${prepared.eventType.id.value}|${eventSourceId}|${JSON.stringify(canonical)}`).digest('base64');
171
+ const sequenceNumber = new EventSequenceNumber(BigInt(this._history.length));
172
+ const stored: AppendedEvent = {
173
+ eventType: prepared.eventType,
174
+ content,
175
+ context: {
176
+ eventStore: this._store, namespace: this._namespace,
177
+ sequenceNumber: sequenceNumber.value, eventSourceId,
178
+ eventSourceType: 'Default', eventStreamType: 'All', eventStreamId: 'Default',
179
+ subject: eventSourceId, hash, causedBy: prepared.identity, observationState: EventObservationState.Initial,
180
+ eventType: prepared.eventType, occurred, correlationId: prepared.correlationId.toString(),
181
+ causation: prepared.causationChain.map(item => ({ type: item.type.name, occurred: item.occurred, properties: { ...item.properties } })),
182
+ tags: prepared.tags.map(tag => new Tag(tag))
183
+ }
184
+ };
185
+ this._history.push(this.snapshot(stored));
186
+ const result: AppendResult = Object.freeze({
187
+ sequenceNumber: Object.freeze(sequenceNumber), constraintViolations: Object.freeze([]), errors: Object.freeze([]), isSuccess: true,
188
+ waitForCompletion: async () => { throw this.unsupported('waitForCompletion', event.constructor.name, 'No observers run in process.'); }
189
+ });
190
+ if (!this._setup) this._results.push(result);
191
+ return result;
192
+ } finally {
193
+ // Keep the append in flight until its promise crosses an async boundary.
194
+ await Promise.resolve();
195
+ this._busy = false;
196
+ }
197
+ }
198
+
199
+ appendMany(_eventSourceId: string, _events: object[], _options?: AppendOptions): Promise<AppendResult[]>;
200
+ appendMany(_events: EventForEventSourceId[], _options?: AppendOptions): Promise<AppendResult[]>;
201
+ async appendMany(): Promise<AppendResult[]> {
202
+ throw this.unsupported('appendMany', this.id.value, 'Batch atomicity and result mapping require kernel fixtures.');
203
+ }
204
+
205
+ async getNextSequenceNumber(): Promise<EventSequenceNumber> { return new EventSequenceNumber(BigInt(this._history.length)); }
206
+
207
+ async getTailSequenceNumber(source?: string, sourceType?: string, streamType?: string, streamId?: string, types?: Constructor[]): Promise<EventSequenceNumber> {
208
+ if (sourceType !== undefined || streamType !== undefined || streamId !== undefined || types?.length) {
209
+ throw this.unsupported('getTailSequenceNumber.filters', this.id.value, 'Route and event-type tail filters are not fixture-backed.');
210
+ }
211
+ if (source !== undefined) this.validateReadSource('getTailSequenceNumber.source', source);
212
+ const last = source === undefined ? this._history.at(-1) : [...this._history].reverse().find(event => event.context.eventSourceId === source);
213
+ return last ? new EventSequenceNumber(last.context.sequenceNumber) : EventSequenceNumber.unset;
214
+ }
215
+
216
+ async getTailSequenceNumberForObserver(observer: Constructor): Promise<EventSequenceNumber> {
217
+ throw this.unsupported('getTailSequenceNumberForObserver', observer.name, 'Observer discovery is not fixture-backed.');
218
+ }
219
+
220
+ async hasEventsFor(source: string): Promise<boolean> {
221
+ this.validateReadSource('hasEventsFor.source', source);
222
+ return this._history.some(event => event.context.eventSourceId === source);
223
+ }
224
+
225
+ async getForEventSourceIdAndEventTypes(source: string, types: Constructor[], streamType?: string, streamId?: string, sourceType?: string): Promise<AppendedEvent[]> {
226
+ if (streamType !== undefined || streamId !== undefined || sourceType !== undefined || !types.length) {
227
+ throw this.unsupported('getForEventSourceIdAndEventTypes.filters', source, 'Only explicit event-type/source filtering is fixture-backed.');
228
+ }
229
+ this.validateReadSource('getForEventSourceIdAndEventTypes.source', source);
230
+ const ids = types.map(type => {
231
+ const metadata = this._catalog.get(type);
232
+ if (!metadata) throw this.unsupported('read.eventTypes', type.name, 'Event type is not in the selected catalog.');
233
+ return metadata.eventType.id.value;
234
+ });
235
+ return this._history.filter(event => event.context.eventSourceId === source && ids.includes(event.eventType.id.value)).map(event => this.snapshot(event));
236
+ }
237
+
238
+ async getFromSequenceNumber(sequence: EventSequenceNumber, source?: string, types?: Constructor[]): Promise<AppendedEvent[]> {
239
+ if (sequence.value < 0n || sequence.value > EventSequenceNumber.unset.value) {
240
+ throw this.unsupported('getFromSequenceNumber.sequenceNumber', sequence.value.toString(), 'Sequence numbers must fit unsigned 64-bit wire values.');
241
+ }
242
+ if (types?.length) throw this.unsupported('getFromSequenceNumber.filterEventTypes', this.id.value, 'Event-type filtering on sequence reads is not fixture-backed.');
243
+ if (source !== undefined) this.validateReadSource('getFromSequenceNumber.source', source);
244
+ return this._history.filter(event => event.context.sequenceNumber >= sequence.value &&
245
+ (source === undefined || event.context.eventSourceId === source)).map(event => this.snapshot(event));
246
+ }
247
+
248
+ async redact(): Promise<void> { throw this.unsupported('redact', this.id.value, 'Redaction requires kernel storage.'); }
249
+ async redactForEventSource(): Promise<void> { throw this.unsupported('redactForEventSource', this.id.value, 'Redaction requires kernel storage.'); }
250
+ async completeStream(): Promise<CompleteStreamResult> { throw this.unsupported('completeStream', this.id.value, 'Stream completion requires a kernel.'); }
251
+
252
+ private snapshot(event: AppendedEvent): AppendedEvent {
253
+ const eventType = new EventType(new EventTypeId(event.eventType.id.value),
254
+ new EventTypeGeneration(event.eventType.generation.value), event.eventType.tombstone);
255
+ const causedBy = event.context.causedBy;
256
+ return {
257
+ eventType,
258
+ content: JSON.parse(JSON.stringify(event.content)) as Record<string, unknown>,
259
+ context: { ...event.context, eventType, occurred: new Date(event.context.occurred),
260
+ causedBy: causedBy && new Identity(causedBy.subject, causedBy.name, causedBy.userName, causedBy.onBehalfOf),
261
+ causation: event.context.causation.map(item => ({ ...item, occurred: item.occurred && new Date(item.occurred), properties: { ...item.properties } })),
262
+ tags: event.context.tags.map(item => new Tag(item.value)) }
263
+ };
264
+ }
265
+
266
+ private validateReadSource(operation: string, source: string): void {
267
+ if (!source.trim() || source !== source.trim() || unprovenFilterCharacters.test(source)) {
268
+ throw this.unsupported(operation, source, 'Blank and padded source filters have unproven kernel normalization.');
269
+ }
270
+ }
271
+
272
+ private unsupported(operation: string, artifact: string, reason: string): UnsupportedEventSequenceOperation {
273
+ return new UnsupportedEventSequenceOperation(operation, artifact, reason);
274
+ }
275
+ }
@@ -0,0 +1,10 @@
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
+ /** A kernel operation or definition outside the fixture-backed in-process boundary. */
5
+ export class UnsupportedEventSequenceOperation extends Error {
6
+ constructor(operation: string, artifact: string, reason: string) {
7
+ super(`UnsupportedEventSequenceOperation: ${operation} (${artifact}): ${reason} Use a kernel-backed test.`);
8
+ this.name = 'UnsupportedEventSequenceOperation';
9
+ }
10
+ }
package/testing/index.ts CHANGED
@@ -1,6 +1,13 @@
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
+ export { EventScenario } from './EventScenario.js';
5
+ export type { EventScenarioOptions } from './EventScenarioOptions.js';
6
+ export { EventScenarioGivenBuilder } from './EventScenarioGivenBuilder.js';
7
+ export { EventScenarioGiven } from './EventScenarioGiven.js';
8
+ export { EventScenarioWhenBuilder } from './EventScenarioWhenBuilder.js';
9
+ export { EventScenarioWhen } from './EventScenarioWhen.js';
10
+ export { UnsupportedEventSequenceOperation } from './UnsupportedEventSequenceOperation.js';
4
11
  export { ReadModelScenario } from './ReadModelScenario.js';
5
12
  export { ReadModelScenarioGiven } from './ReadModelScenarioGiven.js';
6
13
  export { ReadModelScenarioGivenBuilder } from './ReadModelScenarioGivenBuilder.js';