@dudousxd/nestjs-durable-testing 0.3.0 → 0.4.1
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/LICENSE +21 -0
- package/dist/index.d.ts +130 -6
- package/dist/index.js +5434 -21
- package/dist/index.js.map +1 -1
- package/package.json +24 -7
- package/dist/assertions.d.ts +0 -10
- package/dist/assertions.d.ts.map +0 -1
- package/dist/assertions.js +0 -48
- package/dist/assertions.js.map +0 -1
- package/dist/harness.d.ts +0 -28
- package/dist/harness.d.ts.map +0 -1
- package/dist/harness.js +0 -47
- package/dist/harness.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/replay.d.ts +0 -22
- package/dist/replay.d.ts.map +0 -1
- package/dist/replay.js +0 -41
- package/dist/replay.js.map +0 -1
- package/dist/steps.d.ts +0 -5
- package/dist/steps.d.ts.map +0 -1
- package/dist/steps.js +0 -19
- package/dist/steps.js.map +0 -1
- package/dist/transport-conformance.d.ts +0 -16
- package/dist/transport-conformance.d.ts.map +0 -1
- package/dist/transport-conformance.js +0 -67
- package/dist/transport-conformance.js.map +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Davide Carvalho
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,130 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { StateStore, RunStatus, WorkflowEngine, InMemoryStateStore, InMemoryTransport, RunResult, WorkflowRef, StartOptions, WorkflowRun, StepCheckpoint, Transport } from '@dudousxd/nestjs-durable-core';
|
|
2
|
+
|
|
3
|
+
declare function assertRunStatus(store: StateStore, runId: string, status: RunStatus): Promise<void>;
|
|
4
|
+
declare function assertOutput(store: StateStore, runId: string, expected: unknown): Promise<void>;
|
|
5
|
+
/** The names of the steps that have run, in order. */
|
|
6
|
+
declare function recordedSteps(store: StateStore, runId: string): Promise<string[]>;
|
|
7
|
+
/** Assert every named step ran (in any order). */
|
|
8
|
+
declare function assertStepsRan(store: StateStore, runId: string, names: string[]): Promise<void>;
|
|
9
|
+
/** Assert a step ran exactly the given number of attempts (e.g. to verify retries). */
|
|
10
|
+
declare function assertStepAttempts(store: StateStore, runId: string, name: string, attempts: number): Promise<void>;
|
|
11
|
+
|
|
12
|
+
/** A clock you control, for testing durable sleeps without real time. */
|
|
13
|
+
declare class MutableClock {
|
|
14
|
+
private current;
|
|
15
|
+
constructor(current?: number);
|
|
16
|
+
readonly now: () => number;
|
|
17
|
+
set(ms: number): void;
|
|
18
|
+
advance(ms: number): void;
|
|
19
|
+
}
|
|
20
|
+
interface TestEngine {
|
|
21
|
+
engine: WorkflowEngine;
|
|
22
|
+
store: InMemoryStateStore;
|
|
23
|
+
transport: InMemoryTransport;
|
|
24
|
+
clock: MutableClock;
|
|
25
|
+
/** Advance the clock by `ms` and resume any durable sleeps now due. */
|
|
26
|
+
tick(ms: number): Promise<RunResult[]>;
|
|
27
|
+
/**
|
|
28
|
+
* Enqueue a run and wait for it to settle (terminal or suspended). `engine.start` only enqueues
|
|
29
|
+
* now (dispatch model), so use this when a test needs the outcome synchronously.
|
|
30
|
+
*/
|
|
31
|
+
run(workflow: WorkflowRef, input: unknown, runId: string, opts?: StartOptions): Promise<RunResult>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A self-contained engine wired to in-memory store + transport and a controllable clock — run a
|
|
35
|
+
* whole workflow in a unit test, with no Postgres, no Redis, and no real time.
|
|
36
|
+
*/
|
|
37
|
+
declare function createTestEngine(): TestEngine;
|
|
38
|
+
|
|
39
|
+
interface RunHistory {
|
|
40
|
+
run: WorkflowRun;
|
|
41
|
+
checkpoints: StepCheckpoint[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Replay a recorded run's history against the CURRENT workflow code and throw if they diverged.
|
|
45
|
+
*
|
|
46
|
+
* Capture a real (ideally in-flight or representative) run from production —
|
|
47
|
+
* `{ run: await store.getRun(id), checkpoints: await store.listCheckpoints(id) }` — commit it as a
|
|
48
|
+
* fixture, and assert here in CI. If a code change renamed/reordered/removed a step at a position the
|
|
49
|
+
* history already recorded, the engine raises a `NonDeterminismError` on replay and this rethrows it,
|
|
50
|
+
* catching the break *before* it reaches an in-flight run on deploy (the moment you'd otherwise
|
|
51
|
+
* silently replay the wrong checkpoint into the wrong step). Register the workflow exactly as the app
|
|
52
|
+
* does:
|
|
53
|
+
*
|
|
54
|
+
* ```ts
|
|
55
|
+
* await assertReplayable((engine) => engine.register('pipeline', '1', pipeline.run), fixture);
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
declare function assertReplayable(register: (engine: WorkflowEngine) => void, history: RunHistory): Promise<void>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A freshly provisioned, empty {@link StateStore} plus a `cleanup` to release its resources (close
|
|
62
|
+
* the connection, drop the schema, etc.). The conformance suite asks for one before each test so
|
|
63
|
+
* every case starts from a clean slate — exactly like the per-store specs that build a `:memory:` DB.
|
|
64
|
+
*/
|
|
65
|
+
interface StateStoreContext {
|
|
66
|
+
store: StateStore;
|
|
67
|
+
cleanup: () => Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Whether the store's optional `transaction` can run an ASYNC work callback. Defaults to `true`.
|
|
70
|
+
* Set `false` only for a driver that genuinely can't — e.g. Drizzle on the SYNCHRONOUS
|
|
71
|
+
* `better-sqlite3` driver, whose `transaction()` rejects a promise-returning callback ("Transaction
|
|
72
|
+
* function cannot return a promise"). The same Drizzle adapter on an async driver (libSQL) works,
|
|
73
|
+
* and the contract still asserts `transaction` for every other store, so this never hides drift.
|
|
74
|
+
*/
|
|
75
|
+
supportsAsyncTransaction?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Whether the store can filter `listRuns({ tag })`. Defaults to `true`. Set `false` ONLY for
|
|
78
|
+
* Prisma-on-SQLite: the Prisma adapter filters tags with the `array_contains` JSON predicate, which
|
|
79
|
+
* Prisma supports on its real targets (Postgres + MySQL) but NOT on SQLite. SQLite is only the
|
|
80
|
+
* adapter's local TEST database, so the tag case is skipped there; every other store (and Prisma on
|
|
81
|
+
* a real engine) still asserts it, so this doesn't hide drift in the supported configurations.
|
|
82
|
+
*/
|
|
83
|
+
supportsTagFilter?: boolean;
|
|
84
|
+
}
|
|
85
|
+
/** Builds a fresh, empty store for one test. Called once per `it` (in `beforeEach`). */
|
|
86
|
+
type StateStoreFactory = () => Promise<StateStoreContext>;
|
|
87
|
+
/**
|
|
88
|
+
* Thrown by a {@link StateStoreFactory} to SKIP the contract for an unavailable backend instead of
|
|
89
|
+
* failing it — e.g. a testcontainers DB spec when Docker isn't running. Every contract case catches
|
|
90
|
+
* it, logs once, and returns (a no-op pass), so `pnpm test:db` is green-with-skips off a dev box
|
|
91
|
+
* without Docker rather than red.
|
|
92
|
+
*/
|
|
93
|
+
declare class StateStoreUnavailableError extends Error {
|
|
94
|
+
readonly isStateStoreUnavailable = true;
|
|
95
|
+
constructor(message: string);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The SHARED behavioral contract every `StateStore` must satisfy — in-memory and each ORM adapter
|
|
99
|
+
* alike. Registered as a vitest `describe`; pass a `name` and a {@link StateStoreFactory} that builds
|
|
100
|
+
* a fresh, empty store. The same assertions run against SQLite/in-memory under `pnpm test` and
|
|
101
|
+
* against real Postgres/MySQL (via testcontainers) under `pnpm test:db`, so any drift between an
|
|
102
|
+
* adapter and the canonical (in-memory) semantics — especially the search-attribute side-table
|
|
103
|
+
* pushdown, which is implemented per-store — fails here instead of silently in production.
|
|
104
|
+
*
|
|
105
|
+
* Each adapter's own spec keeps its dialect-specific tests (DDL/longtext/EXISTS-SQL/JSON-tolerance);
|
|
106
|
+
* this suite owns the cross-store behavior so those never drift apart.
|
|
107
|
+
*/
|
|
108
|
+
declare function runStateStoreContract(name: string, makeStore: StateStoreFactory): void;
|
|
109
|
+
|
|
110
|
+
/** A step body that throws once, then returns `value` — drives a retry, or a crash-then-resume. */
|
|
111
|
+
declare function failOnce<T>(value: T, error?: Error): () => Promise<T>;
|
|
112
|
+
/** A step body that throws the first `n` times, then returns `value`. */
|
|
113
|
+
declare function failTimes<T>(n: number, value: T, error?: Error): () => Promise<T>;
|
|
114
|
+
|
|
115
|
+
/** A transport that can register worker handlers (every shipped transport does). */
|
|
116
|
+
type HandleableTransport = Transport & {
|
|
117
|
+
handle(name: string, fn: (input: unknown) => Promise<unknown>): void;
|
|
118
|
+
close?(): Promise<void>;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* The contract every `Transport` must satisfy: a remote step dispatched through it round-trips to a
|
|
122
|
+
* worker and back (success), and a throwing handler surfaces as a failed run (failure). Point it at
|
|
123
|
+
* a freshly constructed transport (engine-side + worker `group` in one instance) to prove parity —
|
|
124
|
+
* useful when adding a new transport. Throws on any mismatch; closes the transport when done.
|
|
125
|
+
*
|
|
126
|
+
* `idPrefix` keeps run ids unique across invocations against real, shared infra (queues/tables).
|
|
127
|
+
*/
|
|
128
|
+
declare function assertTransportConformance(transport: HandleableTransport, idPrefix?: string): Promise<void>;
|
|
129
|
+
|
|
130
|
+
export { type HandleableTransport, MutableClock, type RunHistory, type StateStoreContext, type StateStoreFactory, StateStoreUnavailableError, type TestEngine, assertOutput, assertReplayable, assertRunStatus, assertStepAttempts, assertStepsRan, assertTransportConformance, createTestEngine, failOnce, failTimes, recordedSteps, runStateStoreContract };
|