@shirudo/ddd-kit 2.2.0 → 3.0.0-rc.11

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.d.ts CHANGED
@@ -1,374 +1,722 @@
1
- import { I as IAggregateRoot, a as Id, A as AnyDomainEvent } from './aggregate-DFi6HlEh.js';
2
- import '@shirudo/result';
3
- import '@shirudo/base-error';
4
-
1
+ import { A as IdempotencyClaimHandle, E as ProjectionCheckpointStore, Et as EventCommitCandidate, F as IdempotencyStore, H as DeadlineStore, Ot as AnyDomainEvent, Q as EventBus, Qt as AggregateIdentity, Tt as CommittedDomainEvent, bt as Outbox, c as StreamReadResult, ct as Id, ht as CommandOutboxWriter, i as EventStore, n as ReplayableStreamPages, nt as Aggregate, pt as CommandOutboxCommitCandidate, s as ReadStreamOptions, t as SnapshotStore, tn as PublishedCommand, yt as DispatchTrackingOutbox } from "./chunks/snapshot-store.js";
2
+ //#region src/testing/contract-assertions.d.ts
5
3
  /**
6
- * The repository surface the event-sourced contract suite exercises.
7
- * Deliberately smaller than the state-stored `ContractRepository`: pure
8
- * event-sourced aggregates rarely have a meaningful `delete` (the
9
- * lifecycle ends with a `Closed` / `Terminated` event in the stream),
10
- * so the suite does not require one.
4
+ * One entry of a contract test suite. Every suite (repository,
5
+ * event-sourced repository, outbox, idempotency store) returns a list
6
+ * of these; bind them with
7
+ * `(test.skipped ? it.skip : it)(test.name, test.run)`.
11
8
  */
12
- interface EsContractRepository<TAgg extends IAggregateRoot<Id<string>, AnyDomainEvent>> {
13
- getById(id: TAgg["id"]): Promise<TAgg | null>;
14
- save(aggregate: TAgg): Promise<void>;
9
+ interface ContractTest {
10
+ name: string;
11
+ run: () => Promise<void>;
12
+ /** Present when the harness lacks the capability this test needs. */
13
+ skipped?: {
14
+ capability: string;
15
+ };
16
+ }
17
+ //#endregion
18
+ //#region src/testing/command-outbox-contract.d.ts
19
+ interface CommandOutboxContractEnvironment<C extends PublishedCommand> {
20
+ readonly outbox: CommandOutboxWriter<C>;
21
+ readonly addCommitted: (commits: ReadonlyArray<CommandOutboxCommitCandidate<C>>) => Promise<void>;
22
+ readonly addRolledBack?: (commits: ReadonlyArray<CommandOutboxCommitCandidate<C>>) => Promise<void>;
23
+ /** Runs `outbox.endEventSources(sources)` in a transaction that commits. */
24
+ readonly endEventSourcesCommitted: (sources: ReadonlyArray<AggregateIdentity>) => Promise<void>;
25
+ /** Runs `outbox.endEventSources(sources)` in a transaction that rolls back. */
26
+ readonly endEventSourcesRolledBack?: (sources: ReadonlyArray<AggregateIdentity>) => Promise<void>;
27
+ readonly readAll: () => Promise<ReadonlyArray<CommandOutboxCommitCandidate<C>>>;
28
+ readonly teardown?: () => Promise<void>;
29
+ }
30
+ interface CommandOutboxContractHarness<C extends PublishedCommand> {
31
+ readonly createEnvironment: () => Promise<CommandOutboxContractEnvironment<C>>;
32
+ /**
33
+ * Builds one command for the given seed. The suite derives conflicting
34
+ * commits from different seeds, so `createCommand` MUST return distinct
35
+ * command content per seed (put the seed in the payload). A constant
36
+ * command makes a manufactured conflict deep-equal to its original, and
37
+ * the conflict tests then fail a compliant adapter that deduplicates
38
+ * the exact retry.
39
+ */
40
+ readonly createCommand: (seed: number) => C;
41
+ readonly providesRolledBackAdds?: boolean;
42
+ /** Declare `true` when environments provide `endEventSourcesRolledBack`. */
43
+ readonly providesRolledBackEnds?: boolean;
44
+ }
45
+ type CommandOutboxContractTest = ContractTest;
46
+ declare function createCommandOutboxContractTests<C extends PublishedCommand>(harness: CommandOutboxContractHarness<C>): ReadonlyArray<CommandOutboxContractTest>;
47
+ //#endregion
48
+ //#region src/testing/deadline-store-contract.d.ts
49
+ /** One contract test; bind with `(test.skipped ? it.skip : it)(test.name, test.run)`. */
50
+ type DeadlineStoreContractTest = ContractTest;
51
+ /** The plain-data payload shape the suite round-trips. */
52
+ interface SuitePayload {
53
+ kind: string;
54
+ step?: number;
15
55
  }
16
56
  /**
17
- * One isolated test environment: fresh event store, fresh outbox. The
18
- * suite creates one per test and tears it down afterwards.
57
+ * One isolated test environment: a fresh deadline store. The suite
58
+ * creates one per test and tears it down afterwards.
19
59
  */
20
- interface EsRepositoryContractEnvironment<TAgg extends IAggregateRoot<Id<string>, Evt>, Evt extends AnyDomainEvent = AnyDomainEvent> {
21
- /**
22
- * Execute one unit of work against the adapter under test. Wire this
23
- * through your real `UnitOfWork` / `withCommit` setup: the commit
24
- * boundary IS part of what the suite proves (outbox harvest,
25
- * `markPersisted`, rollback purity).
26
- */
27
- run<R>(work: (ctx: {
28
- repository: EsContractRepository<TAgg>;
29
- }) => Promise<R>): Promise<R>;
30
- /**
31
- * All events currently persisted in the outbox (committed writes
32
- * only; a rolled-back transaction's events must not appear here).
33
- */
34
- committedOutboxEvents(): Promise<ReadonlyArray<Evt>>;
35
- /**
36
- * The COMMITTED stream for the given aggregate id, in stream order,
37
- * optionally only the events after `fromVersion` (the snapshot
38
- * catch-up read). Implement this through your adapter's
39
- * `EventStore.readStream` so the suite's ordering and slicing
40
- * assertions exercise your real read path. A rolled-back
41
- * transaction's events must not appear here.
42
- */
43
- committedStreamEvents(id: TAgg["id"], fromVersion?: number): Promise<ReadonlyArray<Evt>>;
44
- /** Release connections, drop schemas, etc. Called in a finally. */
45
- teardown?(): Promise<void>;
60
+ interface DeadlineStoreContractEnvironment {
61
+ /** The adapter under test. */
62
+ store: DeadlineStore<SuitePayload>;
63
+ /**
64
+ * Runs `work` (schedule/cancel calls) the way production does:
65
+ * inside a transaction that COMMITS. For a non-transactional store
66
+ * this simply invokes `work`.
67
+ */
68
+ run<R>(work: () => Promise<R>): Promise<R>;
69
+ /**
70
+ * Optional capability: runs `work` inside a transaction that ROLLS
71
+ * BACK. Enables the rollback tests: a rolled-back schedule must not
72
+ * leave a deadline behind (a ghost input for a state change that
73
+ * never happened), and a rolled-back cancel must not have removed
74
+ * one. Transactional adapters should always provide this.
75
+ */
76
+ runRolledBack?<R>(work: () => Promise<R>): Promise<R>;
77
+ /** Release connections, drop schemas, etc. Called in a finally. */
78
+ teardown?(): Promise<void>;
46
79
  }
47
80
  /**
48
- * What an adapter supplies to run the event-sourced contract suite.
49
- *
50
- * The harness MUST provide isolation per environment. **For SQL-backed
51
- * event stores this must run against a real database** (testcontainers
52
- * or equivalent): the mandatory two-writer test proves YOUR
53
- * expectedVersion guard, and an in-memory stand-in proves only itself.
81
+ * What an adapter supplies to run the deadline-store contract suite.
82
+ * For SQL adapters, run against a real database (testcontainers or
83
+ * equivalent): the rollback tests prove YOUR transaction wiring, and
84
+ * schedule/cancel joining the write transaction is the port's central
85
+ * correctness rule.
86
+ */
87
+ interface DeadlineStoreContractHarness {
88
+ createEnvironment(): Promise<DeadlineStoreContractEnvironment>;
89
+ /**
90
+ * The adapter's attempt ceiling: how many `markFailed` reports move
91
+ * a deadline to the dead-letter set. Must be at least 2 so the
92
+ * attempts-surfacing test can observe a survivor.
93
+ */
94
+ failuresToDeadLetter: number;
95
+ /**
96
+ * Declare `true` when environments provide {@link
97
+ * DeadlineStoreContractEnvironment.runRolledBack}. Without it, the
98
+ * rollback tests are marked skipped: the honest state of a store
99
+ * without a rollback, and a loud gap for a transactional adapter.
100
+ */
101
+ providesRolledBackRuns?: boolean;
102
+ /**
103
+ * Declare `true` when the adapter's `due` CLAIMS the returned
104
+ * records for competing pollers (lease, visibility timeout), as the
105
+ * port sanctions. Tests that re-poll records an earlier poll
106
+ * returned without resolving them (attempts surfacing, neighbor
107
+ * flow after a dead-letter, successor visibility during a
108
+ * reschedule race) assume a non-claiming read and are marked
109
+ * skipped for claiming adapters; prove your claim/expiry semantics
110
+ * in your own suite.
111
+ */
112
+ claimsOnDue?: boolean;
113
+ }
114
+ /**
115
+ * The deadline-store contract test suite: the proof that an adapter
116
+ * delivers the schedule/cancel/due/acknowledge semantics the port
117
+ * documents. Store semantics are an **adapter contract, not a kit
118
+ * guarantee**; this suite is how an adapter demonstrates them.
54
119
  *
55
- * Aggregate arithmetic the suite relies on:
56
- * - `createAggregate()` returns a fresh aggregate with exactly ONE
57
- * applied creation event (version 1, `persistedVersion === undefined`).
58
- * - `mutate()` applies exactly ONE event (+1 version).
120
+ * Framework-agnostic: bind with
121
+ * `(test.skipped ? it.skip : it)(test.name, test.run)`.
59
122
  */
60
- interface EsRepositoryContractHarness<TAgg extends IAggregateRoot<Id<string>, Evt>, Evt extends AnyDomainEvent = AnyDomainEvent> {
61
- createEnvironment(): Promise<EsRepositoryContractEnvironment<TAgg, Evt>>;
62
- /**
63
- * A brand-new aggregate: exactly one applied creation event, unique
64
- * id, version 1, `persistedVersion === undefined`.
65
- */
66
- createAggregate(): TAgg;
67
- /** Apply exactly ONE domain event via `apply()` (+1 version). */
68
- mutate(aggregate: TAgg): void;
69
- /**
70
- * Optional: construct a NEW (never-persisted) aggregate carrying a
71
- * SPECIFIC id, with its creation event applied. Enables the
72
- * duplicate-create conflict test (two creators racing on one stream).
73
- */
74
- createAggregateWithId?(id: TAgg["id"]): TAgg;
75
- /**
76
- * Optional: a plain-data projection of the aggregate's state,
77
- * compared with deep equality. Enables the state assertions in the
78
- * mandatory and replay-equality tests. Must be roundtrip-stable
79
- * across your store (see the state-stored harness JSDoc for the
80
- * normalization checklist).
81
- */
82
- snapshotState?(aggregate: TAgg): unknown;
123
+ declare function createDeadlineStoreContractTests(harness: DeadlineStoreContractHarness): DeadlineStoreContractTest[];
124
+ //#endregion
125
+ //#region src/testing/es-repository-contract.d.ts
126
+ /** Event-sourced repositories normally expose no physical removal. */
127
+ interface EsContractRepository<TAggregate extends Aggregate<Id<string>, AnyDomainEvent>> {
128
+ findById(id: TAggregate["id"]): Promise<TAggregate | undefined>;
129
+ add(aggregate: TAggregate): void;
130
+ update(aggregate: TAggregate): void;
131
+ }
132
+ interface EsRepositoryContractEnvironment<TAggregate extends Aggregate<Id<string>, TEvent>, TEvent extends AnyDomainEvent = AnyDomainEvent> {
133
+ run<R>(work: (context: {
134
+ repository: EsContractRepository<TAggregate>;
135
+ }) => Promise<R>): Promise<R>;
136
+ committedOutboxEvents(): Promise<ReadonlyArray<CommittedDomainEvent<TEvent>>>;
137
+ failNextOutboxWrite(error: Error): void;
138
+ committedStreamEvents(stream: AggregateIdentity<TAggregate["id"]>, options: ReadStreamOptions): Promise<StreamReadResult<TEvent>>;
139
+ teardown?(): Promise<void>;
83
140
  }
84
- /** One named contract test; see the state-stored suite for the binding pattern. */
85
- interface EsRepositoryContractTest {
86
- name: string;
87
- run: () => Promise<void>;
88
- /** Present when the harness lacks the capability this test needs. */
89
- skipped?: {
90
- capability: string;
91
- };
141
+ interface EsRepositoryContractHarness<TAggregate extends Aggregate<Id<string>, TEvent>, TEvent extends AnyDomainEvent = AnyDomainEvent> {
142
+ createEnvironment(): Promise<EsRepositoryContractEnvironment<TAggregate, TEvent>>;
143
+ /** Fresh aggregate with exactly one recorded creation event. */
144
+ createAggregate(): TAggregate;
145
+ createAggregateWithId?(id: TAggregate["id"]): TAggregate;
146
+ streamKeyFor(id: TAggregate["id"]): AggregateIdentity<TAggregate["id"]>;
147
+ /** Applies exactly one event and advances the aggregate version by one. */
148
+ mutate(aggregate: TAggregate): void;
149
+ snapshotState?(aggregate: TAggregate): unknown;
150
+ /**
151
+ * Persists a snapshot of the aggregate at its current version in the
152
+ * environment's snapshot store, so the next load there starts from it.
153
+ * Enables the snapshot catch-up proof.
154
+ */
155
+ captureSnapshot?(aggregate: TAggregate, environment: EsRepositoryContractEnvironment<TAggregate, TEvent>): Promise<void>;
156
+ /**
157
+ * Bound for the overlapping `run` calls, in milliseconds: the second call
158
+ * of the environment preflight, and the committing call of each
159
+ * stale-writer proof. Raise it only for a second connection that needs
160
+ * more time to open, or for a slow commit. Keep twice the bound, plus
161
+ * environment creation and teardown, below the test timeout of the runner.
162
+ */
163
+ overlappingCallsBoundMs?: number;
92
164
  }
165
+ type EsRepositoryContractTest = ContractTest;
93
166
  /**
94
- * The event-sourced repository contract test suite: the proof that an
95
- * adapter delivers what the kit's `EventStore` port and Unit of Work
96
- * document. The kit is store-agnostic: the expectedVersion guard lives
97
- * in YOUR adapter's append. That makes stream OCC a **repository
98
- * contract, not a kit guarantee**; an adapter that has not passed this
99
- * suite (against a real store, for SQL-backed adapters) has not
100
- * demonstrated it.
167
+ * Contract suite for event-stream adapters using v3 Unit-of-Work receipts.
101
168
  *
102
- * What each test proves:
103
- * - The MANDATORY two-writer test proves your append's expectedVersion
104
- * guard and its atomicity.
105
- * - The replay/lifecycle tests prove your read path (fold order,
106
- * identity map wiring) and the commit lifecycle (outbox harvest,
107
- * `markPersisted`, rollback purity).
108
- * - The duplicate-create and fromVersion tests prove the create race
109
- * and the snapshot catch-up read.
169
+ * `add` and `update` register intent only. At commit, the adapter appends the
170
+ * receipt's exact event batch with the Unit of Work's expected version, in the
171
+ * same transaction as the outbox. `run` must support overlapping calls so the
172
+ * mandatory stale-writer proof exercises a real stream OCC predicate.
173
+ */
174
+ declare function createEsRepositoryContractTests<TAggregate extends Aggregate<Id<string>, TEvent>, TEvent extends AnyDomainEvent = AnyDomainEvent>(harness: EsRepositoryContractHarness<TAggregate, TEvent>): EsRepositoryContractTest[];
175
+ //#endregion
176
+ //#region src/testing/event-bus-contract.d.ts
177
+ /** One entry of the event-bus contract suite. */
178
+ type EventBusContractTest = ContractTest;
179
+ /**
180
+ * What the suite runs against. The harness creates one per test and tears
181
+ * it down afterwards. No transaction wrapper: the port is in-process and
182
+ * transaction-free by design.
183
+ */
184
+ interface EventBusContractEnvironment<Evt extends AnyDomainEvent> {
185
+ /** The implementation under test. */
186
+ readonly bus: EventBus<Evt>;
187
+ /** Release connections, drop schemas, etc. Called in a finally. */
188
+ teardown?(): Promise<void>;
189
+ }
190
+ /**
191
+ * What an implementation supplies to run the event-bus contract suite.
110
192
  *
111
- * Error matching is by NAME along the `cause` chain, not `instanceof`
112
- * (same rationale as the state-stored suite). Binding:
193
+ * The suite mints no events, so it stays free of any event union. The two
194
+ * factories must produce DIFFERENT `type` values: the suite subscribes to
195
+ * both to prove that ordering holds across types and that a catch-all
196
+ * subscription sees every type.
197
+ */
198
+ interface EventBusContractHarness<Evt extends AnyDomainEvent> {
199
+ createEnvironment(): Promise<EventBusContractEnvironment<Evt>>;
200
+ /** An event of the first type. Called repeatedly; each call may differ. */
201
+ createFirstEvent(): Evt;
202
+ /** An event of the second type, whose `type` differs from the first. */
203
+ createSecondEvent(): Evt;
204
+ }
205
+ /**
206
+ * The event-bus contract test suite: the proof that an implementation
207
+ * delivers the guarantees the `EventBus` port documents. Ordering,
208
+ * parallelism within one event, and error collection after the batch are
209
+ * a **port contract, not a kit guarantee**; this suite is how an
210
+ * implementation demonstrates them.
113
211
  *
114
- * ```ts
115
- * for (const test of createEsRepositoryContractTests(harness)) {
116
- * (test.skipped ? it.skip : it)(test.name, test.run);
117
- * }
118
- * ```
212
+ * The suite covers the port and nothing else. Construction options of the
213
+ * kit's own adapter, such as a publish-depth bound or an observer bundle,
214
+ * are not port behavior and are not pinned here.
119
215
  *
120
- * **Known limitation:** like the state-stored suite, the two-writer
121
- * test is sequential-deterministic; lock interaction and raw
122
- * serialization failures (Postgres 40001) need adapter-specific tests
123
- * on top.
216
+ * Framework-agnostic: bind with
217
+ * `(test.skipped ? it.skip : it)(test.name, test.run)`.
124
218
  */
125
- declare function createEsRepositoryContractTests<TAgg extends IAggregateRoot<Id<string>, Evt>, Evt extends AnyDomainEvent = AnyDomainEvent>(harness: EsRepositoryContractHarness<TAgg, Evt>): EsRepositoryContractTest[];
126
-
219
+ declare function createEventBusContractTests<Evt extends AnyDomainEvent>(harness: EventBusContractHarness<Evt>): EventBusContractTest[];
220
+ //#endregion
221
+ //#region src/testing/event-store-contract.d.ts
222
+ /** One named contract test for an EventStore adapter. */
223
+ type EventStoreContractTest = ContractTest;
224
+ /** One isolated adapter instance. The suite creates one per test. */
225
+ interface EventStoreContractEnvironment<Evt extends AnyDomainEvent> {
226
+ readonly store: EventStore<Evt>;
227
+ teardown?(): Promise<void>;
228
+ }
127
229
  /**
128
- * The repository surface the contract suite exercises: the minimal
129
- * structural subset of the canonical `IUnitOfWorkRepository` (exported
130
- * from the main entry) that the tests need. `getById` is typed over
131
- * the aggregate's own branded id (`TAgg["id"]`), so concrete adapters,
132
- * including arrow-function-property style repositories, which are
133
- * checked contravariantly, match without casts.
230
+ * Inputs needed to prove the EventStore's observable port contract.
231
+ *
232
+ * `createCollidingStreamKeys` must return two valid stream keys with the same
233
+ * raw aggregate id and different aggregate types. `createEvent` must return an
234
+ * event that belongs to the supplied key; different sequence values must produce
235
+ * different event ids.
134
236
  */
135
- interface ContractRepository<TAgg extends IAggregateRoot<Id<string>, AnyDomainEvent>> {
136
- getById(id: TAgg["id"]): Promise<TAgg | null>;
137
- save(aggregate: TAgg): Promise<void>;
138
- delete(aggregate: TAgg): Promise<void>;
237
+ interface EventStoreContractHarness<Evt extends AnyDomainEvent> {
238
+ createEnvironment(): Promise<EventStoreContractEnvironment<Evt>>;
239
+ createCollidingStreamKeys(): readonly [AggregateIdentity, AggregateIdentity];
240
+ createEvent(stream: AggregateIdentity, sequence: number): Evt;
139
241
  }
140
242
  /**
141
- * One isolated test environment: fresh storage, fresh outbox. The
142
- * suite creates one per test via {@link RepositoryContractHarness} and
143
- * tears it down afterwards (a teardown failure never masks an
144
- * in-flight contract violation).
243
+ * Reusable proof of an EventStore adapter's portable semantics: qualified
244
+ * value identity, ordered reads and slicing, OCC error mapping and atomicity,
245
+ * no-op empty appends, and detached return arrays. Physical-position
246
+ * corruption needs adapter-specific fixture support and is tested there.
247
+ */
248
+ declare function createEventStoreContractTests<Evt extends AnyDomainEvent>(harness: EventStoreContractHarness<Evt>): EventStoreContractTest[];
249
+ //#endregion
250
+ //#region src/testing/idempotency-store-contract.d.ts
251
+ /** One contract test; bind with `(test.skipped ? it.skip : it)(test.name, test.run)`. */
252
+ type IdempotencyStoreContractTest = ContractTest;
253
+ /**
254
+ * One isolated test environment: a fresh idempotency store. The suite
255
+ * creates one per test and tears it down afterwards.
145
256
  */
146
- interface RepositoryContractEnvironment<TAgg extends IAggregateRoot<Id<string>, Evt>, Evt extends AnyDomainEvent = AnyDomainEvent> {
147
- /**
148
- * Execute one unit of work against the adapter under test: open the
149
- * transaction, hand the suite a tx-bound repository, commit on
150
- * resolve, roll back on throw, and run the post-commit lifecycle
151
- * (event harvest into the outbox, `markPersisted`). Wire this
152
- * through your real `UnitOfWork` / `withCommit` setup: the commit
153
- * boundary IS part of what the suite proves.
154
- */
155
- run<R>(work: (ctx: {
156
- repository: ContractRepository<TAgg>;
157
- }) => Promise<R>): Promise<R>;
158
- /**
159
- * All events currently persisted in the outbox (committed writes
160
- * only; a rolled-back transaction's events must not appear here).
161
- */
162
- committedOutboxEvents(): Promise<ReadonlyArray<Evt>>;
163
- /** Release connections, drop schemas, etc. Called in a finally. */
164
- teardown?(): Promise<void>;
257
+ interface IdempotencyStoreContractEnvironment<TCtx> {
258
+ /** The adapter under test. */
259
+ store: IdempotencyStore<TCtx>;
260
+ /**
261
+ * Runs `work` inside a transaction that COMMITS, handing it the
262
+ * transaction context the store methods expect, the way
263
+ * `withIdempotentCommit` calls them in production. For a
264
+ * non-transactional store this simply invokes `work` with a dummy
265
+ * context.
266
+ */
267
+ run<R>(work: (ctx: TCtx) => Promise<R>): Promise<R>;
268
+ /**
269
+ * For the `"transactional"` family: runs `work` inside a transaction
270
+ * that ROLLS BACK. Required there; the rollback-releases-the-claim
271
+ * test is that family's core proof. Irrelevant for the
272
+ * `"non-transactional"` family.
273
+ */
274
+ runRolledBack?<R>(work: (ctx: TCtx) => Promise<R>): Promise<R>;
275
+ /**
276
+ * For the `"non-transactional"` family: advances or edits adapter test
277
+ * state so this exact claim's CURRENT lease is expired. Required there;
278
+ * a fake clock or test-only row update keeps the suite deterministic.
279
+ */
280
+ expireLease?(claim: IdempotencyClaimHandle): Promise<void>;
281
+ /**
282
+ * Moves the adapter's test clock to an exact instant. Required by the
283
+ * non-transactional family so renewal is proved without wall-clock sleeps.
284
+ */
285
+ advanceTimeTo?(instant: Date): Promise<void>;
286
+ /** Release connections, drop schemas, etc. Called in a finally. */
287
+ teardown?(): Promise<void>;
165
288
  }
166
289
  /**
167
- * What an adapter supplies to run the contract suite.
290
+ * What an adapter supplies to run the idempotency-store contract suite.
168
291
  *
169
- * The harness MUST provide isolation per environment (fresh
170
- * tables/keyspace or a truncate); tests assume they see only their
171
- * own writes. **For SQL/ORM adapters this must run against a real
172
- * database** (testcontainers or equivalent), not an in-memory fake:
173
- * the mandatory two-writer test proves YOUR `WHERE version = ?`
174
- * predicate, and an in-memory stand-in proves only itself.
292
+ * The port (`IdempotencyStore`) deliberately supports two adapter
293
+ * families with different lifecycle semantics, and the suite follows
294
+ * the declared family instead of forcing one onto the other:
175
295
  *
176
- * Optional capabilities widen the suite: tests for an absent
177
- * capability come back **marked `skipped`** with a `run()` that
178
- * rejects loudly: bind them with `it.skip` so the gap stays visible
179
- * in every report (see {@link RepositoryContractTest}); a naive
180
- * binding fails instead of green-no-op'ing. Capabilities are captured
181
- * once at suite creation. Provide every capability your adapter can
182
- * support; each one closes a real OCC hole.
296
+ * - `"transactional"` (the single-transaction pattern): the record
297
+ * lives in the same database as the aggregates; a committed
298
+ * `complete` is final and replayable, a rollback releases everything,
299
+ * and `renew`/`confirm`/`abandon`/`reconcile` are no-ops. The family's core proof is the
300
+ * rollback test, so environments MUST provide `runRolledBack`, and
301
+ * run against a real database for SQL adapters.
302
+ * - `"non-transactional"` (the leased two-phase pattern, e.g. the
303
+ * in-memory reference): the store cannot see commits, so `complete` only
304
+ * STAGES the outcome, `confirm` finalizes it post-commit, `abandon`
305
+ * compensates failed attempts, and expired staged records require
306
+ * reconciliation. Environments MUST provide deterministic `expireLease` and
307
+ * `advanceTimeTo` controls. The rollback test is skipped.
183
308
  */
184
- interface RepositoryContractHarness<TAgg extends IAggregateRoot<Id<string>, Evt>, Evt extends AnyDomainEvent = AnyDomainEvent> {
185
- createEnvironment(): Promise<RepositoryContractEnvironment<TAgg, Evt>>;
186
- /**
187
- * A brand-new aggregate (never persisted, unique id,
188
- * `persistedVersion === undefined`).
189
- */
190
- createAggregate(): TAgg;
191
- /**
192
- * Apply exactly ONE version-bumping domain mutation that records at
193
- * least one domain event (a `commit()`-style state change). The
194
- * suite relies on the +1-per-call arithmetic and on the event for
195
- * its outbox assertions.
196
- */
197
- mutate(aggregate: TAgg): void;
198
- /**
199
- * Optional: a version-bumping mutation whose state is deep-equal to
200
- * the previous state (`setState({...state}, true)`). Enables the
201
- * version-only-change-still-persists test: the skip-save/OCC-desync
202
- * trap.
203
- */
204
- mutateVersionOnly?(aggregate: TAgg): void;
205
- /**
206
- * Optional: a mutation that changes ONLY a child collection (a
207
- * non-root-row `changedKeys` entry). Enables the
208
- * child-change-bumps-root-version test for partial-write
209
- * repositories.
210
- */
211
- mutateChildCollection?(aggregate: TAgg): void;
212
- /**
213
- * Optional: construct a NEW (never-persisted) aggregate instance
214
- * carrying a SPECIFIC id. Enables TWO tests: deletion-is-final-
215
- * across-instances (resurrection via a factory after delete) and
216
- * the duplicate-insert test (see
217
- * {@link insertsAreDuplicateChecked} to opt out of the latter
218
- * independently).
219
- */
220
- createAggregateWithId?(id: TAgg["id"]): TAgg;
221
- /**
222
- * Semantic opt-OUT (default `true`): whether `save()`'s INSERT path
223
- * rejects an existing id with `DuplicateAggregateError` (mapping the
224
- * driver's unique-violation: Postgres `23505`, MySQL `1062`, SQLite
225
- * `SQLITE_CONSTRAINT_UNIQUE`). This is the near-mandatory contract:
226
- * `save()` is insert-or-update, never upsert. Create-idempotency
227
- * belongs in the USE CASE (load, then decide), not in the save path.
228
- * Set `false` ONLY for a deliberately upserting adapter
229
- * (idempotent-create design); the duplicate-insert test is then
230
- * reported as skipped under this capability name, without costing
231
- * the deletion-finality coverage that `createAggregateWithId` also
232
- * gates.
233
- */
234
- insertsAreDuplicateChecked?: boolean;
235
- /**
236
- * Optional: a plain-data projection of the aggregate's persisted
237
- * state, compared with deep equality. Enables the mandatory test's
238
- * state assertion (without it, only the version and the outbox are
239
- * compared, and an adapter whose predicate guards the version write
240
- * but not the state write would slip through).
241
- *
242
- * **The projection must be roundtrip-stable**: it compares a
243
- * DB-reloaded aggregate against an in-memory one, so normalize
244
- * anything your store changes in transit: dates to ISO strings at
245
- * your store's precision (MySQL DATETIME truncates millis), no
246
- * `undefined`-valued keys (JSON columns drop them), decimals/bigints
247
- * to one consistent representation. A mismatch here fails the
248
- * mandatory test; the message names the projection as a suspect.
249
- */
250
- snapshotState?(aggregate: TAgg): unknown;
251
- /**
252
- * Optional flag: declare it when your `delete(aggregate)` runs an
253
- * OCC predicate (`DELETE … WHERE id = ? AND version = ?`). Enables
254
- * the stale-delete conflict test. Unpredicated deletes are
255
- * last-write-wins by construction: acceptable for GC-style
256
- * cleanup, rarely for user-initiated deletion of contended
257
- * aggregates (see the repository guide).
258
- */
259
- deletesAreVersionChecked?: boolean;
309
+ interface IdempotencyStoreContractHarness<TCtx> {
310
+ createEnvironment(): Promise<IdempotencyStoreContractEnvironment<TCtx>>;
311
+ /** Which lifecycle family the adapter implements; see above. */
312
+ family: "transactional" | "non-transactional";
260
313
  }
261
314
  /**
262
- * One named contract test; `run` rejects with a descriptive Error on
263
- * violation. When the harness lacks the capability a test needs, the
264
- * entry is still returned with {@link skipped} set and a `run` that
265
- * REJECTS with an explanatory error: bind it with your runner's skip
266
- * (`(test.skipped ? it.skip : it)(test.name, test.run)`) so the gap is
267
- * visible in every test report: a missing capability must never look
268
- * like green coverage, and a naive binding that ignores `skipped`
269
- * fails loud instead of passing silently.
315
+ * The idempotency-store contract test suite: the proof that an adapter
316
+ * delivers the claim/renew/complete/confirm/abandon/reconcile lifecycle
317
+ * `withIdempotentCommit` documents, for its declared family. Store
318
+ * semantics are an **adapter contract, not a kit guarantee**; this
319
+ * suite is how an adapter demonstrates them.
320
+ *
321
+ * Framework-agnostic: bind with
322
+ * `(test.skipped ? it.skip : it)(test.name, test.run)`.
323
+ */
324
+ declare function createIdempotencyStoreContractTests<TCtx>(harness: IdempotencyStoreContractHarness<TCtx>): IdempotencyStoreContractTest[];
325
+ //#endregion
326
+ //#region src/testing/outbox-contract.d.ts
327
+ /** One contract test; bind with `(test.skipped ? it.skip : it)(test.name, test.run)`. */
328
+ type OutboxContractTest = ContractTest;
329
+ /**
330
+ * One isolated test environment: a fresh outbox store. The suite
331
+ * creates one per test and tears it down afterwards.
270
332
  */
271
- interface RepositoryContractTest {
272
- name: string;
273
- run: () => Promise<void>;
274
- /** Present when the harness lacks the capability this test needs. */
275
- skipped?: {
276
- capability: string;
277
- };
333
+ interface OutboxContractEnvironment<Evt extends AnyDomainEvent> {
334
+ /** The adapter under test. */
335
+ outbox: Outbox<Evt> | DispatchTrackingOutbox<Evt>;
336
+ /**
337
+ * Runs `outbox.add(candidates)` inside a transaction that COMMITS, the
338
+ * way `withCommit` calls it in production. The suite supplies complete
339
+ * candidates with explicit source, aggregate version, zero-based commit
340
+ * sequence, and commit size. For a non-transactional store this is simply
341
+ * `outbox.add(candidates)`.
342
+ */
343
+ addCommitted(events: ReadonlyArray<EventCommitCandidate<Evt>>): Promise<void>;
344
+ /**
345
+ * Optional capability: runs `outbox.add(candidates)` inside a
346
+ * transaction that ROLLS BACK. Enables the rollback-purity test: a
347
+ * rolled-back add must leave nothing behind. Transactional adapters
348
+ * should always provide this.
349
+ */
350
+ addRolledBack?(events: ReadonlyArray<EventCommitCandidate<Evt>>): Promise<void>;
351
+ /**
352
+ * Runs `outbox.endEventSources(sources)` inside a transaction that
353
+ * COMMITS, the way `withCommit` calls it for a removal. For a
354
+ * non-transactional store this is simply `outbox.endEventSources(sources)`.
355
+ */
356
+ endEventSourcesCommitted(sources: ReadonlyArray<AggregateIdentity>): Promise<void>;
357
+ /**
358
+ * Optional capability: runs `outbox.endEventSources(sources)` inside a
359
+ * transaction that ROLLS BACK. Enables the test that a rolled-back
360
+ * removal leaves its event source open.
361
+ */
362
+ endEventSourcesRolledBack?(sources: ReadonlyArray<AggregateIdentity>): Promise<void>;
363
+ /** Release connections, drop schemas, etc. Called in a finally. */
364
+ teardown?(): Promise<void>;
278
365
  }
279
366
  /**
280
- * The repository contract test suite: the proof that an adapter
281
- * actually delivers the guarantees the kit's Unit of Work documents.
367
+ * What an adapter supplies to run the outbox contract suite.
282
368
  *
283
- * The kit is ORM-agnostic: the OCC version predicate lives in YOUR
284
- * repository's SQL. That makes optimistic concurrency a **repository
285
- * contract, not a kit guarantee**: the kit ships the boundary, the
286
- * `persistedVersion` baseline, `ConcurrencyConflictError`, and this
287
- * suite; your adapter must pass it. An adapter that has not passed the
288
- * suite (against a real database, for SQL adapters) has not
289
- * demonstrated OCC.
369
+ * For SQL adapters, run against a real database (testcontainers or
370
+ * equivalent): the commit-order and rollback tests prove YOUR schema
371
+ * and transaction wiring, not the kit's.
290
372
  *
291
- * Framework-agnostic: assertions throw plain `Error`s, so the suite
292
- * binds to vitest, jest, or `node:test` the same way:
293
- *
294
- * ```ts
295
- * import { describe, it } from "vitest";
296
- * import { createRepositoryContractTests } from "@shirudo/ddd-kit/testing";
373
+ * Note on claiming: multi-instance safety (`getPending` claiming via
374
+ * `FOR UPDATE SKIP LOCKED` or equivalent) is part of the port contract
375
+ * for competing dispatchers but is not covered here; concurrency
376
+ * cannot be proven portably by a generic suite. Test it in your
377
+ * adapter's own suite if you run more than one dispatcher.
378
+ */
379
+ interface OutboxContractHarness<Evt extends AnyDomainEvent> {
380
+ createEnvironment(): Promise<OutboxContractEnvironment<Evt>>;
381
+ /**
382
+ * Deterministic event factory: the same `seed` yields an event with
383
+ * the SAME `eventId` (the suite uses this for the dedupe test), and
384
+ * different seeds yield distinct `eventId`s.
385
+ */
386
+ createEvent(seed: number): Evt;
387
+ /**
388
+ * For a `DispatchTrackingOutbox` adapter: how many `markFailed`
389
+ * reports move a record to the dead-letter set (the adapter's
390
+ * configured attempt ceiling). Omit for plain `Outbox` adapters;
391
+ * the dispatch-tracking tests are then marked skipped. With a
392
+ * ceiling of 1 the attempts-surfacing test is marked skipped too:
393
+ * observing attempts on a PENDING record needs a record that
394
+ * survives one failure.
395
+ */
396
+ failuresToDeadLetter?: number;
397
+ /**
398
+ * Declare `true` when environments provide {@link
399
+ * OutboxContractEnvironment.addRolledBack}. Without it, the
400
+ * rollback-purity test is marked skipped: the honest state of a store
401
+ * without a rollback, and a loud gap for a transactional adapter.
402
+ */
403
+ providesRolledBackAdds?: boolean;
404
+ /**
405
+ * Declare `true` when environments provide {@link
406
+ * OutboxContractEnvironment.endEventSourcesRolledBack}. Without it, the
407
+ * test that a rolled-back end leaves the source open is marked skipped.
408
+ */
409
+ providesRolledBackEnds?: boolean;
410
+ /**
411
+ * Declare `true` when the adapter's `getPending` CLAIMS the returned
412
+ * records for competing dispatchers (lease, visibility timeout,
413
+ * `FOR UPDATE SKIP LOCKED`), as the port sanctions. Every test that
414
+ * re-polls records a previous poll returned without resolving them
415
+ * (head stability, re-ack non-disturbance, attempts surfacing)
416
+ * assumes a non-claiming read and is marked skipped for claiming
417
+ * adapters; prove your claim/expiry semantics in your own suite.
418
+ */
419
+ claimsOnGetPending?: boolean;
420
+ /**
421
+ * Declare `true` when `add()` dedupes on `eventId` (the unique-key
422
+ * constraint the port RECOMMENDS). The dedupe test is gated on this:
423
+ * an adapter without the constraint satisfies the port's normative
424
+ * requirements and must not fail the suite, but the skip stays
425
+ * visible as the unproven recommendation it is.
426
+ */
427
+ dedupesOnEventId?: boolean;
428
+ }
429
+ /**
430
+ * The outbox contract test suite: the proof that an adapter delivers
431
+ * the guarantees `withCommit` and `OutboxDispatcher` document. The kit
432
+ * is store-agnostic, so commit-order reads, qualified source-position
433
+ * identity, eventful-predecessor linkage, idempotent acks, and rollback
434
+ * purity are an **adapter contract, not a kit guarantee**; this suite is how
435
+ * an adapter demonstrates them. Its source-law tests also prove that colliding
436
+ * raw ids stay isolated by aggregate type and aggregate id.
297
437
  *
298
- * const harness: RepositoryContractHarness<Order, OrderEvent> = {
299
- * createEnvironment: async () => {
300
- * const schema = await provisionTestSchema(); // testcontainers etc.
301
- * const uowDeps = {
302
- * scope: schema.scope,
303
- * outbox: schema.outbox,
304
- * repositories: {
305
- * orders: (tx, session) => new DrizzleOrderRepository(tx, session),
306
- * },
307
- * };
308
- * return {
309
- * run: (work) =>
310
- * new UnitOfWork(uowDeps).run(({ repositories }) =>
311
- * work({ repository: repositories.orders })),
312
- * committedOutboxEvents: () => schema.readOutboxEvents(),
313
- * teardown: () => schema.drop(),
314
- * };
315
- * },
316
- * createAggregate: () => Order.draft(orderIds.next()),
317
- * mutate: (order) => order.changeNote(`note-${counter++}`), // ONE bump + event
318
- * // provide every optional capability your adapter supports:
319
- * createAggregateWithId: (id) => Order.draft(id),
320
- * snapshotState: (order) => normalizeForRoundtrip(order.state),
321
- * deletesAreVersionChecked: true,
322
- * };
438
+ * Framework-agnostic: bind with
439
+ * `(test.skipped ? it.skip : it)(test.name, test.run)`.
440
+ */
441
+ declare function createOutboxContractTests<Evt extends AnyDomainEvent>(harness: OutboxContractHarness<Evt>): OutboxContractTest[];
442
+ //#endregion
443
+ //#region src/testing/projection-checkpoint-contract.d.ts
444
+ /** One contract test; bind with `(test.skipped ? it.skip : it)(test.name, test.run)`. */
445
+ type ProjectionCheckpointStoreContractTest = ContractTest;
446
+ /**
447
+ * One isolated test environment: a fresh checkpoint store. The suite
448
+ * creates one per test and tears it down afterwards.
449
+ */
450
+ interface ProjectionCheckpointStoreContractEnvironment<TCtx> {
451
+ /** The adapter under test. */
452
+ store: ProjectionCheckpointStore<TCtx>;
453
+ /**
454
+ * Runs `work` inside a transaction that COMMITS, handing it the
455
+ * transaction context the store methods expect, the way the
456
+ * `Projector` calls them in production. For a non-transactional
457
+ * store this simply invokes `work` with a dummy context.
458
+ */
459
+ run<R>(work: (ctx: TCtx) => Promise<R>): Promise<R>;
460
+ /**
461
+ * Optional capability: starts every supplied transaction independently and
462
+ * concurrently. Each callback must receive its own transaction context for
463
+ * database adapters (normally a separate pooled connection). Enables the
464
+ * missing-key/existing-key exclusion test; implementing this as sequential
465
+ * calls would make that proof meaningless.
466
+ */
467
+ runConcurrently?<R>(works: ReadonlyArray<(ctx: TCtx) => Promise<R>>): Promise<R[]>;
468
+ /**
469
+ * Optional capability: runs `work` inside a transaction that ROLLS
470
+ * BACK. Enables the rollback test: a rolled-back save must leave no
471
+ * checkpoint behind, the half of the atomic update+checkpoint
472
+ * promise the store contributes. Transactional adapters should
473
+ * always provide this.
474
+ */
475
+ runRolledBack?<R>(work: (ctx: TCtx) => Promise<R>): Promise<R>;
476
+ /** Release connections, drop schemas, etc. Called in a finally. */
477
+ teardown?(): Promise<void>;
478
+ }
479
+ /**
480
+ * What an adapter supplies to run the projection-checkpoint-store
481
+ * contract suite. For SQL adapters, run against a real database
482
+ * (testcontainers or equivalent): concurrent runs prove missing-key locking,
483
+ * the rollback test proves YOUR transaction wiring, and the checkpoint table
484
+ * must live in the same database as the read models it accounts for.
485
+ */
486
+ interface ProjectionCheckpointStoreContractHarness<TCtx> {
487
+ createEnvironment(): Promise<ProjectionCheckpointStoreContractEnvironment<TCtx>>;
488
+ /**
489
+ * Declare `true` when environments provide {@link
490
+ * ProjectionCheckpointStoreContractEnvironment.runRolledBack}.
491
+ * Without it, the rollback test is marked skipped: the honest state
492
+ * of a store without a rollback, and a loud gap for a transactional
493
+ * adapter.
494
+ */
495
+ providesRolledBackRuns?: boolean;
496
+ /**
497
+ * Declare `true` when environments provide {@link
498
+ * ProjectionCheckpointStoreContractEnvironment.runConcurrently}. Without
499
+ * it, missing-key lock safety is marked skipped and remains an explicitly
500
+ * unproven adapter guarantee.
501
+ */
502
+ providesConcurrentRuns?: boolean;
503
+ }
504
+ /**
505
+ * The projection-checkpoint-store contract test suite: the proof that
506
+ * an adapter delivers the watermark semantics the `Projector`
507
+ * documents. Checkpoint semantics are an **adapter contract, not a
508
+ * kit guarantee**; this suite is how an adapter demonstrates them. Enable its
509
+ * concurrent-runs capability to prove genesis-safe exclusion rather than
510
+ * leaving that guarantee visibly skipped.
323
511
  *
324
- * describe("DrizzleOrderRepository: repository contract", () => {
325
- * for (const test of createRepositoryContractTests(harness)) {
326
- * (test.skipped ? it.skip : it)(test.name, test.run);
327
- * }
328
- * });
329
- * ```
512
+ * The concurrent test exercises commit visibility, but cannot deterministically
513
+ * hold an adapter between return from `withCheckpointLocks` and its surrounding
514
+ * transaction commit. It can therefore expose an early lock release only when
515
+ * a waiter enters during that window; holding database locks through commit or
516
+ * rollback remains an explicit adapter responsibility, not a complete proof
517
+ * supplied by this suite.
330
518
  *
331
- * **`env.run` must provide unit-of-work semantics.** Three core tests
332
- * (identity-map sameness, getById-null-after-delete, deletion
333
- * finality) exercise the session machinery: `session.identityMap`,
334
- * the `isDeleted` probe, the deleted-gate. Wiring `run` through the
335
- * kit's `UnitOfWork` gives you all of it; a hand-rolled `withCommit`
336
- * wiring must provide equivalents or those tests will fail. A
337
- * `withCommit`-only setup that deliberately makes no identity-map /
338
- * deletion-finality claims is outside this suite's scope; the suite
339
- * is the compliance bar for unit-of-work repositories.
519
+ * Framework-agnostic: bind with
520
+ * `(test.skipped ? it.skip : it)(test.name, test.run)`.
521
+ */
522
+ declare function createProjectionCheckpointStoreContractTests<TCtx>(harness: ProjectionCheckpointStoreContractHarness<TCtx>): ProjectionCheckpointStoreContractTest[];
523
+ //#endregion
524
+ //#region src/testing/replayable-stream-pages.d.ts
525
+ /** Options for {@link createReplayableStreamPages}. */
526
+ interface CreateReplayableStreamPagesOptions<Evt extends AnyDomainEvent> extends Pick<ReplayableStreamPages<Evt>, "fromVersion" | "targetVersion"> {
527
+ /** The events after the cursor through the target, in append order. */
528
+ readonly tail: ReadonlyArray<Evt>;
529
+ /**
530
+ * The maximum number of events per page. Without it, the tail comes back
531
+ * as one page. Set it to make a test cross a page boundary. Must be a
532
+ * positive safe integer when present.
533
+ */
534
+ readonly limit?: number;
535
+ }
536
+ /**
537
+ * Builds a replayable stream read from an in-memory tail, for a test of the
538
+ * code that consumes a read: a repository, or the replay over a fixed window.
539
+ * It stands in for `readStreamPages` or an adapter that pages on its own,
540
+ * and it tests neither. The tail is copied once and frozen deeply, and it
541
+ * comes back in the same pages on every iteration. So a consumer that
542
+ * changes an event in place fails at the write in its own test. An empty
543
+ * tail yields no page, as `readStreamPages` does.
544
+ */
545
+ declare function createReplayableStreamPages<Evt extends AnyDomainEvent>(stream: AggregateIdentity, options: CreateReplayableStreamPagesOptions<Evt>): ReplayableStreamPages<Evt>;
546
+ //#endregion
547
+ //#region src/testing/replayable-stream-pages-contract.d.ts
548
+ /** One named contract test for code that builds replayable stream reads. */
549
+ type ReplayableStreamPagesContractTest = ContractTest;
550
+ /** The window the suite asks the code under test for. */
551
+ interface ReplayableStreamPagesContractWindow {
552
+ /** The cursor: the pages hold the events after this version. */
553
+ readonly fromVersion: number;
554
+ /** The version a point-in-time read asks for. Absent for a read to the head. */
555
+ readonly toVersion?: number;
556
+ /** The maximum number of events per page. */
557
+ readonly limit: number;
558
+ }
559
+ /** The code under test and the store it reads. The suite creates one per test. */
560
+ interface ReplayableStreamPagesContractEnvironment<Evt extends AnyDomainEvent> {
561
+ /** Appends events to the end of the stream in the store that the code reads. */
562
+ append(stream: AggregateIdentity, events: ReadonlyArray<Evt>): Promise<void>;
563
+ /**
564
+ * Reads the stream through the code under test. Returns `undefined`
565
+ * for an absent stream and for a window that lies outside the stream.
566
+ */
567
+ read(stream: AggregateIdentity, window: ReplayableStreamPagesContractWindow): Promise<ReplayableStreamPages<Evt> | undefined>;
568
+ teardown?(): Promise<void>;
569
+ }
570
+ /**
571
+ * Inputs the suite needs. `createStream` returns an aggregate identity that no
572
+ * other test uses. `createEvent` returns an event that belongs to that
573
+ * stream; different sequence values give different event ids.
574
+ */
575
+ interface ReplayableStreamPagesContractHarness<Evt extends AnyDomainEvent> {
576
+ createEnvironment(): Promise<ReplayableStreamPagesContractEnvironment<Evt>>;
577
+ createStream(): AggregateIdentity;
578
+ createEvent(stream: AggregateIdentity, sequence: number): Evt;
579
+ }
580
+ /**
581
+ * Reusable proof of code that builds `ReplayableStreamPages` values:
582
+ * `readStreamPages`, or an adapter that pages on its own.
583
+ * The suite proves that the pages hold the events after the cursor
584
+ * through the target version in append order, that no page is empty or
585
+ * larger than the limit, that every iteration yields the same prefix, and
586
+ * that an append during the iteration stays out. It also proves that an
587
+ * absent stream and a window outside the stream give no replayable read.
588
+ */
589
+ declare function createReplayableStreamPagesContractTests<Evt extends AnyDomainEvent>(harness: ReplayableStreamPagesContractHarness<Evt>): ReplayableStreamPagesContractTest[];
590
+ //#endregion
591
+ //#region src/testing/repository-contract.d.ts
592
+ /** Application-facing state-stored repository exercised by the suite. */
593
+ interface ContractRepository<TAggregate extends Aggregate<Id<string>, AnyDomainEvent>> {
594
+ findById(id: TAggregate["id"]): Promise<TAggregate | undefined>;
595
+ add(aggregate: TAggregate): void;
596
+ /** An append-only port declares no update. */
597
+ update?(aggregate: TAggregate): void;
598
+ /** Physical removal is an optional persistence capability. */
599
+ remove?(aggregate: TAggregate): void;
600
+ }
601
+ /** One isolated real-adapter environment. `run` must permit overlapping calls. */
602
+ interface RepositoryContractEnvironment<TAggregate extends Aggregate<Id<string>, TEvent>, TEvent extends AnyDomainEvent = AnyDomainEvent> {
603
+ run<R>(work: (context: {
604
+ repository: ContractRepository<TAggregate>;
605
+ }) => Promise<R>): Promise<R>;
606
+ committedOutboxEvents(): Promise<ReadonlyArray<CommittedDomainEvent<TEvent>>>;
607
+ /** Makes the next transactional outbox write fail for atomicity proof. */
608
+ failNextOutboxWrite(error: Error): void;
609
+ teardown?(): Promise<void>;
610
+ }
611
+ /** Fixtures and observable projections supplied by an adapter package. */
612
+ interface RepositoryContractHarness<TAggregate extends Aggregate<Id<string>, TEvent>, TEvent extends AnyDomainEvent = AnyDomainEvent> {
613
+ createEnvironment(): Promise<RepositoryContractEnvironment<TAggregate, TEvent>>;
614
+ /** A fresh aggregate with a unique id. */
615
+ createAggregate(): TAggregate;
616
+ /** One version-bumping decision that records at least one event. */
617
+ mutate(aggregate: TAggregate): void;
618
+ /** Required for duplicate-add and same-UoW deletion-finality proofs. */
619
+ createAggregateWithId?(id: TAggregate["id"]): TAggregate;
620
+ /**
621
+ * A version-bumping decision with no event whose resulting state is
622
+ * deep-equal to the previous state (`setState({ ...state })`). The
623
+ * deep-equal requirement is load-bearing: it forces a diff-based
624
+ * `PersistenceModel` to derive an EMPTY change set, so the suite proves
625
+ * that an adapter persists the bumped version even when
626
+ * `changes.empty` is true. Skipping that write desyncs the persisted
627
+ * version and produces false concurrency conflicts later.
628
+ */
629
+ mutateVersionOnly?(aggregate: TAggregate): void;
630
+ /** A decision that changes a nested collection. */
631
+ mutateChildCollection?(aggregate: TAggregate): void;
632
+ /** Round-trip-stable adapter persistence projection. */
633
+ snapshotState?(aggregate: TAggregate): unknown;
634
+ /** Opt out only for an intentionally upserting add implementation. */
635
+ insertsAreDuplicateChecked?: boolean;
636
+ /**
637
+ * Opt out only for an append-only port, one that the definition marks
638
+ * with `appendOnly: true`. The suite then skips every update proof. The
639
+ * duplicate-add proof is the concurrency proof that remains, so it is
640
+ * mandatory. Provide `createAggregateWithId` and keep
641
+ * `insertsAreDuplicateChecked`, or the proof fails instead of skipping.
642
+ */
643
+ updatesAreSupported?: boolean;
644
+ /** Enables physical-remove behavior and stale-remove OCC tests. */
645
+ removesAreSupported?: boolean;
646
+ /** The remove flush predicates on the version captured at load. */
647
+ removesAreVersionChecked?: boolean;
648
+ /**
649
+ * Bound for the overlapping `run` calls, in milliseconds: the second call
650
+ * of the environment preflight, and the committing call of each
651
+ * stale-writer proof. Raise it only for a second connection that needs
652
+ * more time to open, or for a slow commit. Keep twice the bound, plus
653
+ * environment creation and teardown, below the test timeout of the runner.
654
+ */
655
+ overlappingCallsBoundMs?: number;
656
+ }
657
+ type RepositoryContractTest = ContractTest;
658
+ /**
659
+ * Contract suite for the v3 explicit-intent, commit-time-flush protocol.
340
660
  *
341
- * **Error matching is by NAME along the `cause` chain, not by
342
- * `instanceof`.** The suite ships in its own bundle entry; comparing
343
- * class identity would spuriously fail whenever the adapter's errors
344
- * come from a different copy of the kit (the main entry's bundle, or a
345
- * second installed version). `error.name === "ConcurrencyConflictError"`
346
- * anywhere in the chain is the contract.
661
+ * The harness must use the public `UnitOfWork` with a real adapter. In
662
+ * particular, `run` must create a fresh Unit of Work and transaction for each
663
+ * call and allow two calls to overlap; the mandatory stale-writer proof keeps
664
+ * writer B open while writer A commits. SQL/ORM adapters therefore need a
665
+ * real database and connection pool. An in-memory harness proves only itself.
347
666
  *
348
- * **What each test proves.** The OCC, routing, rollback, and outbox
349
- * tests prove YOUR adapter's SQL and transaction wiring. The
350
- * identity-map, deletion-finality, and event-lifecycle tests prove
351
- * your READ-PATH and unit-of-work WIRING (they exercise kit-provided
352
- * machinery, namely `session.identityMap`, the deleted-gate, and
353
- * `withCommit`'s harvest, and fail when your repository bypasses or
354
- * mis-wires it).
355
- * A deletion-finality failure usually means a missing
356
- * `identityMap.isDeleted` check or an `enrollSaved` placed after the
357
- * row write, not a broken DELETE statement.
667
+ * Writes are synchronous registrations. Durable adapter I/O happens after the
668
+ * callback returns, while the transaction is still open. A test that passes
669
+ * because `add` or `update` writes early is not a conforming implementation.
670
+ */
671
+ declare function createRepositoryContractTests<TAggregate extends Aggregate<Id<string>, TEvent>, TEvent extends AnyDomainEvent = AnyDomainEvent>(harness: RepositoryContractHarness<TAggregate, TEvent>): RepositoryContractTest[];
672
+ //#endregion
673
+ //#region src/testing/snapshot-store-contract.d.ts
674
+ /** One contract test; bind with `(test.skipped ? it.skip : it)(test.name, test.run)`. */
675
+ type SnapshotStoreContractTest = ContractTest;
676
+ /** The plain-data state shape the suite round-trips. */
677
+ interface SuiteState {
678
+ total: number;
679
+ items: Array<{
680
+ sku: string;
681
+ qty: number;
682
+ }>;
683
+ note?: string;
684
+ }
685
+ /**
686
+ * One isolated test environment: a fresh snapshot store. The suite
687
+ * creates one per test and tears it down afterwards. No transaction
688
+ * wrapper: the port is transaction-free by design (snapshots are
689
+ * derived data written after the commit; see `SnapshotStore`).
690
+ */
691
+ interface SnapshotStoreContractEnvironment {
692
+ /** The adapter under test. */
693
+ store: SnapshotStore<SuiteState>;
694
+ /** Release connections, drop schemas, etc. Called in a finally. */
695
+ teardown?(): Promise<void>;
696
+ }
697
+ /**
698
+ * What an adapter supplies to run the snapshot-store contract suite.
699
+ * For SQL adapters, run against a real database (testcontainers or
700
+ * equivalent). Note the fidelity demands the suite enforces:
701
+ * `snapshotAt` must survive with millisecond precision (store it as
702
+ * ISO-8601 text or epoch milliseconds; MySQL `DATETIME` without
703
+ * fractional seconds truncates), and an ABSENT `schemaVersion` must
704
+ * come back absent, not as `0` or `null`-coerced.
705
+ */
706
+ interface SnapshotStoreContractHarness {
707
+ createEnvironment(): Promise<SnapshotStoreContractEnvironment>;
708
+ }
709
+ /**
710
+ * The snapshot-store contract test suite: the proof that an adapter
711
+ * delivers the round-trip and isolation semantics the
712
+ * snapshot-plus-recent-events load path relies on. Store semantics are
713
+ * an **adapter contract, not a kit guarantee**; this suite is how an
714
+ * adapter demonstrates them.
358
715
  *
359
- * **Known limitation: no truly concurrent runs.** The mandatory
360
- * two-writer test is deliberately sequential-deterministic: writer B
361
- * loads, writer A loads/mutates/commits, then B commits its stale
362
- * instance. The stale `persistedVersion` baseline travels with B's
363
- * instance, so the version predicate is exercised exactly as in a true
364
- * race, without depending on lock timing, pool sizes, or
365
- * engine-specific blocking. The flip side: lock interaction is NOT
366
- * covered. A `SELECT … FOR UPDATE`-style repository that blocks
367
- * instead of conflicting, or a SERIALIZABLE engine surfacing raw
368
- * serialization failures (Postgres 40001) your adapter must map to
369
- * `ConcurrencyConflictError`, needs adapter-specific tests on top of
370
- * this suite.
716
+ * Framework-agnostic: bind with
717
+ * `(test.skipped ? it.skip : it)(test.name, test.run)`.
371
718
  */
372
- declare function createRepositoryContractTests<TAgg extends IAggregateRoot<Id<string>, Evt>, Evt extends AnyDomainEvent = AnyDomainEvent>(harness: RepositoryContractHarness<TAgg, Evt>): RepositoryContractTest[];
373
-
374
- export { type ContractRepository, type EsContractRepository, type EsRepositoryContractEnvironment, type EsRepositoryContractHarness, type EsRepositoryContractTest, type RepositoryContractEnvironment, type RepositoryContractHarness, type RepositoryContractTest, createEsRepositoryContractTests, createRepositoryContractTests };
719
+ declare function createSnapshotStoreContractTests(harness: SnapshotStoreContractHarness): SnapshotStoreContractTest[];
720
+ //#endregion
721
+ export { type CommandOutboxContractEnvironment, type CommandOutboxContractHarness, type CommandOutboxContractTest, type ContractRepository, type CreateReplayableStreamPagesOptions, type DeadlineStoreContractEnvironment, type DeadlineStoreContractHarness, type DeadlineStoreContractTest, type EsContractRepository, type EsRepositoryContractEnvironment, type EsRepositoryContractHarness, type EsRepositoryContractTest, type EventBusContractEnvironment, type EventBusContractHarness, type EventBusContractTest, type EventStoreContractEnvironment, type EventStoreContractHarness, type EventStoreContractTest, type IdempotencyStoreContractEnvironment, type IdempotencyStoreContractHarness, type IdempotencyStoreContractTest, type OutboxContractEnvironment, type OutboxContractHarness, type OutboxContractTest, type ProjectionCheckpointStoreContractEnvironment, type ProjectionCheckpointStoreContractHarness, type ProjectionCheckpointStoreContractTest, type ReplayableStreamPagesContractEnvironment, type ReplayableStreamPagesContractHarness, type ReplayableStreamPagesContractTest, type ReplayableStreamPagesContractWindow, type RepositoryContractEnvironment, type RepositoryContractHarness, type RepositoryContractTest, type SnapshotStoreContractEnvironment, type SnapshotStoreContractHarness, type SnapshotStoreContractTest, createCommandOutboxContractTests, createDeadlineStoreContractTests, createEsRepositoryContractTests, createEventBusContractTests, createEventStoreContractTests, createIdempotencyStoreContractTests, createOutboxContractTests, createProjectionCheckpointStoreContractTests, createReplayableStreamPages, createReplayableStreamPagesContractTests, createRepositoryContractTests, createSnapshotStoreContractTests };
722
+ //# sourceMappingURL=testing.d.ts.map