@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/dist/index.d.ts CHANGED
@@ -1,36 +1,46 @@
1
+ import type { AbortInstanceArgs } from "@sanity/workflow-engine";
1
2
  import { ActivityStatus } from "@sanity/workflow-engine";
2
3
  import { Actor } from "@sanity/workflow-engine";
4
+ import { ChildrenArgs } from "@sanity/workflow-engine";
5
+ import { Clock } from "@sanity/workflow-engine";
3
6
  import type { CompleteEffectArgs } from "@sanity/workflow-engine";
7
+ import { CreateEngineArgs } from "@sanity/workflow-engine";
8
+ import type { DeclaredExecutionContext } from "@sanity/workflow-engine";
9
+ import { DefinitionsForDocumentArgs } from "@sanity/workflow-engine";
10
+ import type { DeleteDefinitionArgs } from "@sanity/workflow-engine";
11
+ import { DeleteDefinitionResult } from "@sanity/workflow-engine";
4
12
  import { DeployDefinitionsArgs } from "@sanity/workflow-engine";
5
13
  import { DeployDefinitionsResult } from "@sanity/workflow-engine";
14
+ import { DeployedDefinition } from "@sanity/workflow-engine";
6
15
  import type { EditFieldArgs } from "@sanity/workflow-engine";
16
+ import { Engine } from "@sanity/workflow-engine";
7
17
  import type { EvaluateArgs } from "@sanity/workflow-engine";
18
+ import { FindPendingEffectsArgs } from "@sanity/workflow-engine";
8
19
  import type { FireActionArgs } from "@sanity/workflow-engine";
9
- import { GdrUri } from "@sanity/workflow-engine";
10
20
  import type { Grant } from "@sanity/workflow-engine";
11
21
  import { InitialFieldValue } from "@sanity/workflow-engine";
22
+ import { InstanceRefArgs } from "@sanity/workflow-engine";
23
+ import { InstancesForDocumentArgs } from "@sanity/workflow-engine";
24
+ import { InstancesQueryFilter } from "@sanity/workflow-engine";
12
25
  import { MutationGuardAction } from "@sanity/workflow-engine";
13
26
  import { MutationGuardDoc } from "@sanity/workflow-engine";
14
27
  import type { OperationArgs } from "@sanity/workflow-engine";
15
28
  import { OperationResult } from "@sanity/workflow-engine";
16
29
  import { PendingEffect } from "@sanity/workflow-engine";
30
+ import type { SetStageArgs } from "@sanity/workflow-engine";
17
31
  import type { StartInstanceArgs } from "@sanity/workflow-engine";
32
+ import type { TestClient } from "@sanity-labs/client-fake-for-test";
18
33
  import type { WorkflowAccess } from "@sanity/workflow-engine";
34
+ import { WorkflowDefinitionInput } from "@sanity/workflow-engine";
19
35
  import { WorkflowEvaluation } from "@sanity/workflow-engine";
20
36
  import { WorkflowInstance } from "@sanity/workflow-engine";
21
37
  import type { WorkflowResource } from "@sanity/workflow-engine";
22
38
 
23
- declare type Action = ReleaseAction | VersionAction;
24
-
25
- declare interface ActionResult {
26
- transactionId: string;
27
- }
28
-
29
39
  /**
30
- * The bench's default access state — wildcard user + wildcard
31
- * grants. The bench injects this as the `access` override on every
32
- * engine call so the underlying TestClient (which has no token, so
33
- * no `/users/me` resolution path) never needs to do auth resolution.
40
+ * The bench's default identity — wildcard user + wildcard grants,
41
+ * registered as the default token on the bench's fake client so the
42
+ * engine token-resolves it (`/users/me` + the ACL path) exactly like
43
+ * production.
34
44
  *
35
45
  * Production code never sees this; pass `createBench({ access })` to
36
46
  * test denial paths.
@@ -55,56 +65,73 @@ export declare type Bench = BenchState &
55
65
  QueryHelpers &
56
66
  ComposedHelpers;
57
67
 
58
- declare type BenchCompleteEffectArgs = Omit<CompleteEffectArgs, "access"> & {
59
- access?: WorkflowAccess;
60
- /** Shortcut for `access.actor`. */
61
- actor?: Actor;
62
- };
68
+ /** Default engine-scope partition tag for benches that don't pass one. */
69
+ export declare const BENCH_TAG = "bench";
63
70
 
64
- declare type BenchEditFieldArgs = Omit<EditFieldArgs, "access"> & {
65
- access?: WorkflowAccess;
66
- /** Shortcut for `access.actor`. */
67
- actor?: Actor;
68
- /** Shortcut for `access.grants`. */
69
- grants?: Grant[];
70
- };
71
+ declare type BenchAbortInstanceArgs = BenchArgs<AbortInstanceArgs>;
71
72
 
72
- declare type BenchEvaluateArgs = Omit<EvaluateArgs, "access"> & {
73
+ /**
74
+ * A bench verb's args: the engine verb's args plus the bench's per-call
75
+ * identity shortcuts — the composed pair runs the verb through a
76
+ * token-scoped sibling client.
77
+ */
78
+ declare type BenchArgs<T> = T & {
73
79
  access?: WorkflowAccess;
74
80
  /** Shortcut for `access.actor`. */
75
81
  actor?: Actor;
76
- /** Shortcut for `access.grants`. */
77
- grants?: Grant[];
78
82
  };
79
83
 
80
- declare type BenchFireActionArgs = Omit<FireActionArgs, "access"> & {
81
- access?: WorkflowAccess;
82
- /** Shortcut for `access.actor`. */
83
- actor?: Actor;
84
+ /** {@link BenchArgs} for the verbs that also take the `grants` shortcut. */
85
+ declare type BenchArgsWithGrants<T> = BenchArgs<T> & {
84
86
  /** Shortcut for `access.grants`. */
85
87
  grants?: Grant[];
86
88
  };
87
89
 
88
- declare type BenchStartInstanceArgs = Omit<StartInstanceArgs, "access"> & {
89
- access?: WorkflowAccess;
90
- /** Shortcut for `access.actor`. */
91
- actor?: Actor;
92
- };
90
+ declare type BenchCompleteEffectArgs = BenchArgs<CompleteEffectArgs>;
91
+
92
+ declare type BenchDeleteDefinitionArgs =
93
+ BenchArgsWithGrants<DeleteDefinitionArgs>;
94
+
95
+ declare type BenchEditFieldArgs = BenchArgsWithGrants<EditFieldArgs>;
96
+
97
+ declare type BenchEvaluateArgs = BenchArgsWithGrants<EvaluateArgs>;
98
+
99
+ declare type BenchFireActionArgs = BenchArgsWithGrants<FireActionArgs>;
100
+
101
+ declare type BenchSetStageArgs = BenchArgs<SetStageArgs>;
102
+
103
+ declare type BenchStartInstanceArgs = BenchArgs<StartInstanceArgs>;
93
104
 
94
105
  /** Base bench state, distinct from the engine/read/guard/query helper groups. */
95
106
  declare interface BenchState {
96
107
  /** The fake Sanity client. Passed automatically by bench wrappers. */
97
108
  client: TestClient;
109
+ /** The engine-scope partition tag every wrapped call runs under. */
110
+ tag: string;
111
+ /** The workflow resource every wrapped call targets. */
112
+ workflowResource: WorkflowResource;
113
+ /** The declared execution context stamped on every wrapped call's history
114
+ * entries (default `{kind: 'test'}`) — inherited by {@link createBenchEngine}. */
115
+ executionContext: DeclaredExecutionContext;
98
116
  /**
99
- * The access state (actor + grants) the bench injects as the
100
- * `access` override on every wrapped engine call. Defaults to
101
- * `ALL_ACCESS`.
117
+ * The bench's default identity (actor + grants) registered as the
118
+ * default token on the fake client. Defaults to `ALL_ACCESS`.
102
119
  */
103
120
  access: WorkflowAccess;
104
121
  /** Convenience: `access.actor`. */
105
122
  currentUser: Actor;
106
123
  /** Convenience: `access.grants ?? []`. */
107
124
  grants: Grant[];
125
+ /**
126
+ * A token-bound sibling client acting as `access` — the bench's ONE
127
+ * identity-registration seam. Registers the pair on the fake client's
128
+ * family-shared registry (idempotent, cached per identity; omitted
129
+ * grants default to wildcard) and returns the sibling whose
130
+ * `/users/me` + ACL path serve it — the engine token-resolves it like
131
+ * production. Bind it to {@link createBenchEngine} to act as another
132
+ * user end-to-end; never hand-roll `addIdentity` + `withConfig`.
133
+ */
134
+ clientFor: (access: WorkflowAccess) => TestClient;
108
135
  /** Add documents to the store after construction. */
109
136
  seedDocuments: (documents: StoreDocument[]) => void;
110
137
  /**
@@ -117,8 +144,7 @@ declare interface BenchState {
117
144
  * Freeze the bench clock at an explicit ISO instant. Every subsequent
118
145
  * engine call (`startInstance` / `fireAction` / `tick` / `completeEffect`
119
146
  * / `evaluate`) sees this as `$now` and stamps it on history, so
120
- * time-based `completeWhen` / `failWhen` / SLA predicates are
121
- * deterministic.
147
+ * time-based `when` triggers and SLA predicates are deterministic.
122
148
  */
123
149
  setNow: (iso: string) => void;
124
150
  /**
@@ -129,31 +155,7 @@ declare interface BenchState {
129
155
  advance: (ms: number) => void;
130
156
  }
131
157
 
132
- declare type BenchTickArgs = Omit<OperationArgs, "access"> & {
133
- access?: WorkflowAccess;
134
- /** Shortcut for `access.actor`. */
135
- actor?: Actor;
136
- };
137
-
138
- /**
139
- * One recorded invocation of a client method — the spy layer behind
140
- * `client.calls()` / `client.callCount()`. `result` holds the resolved
141
- * value (awaited, for async methods); `error` holds the thrown/rejected
142
- * value. Builder-returning methods (`patch`, `transaction`) record the
143
- * builder as `result`; their eventual writes show up in the mutation log.
144
- */
145
- declare interface CallRecord {
146
- /** Method name; namespace methods are dotted, e.g. `releases.create`. */
147
- method: string;
148
- /** Arguments as passed (stored by reference, like `vi.fn`). */
149
- args: unknown[];
150
- /** Resolved/returned value, set once the call settles successfully. */
151
- result?: unknown;
152
- /** Thrown or rejected value, set if the call failed. */
153
- error?: unknown;
154
- projectId: string;
155
- dataset: string;
156
- }
158
+ declare type BenchTickArgs = BenchArgs<OperationArgs>;
157
159
 
158
160
  /** Helpers composed across the wrapper + read groups at bench assembly. */
159
161
  declare interface ComposedHelpers {
@@ -177,14 +179,28 @@ declare interface ComposedHelpers {
177
179
  */
178
180
  export declare function createBench(options?: CreateBenchOptions): Bench;
179
181
 
182
+ /**
183
+ * Build an out-of-band {@link Engine} on a bench's client, inheriting the
184
+ * bench's tag, workflow resource, and clock (so `setNow` / `advance` govern
185
+ * the engine's `$now` too). Use this when a test needs an engine of its own —
186
+ * custom effect handlers, racing two drainers — rather than the wrappers the
187
+ * bench exposes. `overrides` win over every inherited value.
188
+ */
189
+ export declare function createBenchEngine(
190
+ bench: EngineScope & {
191
+ now: Clock;
192
+ },
193
+ overrides?: Partial<CreateEngineArgs>,
194
+ ): Engine;
195
+
180
196
  export declare interface CreateBenchOptions {
181
197
  /** Subject documents seeded into the store. */
182
198
  documents?: StoreDocument[];
183
199
  /**
184
- * Access state (actor + grants) injected as the engine `access`
185
- * override on every wrapped call. Defaults to `ALL_ACCESS`
186
- * wildcard user + wildcard grants. Pass a restrictive value to
187
- * test denial paths.
200
+ * The bench's default identity (actor + grants), registered as the
201
+ * default token on the fake client the engine token-resolves it like
202
+ * production. Defaults to `ALL_ACCESS` wildcard user + wildcard
203
+ * grants. Pass a restrictive value to test denial paths.
188
204
  *
189
205
  * Mutually exclusive with the `currentUser` / `grants` shortcuts
190
206
  * (which compose into the same shape internally).
@@ -195,28 +211,7 @@ export declare interface CreateBenchOptions {
195
211
  /** Shortcut for `access.grants`. Composed with `currentUser`. */
196
212
  grants?: Grant[];
197
213
  /**
198
- * Opt-in: simulate Sanity's hard write-gate inside the TestClient.
199
- *
200
- * When set, the bench's underlying TestClient:
201
- * - Exposes `client.request({ url })` serving `/users/me` + the
202
- * configured ACL path, so the engine can resolve `WorkflowAccess`
203
- * from "the token" exactly like production.
204
- * - Rejects writes the grants don't permit with a Sanity-style
205
- * `WriteAccessDeniedError` — the same 403 a real lake would
206
- * return.
207
- *
208
- * Use this to demonstrate what would happen if the engine's
209
- * soft-gate were bypassed (or disagreed with) — the lake is still
210
- * authoritative. The bench's `access` field is the engine-side
211
- * override; `accessControl` is the lake-side enforcement. They
212
- * usually carry the same `actor` + `grants` but can be made to
213
- * disagree to test edge cases.
214
- *
215
- * Mutually exclusive with passing a pre-built `client`.
216
- */
217
- accessControl?: TestClientAccessControl;
218
- /**
219
- * Engine-scope environment partition. Defaults to `"bench"`. Pass
214
+ * Engine-scope environment partition. Defaults to {@link BENCH_TAG}. Pass
220
215
  * different tags to two benches sharing a `client` to assert partition
221
216
  * isolation; pass the same tag to assert visibility through a shared
222
217
  * partition.
@@ -246,140 +241,71 @@ export declare interface CreateBenchOptions {
246
241
  * start on the wall clock (`advance` / `setNow` can freeze it later).
247
242
  */
248
243
  now?: string;
249
- }
250
-
251
- declare interface CurrentUser {
252
- id: string;
253
- name?: string;
254
- email?: string;
255
- /** @deprecated singular `role` on legacy responses */
256
- role?: string;
257
- roles?: {
258
- name: string;
259
- title?: string;
260
- description?: string;
261
- }[];
244
+ /**
245
+ * Declared execution context stamped (with the inferred runtime) on every
246
+ * history entry the bench's engine calls append. Defaults to
247
+ * `{ kind: "test" }`; override to exercise provenance rendering.
248
+ */
249
+ executionContext?: DeclaredExecutionContext;
262
250
  }
263
251
 
264
252
  /** Default workflow resource for benches that don't pass one. */
265
253
  export declare const DEFAULT_WORKFLOW_RESOURCE: WorkflowResource;
266
254
 
267
255
  /**
268
- * Document body accepted by `version.create` / `createVersion`. The id
269
- * is derived from `releaseId + publishedId` so callers may omit `_id`
270
- * here. `version.replace` requires `_id` since it identifies the doc
271
- * to replace.
256
+ * The write {@link GuardHelpers.editDocument} performs. Every arm is a real
257
+ * distinct client write the guard seam vets under that action `create` is
258
+ * deliberately absent because editing addresses an existing document; create
259
+ * through the client (`bench.client.create` / `clientFor(...).create`), which
260
+ * the same seam gates as a create.
272
261
  */
273
- declare interface DocumentBody {
274
- _id?: string;
275
- _type: string;
276
- [field: string]: unknown;
277
- }
262
+ export declare type EditDocumentAction = Exclude<MutationGuardAction, "create">;
278
263
 
279
- declare type DocumentValuePermission = "create" | "read" | "update";
264
+ /**
265
+ * How {@link GuardHelpers.editDocument} mutates the document. Each present
266
+ * key becomes one patch op, applied in `set` → `unset` → `insert` order in a
267
+ * single commit; the shapes mirror the client's patch builder.
268
+ */
269
+ export declare type EditDocumentPatch = {
270
+ set?: Record<string, unknown>;
271
+ unset?: string[];
272
+ insert?: {
273
+ position: "before" | "after" | "replace";
274
+ anchor: string;
275
+ items: unknown[];
276
+ };
277
+ };
280
278
 
281
- /** Engine-call wrappers that inject the bench-owned client + tag + access. */
279
+ /** The bench-owned engine call context injected into every wrapped verb. */
280
+ declare type EngineScope = {
281
+ client: TestClient;
282
+ tag: string;
283
+ workflowResource: WorkflowResource;
284
+ executionContext: DeclaredExecutionContext;
285
+ };
286
+
287
+ /** Engine-call wrappers that inject the bench-owned client + tag + identity. */
282
288
  declare type EngineWrappers = {
283
- deployDefinitions: (
284
- args: DeployDefinitionsArgs,
289
+ deployDefinitions: <T extends WorkflowDefinitionInput<T>>(
290
+ args: DeployDefinitionsArgs<T>,
285
291
  ) => Promise<DeployDefinitionsResult>;
292
+ deleteDefinition: (
293
+ args: BenchDeleteDefinitionArgs,
294
+ ) => Promise<DeleteDefinitionResult>;
286
295
  startInstance: (args: BenchStartInstanceArgs) => Promise<OperationResult>;
287
296
  fireAction: (args: BenchFireActionArgs) => Promise<OperationResult>;
288
297
  editField: (args: BenchEditFieldArgs) => Promise<OperationResult>;
289
298
  completeEffect: (args: BenchCompleteEffectArgs) => Promise<OperationResult>;
290
299
  tick: (args: BenchTickArgs) => Promise<OperationResult>;
291
300
  evaluate: (args: BenchEvaluateArgs) => Promise<WorkflowEvaluation>;
301
+ /** Admin override — bypass filters/transitions and force the stage. */
302
+ setStage: (args: BenchSetStageArgs) => Promise<OperationResult>;
303
+ /** Admin override — hard-stop an in-flight instance where it stands. */
304
+ abortInstance: (args: BenchAbortInstanceArgs) => Promise<OperationResult>;
292
305
  guardsForInstance: (instanceId: string) => Promise<MutationGuardDoc[]>;
293
306
  guardsForDefinition: (definition: string) => Promise<MutationGuardDoc[]>;
294
307
  };
295
308
 
296
- /**
297
- * A single mutation in the raw array form accepted by `mutate()` — the
298
- * same shape `@sanity/client` accepts. The inline `patch` form mirrors
299
- * the real client's keyed patch object.
300
- */
301
- declare type FakeMutation =
302
- | {
303
- create: SanityDocumentLike;
304
- }
305
- | {
306
- createOrReplace: SanityDocumentLike;
307
- }
308
- | {
309
- createIfNotExists: SanityDocumentLike;
310
- }
311
- | {
312
- delete: {
313
- id: string;
314
- };
315
- }
316
- | {
317
- patch: {
318
- id: string;
319
- ifRevisionID?: string;
320
- set?: Record<string, unknown>;
321
- setIfMissing?: Record<string, unknown>;
322
- unset?: string[];
323
- inc?: Record<string, number>;
324
- dec?: Record<string, number>;
325
- insert?: {
326
- before?: string;
327
- after?: string;
328
- replace?: string;
329
- items: unknown[];
330
- };
331
- };
332
- };
333
-
334
- /**
335
- * Data-plane subset of `@sanity/client`'s surface, with multi-dataset support.
336
- *
337
- * One `createTestClient()` call creates a registry of stores keyed by
338
- * `(projectId, dataset)`. The returned client points at one of them.
339
- * `withConfig({ projectId, dataset })` returns a new handle into the same
340
- * registry but pointing at a different store — useful for workflows where
341
- * definitions live in one dataset and instances live in another.
342
- *
343
- * Two separate `createTestClient()` calls have separate registries, so
344
- * tests stay isolated from each other.
345
- *
346
- * Methods not on the real client:
347
- *
348
- * - `seed`, `add`, `reset`, `snapshot`, `store` — bulk-load helpers,
349
- * scoped to the current dataset.
350
- * - `simulateConcurrentEdit()` — force the next N revision-guarded writes
351
- * to a document to 409, for optimistic-locking retry tests.
352
- * - `datasets()` — lists known dataset keys in this registry.
353
- *
354
- * Methods deliberately omitted:
355
- *
356
- * - Project / agent / asset / live / listen APIs.
357
- */
358
- declare interface FetchOptions {
359
- /** Override the client's configured perspective for this read. */
360
- perspective?: Perspective;
361
- /**
362
- * When `false`, resolve to a {@link RawQueryResponse} (`{ query, ms,
363
- * result }`) instead of the bare result — mirrors `@sanity/client`.
364
- * Defaults to `true`.
365
- */
366
- filterResponse?: boolean;
367
- /**
368
- * With `filterResponse: false`, set to `false` to omit `query` from the
369
- * raw response. Ignored when `filterResponse` is not `false`.
370
- */
371
- returnQuery?: boolean;
372
- /** Accepted for signature parity; not simulated. */
373
- tag?: string;
374
- /** Accepted for signature parity; not simulated. */
375
- signal?: AbortSignal;
376
- }
377
-
378
- declare interface Grant_2 {
379
- filter: string;
380
- permissions: DocumentValuePermission[];
381
- }
382
-
383
309
  declare type GuardHelpers = {
384
310
  /** All deployed guard docs ({@link MutationGuardDoc}) in the store. */
385
311
  listGuards: () => Promise<MutationGuardDoc[]>;
@@ -398,119 +324,27 @@ declare type GuardHelpers = {
398
324
  * throws `MutationGuardDeniedError` on denial — the lake's intended
399
325
  * rejection, simulated by the bench. `as` routes the write through a
400
326
  * token-scoped sibling so `identity()` resolves to that actor.
327
+ *
328
+ * Each {@link EditDocumentAction} performs that real write, so a guard
329
+ * scoped to it is genuinely consulted: `update` (default) applies `patch`;
330
+ * `delete` deletes; `publish` promotes the existing draft
331
+ * (`drafts.<documentId>`) onto `documentId`; `unpublish` retracts
332
+ * `documentId` into its draft. `documentId` is always the published id.
333
+ * A `patch` ({@link EditDocumentPatch}) is required for `update` and
334
+ * rejected for every other action — the combination is contradictory input.
335
+ *
336
+ * Resolves to the post-write document as the DEFAULT identity reads it —
337
+ * `undefined` after `delete`/`unpublish` (the doc is gone), or when the
338
+ * default identity cannot read the written doc.
401
339
  */
402
340
  editDocument: (opts: {
403
341
  documentId: string;
404
- patch: Record<string, unknown>;
405
- action?: MutationGuardAction;
342
+ patch?: EditDocumentPatch;
343
+ action?: EditDocumentAction;
406
344
  as?: Actor;
407
- }) => Promise<StoreDocument>;
345
+ }) => Promise<StoreDocument | undefined>;
408
346
  };
409
347
 
410
- /** Result of a mutation when ids are requested for all affected documents. */
411
- declare interface MultipleMutationResult {
412
- transactionId: string;
413
- documentIds: string[];
414
- results: {
415
- id: string;
416
- operation: MutationOperation;
417
- }[];
418
- }
419
-
420
- /** Append-only record of one committed transaction (a single mutation is a 1-entry transaction). */
421
- declare interface MutationLogEntry {
422
- transactionId: string;
423
- projectId: string;
424
- dataset: string;
425
- mutations: TxnMutation[];
426
- }
427
-
428
- /** The operation recorded per affected document, matching `@sanity/client`. */
429
- declare type MutationOperation = "create" | "update" | "delete" | "none";
430
-
431
- declare interface MutationOptions {
432
- ifRevisionId?: string;
433
- /**
434
- * Shape the resolved value, mirroring `@sanity/client`:
435
- * - default → the mutated document
436
- * - `returnDocuments: false` → an id-shaped mutation result
437
- * ({@link SingleMutationResult} / {@link MultipleMutationResult})
438
- * - `returnFirst: false` with documents → an array
439
- *
440
- * The static return type reflects the default (the document); pass a
441
- * type argument or cast when requesting an id-shaped result.
442
- */
443
- returnDocuments?: boolean;
444
- returnFirst?: boolean;
445
- /** Override the synthetic transaction id used in the result and log. */
446
- transactionId?: string;
447
- /** When `true`, validate + shape the result but don't persist. */
448
- dryRun?: boolean;
449
- /** Accepted for signature parity; not simulated. */
450
- visibility?: "sync" | "async" | "deferred";
451
- /** Accepted for signature parity; not simulated. */
452
- autoGenerateArrayKeys?: boolean;
453
- /** Accepted for signature parity; not simulated. */
454
- tag?: string;
455
- }
456
-
457
- declare interface PatchHandle {
458
- set: (props: Record<string, unknown>) => PatchHandle;
459
- setIfMissing: (props: Record<string, unknown>) => PatchHandle;
460
- unset: (paths: string[]) => PatchHandle;
461
- inc: (props: Record<string, number>) => PatchHandle;
462
- dec: (props: Record<string, number>) => PatchHandle;
463
- insert: (
464
- position: "before" | "after" | "replace",
465
- anchor: string,
466
- items: unknown[],
467
- ) => PatchHandle;
468
- ifRevisionId: (rev: string) => PatchHandle;
469
- commit: () => Promise<SanityDocument>;
470
- /** Test-only introspection — not on real Patch */
471
- toJSON: () => {
472
- id: string;
473
- ops: PatchOp[];
474
- ifRev?: string;
475
- };
476
- }
477
-
478
- /**
479
- * Patch operations the test client supports.
480
- *
481
- * Subset of `@sanity/client`'s PatchOperations. Each entry is one
482
- * mutation step applied in order at commit time.
483
- */
484
- declare type PatchOp =
485
- | {
486
- kind: "set";
487
- props: Record<string, unknown>;
488
- }
489
- | {
490
- kind: "setIfMissing";
491
- props: Record<string, unknown>;
492
- }
493
- | {
494
- kind: "unset";
495
- paths: string[];
496
- }
497
- | {
498
- kind: "inc";
499
- props: Record<string, number>;
500
- }
501
- | {
502
- kind: "dec";
503
- props: Record<string, number>;
504
- }
505
- | {
506
- kind: "insert";
507
- position: "before" | "after" | "replace";
508
- anchor: string;
509
- items: unknown[];
510
- };
511
-
512
- declare type Perspective = "raw" | "published" | "drafts" | string[];
513
-
514
348
  declare type QueryHelpers = {
515
349
  /**
516
350
  * Run an arbitrary GROQ query through `workflow.query`. The query MUST
@@ -542,34 +376,35 @@ declare type QueryHelpers = {
542
376
  };
543
377
 
544
378
  declare type ReadHelpers = {
545
- /** Read the workflow instance by id. Throws if missing. */
546
- getInstance: (instanceId: string) => Promise<WorkflowInstance>;
379
+ /**
380
+ * The engine's instance point-read: throws typed `InstanceNotFoundError`
381
+ * when the doc is missing or belongs to another engine's tag partition.
382
+ */
383
+ getInstance: (args: InstanceRefArgs) => Promise<WorkflowInstance>;
547
384
  /** Current stage name of an instance. */
548
385
  currentStage: (instanceId: string) => Promise<string>;
549
- /** Pending effects on an instance. */
550
- pendingEffects: (instanceId: string) => Promise<PendingEffect[]>;
386
+ /** Every pending effect on an instance — claimed and unclaimed alike. */
387
+ listPendingEffects: (args: InstanceRefArgs) => Promise<PendingEffect[]>;
388
+ /** Pending effects filtered by claim presence and/or effect names. */
389
+ findPendingEffects: (
390
+ args: FindPendingEffectsArgs,
391
+ ) => Promise<PendingEffect[]>;
551
392
  /** Status of a single activity on an instance, or undefined if missing. */
552
393
  activityStatus: (
553
394
  instanceId: string,
554
395
  activity: string,
555
396
  ) => Promise<ActivityStatus | undefined>;
556
- /** The rendered `$effects` map — context entries keyed by `name`,
557
- * completed-effect outputs namespaced under the effect's name. */
558
- effectsContextMap: (instanceId: string) => Promise<Record<string, unknown>>;
397
+ /** The rendered `$context` map — the start-time seed + a parent's spawn
398
+ * handoff, keyed by entry `name`. */
399
+ contextMap: (instanceId: string) => Promise<Record<string, unknown>>;
400
+ /** The rendered `$effects` map — completed effects' outputs, namespaced
401
+ * by effect name. */
402
+ effectOutputsMap: (instanceId: string) => Promise<Record<string, unknown>>;
559
403
  /**
560
- * Spawned children of `parentInstanceId`. If `activity` is provided,
561
- * only children spawned by that specific activity on the parent.
562
- *
563
- * Walks `history.spawned` entries on the parent — this is the durable
564
- * record. (The per-activity `spawnedInstances` field only lives on the
565
- * open StageEntry's activities; history survives transitions.)
566
- *
567
- * Returns children ordered by `startedAt` ascending.
404
+ * Spawned children of the instance, optionally restricted to those
405
+ * spawned by `activity`. Ordered by `startedAt` ascending.
568
406
  */
569
- children: (
570
- parentInstanceId: string,
571
- activity?: string,
572
- ) => Promise<WorkflowInstance[]>;
407
+ children: (args: ChildrenArgs) => Promise<WorkflowInstance[]>;
573
408
  /**
574
409
  * All workflow instances whose `doc.ref` field entry named
575
410
  * `entryName` (default `"subject"`) matches the given doc id. Useful
@@ -581,74 +416,37 @@ declare type ReadHelpers = {
581
416
  entryName?: string,
582
417
  ) => Promise<WorkflowInstance[]>;
583
418
  /**
584
- * All instances of `workflowName` currently at `stage`. Includes
585
- * completed instances (`completedAt != null`) by default; set
586
- * `{ openOnly: true }` to filter to in-flight only.
419
+ * All instances of `definition` currently at `stage`. In-flight only by
420
+ * default; `includeCompleted: true` widens to completed/aborted instances.
421
+ * The options derive from the engine's {@link InstancesQueryFilter}, so a
422
+ * vocabulary drift is a compile error here, not a silently-dropped filter.
587
423
  */
588
- instancesByStage: (options: {
589
- workflowName: string;
590
- stage: string;
591
- openOnly?: boolean;
592
- }) => Promise<WorkflowInstance[]>;
424
+ instancesByStage: (
425
+ options: Required<Pick<InstancesQueryFilter, "definition" | "stage">> &
426
+ Pick<InstancesQueryFilter, "includeCompleted">,
427
+ ) => Promise<WorkflowInstance[]>;
593
428
  /**
594
429
  * Every in-flight instance whose reactive watch-set includes `document`
595
430
  * (a resource-qualified GDR URI) — the reverse of the forward watch-set.
596
431
  * Answers "which instances should this changed doc `tick`?" the way a
597
432
  * non-reactive content forwarder needs. Sorted by `startedAt` ascending.
598
433
  */
599
- instancesForDocument: (document: GdrUri) => Promise<WorkflowInstance[]>;
434
+ instancesForDocument: (
435
+ args: InstancesForDocumentArgs,
436
+ ) => Promise<WorkflowInstance[]>;
437
+ /**
438
+ * The startable half of {@link ReadHelpers.instancesForDocument}: the
439
+ * latest deployed version of every definition applicable to the LOADED
440
+ * candidate document — startable, a required subject entry accepts its
441
+ * `_type`, `start.filter` passes. All matches, name ascending.
442
+ */
443
+ definitionsForDocument: (
444
+ args: DefinitionsForDocumentArgs,
445
+ ) => Promise<DeployedDefinition[]>;
600
446
  /** Snapshot of all documents in the store — for inspecting the world. */
601
447
  snapshot: () => readonly StoreDocument[];
602
448
  };
603
449
 
604
- declare type ReleaseAction =
605
- | {
606
- actionType: "sanity.action.release.create";
607
- releaseId: string;
608
- metadata?: Partial<ReleaseMetadata>;
609
- }
610
- | {
611
- actionType: "sanity.action.release.edit";
612
- releaseId: string;
613
- patch: {
614
- set?: Record<string, unknown>;
615
- unset?: string[];
616
- };
617
- }
618
- | {
619
- actionType: "sanity.action.release.schedule";
620
- releaseId: string;
621
- publishAt: string;
622
- }
623
- | {
624
- actionType: "sanity.action.release.unschedule";
625
- releaseId: string;
626
- }
627
- | {
628
- actionType: "sanity.action.release.publish";
629
- releaseId: string;
630
- }
631
- | {
632
- actionType: "sanity.action.release.archive";
633
- releaseId: string;
634
- }
635
- | {
636
- actionType: "sanity.action.release.unarchive";
637
- releaseId: string;
638
- }
639
- | {
640
- actionType: "sanity.action.release.delete";
641
- releaseId: string;
642
- };
643
-
644
- declare interface ReleaseDocument extends SanityDocument {
645
- _type: "system.release";
646
- name: string;
647
- state: ReleaseState;
648
- metadata: ReleaseMetadata;
649
- publishAt?: string;
650
- }
651
-
652
450
  /**
653
451
  * A `release.ref` entry pointing at `_.releases.<releaseName>` in the bench's
654
452
  * default resource — what a release-targeting workflow's `release` field
@@ -661,86 +459,6 @@ export declare function releaseField(
661
459
  },
662
460
  ): InitialFieldValue;
663
461
 
664
- declare interface ReleaseMetadata {
665
- title?: string;
666
- description?: string;
667
- releaseType?: ReleaseType;
668
- intendedPublishAt?: string;
669
- }
670
-
671
- declare interface ReleasesHandle {
672
- create: (params: {
673
- releaseId: string;
674
- metadata?: Partial<ReleaseMetadata>;
675
- }) => Promise<
676
- ActionResult & {
677
- releaseId: string;
678
- metadata: ReleaseMetadata;
679
- }
680
- >;
681
- edit: (params: {
682
- releaseId: string;
683
- patch: {
684
- set?: Record<string, unknown>;
685
- unset?: string[];
686
- };
687
- }) => Promise<ActionResult>;
688
- get: (params: { releaseId: string }) => Promise<ReleaseDocument | undefined>;
689
- fetchDocuments: (params: { releaseId: string }) => Promise<SanityDocument[]>;
690
- schedule: (params: {
691
- releaseId: string;
692
- publishAt: string;
693
- }) => Promise<ActionResult>;
694
- unschedule: (params: { releaseId: string }) => Promise<ActionResult>;
695
- publish: (params: { releaseId: string }) => Promise<ActionResult>;
696
- archive: (params: { releaseId: string }) => Promise<ActionResult>;
697
- unarchive: (params: { releaseId: string }) => Promise<ActionResult>;
698
- delete: (params: { releaseId: string }) => Promise<ActionResult>;
699
- }
700
-
701
- declare type ReleaseState = "active" | "scheduled" | "published" | "archived";
702
-
703
- declare type ReleaseType = "asap" | "scheduled" | "undecided";
704
-
705
- /** @public */
706
- declare interface SanityDocument {
707
- _id: string;
708
- _type: string;
709
- _createdAt: string;
710
- _updatedAt: string;
711
- _rev: string;
712
- [key: string]: unknown;
713
- }
714
-
715
- /**
716
- * Similar to `SanityDocument` but only requires the `_id` and `_type`
717
- *
718
- * @see SanityDocument
719
- *
720
- * @public
721
- */
722
- declare interface SanityDocumentLike {
723
- _id: string;
724
- _type: string;
725
- _createdAt?: string;
726
- _updatedAt?: string;
727
- _rev?: string;
728
- _system?: {
729
- delete?: boolean;
730
- };
731
- [key: string]: unknown;
732
- }
733
-
734
- /** Result of a mutation when `returnFirst` resolves true and ids are requested. */
735
- declare interface SingleMutationResult {
736
- transactionId: string;
737
- documentId: string;
738
- results: {
739
- id: string;
740
- operation: MutationOperation;
741
- }[];
742
- }
743
-
744
462
  /** A document as stored in the bench's fake lake. */
745
463
  declare type StoreDocument = {
746
464
  _id: string;
@@ -761,312 +479,6 @@ export declare function subjectField(
761
479
  },
762
480
  ): InitialFieldValue;
763
481
 
764
- declare interface SystemFields {
765
- _rev: string;
766
- _createdAt: string;
767
- _updatedAt: string;
768
- }
769
-
770
- declare interface TestClient {
771
- fetch: <T = unknown>(
772
- query: string,
773
- params?: Record<string, unknown>,
774
- options?: FetchOptions,
775
- ) => Promise<T>;
776
- getDocument: <T = SanityDocument>(
777
- id: string,
778
- options?: FetchOptions,
779
- ) => Promise<T | undefined>;
780
- getDocuments: <T = SanityDocument>(
781
- ids: string[],
782
- options?: FetchOptions,
783
- ) => Promise<(T | null)[]>;
784
- /**
785
- * Sanity-style HTTP request hook. Present iff the client was
786
- * created with `accessControl`. The engine's access resolver pokes
787
- * `/users/me` and the configured ACL path through this — without it
788
- * the engine throws on any verb that needs an actor (which is the
789
- * cue for the bench to supply an `access` override).
790
- *
791
- * Test cases that want to exercise the engine's token-resolution
792
- * path should configure `accessControl`; cases that want bench-style
793
- * always-allow should leave it off and pass `access` through the
794
- * bench.
795
- */
796
- request?: <T>(opts: {
797
- url?: string;
798
- uri?: string;
799
- signal?: AbortSignal;
800
- tag?: string;
801
- }) => Promise<T>;
802
- create: <T extends SanityDocumentLike>(
803
- doc: T,
804
- options?: MutationOptions,
805
- ) => Promise<T & SystemFields>;
806
- createOrReplace: <T extends SanityDocumentLike>(
807
- doc: T,
808
- options?: MutationOptions,
809
- ) => Promise<T & SystemFields>;
810
- createIfNotExists: <T extends SanityDocumentLike>(
811
- doc: T,
812
- options?: MutationOptions,
813
- ) => Promise<T & SystemFields>;
814
- delete: (
815
- id: string,
816
- options?: MutationOptions,
817
- ) => Promise<{
818
- _id: string;
819
- } | null>;
820
- patch: (documentId: string) => PatchHandle;
821
- transaction: () => TransactionHandle;
822
- /**
823
- * Apply a batch of mutations in the raw array form (or a `patch()` /
824
- * `transaction()` builder) — mirrors `@sanity/client`'s `mutate()`.
825
- * Atomic: any failure rolls the whole batch back. The resolved shape
826
- * follows `options.returnDocuments` / `returnFirst` exactly like the
827
- * real client (default → {@link MultipleMutationResult}).
828
- */
829
- mutate: (
830
- operations: FakeMutation[] | PatchHandle | TransactionHandle,
831
- options?: TxnCommitOptions,
832
- ) => Promise<TxnCommitResult>;
833
- /**
834
- * Dispatch one or more release / version actions against the store.
835
- * Returns a synthetic `transactionId`. Multi-action arrays are applied
836
- * atomically — any failure rolls back the batch.
837
- */
838
- action: (action: Action | Action[]) => Promise<ActionResult>;
839
- /** Helpers mirroring `@sanity/client`'s `client.releases.*` surface. */
840
- releases: ReleasesHandle;
841
- /** Helper for `sanity.action.document.version.create`. */
842
- createVersion: (params: {
843
- releaseId: string;
844
- publishedId: string;
845
- document: DocumentBody;
846
- }) => Promise<ActionResult>;
847
- /** Helper for `sanity.action.document.version.unpublish`. */
848
- unpublishVersion: (params: {
849
- releaseId: string;
850
- publishedId: string;
851
- }) => Promise<ActionResult>;
852
- /** Current `(projectId, dataset)` and active perspective. */
853
- config: () => {
854
- projectId: string;
855
- dataset: string;
856
- perspective: Perspective;
857
- };
858
- /**
859
- * Return a new client handle pointing at a different `(projectId, dataset)`
860
- * within the SAME backing registry. Either field can be omitted to keep
861
- * the current value. Two clients produced by `withConfig` see each
862
- * other's writes if they point at the same `(projectId, dataset)`.
863
- */
864
- withConfig: (
865
- config: Partial<{
866
- projectId: string;
867
- dataset: string;
868
- perspective: Perspective;
869
- /**
870
- * Rebind the acting identity (and its grants) on a sibling that shares
871
- * the same store — the test analogue of a token-scoped client. Lets two
872
- * actors write the same dataset, e.g. for `identity()`-based guards.
873
- */
874
- accessControl: TestClientAccessControl;
875
- }>,
876
- ) => TestClient;
877
- /** Replace the current dataset with the given documents. */
878
- seed: (documents: SanityDocumentLike[]) => void;
879
- /** Add documents to the current dataset without dropping existing ones. */
880
- add: (documents: SanityDocumentLike[]) => void;
881
- /** Drop all documents in the current dataset. */
882
- reset: () => void;
883
- /** Read-only snapshot of all documents in the current dataset. */
884
- snapshot: () => readonly SanityDocument[];
885
- /** Direct store access for the current dataset. */
886
- store: () => TestStore;
887
- /**
888
- * Simulate `count` concurrent edits to `id` in the current dataset: the
889
- * next `count` revision-guarded mutations (those passing `ifRevisionId`)
890
- * to that document throw a 409-shaped conflict, as if another client
891
- * committed between the caller's read and write. Lets a test exercise
892
- * optimistic-locking retry paths deterministically, without timing or
893
- * interleaving. Mutations that don't pass `ifRevisionId` are unaffected.
894
- */
895
- simulateConcurrentEdit: (id: string, count?: number) => void;
896
- /** List `(projectId, dataset)` pairs known in this registry. */
897
- datasets: () => {
898
- projectId: string;
899
- dataset: string;
900
- }[];
901
- /**
902
- * Register a handler for a `@sanity/client` member the fake doesn't
903
- * implement (e.g. `listen`, `assets`). Accessing that member on the
904
- * client then returns the handler instead of throwing the
905
- * "not implemented" guard error. Shared across sibling `withConfig`
906
- * handles. Stubs fill gaps only — they don't shadow implemented methods.
907
- */
908
- stub: (name: string, handler: unknown) => void;
909
- /** The currently-registered stubs, keyed by member name. */
910
- stubs: () => Record<string, unknown>;
911
- /**
912
- * Every mutation committed through this client's registry, flattened
913
- * and in order — `create` / `createOrReplace` / `createIfNotExists` /
914
- * `delete` / `patch`, whether issued directly, via `transaction()`, or
915
- * via `mutate()`. The spy hook for "assert exactly what writes my code
916
- * submitted".
917
- */
918
- mutations: () => TxnMutation[];
919
- /**
920
- * Every committed transaction, in order. A direct mutation is recorded
921
- * as a one-mutation transaction. Each entry carries its synthetic
922
- * `transactionId` and the `(projectId, dataset)` it targeted.
923
- */
924
- transactions: () => MutationLogEntry[];
925
- /** Clear the recorded mutation / transaction log. */
926
- clearLog: () => void;
927
- /**
928
- * Every recorded method call, in order — the spy layer for "was `fetch`
929
- * called, how many times, and with what params". Records the
930
- * `@sanity/client`-surface methods (`fetch`, `getDocument`, `create`,
931
- * `patch`, `mutate`, `releases.*`, …), not the test-only helpers.
932
- * Pass a method name to filter (e.g. `calls("fetch")`); namespace
933
- * methods are dotted (`calls("releases.create")`). Shared across
934
- * `withConfig` siblings.
935
- */
936
- calls: (method?: string) => CallRecord[];
937
- /** Number of recorded calls, optionally filtered by method name. */
938
- callCount: (method?: string) => number;
939
- /** Clear the recorded call log. */
940
- clearCalls: () => void;
941
- }
942
-
943
- declare interface TestClientAccessControl {
944
- /** What `client.request({ url: "/users/me" })` returns. */
945
- currentUser: CurrentUser;
946
- /** What `client.request({ url: aclPath })` returns. */
947
- grants: Grant_2[];
948
- /**
949
- * URL path the engine's access resolver hits to fetch grants.
950
- * Defaults to `/projects/<projectId>/datasets/<dataset>/acl`.
951
- */
952
- aclPath?: string;
953
- }
954
-
955
- /**
956
- * In-memory document store backing a TestClient. Closure-scoped — created
957
- * fresh per test, no global state.
958
- *
959
- * The shape mirrors what `groq-js` evaluates against: an array of
960
- * documents. Internally we keep a Map by `_id` for O(1) lookup, and
961
- * project to an array on each query.
962
- */
963
- declare interface TestStore {
964
- /** Replace the entire dataset with the given documents. Existing docs are dropped. */
965
- seed: (documents: SanityDocumentLike[]) => void;
966
- /** Add documents without dropping existing ones. Conflicts (same _id) overwrite. */
967
- add: (documents: SanityDocumentLike[]) => void;
968
- /** Remove all documents. */
969
- reset: () => void;
970
- /** All documents as an array, in insertion order. */
971
- all: () => SanityDocument[];
972
- /** Get one document by _id, or undefined. */
973
- get: (id: string) => SanityDocument | undefined;
974
- /** Insert a fully-formed document by _id. Replaces if it already exists. */
975
- put: (doc: SanityDocument) => void;
976
- /** Remove a document by _id. Returns the removed doc or undefined. */
977
- remove: (id: string) => SanityDocument | undefined;
978
- /** Snapshot of all docs, useful for assertions. */
979
- snapshot: () => readonly SanityDocument[];
980
- }
981
-
982
- declare interface TransactionHandle {
983
- create: (doc: SanityDocumentLike) => TransactionHandle;
984
- createOrReplace: (doc: SanityDocumentLike) => TransactionHandle;
985
- createIfNotExists: (doc: SanityDocumentLike) => TransactionHandle;
986
- delete: (id: string) => TransactionHandle;
987
- /** Accept either a Patch builder (drained at commit) or an inline doc id + ops. */
988
- patch: (
989
- documentIdOrPatch: string | PatchHandle,
990
- ops?: PatchOp[],
991
- ifRev?: string,
992
- ) => TransactionHandle;
993
- commit: (options?: TxnCommitOptions) => Promise<TxnCommitResult>;
994
- /** Test-only introspection — not on real Transaction. */
995
- toJSON: () => TxnMutation[];
996
- }
997
-
998
- declare interface TxnCommitOptions {
999
- /**
1000
- * Shape the resolved value (mirrors `@sanity/client`):
1001
- * - default (both unset) → {@link MultipleMutationResult}
1002
- * - `returnDocuments: true` → the resulting document(s)
1003
- * - `returnFirst: true` with `returnDocuments` → a single document
1004
- * - `returnDocuments: false` → an id-shaped mutation result
1005
- */
1006
- returnDocuments?: boolean;
1007
- returnFirst?: boolean;
1008
- /** Override the synthetic transaction id (mirrors `client.transaction(undefined, { transactionId })`). */
1009
- transactionId?: string;
1010
- }
1011
-
1012
- declare type TxnCommitResult =
1013
- | MultipleMutationResult
1014
- | SingleMutationResult
1015
- | SanityDocument
1016
- | SanityDocument[];
1017
-
1018
- /**
1019
- * A transaction collects mutations and applies them atomically at commit
1020
- * time. Mutations are applied in declaration order; if any one fails, the
1021
- * whole transaction throws and no writes land in the store.
1022
- *
1023
- * Subset of `@sanity/client`'s Transaction. Same shape, narrower surface.
1024
- */
1025
- declare type TxnMutation =
1026
- | {
1027
- kind: "create";
1028
- doc: SanityDocumentLike;
1029
- }
1030
- | {
1031
- kind: "createOrReplace";
1032
- doc: SanityDocumentLike;
1033
- }
1034
- | {
1035
- kind: "createIfNotExists";
1036
- doc: SanityDocumentLike;
1037
- }
1038
- | {
1039
- kind: "delete";
1040
- id: string;
1041
- }
1042
- | {
1043
- kind: "patch";
1044
- documentId: string;
1045
- ops: PatchOp[];
1046
- ifRev?: string;
1047
- };
1048
-
1049
- declare type VersionAction =
1050
- | {
1051
- actionType: "sanity.action.document.version.create";
1052
- publishedId: string;
1053
- document: DocumentBody;
1054
- releaseId?: string;
1055
- }
1056
- | {
1057
- actionType: "sanity.action.document.version.replace";
1058
- document: SanityDocumentLike;
1059
- }
1060
- | {
1061
- actionType: "sanity.action.document.version.discard";
1062
- versionId: string;
1063
- }
1064
- | {
1065
- actionType: "sanity.action.document.version.unpublish";
1066
- versionId: string;
1067
- publishedId: string;
1068
- };
1069
-
1070
482
  /**
1071
483
  * The default grants used when tests don't specify any. A single
1072
484
  * permissive grant matches every document and confers all permissions.