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

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,631 @@
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 { Lt as PublishedCommand, P as EventBus, Pt as AggregateAddress, R as Aggregate, W as Id, Y as CommandOutboxCommitCandidate, Z as CommandOutboxWriter, _ as IdempotencyStore, a as StreamReadResult, c as ProjectionCheckpointStore, ct as EventCommitCandidate, f as IdempotencyClaimHandle, i as ReadStreamOptions, n as EventStore, nt as Outbox, st as CommittedDomainEvent, t as SnapshotStore, tt as DispatchTrackingOutbox, ut as AnyDomainEvent, w as DeadlineStore } 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
+ readonly readAll: () => Promise<ReadonlyArray<CommandOutboxCommitCandidate<C>>>;
24
+ readonly teardown?: () => Promise<void>;
25
+ }
26
+ interface CommandOutboxContractHarness<C extends PublishedCommand> {
27
+ readonly createEnvironment: () => Promise<CommandOutboxContractEnvironment<C>>;
28
+ /**
29
+ * Builds one command for the given seed. The suite derives conflicting
30
+ * commits from different seeds, so `createCommand` MUST return distinct
31
+ * command content per seed (put the seed in the payload). A constant
32
+ * command makes a manufactured conflict deep-equal to its original, and
33
+ * the conflict tests then fail a compliant adapter that deduplicates
34
+ * the exact retry.
35
+ */
36
+ readonly createCommand: (seed: number) => C;
37
+ readonly providesRolledBackAdds?: boolean;
38
+ }
39
+ type CommandOutboxContractTest = ContractTest;
40
+ declare function createCommandOutboxContractTests<C extends PublishedCommand>(harness: CommandOutboxContractHarness<C>): ReadonlyArray<CommandOutboxContractTest>;
41
+ //#endregion
42
+ //#region src/testing/deadline-store-contract.d.ts
43
+ /** One contract test; bind with `(test.skipped ? it.skip : it)(test.name, test.run)`. */
44
+ type DeadlineStoreContractTest = ContractTest;
45
+ /** The plain-data payload shape the suite round-trips. */
46
+ interface SuitePayload {
47
+ kind: string;
48
+ step?: number;
15
49
  }
16
50
  /**
17
- * One isolated test environment: fresh event store, fresh outbox. The
18
- * suite creates one per test and tears it down afterwards.
51
+ * One isolated test environment: a fresh deadline store. The suite
52
+ * creates one per test and tears it down afterwards.
19
53
  */
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>;
54
+ interface DeadlineStoreContractEnvironment {
55
+ /** The adapter under test. */
56
+ store: DeadlineStore<SuitePayload>;
57
+ /**
58
+ * Runs `work` (schedule/cancel calls) the way production does:
59
+ * inside a transaction that COMMITS. For a non-transactional store
60
+ * this simply invokes `work`.
61
+ */
62
+ run<R>(work: () => Promise<R>): Promise<R>;
63
+ /**
64
+ * Optional capability: runs `work` inside a transaction that ROLLS
65
+ * BACK. Enables the rollback tests: a rolled-back schedule must not
66
+ * leave a deadline behind (a ghost input for a state change that
67
+ * never happened), and a rolled-back cancel must not have removed
68
+ * one. Transactional adapters should always provide this.
69
+ */
70
+ runRolledBack?<R>(work: () => Promise<R>): Promise<R>;
71
+ /** Release connections, drop schemas, etc. Called in a finally. */
72
+ teardown?(): Promise<void>;
46
73
  }
47
74
  /**
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.
75
+ * What an adapter supplies to run the deadline-store contract suite.
76
+ * For SQL adapters, run against a real database (testcontainers or
77
+ * equivalent): the rollback tests prove YOUR transaction wiring, and
78
+ * schedule/cancel joining the write transaction is the port's central
79
+ * correctness rule.
80
+ */
81
+ interface DeadlineStoreContractHarness {
82
+ createEnvironment(): Promise<DeadlineStoreContractEnvironment>;
83
+ /**
84
+ * The adapter's attempt ceiling: how many `markFailed` reports move
85
+ * a deadline to the dead-letter set. Must be at least 2 so the
86
+ * attempts-surfacing test can observe a survivor.
87
+ */
88
+ failuresToDeadLetter: number;
89
+ /**
90
+ * Declare `true` when environments provide {@link
91
+ * DeadlineStoreContractEnvironment.runRolledBack}. Without it, the
92
+ * rollback tests are marked skipped: the honest state of an
93
+ * in-memory fake, and a loud gap for a transactional adapter.
94
+ */
95
+ providesRolledBackRuns?: boolean;
96
+ /**
97
+ * Declare `true` when the adapter's `due` CLAIMS the returned
98
+ * records for competing pollers (lease, visibility timeout), as the
99
+ * port sanctions. Tests that re-poll records an earlier poll
100
+ * returned without resolving them (attempts surfacing, neighbor
101
+ * flow after a dead-letter, successor visibility during a
102
+ * reschedule race) assume a non-claiming read and are marked
103
+ * skipped for claiming adapters; prove your claim/expiry semantics
104
+ * in your own suite.
105
+ */
106
+ claimsOnDue?: boolean;
107
+ }
108
+ /**
109
+ * The deadline-store contract test suite: the proof that an adapter
110
+ * delivers the schedule/cancel/due/acknowledge semantics the port
111
+ * documents. Store semantics are an **adapter contract, not a kit
112
+ * guarantee**; this suite is how an adapter demonstrates them.
54
113
  *
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).
114
+ * Framework-agnostic: bind with
115
+ * `(test.skipped ? it.skip : it)(test.name, test.run)`.
59
116
  */
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;
117
+ declare function createDeadlineStoreContractTests(harness: DeadlineStoreContractHarness): DeadlineStoreContractTest[];
118
+ //#endregion
119
+ //#region src/testing/es-repository-contract.d.ts
120
+ /** Event-sourced repositories normally expose no physical removal. */
121
+ interface EsContractRepository<TAggregate extends Aggregate<Id<string>, AnyDomainEvent>> {
122
+ findById(id: TAggregate["id"]): Promise<TAggregate | undefined>;
123
+ add(aggregate: TAggregate): void;
124
+ update(aggregate: TAggregate): void;
83
125
  }
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
- };
126
+ interface EsRepositoryContractEnvironment<TAggregate extends Aggregate<Id<string>, TEvent>, TEvent extends AnyDomainEvent = AnyDomainEvent> {
127
+ run<R>(work: (context: {
128
+ repository: EsContractRepository<TAggregate>;
129
+ }) => Promise<R>): Promise<R>;
130
+ committedOutboxEvents(): Promise<ReadonlyArray<CommittedDomainEvent<TEvent>>>;
131
+ failNextOutboxWrite(error: Error): void;
132
+ committedStreamEvents(stream: AggregateAddress<TAggregate["id"]>, options: ReadStreamOptions): Promise<StreamReadResult<TEvent>>;
133
+ teardown?(): Promise<void>;
92
134
  }
135
+ interface EsRepositoryContractHarness<TAggregate extends Aggregate<Id<string>, TEvent>, TEvent extends AnyDomainEvent = AnyDomainEvent> {
136
+ createEnvironment(): Promise<EsRepositoryContractEnvironment<TAggregate, TEvent>>;
137
+ /** Fresh aggregate with exactly one recorded creation event. */
138
+ createAggregate(): TAggregate;
139
+ createAggregateWithId?(id: TAggregate["id"]): TAggregate;
140
+ streamKeyFor(id: TAggregate["id"]): AggregateAddress<TAggregate["id"]>;
141
+ /** Applies exactly one event and advances the aggregate version by one. */
142
+ mutate(aggregate: TAggregate): void;
143
+ snapshotState?(aggregate: TAggregate): unknown;
144
+ /**
145
+ * Persists a snapshot of the aggregate at its current version in the
146
+ * environment's snapshot store, so the next load there starts from it.
147
+ * Enables the snapshot catch-up proof.
148
+ */
149
+ captureSnapshot?(aggregate: TAggregate, environment: EsRepositoryContractEnvironment<TAggregate, TEvent>): Promise<void>;
150
+ /**
151
+ * Bound for the overlapping `run` calls, in milliseconds: the second call
152
+ * of the environment preflight, and the committing call of each
153
+ * stale-writer proof. Raise it only for a second connection that needs
154
+ * more time to open, or for a slow commit. Keep twice the bound, plus
155
+ * environment creation and teardown, below the test timeout of the runner.
156
+ */
157
+ overlappingCallsBoundMs?: number;
158
+ }
159
+ type EsRepositoryContractTest = ContractTest;
93
160
  /**
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.
161
+ * Contract suite for event-stream adapters using v3 Unit-of-Work receipts.
101
162
  *
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.
163
+ * `add` and `update` register intent only. At commit, the adapter appends the
164
+ * receipt's exact event batch with the Unit of Work's expected version, in the
165
+ * same transaction as the outbox. `run` must support overlapping calls so the
166
+ * mandatory stale-writer proof exercises a real stream OCC predicate.
167
+ */
168
+ declare function createEsRepositoryContractTests<TAggregate extends Aggregate<Id<string>, TEvent>, TEvent extends AnyDomainEvent = AnyDomainEvent>(harness: EsRepositoryContractHarness<TAggregate, TEvent>): EsRepositoryContractTest[];
169
+ //#endregion
170
+ //#region src/testing/event-bus-contract.d.ts
171
+ /** One entry of the event-bus contract suite. */
172
+ type EventBusContractTest = ContractTest;
173
+ /**
174
+ * What the suite runs against. The harness creates one per test and tears
175
+ * it down afterwards. No transaction wrapper: the port is in-process and
176
+ * transaction-free by design.
177
+ */
178
+ interface EventBusContractEnvironment<Evt extends AnyDomainEvent> {
179
+ /** The implementation under test. */
180
+ readonly bus: EventBus<Evt>;
181
+ /** Release connections, drop schemas, etc. Called in a finally. */
182
+ teardown?(): Promise<void>;
183
+ }
184
+ /**
185
+ * What an implementation supplies to run the event-bus contract suite.
110
186
  *
111
- * Error matching is by NAME along the `cause` chain, not `instanceof`
112
- * (same rationale as the state-stored suite). Binding:
187
+ * The suite mints no events, so it stays free of any event union. The two
188
+ * factories must produce DIFFERENT `type` values: the suite subscribes to
189
+ * both to prove that ordering holds across types and that a catch-all
190
+ * subscription sees every type.
191
+ */
192
+ interface EventBusContractHarness<Evt extends AnyDomainEvent> {
193
+ createEnvironment(): Promise<EventBusContractEnvironment<Evt>>;
194
+ /** An event of the first type. Called repeatedly; each call may differ. */
195
+ createFirstEvent(): Evt;
196
+ /** An event of the second type, whose `type` differs from the first. */
197
+ createSecondEvent(): Evt;
198
+ }
199
+ /**
200
+ * The event-bus contract test suite: the proof that an implementation
201
+ * delivers the guarantees the `EventBus` port documents. Ordering,
202
+ * parallelism within one event, and error collection after the batch are
203
+ * a **port contract, not a kit guarantee**; this suite is how an
204
+ * implementation demonstrates them.
113
205
  *
114
- * ```ts
115
- * for (const test of createEsRepositoryContractTests(harness)) {
116
- * (test.skipped ? it.skip : it)(test.name, test.run);
117
- * }
118
- * ```
206
+ * The suite covers the port and nothing else. Construction options of the
207
+ * kit's own adapter, such as a publish-depth bound or an observer bundle,
208
+ * are not port behavior and are not pinned here.
119
209
  *
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.
210
+ * Framework-agnostic: bind with
211
+ * `(test.skipped ? it.skip : it)(test.name, test.run)`.
124
212
  */
125
- declare function createEsRepositoryContractTests<TAgg extends IAggregateRoot<Id<string>, Evt>, Evt extends AnyDomainEvent = AnyDomainEvent>(harness: EsRepositoryContractHarness<TAgg, Evt>): EsRepositoryContractTest[];
126
-
213
+ declare function createEventBusContractTests<Evt extends AnyDomainEvent>(harness: EventBusContractHarness<Evt>): EventBusContractTest[];
214
+ //#endregion
215
+ //#region src/testing/event-store-contract.d.ts
216
+ /** One named contract test for an EventStore adapter. */
217
+ type EventStoreContractTest = ContractTest;
218
+ /** One isolated adapter instance. The suite creates one per test. */
219
+ interface EventStoreContractEnvironment<Evt extends AnyDomainEvent> {
220
+ readonly store: EventStore<Evt>;
221
+ teardown?(): Promise<void>;
222
+ }
127
223
  /**
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.
224
+ * Inputs needed to prove the EventStore's observable port contract.
225
+ *
226
+ * `createCollidingStreamKeys` must return two valid stream keys with the same
227
+ * raw aggregate id and different aggregate types. `createEvent` must return an
228
+ * event addressed to the supplied key; different sequence values must produce
229
+ * different event ids.
134
230
  */
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>;
231
+ interface EventStoreContractHarness<Evt extends AnyDomainEvent> {
232
+ createEnvironment(): Promise<EventStoreContractEnvironment<Evt>>;
233
+ createCollidingStreamKeys(): readonly [AggregateAddress, AggregateAddress];
234
+ createEvent(stream: AggregateAddress, sequence: number): Evt;
139
235
  }
140
236
  /**
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).
237
+ * Reusable proof of an EventStore adapter's portable semantics: qualified
238
+ * value identity, ordered reads and slicing, OCC error mapping and atomicity,
239
+ * no-op empty appends, and detached return arrays. Physical-position
240
+ * corruption needs adapter-specific fixture support and is tested there.
241
+ */
242
+ declare function createEventStoreContractTests<Evt extends AnyDomainEvent>(harness: EventStoreContractHarness<Evt>): EventStoreContractTest[];
243
+ //#endregion
244
+ //#region src/testing/idempotency-store-contract.d.ts
245
+ /** One contract test; bind with `(test.skipped ? it.skip : it)(test.name, test.run)`. */
246
+ type IdempotencyStoreContractTest = ContractTest;
247
+ /**
248
+ * One isolated test environment: a fresh idempotency store. The suite
249
+ * creates one per test and tears it down afterwards.
145
250
  */
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>;
251
+ interface IdempotencyStoreContractEnvironment<TCtx> {
252
+ /** The adapter under test. */
253
+ store: IdempotencyStore<TCtx>;
254
+ /**
255
+ * Runs `work` inside a transaction that COMMITS, handing it the
256
+ * transaction context the store methods expect, the way
257
+ * `withIdempotentCommit` calls them in production. For a
258
+ * non-transactional store this simply invokes `work` with a dummy
259
+ * context.
260
+ */
261
+ run<R>(work: (ctx: TCtx) => Promise<R>): Promise<R>;
262
+ /**
263
+ * For the `"transactional"` family: runs `work` inside a transaction
264
+ * that ROLLS BACK. Required there; the rollback-releases-the-claim
265
+ * test is that family's core proof. Irrelevant for the
266
+ * `"non-transactional"` family.
267
+ */
268
+ runRolledBack?<R>(work: (ctx: TCtx) => Promise<R>): Promise<R>;
269
+ /**
270
+ * For the `"non-transactional"` family: advances or edits adapter test
271
+ * state so this exact claim's CURRENT lease is expired. Required there;
272
+ * a fake clock or test-only row update keeps the suite deterministic.
273
+ */
274
+ expireLease?(claim: IdempotencyClaimHandle): Promise<void>;
275
+ /**
276
+ * Moves the adapter's test clock to an exact instant. Required by the
277
+ * non-transactional family so renewal is proved without wall-clock sleeps.
278
+ */
279
+ advanceTimeTo?(instant: Date): Promise<void>;
280
+ /** Release connections, drop schemas, etc. Called in a finally. */
281
+ teardown?(): Promise<void>;
165
282
  }
166
283
  /**
167
- * What an adapter supplies to run the contract suite.
284
+ * What an adapter supplies to run the idempotency-store contract suite.
168
285
  *
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.
286
+ * The port (`IdempotencyStore`) deliberately supports two adapter
287
+ * families with different lifecycle semantics, and the suite follows
288
+ * the declared family instead of forcing one onto the other:
175
289
  *
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.
290
+ * - `"transactional"` (the single-transaction pattern): the record
291
+ * lives in the same database as the aggregates; a committed
292
+ * `complete` is final and replayable, a rollback releases everything,
293
+ * and `renew`/`confirm`/`abandon`/`reconcile` are no-ops. The family's core proof is the
294
+ * rollback test, so environments MUST provide `runRolledBack`, and
295
+ * run against a real database for SQL adapters.
296
+ * - `"non-transactional"` (the leased two-phase pattern, e.g. the
297
+ * in-memory reference): the store cannot see commits, so `complete` only
298
+ * STAGES the outcome, `confirm` finalizes it post-commit, `abandon`
299
+ * compensates failed attempts, and expired staged records require
300
+ * reconciliation. Environments MUST provide deterministic `expireLease` and
301
+ * `advanceTimeTo` controls. The rollback test is skipped.
183
302
  */
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;
303
+ interface IdempotencyStoreContractHarness<TCtx> {
304
+ createEnvironment(): Promise<IdempotencyStoreContractEnvironment<TCtx>>;
305
+ /** Which lifecycle family the adapter implements; see above. */
306
+ family: "transactional" | "non-transactional";
260
307
  }
261
308
  /**
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.
309
+ * The idempotency-store contract test suite: the proof that an adapter
310
+ * delivers the claim/renew/complete/confirm/abandon/reconcile lifecycle
311
+ * `withIdempotentCommit` documents, for its declared family. Store
312
+ * semantics are an **adapter contract, not a kit guarantee**; this
313
+ * suite is how an adapter demonstrates them.
314
+ *
315
+ * Framework-agnostic: bind with
316
+ * `(test.skipped ? it.skip : it)(test.name, test.run)`.
317
+ */
318
+ declare function createIdempotencyStoreContractTests<TCtx>(harness: IdempotencyStoreContractHarness<TCtx>): IdempotencyStoreContractTest[];
319
+ //#endregion
320
+ //#region src/testing/outbox-contract.d.ts
321
+ /** One contract test; bind with `(test.skipped ? it.skip : it)(test.name, test.run)`. */
322
+ type OutboxContractTest = ContractTest;
323
+ /**
324
+ * One isolated test environment: a fresh outbox store. The suite
325
+ * creates one per test and tears it down afterwards.
270
326
  */
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
- };
327
+ interface OutboxContractEnvironment<Evt extends AnyDomainEvent> {
328
+ /** The adapter under test. */
329
+ outbox: Outbox<Evt> | DispatchTrackingOutbox<Evt>;
330
+ /**
331
+ * Runs `outbox.add(candidates)` inside a transaction that COMMITS, the
332
+ * way `withCommit` calls it in production. The suite supplies complete
333
+ * candidates with explicit source, aggregate version, zero-based commit
334
+ * sequence, and commit size. For a non-transactional store this is simply
335
+ * `outbox.add(candidates)`.
336
+ */
337
+ addCommitted(events: ReadonlyArray<EventCommitCandidate<Evt>>): Promise<void>;
338
+ /**
339
+ * Optional capability: runs `outbox.add(candidates)` inside a
340
+ * transaction that ROLLS BACK. Enables the rollback-purity test: a
341
+ * rolled-back add must leave nothing behind. Transactional adapters
342
+ * should always provide this; it is the half of the outbox promise
343
+ * that in-memory fakes cannot keep.
344
+ */
345
+ addRolledBack?(events: ReadonlyArray<EventCommitCandidate<Evt>>): Promise<void>;
346
+ /** Release connections, drop schemas, etc. Called in a finally. */
347
+ teardown?(): Promise<void>;
278
348
  }
279
349
  /**
280
- * The repository contract test suite: the proof that an adapter
281
- * actually delivers the guarantees the kit's Unit of Work documents.
282
- *
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.
350
+ * What an adapter supplies to run the outbox contract suite.
290
351
  *
291
- * Framework-agnostic: assertions throw plain `Error`s, so the suite
292
- * binds to vitest, jest, or `node:test` the same way:
352
+ * For SQL adapters, run against a real database (testcontainers or
353
+ * equivalent): the commit-order and rollback tests prove YOUR schema
354
+ * and transaction wiring, not the kit's.
293
355
  *
294
- * ```ts
295
- * import { describe, it } from "vitest";
296
- * import { createRepositoryContractTests } from "@shirudo/ddd-kit/testing";
356
+ * Note on claiming: multi-instance safety (`getPending` claiming via
357
+ * `FOR UPDATE SKIP LOCKED` or equivalent) is part of the port contract
358
+ * for competing dispatchers but is not covered here; concurrency
359
+ * cannot be proven portably by a generic suite. Test it in your
360
+ * adapter's own suite if you run more than one dispatcher.
361
+ */
362
+ interface OutboxContractHarness<Evt extends AnyDomainEvent> {
363
+ createEnvironment(): Promise<OutboxContractEnvironment<Evt>>;
364
+ /**
365
+ * Deterministic event factory: the same `seed` yields an event with
366
+ * the SAME `eventId` (the suite uses this for the dedupe test), and
367
+ * different seeds yield distinct `eventId`s.
368
+ */
369
+ createEvent(seed: number): Evt;
370
+ /**
371
+ * For a `DispatchTrackingOutbox` adapter: how many `markFailed`
372
+ * reports move a record to the dead-letter set (the adapter's
373
+ * configured attempt ceiling). Omit for plain `Outbox` adapters;
374
+ * the dispatch-tracking tests are then marked skipped. With a
375
+ * ceiling of 1 the attempts-surfacing test is marked skipped too:
376
+ * observing attempts on a PENDING record needs a record that
377
+ * survives one failure.
378
+ */
379
+ failuresToDeadLetter?: number;
380
+ /**
381
+ * Declare `true` when environments provide {@link
382
+ * OutboxContractEnvironment.addRolledBack}. Without it, the
383
+ * rollback-purity test is marked skipped: the honest state of an
384
+ * in-memory fake, and a loud gap for a transactional adapter.
385
+ */
386
+ providesRolledBackAdds?: boolean;
387
+ /**
388
+ * Declare `true` when the adapter's `getPending` CLAIMS the returned
389
+ * records for competing dispatchers (lease, visibility timeout,
390
+ * `FOR UPDATE SKIP LOCKED`), as the port sanctions. Every test that
391
+ * re-polls records a previous poll returned without resolving them
392
+ * (head stability, re-ack non-disturbance, attempts surfacing)
393
+ * assumes a non-claiming read and is marked skipped for claiming
394
+ * adapters; prove your claim/expiry semantics in your own suite.
395
+ */
396
+ claimsOnGetPending?: boolean;
397
+ /**
398
+ * Declare `true` when `add()` dedupes on `eventId` (the unique-key
399
+ * constraint the port RECOMMENDS). The dedupe test is gated on this:
400
+ * an adapter without the constraint satisfies the port's normative
401
+ * requirements and must not fail the suite, but the skip stays
402
+ * visible as the unproven recommendation it is.
403
+ */
404
+ dedupesOnEventId?: boolean;
405
+ }
406
+ /**
407
+ * The outbox contract test suite: the proof that an adapter delivers
408
+ * the guarantees `withCommit` and `OutboxDispatcher` document. The kit
409
+ * is store-agnostic, so commit-order reads, qualified source-position
410
+ * identity, eventful-predecessor linkage, idempotent acks, and rollback
411
+ * purity are an **adapter contract, not a kit guarantee**; this suite is how
412
+ * an adapter demonstrates them. Its source-law tests also prove that colliding
413
+ * raw ids stay isolated by aggregate type and aggregate id.
297
414
  *
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
- * };
415
+ * Framework-agnostic: bind with
416
+ * `(test.skipped ? it.skip : it)(test.name, test.run)`.
417
+ */
418
+ declare function createOutboxContractTests<Evt extends AnyDomainEvent>(harness: OutboxContractHarness<Evt>): OutboxContractTest[];
419
+ //#endregion
420
+ //#region src/testing/projection-checkpoint-contract.d.ts
421
+ /** One contract test; bind with `(test.skipped ? it.skip : it)(test.name, test.run)`. */
422
+ type ProjectionCheckpointStoreContractTest = ContractTest;
423
+ /**
424
+ * One isolated test environment: a fresh checkpoint store. The suite
425
+ * creates one per test and tears it down afterwards.
426
+ */
427
+ interface ProjectionCheckpointStoreContractEnvironment<TCtx> {
428
+ /** The adapter under test. */
429
+ store: ProjectionCheckpointStore<TCtx>;
430
+ /**
431
+ * Runs `work` inside a transaction that COMMITS, handing it the
432
+ * transaction context the store methods expect, the way the
433
+ * `Projector` calls them in production. For a non-transactional
434
+ * store this simply invokes `work` with a dummy context.
435
+ */
436
+ run<R>(work: (ctx: TCtx) => Promise<R>): Promise<R>;
437
+ /**
438
+ * Optional capability: starts every supplied transaction independently and
439
+ * concurrently. Each callback must receive its own transaction context for
440
+ * database adapters (normally a separate pooled connection). Enables the
441
+ * missing-key/existing-key exclusion test; implementing this as sequential
442
+ * calls would make that proof meaningless.
443
+ */
444
+ runConcurrently?<R>(works: ReadonlyArray<(ctx: TCtx) => Promise<R>>): Promise<R[]>;
445
+ /**
446
+ * Optional capability: runs `work` inside a transaction that ROLLS
447
+ * BACK. Enables the rollback test: a rolled-back save must leave no
448
+ * checkpoint behind, the half of the atomic update+checkpoint
449
+ * promise the store contributes. Transactional adapters should
450
+ * always provide this; in-memory fakes cannot.
451
+ */
452
+ runRolledBack?<R>(work: (ctx: TCtx) => Promise<R>): Promise<R>;
453
+ /** Release connections, drop schemas, etc. Called in a finally. */
454
+ teardown?(): Promise<void>;
455
+ }
456
+ /**
457
+ * What an adapter supplies to run the projection-checkpoint-store
458
+ * contract suite. For SQL adapters, run against a real database
459
+ * (testcontainers or equivalent): concurrent runs prove missing-key locking,
460
+ * the rollback test proves YOUR transaction wiring, and the checkpoint table
461
+ * must live in the same database as the read models it accounts for.
462
+ */
463
+ interface ProjectionCheckpointStoreContractHarness<TCtx> {
464
+ createEnvironment(): Promise<ProjectionCheckpointStoreContractEnvironment<TCtx>>;
465
+ /**
466
+ * Declare `true` when environments provide {@link
467
+ * ProjectionCheckpointStoreContractEnvironment.runRolledBack}.
468
+ * Without it, the rollback test is marked skipped: the honest state
469
+ * of an in-memory fake, and a loud gap for a transactional adapter.
470
+ */
471
+ providesRolledBackRuns?: boolean;
472
+ /**
473
+ * Declare `true` when environments provide {@link
474
+ * ProjectionCheckpointStoreContractEnvironment.runConcurrently}. Without
475
+ * it, missing-key lock safety is marked skipped and remains an explicitly
476
+ * unproven adapter guarantee.
477
+ */
478
+ providesConcurrentRuns?: boolean;
479
+ }
480
+ /**
481
+ * The projection-checkpoint-store contract test suite: the proof that
482
+ * an adapter delivers the watermark semantics the `Projector`
483
+ * documents. Checkpoint semantics are an **adapter contract, not a
484
+ * kit guarantee**; this suite is how an adapter demonstrates them. Enable its
485
+ * concurrent-runs capability to prove genesis-safe exclusion rather than
486
+ * leaving that guarantee visibly skipped.
323
487
  *
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
- * ```
488
+ * The concurrent test exercises commit visibility, but cannot deterministically
489
+ * hold an adapter between return from `withCheckpointLocks` and its surrounding
490
+ * transaction commit. It can therefore expose an early lock release only when
491
+ * a waiter enters during that window; holding database locks through commit or
492
+ * rollback remains an explicit adapter responsibility, not a complete proof
493
+ * supplied by this suite.
330
494
  *
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.
495
+ * Framework-agnostic: bind with
496
+ * `(test.skipped ? it.skip : it)(test.name, test.run)`.
497
+ */
498
+ declare function createProjectionCheckpointStoreContractTests<TCtx>(harness: ProjectionCheckpointStoreContractHarness<TCtx>): ProjectionCheckpointStoreContractTest[];
499
+ //#endregion
500
+ //#region src/testing/repository-contract.d.ts
501
+ /** Application-facing state-stored repository exercised by the suite. */
502
+ interface ContractRepository<TAggregate extends Aggregate<Id<string>, AnyDomainEvent>> {
503
+ findById(id: TAggregate["id"]): Promise<TAggregate | undefined>;
504
+ add(aggregate: TAggregate): void;
505
+ /** An append-only port declares no update. */
506
+ update?(aggregate: TAggregate): void;
507
+ /** Physical removal is an optional persistence capability. */
508
+ remove?(aggregate: TAggregate): void;
509
+ }
510
+ /** One isolated real-adapter environment. `run` must permit overlapping calls. */
511
+ interface RepositoryContractEnvironment<TAggregate extends Aggregate<Id<string>, TEvent>, TEvent extends AnyDomainEvent = AnyDomainEvent> {
512
+ run<R>(work: (context: {
513
+ repository: ContractRepository<TAggregate>;
514
+ }) => Promise<R>): Promise<R>;
515
+ committedOutboxEvents(): Promise<ReadonlyArray<CommittedDomainEvent<TEvent>>>;
516
+ /** Makes the next transactional outbox write fail for atomicity proof. */
517
+ failNextOutboxWrite(error: Error): void;
518
+ teardown?(): Promise<void>;
519
+ }
520
+ /** Fixtures and observable projections supplied by an adapter package. */
521
+ interface RepositoryContractHarness<TAggregate extends Aggregate<Id<string>, TEvent>, TEvent extends AnyDomainEvent = AnyDomainEvent> {
522
+ createEnvironment(): Promise<RepositoryContractEnvironment<TAggregate, TEvent>>;
523
+ /** A fresh aggregate with a unique id. */
524
+ createAggregate(): TAggregate;
525
+ /** One version-bumping decision that records at least one event. */
526
+ mutate(aggregate: TAggregate): void;
527
+ /** Required for duplicate-add and same-UoW deletion-finality proofs. */
528
+ createAggregateWithId?(id: TAggregate["id"]): TAggregate;
529
+ /**
530
+ * A version-bumping decision with no event whose resulting state is
531
+ * deep-equal to the previous state (`setState({ ...state })`). The
532
+ * deep-equal requirement is load-bearing: it forces a diff-based
533
+ * `PersistenceModel` to derive an EMPTY change set, so the suite proves
534
+ * that an adapter persists the bumped version even when
535
+ * `changes.empty` is true. Skipping that write desyncs the persisted
536
+ * version and produces false concurrency conflicts later.
537
+ */
538
+ mutateVersionOnly?(aggregate: TAggregate): void;
539
+ /** A decision that changes a nested collection. */
540
+ mutateChildCollection?(aggregate: TAggregate): void;
541
+ /** Round-trip-stable adapter persistence projection. */
542
+ snapshotState?(aggregate: TAggregate): unknown;
543
+ /** Opt out only for an intentionally upserting add implementation. */
544
+ insertsAreDuplicateChecked?: boolean;
545
+ /**
546
+ * Opt out only for an append-only port, one that the definition marks
547
+ * with `appendOnly: true`. The suite then skips every update proof. The
548
+ * duplicate-add proof is the concurrency proof that remains, so it is
549
+ * mandatory. Provide `createAggregateWithId` and keep
550
+ * `insertsAreDuplicateChecked`, or the proof fails instead of skipping.
551
+ */
552
+ updatesAreSupported?: boolean;
553
+ /** Enables physical-remove behavior and stale-remove OCC tests. */
554
+ removesAreSupported?: boolean;
555
+ /** The remove flush predicates on the version captured at load. */
556
+ removesAreVersionChecked?: boolean;
557
+ /**
558
+ * Bound for the overlapping `run` calls, in milliseconds: the second call
559
+ * of the environment preflight, and the committing call of each
560
+ * stale-writer proof. Raise it only for a second connection that needs
561
+ * more time to open, or for a slow commit. Keep twice the bound, plus
562
+ * environment creation and teardown, below the test timeout of the runner.
563
+ */
564
+ overlappingCallsBoundMs?: number;
565
+ }
566
+ type RepositoryContractTest = ContractTest;
567
+ /**
568
+ * Contract suite for the v3 explicit-intent, commit-time-flush protocol.
340
569
  *
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.
570
+ * The harness must use the public `UnitOfWork` with a real adapter. In
571
+ * particular, `run` must create a fresh Unit of Work and transaction for each
572
+ * call and allow two calls to overlap; the mandatory stale-writer proof keeps
573
+ * writer B open while writer A commits. SQL/ORM adapters therefore need a
574
+ * real database and connection pool. An in-memory harness proves only itself.
347
575
  *
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.
576
+ * Writes are synchronous registrations. Durable adapter I/O happens after the
577
+ * callback returns, while the transaction is still open. A test that passes
578
+ * because `add` or `update` writes early is not a conforming implementation.
579
+ */
580
+ declare function createRepositoryContractTests<TAggregate extends Aggregate<Id<string>, TEvent>, TEvent extends AnyDomainEvent = AnyDomainEvent>(harness: RepositoryContractHarness<TAggregate, TEvent>): RepositoryContractTest[];
581
+ //#endregion
582
+ //#region src/testing/snapshot-store-contract.d.ts
583
+ /** One contract test; bind with `(test.skipped ? it.skip : it)(test.name, test.run)`. */
584
+ type SnapshotStoreContractTest = ContractTest;
585
+ /** The plain-data state shape the suite round-trips. */
586
+ interface SuiteState {
587
+ total: number;
588
+ items: Array<{
589
+ sku: string;
590
+ qty: number;
591
+ }>;
592
+ note?: string;
593
+ }
594
+ /**
595
+ * One isolated test environment: a fresh snapshot store. The suite
596
+ * creates one per test and tears it down afterwards. No transaction
597
+ * wrapper: the port is transaction-free by design (snapshots are
598
+ * derived data written after the commit; see `SnapshotStore`).
599
+ */
600
+ interface SnapshotStoreContractEnvironment {
601
+ /** The adapter under test. */
602
+ store: SnapshotStore<SuiteState>;
603
+ /** Release connections, drop schemas, etc. Called in a finally. */
604
+ teardown?(): Promise<void>;
605
+ }
606
+ /**
607
+ * What an adapter supplies to run the snapshot-store contract suite.
608
+ * For SQL adapters, run against a real database (testcontainers or
609
+ * equivalent). Note the fidelity demands the suite enforces:
610
+ * `snapshotAt` must survive with millisecond precision (store it as
611
+ * ISO-8601 text or epoch milliseconds; MySQL `DATETIME` without
612
+ * fractional seconds truncates), and an ABSENT `schemaVersion` must
613
+ * come back absent, not as `0` or `null`-coerced.
614
+ */
615
+ interface SnapshotStoreContractHarness {
616
+ createEnvironment(): Promise<SnapshotStoreContractEnvironment>;
617
+ }
618
+ /**
619
+ * The snapshot-store contract test suite: the proof that an adapter
620
+ * delivers the round-trip and isolation semantics the
621
+ * snapshot-plus-recent-events load path relies on. Store semantics are
622
+ * an **adapter contract, not a kit guarantee**; this suite is how an
623
+ * adapter demonstrates them.
358
624
  *
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.
625
+ * Framework-agnostic: bind with
626
+ * `(test.skipped ? it.skip : it)(test.name, test.run)`.
371
627
  */
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 };
628
+ declare function createSnapshotStoreContractTests(harness: SnapshotStoreContractHarness): SnapshotStoreContractTest[];
629
+ //#endregion
630
+ export { type CommandOutboxContractEnvironment, type CommandOutboxContractHarness, type CommandOutboxContractTest, type ContractRepository, 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 RepositoryContractEnvironment, type RepositoryContractHarness, type RepositoryContractTest, type SnapshotStoreContractEnvironment, type SnapshotStoreContractHarness, type SnapshotStoreContractTest, createCommandOutboxContractTests, createDeadlineStoreContractTests, createEsRepositoryContractTests, createEventBusContractTests, createEventStoreContractTests, createIdempotencyStoreContractTests, createOutboxContractTests, createProjectionCheckpointStoreContractTests, createRepositoryContractTests, createSnapshotStoreContractTests };
631
+ //# sourceMappingURL=testing.d.ts.map