@sanity/workflow-engine-test 0.9.0 → 0.11.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/README.md CHANGED
@@ -13,6 +13,60 @@ boilerplate.
13
13
  npm install -D @sanity/workflow-engine-test
14
14
  ```
15
15
 
16
+ ## The bench
17
+
18
+ `createBench()` builds an isolated bench: the real engine, a fake in-memory
19
+ Sanity client, an all-access default actor, and a bench-owned clock. Every
20
+ wrapped verb injects the bench's client, tag, workflow resource, access
21
+ default, and clock, so a test reads as the workflow it exercises:
22
+
23
+ - **Runtime verbs** — `deployDefinitions`, `deleteDefinition`, `startInstance`,
24
+ `fireAction`, `editField`, `completeEffect` (and `completePendingEffect`,
25
+ addressed by effect name), `tick`, `evaluate`.
26
+ - **Instance-admin overrides** — `setStage`, `abortInstance`. Same injection,
27
+ so a forced move into a deadline-gated stage evaluates `$now` at bench time,
28
+ not wall-clock.
29
+ - **Reads and probes** — `getInstance`, `currentStage`, `activityStatus`,
30
+ `listPendingEffects`, `findPendingEffects`, `children`, `instancesForSubject`,
31
+ `instancesByStage`, `instancesForDocument`, `definitionsForDocument`, `query`,
32
+ plus the guard helpers below. Helpers sharing an engine verb's name take the
33
+ engine's own args objects and throw its typed errors — bench reads and engine
34
+ reads cannot drift.
35
+
36
+ ```ts
37
+ const bench = createBench({now: '2026-01-01T00:00:00Z'})
38
+ await bench.deployDefinitions({definitions: [reviewWorkflow]})
39
+
40
+ const {instance} = await bench.startInstance({
41
+ definition: 'review-workflow',
42
+ initialFields: [subjectField('article-1')],
43
+ })
44
+ await bench.fireAction({instanceId: instance._id, activity: 'review', action: 'approve'})
45
+ expect(await bench.currentStage(instance._id)).toBe('approved')
46
+
47
+ // Admin overrides ride the same defaults:
48
+ await bench.abortInstance({instanceId: instance._id, reason: 'hard stop'})
49
+ ```
50
+
51
+ `subjectField(docId)` and `releaseField(name)` build the runtime
52
+ initial-field entries for the bench's default resource, so tests never
53
+ hand-roll GDR URI strings. For denial paths, pass a restrictive
54
+ `currentUser` / `grants` (or a full `access`) at construction, or override
55
+ per call with the `actor` shortcut / an explicit `access`.
56
+
57
+ **Time is bench-owned.** `createBench({now})` starts frozen; `setNow` /
58
+ `advance` move it. Every wrapped verb evaluates `$now` and stamps history at
59
+ bench time, so deadline predicates are tested by advancing the clock and
60
+ ticking — never by waiting.
61
+
62
+ ## Out-of-band engines
63
+
64
+ Some tests need an engine of their own — custom effect handlers, racing two
65
+ drainers, a custom clock. `createBenchEngine(bench, overrides?)`
66
+ builds one on the bench's client, inheriting its tag, workflow resource, and
67
+ clock (`overrides` win). The bench exposes `tag` and `workflowResource` for
68
+ raw `workflow.*` calls, and the default tag ships as the `BENCH_TAG` constant.
69
+
16
70
  ## Mutation guards
17
71
 
18
72
  A workflow stage can deploy **mutation guards** — `temp.system.guard` documents
@@ -26,10 +80,15 @@ once the lake enforces them. There are two distinct things you test, and the
26
80
  bench gives you a helper for each:
27
81
 
28
82
  - **Write-time rejection (simulated)** — `bench.editDocument({documentId, patch})`
29
- writes through the bench client, and a write a deployed guard denies throws
30
- `MutationGuardDeniedError` the rejection the lake is designed to apply to
31
- _any_ client once guard enforcement ships; the bench reproduces it on the
32
- client it mints.
83
+ writes through the bench client (`patch` is a `{set?, unset?, insert?}` spec),
84
+ and a write a deployed guard denies throws `MutationGuardDeniedError` the
85
+ rejection the lake is designed to apply to _any_ client once guard
86
+ enforcement ships; the bench reproduces it on the client it mints. Every
87
+ guard action is a real write: `action: 'delete'` deletes, `'publish'`
88
+ promotes the existing `drafts.<id>` draft onto the published id, and
89
+ `'unpublish'` retracts it — so a guard scoped to `actions: ['publish']` is
90
+ genuinely consulted. The helper resolves to the post-write document, or
91
+ `undefined` once a delete/unpublish removed it.
33
92
  - **Read-time preflight** — `bench.activeGuardsForDocument(id)` answers "_would_
34
93
  a write be denied right now?" _without_ writing. This is the check a UI uses
35
94
  to disable a button or show a lock ahead of time. It's a pure read over the
@@ -46,9 +105,9 @@ const bench = createBench({documents: [{_id: 'doc-1', _type: 'article', body: 'b
46
105
  const active = await bench.activeGuardsForDocument('doc-1')
47
106
 
48
107
  // Rejection: a guard-violating write throws (bench-simulated lake contract).
49
- await expect(bench.editDocument({documentId: 'doc-1', patch: {body: 'edited'}})).rejects.toThrow(
50
- MutationGuardDeniedError,
51
- )
108
+ await expect(
109
+ bench.editDocument({documentId: 'doc-1', patch: {set: {body: 'edited'}}}),
110
+ ).rejects.toThrow(MutationGuardDeniedError)
52
111
  ```
53
112
 
54
113
  > To share one store across benches with the write seam intact, pass another