@shirudo/ddd-kit 2.0.0 → 3.0.0-rc.3

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