@shirudo/ddd-kit 1.3.0 → 2.2.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.
package/dist/testing.js CHANGED
@@ -14,6 +14,7 @@ var BUILT_IN_TAGS = /* @__PURE__ */ new Set([
14
14
  "[object Boolean]",
15
15
  "[object Number]",
16
16
  "[object String]",
17
+ "[object BigInt]",
17
18
  "[object ArrayBuffer]",
18
19
  "[object SharedArrayBuffer]",
19
20
  "[object DataView]"
@@ -34,11 +35,113 @@ var arrayBufferByteLengthGet = intrinsicGetter(
34
35
  ArrayBuffer.prototype,
35
36
  "byteLength"
36
37
  );
38
+ var sharedArrayBufferByteLengthGet = typeof SharedArrayBuffer === "undefined" ? void 0 : intrinsicGetter(SharedArrayBuffer.prototype, "byteLength");
37
39
  var regExpSourceGet = intrinsicGetter(RegExp.prototype, "source");
38
40
  var booleanValueOf = Boolean.prototype.valueOf;
39
41
  var numberValueOf = Number.prototype.valueOf;
40
42
  var stringValueOf = String.prototype.valueOf;
43
+ var bigIntValueOf = BigInt.prototype.valueOf;
44
+ var functionToString = Function.prototype.toString;
41
45
  var PROBE_KEY = {};
46
+ var INTRINSIC_CONSTRUCTOR_NAMES = [
47
+ "Object",
48
+ "Array",
49
+ "Date",
50
+ "RegExp",
51
+ "Map",
52
+ "Set",
53
+ "WeakMap",
54
+ "WeakSet",
55
+ "Promise",
56
+ "Error",
57
+ "EvalError",
58
+ "RangeError",
59
+ "ReferenceError",
60
+ "SyntaxError",
61
+ "TypeError",
62
+ "URIError",
63
+ "AggregateError",
64
+ "Boolean",
65
+ "Number",
66
+ "String",
67
+ "BigInt",
68
+ "ArrayBuffer",
69
+ "SharedArrayBuffer",
70
+ "DataView",
71
+ "Int8Array",
72
+ "Uint8Array",
73
+ "Uint8ClampedArray",
74
+ "Int16Array",
75
+ "Uint16Array",
76
+ "Int32Array",
77
+ "Uint32Array",
78
+ "Float32Array",
79
+ "Float64Array",
80
+ "BigInt64Array",
81
+ "BigUint64Array"
82
+ ];
83
+ var intrinsicConstructorSources = new Map(
84
+ INTRINSIC_CONSTRUCTOR_NAMES.flatMap((name) => {
85
+ const descriptor = Object.getOwnPropertyDescriptor(globalThis, name);
86
+ const intrinsic = descriptor?.value;
87
+ return typeof intrinsic === "function" ? [[name, functionToString.call(intrinsic)]] : [];
88
+ })
89
+ );
90
+ var intrinsicConstructorSourceSet = new Set(
91
+ intrinsicConstructorSources.values()
92
+ );
93
+ var ERROR_INTRINSIC_NAMES = [
94
+ "Error",
95
+ "EvalError",
96
+ "RangeError",
97
+ "ReferenceError",
98
+ "SyntaxError",
99
+ "TypeError",
100
+ "URIError",
101
+ "AggregateError"
102
+ ];
103
+ function isIntrinsicConstructorPrototype(prototype, expectedName) {
104
+ const constructorDescriptor = Object.getOwnPropertyDescriptor(
105
+ prototype,
106
+ "constructor"
107
+ );
108
+ const candidateConstructor = constructorDescriptor?.value;
109
+ if (constructorDescriptor === void 0 || !("value" in constructorDescriptor) || typeof candidateConstructor !== "function") {
110
+ return false;
111
+ }
112
+ let candidateSource;
113
+ try {
114
+ candidateSource = functionToString.call(candidateConstructor);
115
+ } catch {
116
+ return false;
117
+ }
118
+ const expectedSource = expectedName === void 0 ? void 0 : intrinsicConstructorSources.get(expectedName);
119
+ if (expectedSource !== void 0 && candidateSource !== expectedSource || expectedSource === void 0 && !intrinsicConstructorSourceSet.has(candidateSource)) {
120
+ return false;
121
+ }
122
+ const nameDescriptor = Object.getOwnPropertyDescriptor(
123
+ candidateConstructor,
124
+ "name"
125
+ );
126
+ const candidateName = nameDescriptor !== void 0 && "value" in nameDescriptor && typeof nameDescriptor.value === "string" ? nameDescriptor.value : void 0;
127
+ const intrinsicName = expectedName ?? candidateName;
128
+ const intrinsicSource = intrinsicName === void 0 ? void 0 : intrinsicConstructorSources.get(intrinsicName);
129
+ return candidateName === intrinsicName && intrinsicSource !== void 0 && candidateSource === intrinsicSource && Object.getOwnPropertyDescriptor(candidateConstructor, "prototype")?.value === prototype;
130
+ }
131
+ __name(isIntrinsicConstructorPrototype, "isIntrinsicConstructorPrototype");
132
+ function hasNativePrototype(value, expectedName) {
133
+ const visited = /* @__PURE__ */ new WeakSet();
134
+ let prototype = Object.getPrototypeOf(value);
135
+ while (prototype !== null && !visited.has(prototype)) {
136
+ visited.add(prototype);
137
+ if (isIntrinsicConstructorPrototype(prototype, expectedName)) {
138
+ return true;
139
+ }
140
+ prototype = Object.getPrototypeOf(prototype);
141
+ }
142
+ return false;
143
+ }
144
+ __name(hasNativePrototype, "hasNativePrototype");
42
145
  function hasBrand(obj, tag) {
43
146
  try {
44
147
  switch (tag) {
@@ -66,6 +169,10 @@ function hasBrand(obj, tag) {
66
169
  case "[object ArrayBuffer]":
67
170
  arrayBufferByteLengthGet.call(obj);
68
171
  return true;
172
+ case "[object SharedArrayBuffer]":
173
+ if (!sharedArrayBufferByteLengthGet) return false;
174
+ sharedArrayBufferByteLengthGet.call(obj);
175
+ return true;
69
176
  case "[object Boolean]":
70
177
  booleanValueOf.call(obj);
71
178
  return true;
@@ -75,8 +182,17 @@ function hasBrand(obj, tag) {
75
182
  case "[object String]":
76
183
  stringValueOf.call(obj);
77
184
  return true;
78
- default:
185
+ case "[object BigInt]":
186
+ bigIntValueOf.call(obj);
79
187
  return true;
188
+ case "[object Promise]":
189
+ return hasNativePrototype(obj, "Promise");
190
+ case "[object Error]":
191
+ return ERROR_INTRINSIC_NAMES.some(
192
+ (name) => hasNativePrototype(obj, name)
193
+ );
194
+ default:
195
+ return false;
80
196
  }
81
197
  } catch {
82
198
  return false;
@@ -149,12 +265,17 @@ function deepEqualInner(a, b, visited) {
149
265
  }
150
266
  if (Array.isArray(objA) || Array.isArray(objB)) {
151
267
  if (!Array.isArray(objA) || !Array.isArray(objB)) return false;
268
+ if (objA.length !== objB.length) return false;
269
+ const keysA = Reflect.ownKeys(objA).filter((key) => key !== "length");
270
+ const keysB = Reflect.ownKeys(objB).filter((key) => key !== "length");
271
+ if (keysA.length !== keysB.length) return false;
152
272
  const arrA = objA;
153
273
  const arrB = objB;
154
- const len = arrA.length;
155
- if (len !== arrB.length) return false;
156
- for (let i = 0; i < len; i++) {
157
- if (!deepEqualInner(arrA[i], arrB[i], visited)) return false;
274
+ for (const key of keysA) {
275
+ if (!objHasOwn.call(objB, key)) return false;
276
+ if (!deepEqualInner(arrA[key], arrB[key], visited)) {
277
+ return false;
278
+ }
158
279
  }
159
280
  return true;
160
281
  }
@@ -198,7 +319,8 @@ function deepEqualInner(a, b, visited) {
198
319
  }
199
320
  case "[object Boolean]":
200
321
  case "[object Number]":
201
- case "[object String]": {
322
+ case "[object String]":
323
+ case "[object BigInt]": {
202
324
  return sameValueZero(
203
325
  objA.valueOf(),
204
326
  objB.valueOf()
@@ -213,8 +335,8 @@ __name(deepEqualInner, "deepEqualInner");
213
335
  function comparePlainObjects(objA, objB, visited) {
214
336
  const recA = objA;
215
337
  const recB = objB;
216
- const stringKeysA = Object.keys(objA);
217
- const stringKeysB = Object.keys(objB);
338
+ const stringKeysA = Object.getOwnPropertyNames(objA);
339
+ const stringKeysB = Object.getOwnPropertyNames(objB);
218
340
  if (stringKeysA.length !== stringKeysB.length) return false;
219
341
  const symbolKeysA = Object.getOwnPropertySymbols(objA);
220
342
  const symbolKeysB = Object.getOwnPropertySymbols(objB);
@@ -240,42 +362,133 @@ function comparePlainObjects(objA, objB, visited) {
240
362
  }
241
363
  __name(comparePlainObjects, "comparePlainObjects");
242
364
 
243
- // src/testing/repository-contract.ts
244
- function createRepositoryContractTests(harness) {
245
- async function withEnvironment(body) {
246
- const env = await harness.createEnvironment();
247
- let bodyFailed = false;
248
- let bodyError;
249
- try {
250
- await body(env);
251
- } catch (error) {
252
- bodyFailed = true;
253
- bodyError = error;
365
+ // src/testing/contract-assertions.ts
366
+ async function runInContractEnvironment(createEnvironment, body) {
367
+ const env = await createEnvironment();
368
+ let bodyFailed = false;
369
+ let bodyError;
370
+ try {
371
+ await body(env);
372
+ } catch (error) {
373
+ bodyFailed = true;
374
+ bodyError = error;
375
+ }
376
+ try {
377
+ await env.teardown?.();
378
+ } catch (teardownError) {
379
+ if (!bodyFailed) {
380
+ throw teardownError;
254
381
  }
382
+ }
383
+ if (bodyFailed) {
384
+ throw bodyError;
385
+ }
386
+ }
387
+ __name(runInContractEnvironment, "runInContractEnvironment");
388
+ function captureRejection(promise) {
389
+ return promise.then(
390
+ () => void 0,
391
+ (error) => error
392
+ );
393
+ }
394
+ __name(captureRejection, "captureRejection");
395
+ async function loadAggregateOrFail(repository, id, suspectHint) {
396
+ const loaded = await repository.getById(id);
397
+ assert(
398
+ loaded !== null,
399
+ `getById(${String(id)}) returned null for an aggregate that must exist: ${suspectHint}`
400
+ );
401
+ return loaded;
402
+ }
403
+ __name(loadAggregateOrFail, "loadAggregateOrFail");
404
+ function skippedContractTest(name, capability) {
405
+ return {
406
+ name,
407
+ skipped: { capability },
408
+ run: /* @__PURE__ */ __name(async () => {
409
+ throw new Error(
410
+ `Repository contract test skipped: harness capability '${capability}' is not provided. Bind skipped tests with it.skip ((test.skipped ? it.skip : it)(test.name, test.run)) or provide the capability; each one closes a real OCC hole.`
411
+ );
412
+ }, "run")
413
+ };
414
+ }
415
+ __name(skippedContractTest, "skippedContractTest");
416
+ function assert(condition, message) {
417
+ if (!condition) {
418
+ throw new Error(`Repository contract violated: ${message}`);
419
+ }
420
+ }
421
+ __name(assert, "assert");
422
+ function assertEqual(actual, expected, message) {
423
+ if (actual !== expected) {
424
+ throw new Error(
425
+ `Repository contract violated: ${message} (expected ${String(expected)}, got ${String(actual)})`
426
+ );
427
+ }
428
+ }
429
+ __name(assertEqual, "assertEqual");
430
+ function chainContainsErrorNamed(error, name) {
431
+ const seen = /* @__PURE__ */ new Set();
432
+ let current = error;
433
+ while (current !== null && current !== void 0 && !seen.has(current)) {
434
+ if (typeof current !== "object") {
435
+ return false;
436
+ }
437
+ if (errorMatchesName(current, name)) {
438
+ return true;
439
+ }
440
+ seen.add(current);
255
441
  try {
256
- await env.teardown?.();
257
- } catch (teardownError) {
258
- if (!bodyFailed) {
259
- throw teardownError;
260
- }
442
+ current = current.cause;
443
+ } catch {
444
+ return false;
445
+ }
446
+ }
447
+ return false;
448
+ }
449
+ __name(chainContainsErrorNamed, "chainContainsErrorNamed");
450
+ function chainContainsErrorNamedAnyOf(error, names) {
451
+ return names.some((name) => chainContainsErrorNamed(error, name));
452
+ }
453
+ __name(chainContainsErrorNamedAnyOf, "chainContainsErrorNamedAnyOf");
454
+ function errorMatchesName(candidate, name) {
455
+ try {
456
+ if (candidate.name === name) {
457
+ return true;
261
458
  }
262
- if (bodyFailed) {
263
- throw bodyError;
459
+ } catch {
460
+ }
461
+ try {
462
+ let proto = Object.getPrototypeOf(candidate);
463
+ for (let depth = 0; proto !== null && depth < 20; depth++) {
464
+ if (proto.constructor?.name === name) {
465
+ return true;
466
+ }
467
+ proto = Object.getPrototypeOf(proto);
264
468
  }
469
+ } catch {
265
470
  }
266
- __name(withEnvironment, "withEnvironment");
267
- async function loadOrFail(repository, id) {
268
- const loaded = await repository.getById(id);
269
- assert(
270
- loaded !== null,
271
- `getById(${String(id)}) returned null for an aggregate that must exist - broken hydration or a write that did not commit`
272
- );
273
- return loaded;
471
+ return false;
472
+ }
473
+ __name(errorMatchesName, "errorMatchesName");
474
+ function describeError(error) {
475
+ if (error instanceof Error) {
476
+ return `${error.name}: ${error.message}`;
274
477
  }
275
- __name(loadOrFail, "loadOrFail");
478
+ return String(error);
479
+ }
480
+ __name(describeError, "describeError");
481
+
482
+ // src/testing/es-repository-contract.ts
483
+ function createEsRepositoryContractTests(harness) {
484
+ const withEnvironment = /* @__PURE__ */ __name((body) => runInContractEnvironment(() => harness.createEnvironment(), body), "withEnvironment");
485
+ const loadOrFail = /* @__PURE__ */ __name((repository, id) => loadAggregateOrFail(
486
+ repository,
487
+ id,
488
+ "broken replay read or a write that did not commit"
489
+ ), "loadOrFail");
276
490
  async function seed(env) {
277
491
  const aggregate = harness.createAggregate();
278
- harness.mutate(aggregate);
279
492
  await env.run(async ({ repository }) => {
280
493
  await repository.save(aggregate);
281
494
  });
@@ -286,13 +499,291 @@ function createRepositoryContractTests(harness) {
286
499
  return env.run(({ repository }) => loadOrFail(repository, id));
287
500
  }
288
501
  __name(reload, "reload");
289
- function captureRejection(promise) {
290
- return promise.then(
291
- () => void 0,
292
- (error) => error
293
- );
502
+ const orderedIds = /* @__PURE__ */ __name((events) => events.map((event) => event.eventId), "orderedIds");
503
+ const sortedIds = /* @__PURE__ */ __name((events) => orderedIds(events).sort(), "sortedIds");
504
+ const snapshotState = harness.snapshotState;
505
+ const createAggregateWithId = harness.createAggregateWithId;
506
+ const skippedTest = /* @__PURE__ */ __name((name, capability) => skippedContractTest(name, capability), "skippedTest");
507
+ const tests = [
508
+ {
509
+ name: "MANDATORY two-writer conflict: the stale writer's append throws ConcurrencyConflictError and the stream is untouched",
510
+ run: /* @__PURE__ */ __name(() => withEnvironment(async (env) => {
511
+ const seeded = await seed(env);
512
+ const seedStream = await env.committedStreamEvents(seeded.id);
513
+ assert(
514
+ seedStream.length > 0,
515
+ "seeding must have appended the creation event to the stream"
516
+ );
517
+ const staleB = await reload(env, seeded.id);
518
+ const committedA = await env.run(async ({ repository }) => {
519
+ const a = await loadOrFail(repository, seeded.id);
520
+ harness.mutate(a);
521
+ await repository.save(a);
522
+ return a;
523
+ });
524
+ const streamAfterA = await env.committedStreamEvents(seeded.id);
525
+ assert(
526
+ streamAfterA.length === seedStream.length + 1,
527
+ "writer A's event must reach the stream on commit"
528
+ );
529
+ harness.mutate(staleB);
530
+ harness.mutate(staleB);
531
+ const rejection = await captureRejection(
532
+ env.run(async ({ repository }) => {
533
+ await repository.save(staleB);
534
+ })
535
+ );
536
+ assert(
537
+ rejection !== void 0,
538
+ "the second writer's commit must reject; it appended on a stale expectedVersion instead (append guard missing?)"
539
+ );
540
+ assert(
541
+ chainContainsErrorNamed(rejection, "ConcurrencyConflictError"),
542
+ `the second writer's rejection must be (or wrap, via the cause chain) ConcurrencyConflictError; got: ${describeError(rejection)}`
543
+ );
544
+ const finalStream = await env.committedStreamEvents(seeded.id);
545
+ assert(
546
+ deepEqual(orderedIds(finalStream), orderedIds(streamAfterA)),
547
+ "the stream must contain exactly the winning writer's events in order: a rejected append must leave the stream untouched"
548
+ );
549
+ const final = await reload(env, seeded.id);
550
+ assertEqual(
551
+ final.version,
552
+ committedA.version,
553
+ "the reloaded version must equal writer A's committed version (version IS the event count)"
554
+ );
555
+ if (snapshotState) {
556
+ assert(
557
+ deepEqual(
558
+ snapshotState.call(harness, final),
559
+ snapshotState.call(harness, committedA)
560
+ ),
561
+ "the reloaded state must fold to writer A's state. Suspects: a partial append survived the rejection, or your snapshotState projection is not roundtrip-stable"
562
+ );
563
+ }
564
+ const outbox = await env.committedOutboxEvents();
565
+ assert(
566
+ deepEqual(sortedIds(outbox), sortedIds(streamAfterA)),
567
+ "the outbox must contain exactly the committed events (compared by eventId); nothing from the stale writer"
568
+ );
569
+ }), "run")
570
+ },
571
+ {
572
+ name: "replay equality: a reloaded aggregate folds the committed stream in emission order",
573
+ run: /* @__PURE__ */ __name(() => withEnvironment(async (env) => {
574
+ const aggregate = harness.createAggregate();
575
+ harness.mutate(aggregate);
576
+ harness.mutate(aggregate);
577
+ const emittedIds = orderedIds(aggregate.pendingEvents);
578
+ assertEqual(
579
+ emittedIds.length,
580
+ 3,
581
+ "harness contract: createAggregate applies ONE creation event and mutate applies ONE event each"
582
+ );
583
+ await env.run(async ({ repository }) => {
584
+ await repository.save(aggregate);
585
+ });
586
+ const stream = await env.committedStreamEvents(aggregate.id);
587
+ assert(
588
+ deepEqual(orderedIds(stream), emittedIds),
589
+ "the committed stream must contain exactly the emitted events in emission order; reordering breaks every consumer's fold"
590
+ );
591
+ assertEqual(
592
+ aggregate.pendingEvents.length,
593
+ 0,
594
+ "pending events must be cleared after a successful commit (markPersisted ran)"
595
+ );
596
+ assertEqual(
597
+ aggregate.persistedVersion,
598
+ aggregate.version,
599
+ "after a successful commit, persistedVersion must equal version"
600
+ );
601
+ const reloaded = await reload(env, aggregate.id);
602
+ assertEqual(
603
+ reloaded.version,
604
+ aggregate.version,
605
+ "the reloaded version must equal the event count"
606
+ );
607
+ assertEqual(
608
+ reloaded.persistedVersion,
609
+ reloaded.version,
610
+ "a reloaded aggregate's persistedVersion must equal its version"
611
+ );
612
+ assertEqual(
613
+ reloaded.pendingEvents.length,
614
+ 0,
615
+ "a reloaded aggregate must not carry pending events (replay is not re-recording)"
616
+ );
617
+ if (snapshotState) {
618
+ assert(
619
+ deepEqual(
620
+ snapshotState.call(harness, reloaded),
621
+ snapshotState.call(harness, aggregate)
622
+ ),
623
+ "the reloaded aggregate must fold to the same state as the in-memory instance. Suspects: fold order (readStream must return emission order), or a snapshotState projection that is not roundtrip-stable"
624
+ );
625
+ }
626
+ }), "run")
627
+ },
628
+ {
629
+ name: "getById returns null for a stream that does not exist",
630
+ run: /* @__PURE__ */ __name(() => withEnvironment(async (env) => {
631
+ const never = harness.createAggregate();
632
+ const probe = await env.run(
633
+ ({ repository }) => repository.getById(never.id)
634
+ );
635
+ assert(
636
+ probe === null,
637
+ "getById of a never-persisted id must return null (empty stream = no aggregate)"
638
+ );
639
+ }), "run")
640
+ },
641
+ {
642
+ name: "identity map: two getById calls in one unit of work return the same instance",
643
+ run: /* @__PURE__ */ __name(() => withEnvironment(async (env) => {
644
+ const seeded = await seed(env);
645
+ await env.run(async ({ repository }) => {
646
+ const first = await repository.getById(seeded.id);
647
+ const second = await repository.getById(seeded.id);
648
+ assert(
649
+ first !== null && first === second,
650
+ "repeated loads within one unit of work must return the SAME instance (identity map); distinct instances double-harvest events"
651
+ );
652
+ });
653
+ }), "run")
654
+ },
655
+ {
656
+ name: "rollback persists nothing: stream and outbox untouched, pending events survive, first save can be retried",
657
+ run: /* @__PURE__ */ __name(() => withEnvironment(async (env) => {
658
+ const aggregate = harness.createAggregate();
659
+ harness.mutate(aggregate);
660
+ const pendingBefore = aggregate.pendingEvents.length;
661
+ await captureRejection(
662
+ env.run(async ({ repository }) => {
663
+ await repository.save(aggregate);
664
+ throw new Error("contract rollback probe");
665
+ })
666
+ );
667
+ assertEqual(
668
+ (await env.committedStreamEvents(aggregate.id)).length,
669
+ 0,
670
+ "a rolled-back transaction must not leave events in the stream"
671
+ );
672
+ assertEqual(
673
+ (await env.committedOutboxEvents()).length,
674
+ 0,
675
+ "a rolled-back transaction must not leave events in the outbox"
676
+ );
677
+ assertEqual(
678
+ aggregate.pendingEvents.length,
679
+ pendingBefore,
680
+ "pending events must survive a rollback (so the first save can be retried)"
681
+ );
682
+ assert(
683
+ aggregate.persistedVersion === void 0,
684
+ "a rolled-back first save must leave persistedVersion undefined (the stream does not exist)"
685
+ );
686
+ await env.run(async ({ repository }) => {
687
+ await repository.save(aggregate);
688
+ });
689
+ assertEqual(
690
+ (await env.committedStreamEvents(aggregate.id)).length,
691
+ pendingBefore,
692
+ "the retried first save must append the full pending history"
693
+ );
694
+ assertEqual(
695
+ aggregate.persistedVersion,
696
+ aggregate.version,
697
+ "after the successful retry, persistedVersion must equal version"
698
+ );
699
+ }), "run")
700
+ },
701
+ {
702
+ name: "readStream honors fromVersion: the snapshot catch-up read returns exactly the events after the position",
703
+ run: /* @__PURE__ */ __name(() => withEnvironment(async (env) => {
704
+ const aggregate = harness.createAggregate();
705
+ harness.mutate(aggregate);
706
+ harness.mutate(aggregate);
707
+ await env.run(async ({ repository }) => {
708
+ await repository.save(aggregate);
709
+ });
710
+ const full = await env.committedStreamEvents(aggregate.id);
711
+ assertEqual(full.length, 3, "seeding must have committed 3 events");
712
+ const afterOne = await env.committedStreamEvents(aggregate.id, 1);
713
+ assert(
714
+ deepEqual(orderedIds(afterOne), orderedIds(full.slice(1))),
715
+ "fromVersion=1 must return exactly the events after stream position 1, in order; restoreFromSnapshotWithEvents replays exactly this window"
716
+ );
717
+ const afterAll = await env.committedStreamEvents(
718
+ aggregate.id,
719
+ full.length
720
+ );
721
+ assertEqual(
722
+ afterAll.length,
723
+ 0,
724
+ "fromVersion at the stream head must return no events"
725
+ );
726
+ }), "run")
727
+ }
728
+ ];
729
+ tests.push(
730
+ createAggregateWithId ? {
731
+ name: "duplicate create: two creators racing on one stream; the second append conflicts and the stream is untouched",
732
+ run: /* @__PURE__ */ __name(() => withEnvironment(async (env) => {
733
+ const seeded = await seed(env);
734
+ const seedStream = await env.committedStreamEvents(seeded.id);
735
+ const duplicate = createAggregateWithId.call(
736
+ harness,
737
+ seeded.id
738
+ );
739
+ const rejection = await captureRejection(
740
+ env.run(async ({ repository }) => {
741
+ await repository.save(duplicate);
742
+ })
743
+ );
744
+ assert(
745
+ chainContainsErrorNamedAnyOf(rejection, [
746
+ "ConcurrencyConflictError",
747
+ "DuplicateAggregateError"
748
+ ]),
749
+ `the duplicate creator's append (expectedVersion 0 on an existing stream) must reject with (or wrap) ConcurrencyConflictError or DuplicateAggregateError; got: ${describeError(rejection)}`
750
+ );
751
+ const finalStream = await env.committedStreamEvents(seeded.id);
752
+ assert(
753
+ deepEqual(orderedIds(finalStream), orderedIds(seedStream)),
754
+ "the existing stream must be untouched by the rejected duplicate create"
755
+ );
756
+ }), "run")
757
+ } : skippedTest(
758
+ "duplicate create: two creators racing on one stream; the second append conflicts and the stream is untouched",
759
+ "createAggregateWithId"
760
+ )
761
+ );
762
+ return tests;
763
+ }
764
+ __name(createEsRepositoryContractTests, "createEsRepositoryContractTests");
765
+
766
+ // src/testing/repository-contract.ts
767
+ function createRepositoryContractTests(harness) {
768
+ const withEnvironment = /* @__PURE__ */ __name((body) => runInContractEnvironment(() => harness.createEnvironment(), body), "withEnvironment");
769
+ const loadOrFail = /* @__PURE__ */ __name((repository, id) => loadAggregateOrFail(
770
+ repository,
771
+ id,
772
+ "broken hydration or a write that did not commit"
773
+ ), "loadOrFail");
774
+ async function seed(env) {
775
+ const aggregate = harness.createAggregate();
776
+ harness.mutate(aggregate);
777
+ await env.run(async ({ repository }) => {
778
+ await repository.save(aggregate);
779
+ });
780
+ return aggregate;
294
781
  }
295
- __name(captureRejection, "captureRejection");
782
+ __name(seed, "seed");
783
+ async function reload(env, id) {
784
+ return env.run(({ repository }) => loadOrFail(repository, id));
785
+ }
786
+ __name(reload, "reload");
296
787
  const eventIds = /* @__PURE__ */ __name((events) => events.map((event) => event.eventId).sort(), "eventIds");
297
788
  const snapshotState = harness.snapshotState;
298
789
  const mutateVersionOnly = harness.mutateVersionOnly;
@@ -300,18 +791,7 @@ function createRepositoryContractTests(harness) {
300
791
  const createAggregateWithId = harness.createAggregateWithId;
301
792
  const deletesAreVersionChecked = harness.deletesAreVersionChecked === true;
302
793
  const insertsAreDuplicateChecked = harness.insertsAreDuplicateChecked !== false;
303
- function skippedTest(name, capability) {
304
- return {
305
- name,
306
- skipped: { capability },
307
- run: /* @__PURE__ */ __name(async () => {
308
- throw new Error(
309
- `Repository contract test skipped: harness capability '${capability}' is not provided. Bind skipped tests with it.skip ((test.skipped ? it.skip : it)(test.name, test.run)) or provide the capability - each one closes a real OCC hole.`
310
- );
311
- }, "run")
312
- };
313
- }
314
- __name(skippedTest, "skippedTest");
794
+ const skippedTest = /* @__PURE__ */ __name((name, capability) => skippedContractTest(name, capability), "skippedTest");
315
795
  const tests = [
316
796
  {
317
797
  name: "MANDATORY two-writer conflict: the stale writer throws ConcurrencyConflictError and persists nothing",
@@ -726,68 +1206,7 @@ function createRepositoryContractTests(harness) {
726
1206
  return tests;
727
1207
  }
728
1208
  __name(createRepositoryContractTests, "createRepositoryContractTests");
729
- function assert(condition, message) {
730
- if (!condition) {
731
- throw new Error(`Repository contract violated: ${message}`);
732
- }
733
- }
734
- __name(assert, "assert");
735
- function assertEqual(actual, expected, message) {
736
- if (actual !== expected) {
737
- throw new Error(
738
- `Repository contract violated: ${message} (expected ${String(expected)}, got ${String(actual)})`
739
- );
740
- }
741
- }
742
- __name(assertEqual, "assertEqual");
743
- function chainContainsErrorNamed(error, name) {
744
- const seen = /* @__PURE__ */ new Set();
745
- let current = error;
746
- while (current !== null && current !== void 0 && !seen.has(current)) {
747
- if (typeof current !== "object") {
748
- return false;
749
- }
750
- if (errorMatchesName(current, name)) {
751
- return true;
752
- }
753
- seen.add(current);
754
- try {
755
- current = current.cause;
756
- } catch {
757
- return false;
758
- }
759
- }
760
- return false;
761
- }
762
- __name(chainContainsErrorNamed, "chainContainsErrorNamed");
763
- function errorMatchesName(candidate, name) {
764
- try {
765
- if (candidate.name === name) {
766
- return true;
767
- }
768
- } catch {
769
- }
770
- try {
771
- let proto = Object.getPrototypeOf(candidate);
772
- for (let depth = 0; proto !== null && depth < 20; depth++) {
773
- if (proto.constructor?.name === name) {
774
- return true;
775
- }
776
- proto = Object.getPrototypeOf(proto);
777
- }
778
- } catch {
779
- }
780
- return false;
781
- }
782
- __name(errorMatchesName, "errorMatchesName");
783
- function describeError(error) {
784
- if (error instanceof Error) {
785
- return `${error.name}: ${error.message}`;
786
- }
787
- return String(error);
788
- }
789
- __name(describeError, "describeError");
790
1209
 
791
- export { createRepositoryContractTests };
1210
+ export { createEsRepositoryContractTests, createRepositoryContractTests };
792
1211
  //# sourceMappingURL=testing.js.map
793
1212
  //# sourceMappingURL=testing.js.map