@dudousxd/nestjs-durable-testing 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -9,6 +9,94 @@ declare function assertStepsRan(store: StateStore, runId: string, names: string[
9
9
  /** Assert a step ran exactly the given number of attempts (e.g. to verify retries). */
10
10
  declare function assertStepAttempts(store: StateStore, runId: string, name: string, attempts: number): Promise<void>;
11
11
 
12
+ /**
13
+ * The canonical physical column names for the durable tables, keyed by table → entity property →
14
+ * column. This is the cross-adapter contract: a run written by one store adapter must be readable by
15
+ * another, so EVERY adapter (TypeORM, MikroORM, Prisma, Drizzle) must map these properties to these
16
+ * exact columns. The canonical convention is `snake_case`.
17
+ *
18
+ * Each adapter has a spec that asserts its physical columns against this map (see `assertDurableColumns`).
19
+ * Without it, adapters silently diverge — one defaulting to its ORM's `snake_case` naming strategy,
20
+ * another keeping the verbatim camelCase property name — and a store swap fails against an existing
21
+ * table with "Unknown column" only at runtime. The map exists so that divergence is a failing unit
22
+ * test, not a production incident.
23
+ *
24
+ * The camelCase ("preserve") naming is simply the property names themselves (the object keys), so no
25
+ * separate map is needed for it.
26
+ */
27
+ declare const DURABLE_CANONICAL_COLUMNS: {
28
+ readonly durable_workflow_runs: {
29
+ readonly id: "id";
30
+ readonly workflow: "workflow";
31
+ readonly workflowVersion: "workflow_version";
32
+ readonly status: "status";
33
+ readonly input: "input";
34
+ readonly output: "output";
35
+ readonly error: "error";
36
+ readonly wakeAt: "wake_at";
37
+ readonly lockedBy: "locked_by";
38
+ readonly lockedUntil: "locked_until";
39
+ readonly recoveryAttempts: "recovery_attempts";
40
+ readonly tags: "tags";
41
+ readonly searchAttributes: "search_attributes";
42
+ readonly createdAt: "created_at";
43
+ readonly updatedAt: "updated_at";
44
+ };
45
+ readonly durable_step_checkpoints: {
46
+ readonly runId: "run_id";
47
+ readonly seq: "seq";
48
+ readonly name: "name";
49
+ readonly kind: "kind";
50
+ readonly stepId: "step_id";
51
+ readonly status: "status";
52
+ readonly input: "input";
53
+ readonly output: "output";
54
+ readonly error: "error";
55
+ readonly events: "events";
56
+ readonly attempts: "attempts";
57
+ readonly workerGroup: "worker_group";
58
+ readonly wakeAt: "wake_at";
59
+ readonly enqueuedAt: "enqueued_at";
60
+ readonly startedAt: "started_at";
61
+ readonly finishedAt: "finished_at";
62
+ };
63
+ readonly durable_run_attributes: {
64
+ readonly runId: "run_id";
65
+ readonly key: "key";
66
+ readonly strValue: "str_value";
67
+ readonly numValue: "num_value";
68
+ };
69
+ readonly durable_signal_waiters: {
70
+ readonly token: "token";
71
+ readonly runId: "run_id";
72
+ readonly seq: "seq";
73
+ };
74
+ readonly durable_buffered_signals: {
75
+ readonly id: "id";
76
+ readonly token: "token";
77
+ readonly payload: "payload";
78
+ };
79
+ };
80
+ type DurableColumnMap = typeof DURABLE_CANONICAL_COLUMNS;
81
+ /** A single column-name disagreement found by {@link assertDurableColumns}. */
82
+ interface DurableColumnMismatch {
83
+ table: string;
84
+ property: string;
85
+ expected: string;
86
+ actual: string | undefined;
87
+ }
88
+ /**
89
+ * Walk every (table, property) in the canonical contract and compare it to what an adapter actually
90
+ * maps, via the supplied `resolve(table, property) => columnName`. Returns the list of mismatches
91
+ * (empty when the adapter is canonical) so a spec can `expect(assertDurableColumns(resolve)).toEqual([])`.
92
+ *
93
+ * Pass `expected = DURABLE_CANONICAL_COLUMNS` (default) for the snake_case contract, or a map of the
94
+ * property names to themselves to assert the camelCase ("preserve") naming.
95
+ */
96
+ declare function assertDurableColumns(resolve: (table: string, property: string) => string | undefined, expected?: Record<string, Record<string, string>>): DurableColumnMismatch[];
97
+ /** The "preserve" (camelCase) expectation: every property maps to a column of the same name. */
98
+ declare function preserveColumnExpectation(): Record<string, Record<string, string>>;
99
+
12
100
  /** A clock you control, for testing durable sleeps without real time. */
13
101
  declare class MutableClock {
14
102
  private current;
@@ -127,4 +215,4 @@ type HandleableTransport = Transport & {
127
215
  */
128
216
  declare function assertTransportConformance(transport: HandleableTransport, idPrefix?: string): Promise<void>;
129
217
 
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 };
218
+ export { DURABLE_CANONICAL_COLUMNS, type DurableColumnMap, type DurableColumnMismatch, type HandleableTransport, MutableClock, type RunHistory, type StateStoreContext, type StateStoreFactory, StateStoreUnavailableError, type TestEngine, assertDurableColumns, assertOutput, assertReplayable, assertRunStatus, assertStepAttempts, assertStepsRan, assertTransportConformance, createTestEngine, failOnce, failTimes, preserveColumnExpectation, recordedSteps, runStateStoreContract };
package/dist/index.js CHANGED
@@ -48,6 +48,88 @@ async function assertStepAttempts(store, runId, name, attempts) {
48
48
  }
49
49
  __name(assertStepAttempts, "assertStepAttempts");
50
50
 
51
+ // src/durable-columns.ts
52
+ var DURABLE_CANONICAL_COLUMNS = {
53
+ durable_workflow_runs: {
54
+ id: "id",
55
+ workflow: "workflow",
56
+ workflowVersion: "workflow_version",
57
+ status: "status",
58
+ input: "input",
59
+ output: "output",
60
+ error: "error",
61
+ wakeAt: "wake_at",
62
+ lockedBy: "locked_by",
63
+ lockedUntil: "locked_until",
64
+ recoveryAttempts: "recovery_attempts",
65
+ tags: "tags",
66
+ searchAttributes: "search_attributes",
67
+ createdAt: "created_at",
68
+ updatedAt: "updated_at"
69
+ },
70
+ durable_step_checkpoints: {
71
+ runId: "run_id",
72
+ seq: "seq",
73
+ name: "name",
74
+ kind: "kind",
75
+ stepId: "step_id",
76
+ status: "status",
77
+ input: "input",
78
+ output: "output",
79
+ error: "error",
80
+ events: "events",
81
+ attempts: "attempts",
82
+ workerGroup: "worker_group",
83
+ wakeAt: "wake_at",
84
+ enqueuedAt: "enqueued_at",
85
+ startedAt: "started_at",
86
+ finishedAt: "finished_at"
87
+ },
88
+ durable_run_attributes: {
89
+ runId: "run_id",
90
+ key: "key",
91
+ strValue: "str_value",
92
+ numValue: "num_value"
93
+ },
94
+ durable_signal_waiters: {
95
+ token: "token",
96
+ runId: "run_id",
97
+ seq: "seq"
98
+ },
99
+ durable_buffered_signals: {
100
+ id: "id",
101
+ token: "token",
102
+ payload: "payload"
103
+ }
104
+ };
105
+ function assertDurableColumns(resolve, expected = DURABLE_CANONICAL_COLUMNS) {
106
+ const mismatches = [];
107
+ for (const [table, props] of Object.entries(expected)) {
108
+ for (const [property, column] of Object.entries(props)) {
109
+ const actual = resolve(table, property);
110
+ if (actual !== column) mismatches.push({
111
+ table,
112
+ property,
113
+ expected: column,
114
+ actual
115
+ });
116
+ }
117
+ }
118
+ return mismatches;
119
+ }
120
+ __name(assertDurableColumns, "assertDurableColumns");
121
+ function preserveColumnExpectation() {
122
+ const out = {};
123
+ for (const [table, props] of Object.entries(DURABLE_CANONICAL_COLUMNS)) {
124
+ out[table] = Object.fromEntries(Object.keys(props).map((p) => [
125
+ p,
126
+ p
127
+ ]));
128
+ }
129
+ return out;
130
+ }
131
+ __name(preserveColumnExpectation, "preserveColumnExpectation");
132
+
51
133
  // src/harness.ts
52
134
  import { InMemoryStateStore, InMemoryTransport, WorkflowEngine } from "@dudousxd/nestjs-durable-core";
53
135
  var MutableClock = class {
@@ -235,6 +317,81 @@ function runStateStoreContract(name, makeStore) {
235
317
  expect(dead?.status).toBe("dead");
236
318
  expect(dead?.recoveryAttempts).toBe(4);
237
319
  });
320
+ t("deleteRun removes the run and all its rows (checkpoints, waiters, attributes)", async () => {
321
+ await store.createRun(run({
322
+ searchAttributes: {
323
+ tier: "pro"
324
+ }
325
+ }));
326
+ await store.saveCheckpoint(checkpoint());
327
+ await store.putSignalWaiter({
328
+ token: "approve-r1",
329
+ runId: "r1",
330
+ seq: 0
331
+ });
332
+ await store.deleteRun("r1");
333
+ expect(await store.getRun("r1")).toBeNull();
334
+ expect(await store.listCheckpoints("r1")).toEqual([]);
335
+ expect(await store.listSignalWaiters("approve-")).toEqual([]);
336
+ expect(await store.listRuns({
337
+ attributes: [
338
+ {
339
+ key: "tier",
340
+ op: "eq",
341
+ value: "pro"
342
+ }
343
+ ]
344
+ })).toEqual([]);
345
+ });
346
+ t("deleteRun is a no-op for a missing run", async () => {
347
+ await expect(store.deleteRun("nope")).resolves.toBeUndefined();
348
+ });
349
+ t("updateRun maps every patchable field \u2014 clears error and patches tags/lockedBy/input/timers", async () => {
350
+ await store.createRun(run({
351
+ status: "failed",
352
+ error: {
353
+ message: "boom"
354
+ },
355
+ tags: [
356
+ "old"
357
+ ],
358
+ lockedBy: "owner-1"
359
+ }));
360
+ await store.updateRun("r1", {
361
+ status: "completed",
362
+ output: {
363
+ ok: true
364
+ },
365
+ error: void 0,
366
+ input: {
367
+ orderId: "o2"
368
+ },
369
+ tags: [
370
+ "x",
371
+ "y"
372
+ ],
373
+ lockedBy: "owner-2",
374
+ wakeAt: 123456,
375
+ lockedUntil: 222222,
376
+ updatedAt: at
377
+ });
378
+ const r = await store.getRun("r1");
379
+ expect(r?.status).toBe("completed");
380
+ expect(r?.output).toEqual({
381
+ ok: true
382
+ });
383
+ expect(r?.error).toBeUndefined();
384
+ expect(r?.input).toEqual({
385
+ orderId: "o2"
386
+ });
387
+ expect(r?.tags).toEqual([
388
+ "x",
389
+ "y"
390
+ ]);
391
+ expect(r?.lockedBy).toBe("owner-2");
392
+ expect(r?.wakeAt).toBe(123456);
393
+ expect(r?.lockedUntil).toBe(222222);
394
+ });
238
395
  t("upserts checkpoints, reads them by (runId, seq), and lists them ordered by seq", async () => {
239
396
  await store.createRun(run());
240
397
  await store.saveCheckpoint(checkpoint());
@@ -5418,8 +5575,10 @@ async function assertTransportConformance(transport, idPrefix = "conf") {
5418
5575
  }
5419
5576
  __name(assertTransportConformance, "assertTransportConformance");
5420
5577
  export {
5578
+ DURABLE_CANONICAL_COLUMNS,
5421
5579
  MutableClock,
5422
5580
  StateStoreUnavailableError,
5581
+ assertDurableColumns,
5423
5582
  assertOutput,
5424
5583
  assertReplayable,
5425
5584
  assertRunStatus,
@@ -5429,6 +5588,7 @@ export {
5429
5588
  createTestEngine,
5430
5589
  failOnce,
5431
5590
  failTimes,
5591
+ preserveColumnExpectation,
5432
5592
  recordedSteps,
5433
5593
  runStateStoreContract
5434
5594
  };