@sanity/workflow-engine-test 0.11.0 → 0.13.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/CHANGELOG.md CHANGED
@@ -1,5 +1,70 @@
1
1
  # @sanity/workflow-engine-test
2
2
 
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 00c089d: **BREAKING:** Require every definition deployment and workflow deployment configuration to state an `expectedMinReaderModel` literal that exactly matches the installed engine's writer capability.
8
+
9
+ Deploy and definition-diff paths now reject missing or stale acknowledgements before client access or telemetry, with a structured error and readers-first rollout guidance.
10
+
11
+ - 55a54b3: Surfaces understand the new `subject` field kind. Studio plugin: the start dialog's default subject seed lands on a definition's `subject`-kind entry whatever it is named (the doc.ref-named-`subject` convention stays the fallback), subject entries get the document picker / read-only face / state-panel preview / document pill, and mapping validation checks the subject entry's `types` facet. MCP: the subject lookup behind title enrichment prefers the `subject`-kind entry and keeps the name convention as fallback; the authoring guide teaches the kind. Test bench — **BREAKING:** `subjectField` now builds a `subject`-KIND entry value (matching its name and the engine's subject rule); a definition declaring the doc.ref convention needs the new `docRefField(documentId, {name})` instead. `instancesForSubject` matches both single-ref kinds.
12
+
13
+ ### Patch Changes
14
+
15
+ - 71cd58e: Explain that effect-completion field operations require an explicit workflow or stage target scope when validation rejects a missing scope, and document the requirement on `EffectHandler`.
16
+ - 9126299: **BREAKING:** Delete persisted stage guard documents when their stage exits, and remove the lifted-record `GUARD_LIFTED_PREDICATE` and `isGuardLifted` exports.
17
+
18
+ Guard deletion now uses the observed document revision so stale exit reconciliation cannot delete a guard reactivated by a newer stage visit. Reactive guard streams contain only active persisted guards.
19
+
20
+ - Updated dependencies [239d9f6]
21
+ - Updated dependencies [3af2ca0]
22
+ - Updated dependencies [7276702]
23
+ - Updated dependencies [58f8211]
24
+ - Updated dependencies [00c089d]
25
+ - Updated dependencies [71cd58e]
26
+ - Updated dependencies [cd03973]
27
+ - Updated dependencies [d26cc1a]
28
+ - Updated dependencies [9126299]
29
+ - Updated dependencies [72018af]
30
+ - Updated dependencies [5cd8ffd]
31
+ - Updated dependencies [b82bda4]
32
+ - Updated dependencies [55a54b3]
33
+ - Updated dependencies [b82bda4]
34
+ - Updated dependencies [4490442]
35
+ - Updated dependencies [1540dce]
36
+ - @sanity/workflow-engine@0.18.0
37
+
38
+ ## 0.12.0
39
+
40
+ ### Minor Changes
41
+
42
+ - 092a0d4: **BREAKING:** runtime-supplied refs written into field state are gated on the deployment's declared resource surface.
43
+ - A `doc.ref` / `doc.refs` / `release.ref` value a caller or effect handler supplies — start `initialFields` input, an action's param-sourced op values, `editField` values, effect-completion ops — must target a declared resource: the `workflowResource` itself or a resource the `resourceClients` resolver serves. An off-surface ref throws the new `RefResourceUndeclaredError` and the commit aborts, instead of writing cleanly and failing later when something dereferences it. The check is advisory like every engine check; stored documents are unchanged.
44
+ - Definition-authored refs are exempt: literal `initialValue` seeds, `type: 'query'` texts, spawn `with` projections, and op values that bind no action param were validated (and alias-expanded) at deploy, so they materialize into field state without re-checking.
45
+ - Resource aliases stay a deploy-time abstraction: `deployDefinitions({definitions, resourceAliases?})` expands every `@<alias>:` reference to its physical GDR before fingerprinting, and nothing alias-shaped survives past deploy — the runtime holds no alias map, and an alias-shaped ref supplied to a runtime verb is rejected as malformed like any other non-GDR string.
46
+ - `createBench` takes `serveResources` (dataset siblings served — and thereby declared — straight from the bench's own store) plus a raw `resourceClients` resolver for anything beyond same-store siblings. The engine also exports `sameResource` alongside the existing GDR helpers.
47
+ - The CLI wires no `resourceClients`, so refs supplied at runtime through it (`start --field`, `fire-action` ref params) admit the workflow resource only.
48
+
49
+ - 5a1a9fe: New `startedInstanceFixture(bench)` — a real, minimal started instance built through the bench's actual verbs, for consumers that need a persisted-shape-valid instance document without caring about its workflow (hand-crafted instance stubs do not pass the engine's read parse).
50
+ - 1321ba5: The bench exposes `evaluateStart` — the engine's pre-flight of `startInstance`'s gates (the `start.allowed` verdict with its insight, plus the still-missing required inputs), delegating to the verb with the bench clock so `$now` reads the frozen instant.
51
+
52
+ ### Patch Changes
53
+
54
+ - e683875: The bench `queryInScope` doc now describes the scope the engine actually binds: the caller-free rendered scope (every always-bound condition var, `$fields` with the open stage's overlay merged, `$assigned` at its caller-free `false`, and the author's pre-evaluated `$<predicate>` booleans) — the previous enumeration listed a `$subject` param that was never bound.
55
+ - Updated dependencies [f9389e5]
56
+ - Updated dependencies [d0c62ea]
57
+ - Updated dependencies [c3eed2e]
58
+ - Updated dependencies [df4bd80]
59
+ - Updated dependencies [092a0d4]
60
+ - Updated dependencies [5a1a9fe]
61
+ - Updated dependencies [e3a7ba2]
62
+ - Updated dependencies [30fed9e]
63
+ - Updated dependencies [1321ba5]
64
+ - Updated dependencies [e683875]
65
+ - Updated dependencies [a8ace4d]
66
+ - @sanity/workflow-engine@0.17.0
67
+
3
68
  ## 0.11.0
4
69
 
5
70
  ### Minor Changes
package/README.md CHANGED
@@ -18,7 +18,9 @@ npm install -D @sanity/workflow-engine-test
18
18
  `createBench()` builds an isolated bench: the real engine, a fake in-memory
19
19
  Sanity client, an all-access default actor, and a bench-owned clock. Every
20
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:
21
+ default, clock, and cross-resource resolver (`serveResources` sugar for
22
+ same-store dataset siblings, raw `resourceClients` for anything else), so a
23
+ test reads as the workflow it exercises:
22
24
 
23
25
  - **Runtime verbs** — `deployDefinitions`, `deleteDefinition`, `startInstance`,
24
26
  `fireAction`, `editField`, `completeEffect` (and `completePendingEffect`,
@@ -28,14 +30,14 @@ default, and clock, so a test reads as the workflow it exercises:
28
30
  not wall-clock.
29
31
  - **Reads and probes** — `getInstance`, `currentStage`, `activityStatus`,
30
32
  `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.
33
+ `instancesByStage`, `instancesForDocument`, `definitionsForDocument`,
34
+ `evaluateStart`, `query`, plus the guard helpers below. Helpers sharing an
35
+ engine verb's name take the engine's own args objects and throw its typed
36
+ errors — bench reads and engine reads cannot drift.
35
37
 
36
38
  ```ts
37
39
  const bench = createBench({now: '2026-01-01T00:00:00Z'})
38
- await bench.deployDefinitions({definitions: [reviewWorkflow]})
40
+ await bench.deployDefinitions({expectedMinReaderModel: 2, definitions: [reviewWorkflow]})
39
41
 
40
42
  const {instance} = await bench.startInstance({
41
43
  definition: 'review-workflow',
@@ -70,7 +72,7 @@ raw `workflow.*` calls, and the default tag ships as the `BENCH_TAG` constant.
70
72
  ## Mutation guards
71
73
 
72
74
  A workflow stage can deploy **mutation guards** — `temp.system.guard` documents
73
- that lock the documents it governs while the stage is active, and lift when it
75
+ that lock the documents it governs while the stage is active, and are deleted when it
74
76
  exits. The lake does not enforce this doc type yet: in production, guard
75
77
  enforcement is the engine's own **optimistic** evaluation (advisory verdicts
76
78
  and engine-side write denials), and the lake ACL is the only hard boundary.
package/dist/index.cjs CHANGED
@@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: !0
5
5
  });
6
6
 
7
- var workflowEngine = require("@sanity/workflow-engine"), clientFakeForTest = require("@sanity-labs/client-fake-for-test");
7
+ var workflowEngine = require("@sanity/workflow-engine"), clientFakeForTest = require("@sanity-labs/client-fake-for-test"), define = require("@sanity/workflow-engine/define");
8
8
 
9
9
  const DEFAULT_WORKFLOW_RESOURCE = {
10
10
  type: "dataset",
@@ -304,7 +304,7 @@ function createReadHelpers(scope) {
304
304
  ...engineScope,
305
305
  ...args
306
306
  }),
307
- instancesForSubject: async (subjectRef, entryName = "subject") => client.fetch(`*[_type == "${workflowEngine.WORKFLOW_INSTANCE_TYPE}"\n && fields[_type == "doc.ref" && name == $entry][0].value.id == $ref\n && ${workflowEngine.tagScopeFilter()}] | order(startedAt asc)`, {
307
+ instancesForSubject: async (subjectRef, entryName = "subject") => client.fetch(`*[_type == "${workflowEngine.WORKFLOW_INSTANCE_TYPE}"\n && fields[_type in ["doc.ref", "subject"] && name == $entry][0].value.id == $ref\n && ${workflowEngine.tagScopeFilter()}] | order(startedAt asc)`, {
308
308
  ref: subjectRef,
309
309
  entry: entryName,
310
310
  tag: tag
@@ -327,6 +327,13 @@ function createReadHelpers(scope) {
327
327
  clock: clock
328
328
  } : {}
329
329
  }),
330
+ evaluateStart: args => workflowEngine.workflow.evaluateStart({
331
+ ...engineScope,
332
+ ...args,
333
+ ...clock !== void 0 ? {
334
+ clock: clock
335
+ } : {}
336
+ }),
330
337
  snapshot: () => client.snapshot()
331
338
  };
332
339
  }
@@ -335,6 +342,9 @@ function createBenchEngine(bench, overrides = {}) {
335
342
  return workflowEngine.createEngine({
336
343
  client: bench.client,
337
344
  workflowResource: bench.workflowResource,
345
+ ...bench.resourceClients !== void 0 ? {
346
+ resourceClients: bench.resourceClients
347
+ } : {},
338
348
  tag: bench.tag,
339
349
  clock: bench.now,
340
350
  executionContext: bench.executionContext,
@@ -344,7 +354,7 @@ function createBenchEngine(bench, overrides = {}) {
344
354
 
345
355
  function subjectField(documentId, opts) {
346
356
  return {
347
- type: "doc.ref",
357
+ type: "subject",
348
358
  name: opts?.name ?? "subject",
349
359
  value: workflowEngine.gdrRef({
350
360
  res: DEFAULT_WORKFLOW_RESOURCE,
@@ -354,6 +364,18 @@ function subjectField(documentId, opts) {
354
364
  };
355
365
  }
356
366
 
367
+ function docRefField(documentId, opts) {
368
+ return {
369
+ type: "doc.ref",
370
+ name: opts.name,
371
+ value: workflowEngine.gdrRef({
372
+ res: DEFAULT_WORKFLOW_RESOURCE,
373
+ documentId: documentId,
374
+ type: opts.type ?? "document"
375
+ })
376
+ };
377
+ }
378
+
357
379
  function releaseField(releaseName, opts) {
358
380
  return {
359
381
  type: "release.ref",
@@ -365,6 +387,35 @@ function releaseField(releaseName, opts) {
365
387
  };
366
388
  }
367
389
 
390
+ async function startedInstanceFixture(bench, opts) {
391
+ const flow = define.defineWorkflow({
392
+ name: "fixture-flow",
393
+ title: "Fixture flow",
394
+ initialStage: "review",
395
+ stages: [ {
396
+ name: "review",
397
+ transitions: [ {
398
+ name: "go",
399
+ to: "done",
400
+ when: "false"
401
+ } ]
402
+ }, {
403
+ name: "done"
404
+ } ]
405
+ });
406
+ await bench.deployDefinitions({
407
+ expectedMinReaderModel: 2,
408
+ definitions: [ flow ]
409
+ });
410
+ const {instance: instance} = await bench.startInstance({
411
+ definition: flow.name,
412
+ ...opts?.instanceId !== void 0 ? {
413
+ instanceId: opts.instanceId
414
+ } : {}
415
+ });
416
+ return instance;
417
+ }
418
+
368
419
  function createBench(options = {}) {
369
420
  const tag = options.tag ?? BENCH_TAG;
370
421
  workflowEngine.validateTag(tag);
@@ -376,10 +427,16 @@ function createBench(options = {}) {
376
427
  documents: options.documents
377
428
  } : {},
378
429
  defaultAccess: access
379
- }), identities = createIdentityDirectory(client), scope = {
430
+ }), identities = createIdentityDirectory(client), resourceClients = benchResourceClients({
431
+ client: client,
432
+ options: options
433
+ }), scope = {
380
434
  client: client,
381
435
  tag: tag,
382
436
  workflowResource: workflowResource,
437
+ ...resourceClients !== void 0 ? {
438
+ resourceClients: resourceClients
439
+ } : {},
383
440
  executionContext: options.executionContext ?? {
384
441
  kind: workflowEngine.EXECUTION_KINDS.test
385
442
  }
@@ -395,10 +452,7 @@ function createBench(options = {}) {
395
452
  clock: clock
396
453
  });
397
454
  return {
398
- client: client,
399
- tag: tag,
400
- workflowResource: workflowResource,
401
- executionContext: scope.executionContext,
455
+ ...scope,
402
456
  access: access,
403
457
  currentUser: access.actor,
404
458
  grants: access.grants ?? [],
@@ -433,6 +487,17 @@ function createBench(options = {}) {
433
487
  };
434
488
  }
435
489
 
490
+ function benchResourceClients(args) {
491
+ const {client: client} = args, {serveResources: serveResources, resourceClients: resourceClients} = args.options;
492
+ if (serveResources === void 0 || serveResources.length === 0) return resourceClients;
493
+ const nonDataset = serveResources.find(served => served.type !== "dataset");
494
+ if (nonDataset !== void 0) throw new Error(`createBench: serveResources can only serve dataset siblings of the bench store — got ${nonDataset.type} "${nonDataset.id}". Pass resourceClients for other resource types.`);
495
+ return parsed => {
496
+ const target = workflowEngine.resourceFromParsed(parsed);
497
+ return serveResources.some(served => workflowEngine.sameResource(served, target)) ? client.withConfig(workflowEngine.datasetResourceParts(target.id)) : resourceClients?.(parsed);
498
+ };
499
+ }
500
+
436
501
  function assertIsoInstant(iso) {
437
502
  const ms = Date.parse(iso);
438
503
  if (Number.isNaN(ms)) throw new Error(`bench clock: expected an ISO-8601 instant, got ${JSON.stringify(iso)}`);
@@ -453,6 +518,10 @@ exports.createBench = createBench;
453
518
 
454
519
  exports.createBenchEngine = createBenchEngine;
455
520
 
521
+ exports.docRefField = docRefField;
522
+
456
523
  exports.releaseField = releaseField;
457
524
 
525
+ exports.startedInstanceFixture = startedInstanceFixture;
526
+
458
527
  exports.subjectField = subjectField;
package/dist/index.d.cts CHANGED
@@ -15,6 +15,7 @@ import { DeployedDefinition } from "@sanity/workflow-engine";
15
15
  import type { EditFieldArgs } from "@sanity/workflow-engine";
16
16
  import { Engine } from "@sanity/workflow-engine";
17
17
  import type { EvaluateArgs } from "@sanity/workflow-engine";
18
+ import { EvaluateStartArgs } from "@sanity/workflow-engine";
18
19
  import { FindPendingEffectsArgs } from "@sanity/workflow-engine";
19
20
  import type { FireActionArgs } from "@sanity/workflow-engine";
20
21
  import type { Grant } from "@sanity/workflow-engine";
@@ -27,10 +28,13 @@ import { MutationGuardDoc } from "@sanity/workflow-engine";
27
28
  import type { OperationArgs } from "@sanity/workflow-engine";
28
29
  import { OperationResult } from "@sanity/workflow-engine";
29
30
  import { PendingEffect } from "@sanity/workflow-engine";
31
+ import type { ResourceClientResolver } from "@sanity/workflow-engine";
30
32
  import type { SetStageArgs } from "@sanity/workflow-engine";
33
+ import { StartEvaluation } from "@sanity/workflow-engine";
31
34
  import type { StartInstanceArgs } from "@sanity/workflow-engine";
32
35
  import type { TestClient } from "@sanity-labs/client-fake-for-test";
33
36
  import type { WorkflowAccess } from "@sanity/workflow-engine";
37
+ import { WorkflowDefinition } from "@sanity/workflow-engine";
34
38
  import { WorkflowDefinitionInput } from "@sanity/workflow-engine";
35
39
  import { WorkflowEvaluation } from "@sanity/workflow-engine";
36
40
  import { WorkflowInstance } from "@sanity/workflow-engine";
@@ -110,6 +114,9 @@ declare interface BenchState {
110
114
  tag: string;
111
115
  /** The workflow resource every wrapped call targets. */
112
116
  workflowResource: WorkflowResource;
117
+ /** The cross-resource read routing every wrapped call carries, when the
118
+ * bench declared any — see {@link CreateBenchOptions.resourceClients}. */
119
+ resourceClients?: ResourceClientResolver;
113
120
  /** The declared execution context stamped on every wrapped call's history
114
121
  * entries (default `{kind: 'test'}`) — inherited by {@link createBenchEngine}. */
115
122
  executionContext: DeclaredExecutionContext;
@@ -234,6 +241,23 @@ export declare interface CreateBenchOptions {
234
241
  * URIs read as `dataset:test:test:<docId>`.
235
242
  */
236
243
  workflowResource?: WorkflowResource;
244
+ /**
245
+ * Sibling datasets the bench serves (and thereby declares) from its own
246
+ * store — the common cross-resource case. Each listed resource routes to
247
+ * a `withConfig`-derived handle over the bench's store, so reads reach it
248
+ * and runtime-supplied refs into it are admitted. Only `dataset`
249
+ * resources can be siblings of the bench's store; anything else needs
250
+ * {@link CreateBenchOptions.resourceClients}.
251
+ */
252
+ serveResources?: WorkflowResource[];
253
+ /**
254
+ * Cross-resource read routing for the bench engine scope — a served
255
+ * resource is also part of the declared surface for written refs. The
256
+ * escape hatch for resolver logic beyond same-store siblings; prefer
257
+ * {@link CreateBenchOptions.serveResources}, which is consulted first
258
+ * when both are given. Omit for single-resource benches.
259
+ */
260
+ resourceClients?: ResourceClientResolver;
237
261
  /**
238
262
  * Freeze the bench clock at this ISO instant from construction, so
239
263
  * `$now` and every engine timestamp are deterministic. Equivalent to
@@ -252,6 +276,19 @@ export declare interface CreateBenchOptions {
252
276
  /** Default workflow resource for benches that don't pass one. */
253
277
  export declare const DEFAULT_WORKFLOW_RESOURCE: WorkflowResource;
254
278
 
279
+ /**
280
+ * A plain `doc.ref` entry value in the bench's default resource — for
281
+ * document-reference inputs that are NOT the workflow's subject (the
282
+ * `subject` kind is what marks THE subject; see {@link subjectField}).
283
+ */
284
+ export declare function docRefField(
285
+ documentId: string,
286
+ opts: {
287
+ name: string;
288
+ type?: string;
289
+ },
290
+ ): InitialFieldValue;
291
+
255
292
  /**
256
293
  * The write {@link GuardHelpers.editDocument} performs. Every arm is a real
257
294
  * distinct client write the guard seam vets under that action — `create` is
@@ -281,6 +318,7 @@ declare type EngineScope = {
281
318
  client: TestClient;
282
319
  tag: string;
283
320
  workflowResource: WorkflowResource;
321
+ resourceClients?: ResourceClientResolver;
284
322
  executionContext: DeclaredExecutionContext;
285
323
  };
286
324
 
@@ -306,13 +344,28 @@ declare type EngineWrappers = {
306
344
  guardsForDefinition: (definition: string) => Promise<MutationGuardDoc[]>;
307
345
  };
308
346
 
347
+ /** The slice of a bench {@link startedInstanceFixture} drives — structural,
348
+ * so the fixture module needs no import of the bench itself. */
349
+ declare interface FixtureBench {
350
+ deployDefinitions: (args: {
351
+ expectedMinReaderModel: 2;
352
+ definitions: WorkflowDefinition[];
353
+ }) => Promise<unknown>;
354
+ startInstance: (args: {
355
+ definition: string;
356
+ instanceId?: string;
357
+ }) => Promise<{
358
+ instance: WorkflowInstance;
359
+ }>;
360
+ }
361
+
309
362
  declare type GuardHelpers = {
310
363
  /** All deployed guard docs ({@link MutationGuardDoc}) in the store. */
311
364
  listGuards: () => Promise<MutationGuardDoc[]>;
312
365
  /**
313
366
  * Guards that would deny an arbitrary edit to `documentId` right now (i.e.
314
- * active locks). Probes by changing every non-system field; a lifted guard
315
- * (predicate `"true"`) returns nothing.
367
+ * active locks). Probes by changing every non-system field; a guard whose
368
+ * predicate is `"true"` returns nothing.
316
369
  */
317
370
  activeGuardsForDocument: (
318
371
  documentId: string,
@@ -358,11 +411,13 @@ declare type QueryHelpers = {
358
411
  ) => Promise<T>;
359
412
  /**
360
413
  * Snapshot-aware GROQ for `instanceId` — runs against the same
361
- * in-memory view the engine's filters see. Reserved params (`$self`,
362
- * `$subject`, `$parent`, `$ancestors`, `$now`, `$stage`) auto-bound
363
- * in GDR URI form. Use to ask "what does the engine see for this
364
- * workflow?" useful for filter debugging, UIs that mirror engine
365
- * state, etc.
414
+ * in-memory view the engine's filters see, with the caller-free
415
+ * rendered scope auto-bound: every always-bound condition var,
416
+ * `$fields` with the open stage's overlay merged, `$assigned` at its
417
+ * caller-free `false`, and the author's pre-evaluated `$<predicate>`
418
+ * booleans, ids in GDR URI form. Use to ask "what does the engine see
419
+ * for this workflow?" — useful for filter debugging, UIs that mirror
420
+ * engine state, etc.
366
421
  */
367
422
  queryInScope: <T = unknown>({
368
423
  instanceId,
@@ -406,10 +461,10 @@ declare type ReadHelpers = {
406
461
  */
407
462
  children: (args: ChildrenArgs) => Promise<WorkflowInstance[]>;
408
463
  /**
409
- * All workflow instances whose `doc.ref` field entry named
410
- * `entryName` (default `"subject"`) matches the given doc id. Useful
411
- * for "what workflow is running on this document?" questions. Pass
412
- * `entryName` for workflows whose subject entry uses a different name.
464
+ * All workflow instances whose single-ref field entry (`subject` kind or
465
+ * `doc.ref`) named `entryName` (default `"subject"`) matches the given doc
466
+ * id. Useful for "what workflow is running on this document?" questions.
467
+ * Pass `entryName` for workflows whose subject entry uses a different name.
413
468
  */
414
469
  instancesForSubject: (
415
470
  subjectRef: string,
@@ -437,12 +492,20 @@ declare type ReadHelpers = {
437
492
  /**
438
493
  * The startable half of {@link ReadHelpers.instancesForDocument}: the
439
494
  * 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.
495
+ * candidate document — startable, the `subject`-kind entry accepts its
496
+ * `_type`, `start.filter` (browse-time-pure) passes. All matches, name
497
+ * ascending.
442
498
  */
443
499
  definitionsForDocument: (
444
500
  args: DefinitionsForDocumentArgs,
445
501
  ) => Promise<DeployedDefinition[]>;
502
+ /**
503
+ * Pre-flight `startInstance`'s gates for a definition + candidate
504
+ * `initialFields`: the `start.allowed` verdict (with its insight) plus the
505
+ * still-missing required inputs — the read a start surface renders before
506
+ * committing.
507
+ */
508
+ evaluateStart: (args: EvaluateStartArgs) => Promise<StartEvaluation>;
446
509
  /** Snapshot of all documents in the store — for inspecting the world. */
447
510
  snapshot: () => readonly StoreDocument[];
448
511
  };
@@ -459,6 +522,20 @@ export declare function releaseField(
459
522
  },
460
523
  ): InitialFieldValue;
461
524
 
525
+ /**
526
+ * A REAL, minimal started instance — deployed and started through the bench's
527
+ * actual verbs, parked forever on its first stage. The fixture for consumers
528
+ * (adapter tests, validation tests) that need a persisted-shape-valid
529
+ * instance document without caring about its workflow: hand-crafted instance
530
+ * stubs do not pass the engine's read parse.
531
+ */
532
+ export declare function startedInstanceFixture(
533
+ bench: FixtureBench,
534
+ opts?: {
535
+ instanceId?: string;
536
+ },
537
+ ): Promise<WorkflowInstance>;
538
+
462
539
  /** A document as stored in the bench's fake lake. */
463
540
  declare type StoreDocument = {
464
541
  _id: string;
@@ -467,9 +544,12 @@ declare type StoreDocument = {
467
544
  };
468
545
 
469
546
  /**
470
- * The conventional workflow-scope `subject` doc.ref entry, keyed by a bare
471
- * doc id in the bench's default resource. `type` defaults to `'document'`;
472
- * pass the subject's schema type when a filter or projection reads it.
547
+ * A workflow-scope `subject`-KIND entry value, keyed by a bare doc id in the
548
+ * bench's default resource what a definition declaring the first-class
549
+ * `subject` kind expects at start (applicability and document pickers key on
550
+ * that kind). `type` defaults to `'document'`; pass the subject's schema
551
+ * type when a filter or projection reads it. For a plain `doc.ref` input,
552
+ * use {@link docRefField}.
473
553
  */
474
554
  export declare function subjectField(
475
555
  documentId: string,
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ import { DeployedDefinition } from "@sanity/workflow-engine";
15
15
  import type { EditFieldArgs } from "@sanity/workflow-engine";
16
16
  import { Engine } from "@sanity/workflow-engine";
17
17
  import type { EvaluateArgs } from "@sanity/workflow-engine";
18
+ import { EvaluateStartArgs } from "@sanity/workflow-engine";
18
19
  import { FindPendingEffectsArgs } from "@sanity/workflow-engine";
19
20
  import type { FireActionArgs } from "@sanity/workflow-engine";
20
21
  import type { Grant } from "@sanity/workflow-engine";
@@ -27,10 +28,13 @@ import { MutationGuardDoc } from "@sanity/workflow-engine";
27
28
  import type { OperationArgs } from "@sanity/workflow-engine";
28
29
  import { OperationResult } from "@sanity/workflow-engine";
29
30
  import { PendingEffect } from "@sanity/workflow-engine";
31
+ import type { ResourceClientResolver } from "@sanity/workflow-engine";
30
32
  import type { SetStageArgs } from "@sanity/workflow-engine";
33
+ import { StartEvaluation } from "@sanity/workflow-engine";
31
34
  import type { StartInstanceArgs } from "@sanity/workflow-engine";
32
35
  import type { TestClient } from "@sanity-labs/client-fake-for-test";
33
36
  import type { WorkflowAccess } from "@sanity/workflow-engine";
37
+ import { WorkflowDefinition } from "@sanity/workflow-engine";
34
38
  import { WorkflowDefinitionInput } from "@sanity/workflow-engine";
35
39
  import { WorkflowEvaluation } from "@sanity/workflow-engine";
36
40
  import { WorkflowInstance } from "@sanity/workflow-engine";
@@ -110,6 +114,9 @@ declare interface BenchState {
110
114
  tag: string;
111
115
  /** The workflow resource every wrapped call targets. */
112
116
  workflowResource: WorkflowResource;
117
+ /** The cross-resource read routing every wrapped call carries, when the
118
+ * bench declared any — see {@link CreateBenchOptions.resourceClients}. */
119
+ resourceClients?: ResourceClientResolver;
113
120
  /** The declared execution context stamped on every wrapped call's history
114
121
  * entries (default `{kind: 'test'}`) — inherited by {@link createBenchEngine}. */
115
122
  executionContext: DeclaredExecutionContext;
@@ -234,6 +241,23 @@ export declare interface CreateBenchOptions {
234
241
  * URIs read as `dataset:test:test:<docId>`.
235
242
  */
236
243
  workflowResource?: WorkflowResource;
244
+ /**
245
+ * Sibling datasets the bench serves (and thereby declares) from its own
246
+ * store — the common cross-resource case. Each listed resource routes to
247
+ * a `withConfig`-derived handle over the bench's store, so reads reach it
248
+ * and runtime-supplied refs into it are admitted. Only `dataset`
249
+ * resources can be siblings of the bench's store; anything else needs
250
+ * {@link CreateBenchOptions.resourceClients}.
251
+ */
252
+ serveResources?: WorkflowResource[];
253
+ /**
254
+ * Cross-resource read routing for the bench engine scope — a served
255
+ * resource is also part of the declared surface for written refs. The
256
+ * escape hatch for resolver logic beyond same-store siblings; prefer
257
+ * {@link CreateBenchOptions.serveResources}, which is consulted first
258
+ * when both are given. Omit for single-resource benches.
259
+ */
260
+ resourceClients?: ResourceClientResolver;
237
261
  /**
238
262
  * Freeze the bench clock at this ISO instant from construction, so
239
263
  * `$now` and every engine timestamp are deterministic. Equivalent to
@@ -252,6 +276,19 @@ export declare interface CreateBenchOptions {
252
276
  /** Default workflow resource for benches that don't pass one. */
253
277
  export declare const DEFAULT_WORKFLOW_RESOURCE: WorkflowResource;
254
278
 
279
+ /**
280
+ * A plain `doc.ref` entry value in the bench's default resource — for
281
+ * document-reference inputs that are NOT the workflow's subject (the
282
+ * `subject` kind is what marks THE subject; see {@link subjectField}).
283
+ */
284
+ export declare function docRefField(
285
+ documentId: string,
286
+ opts: {
287
+ name: string;
288
+ type?: string;
289
+ },
290
+ ): InitialFieldValue;
291
+
255
292
  /**
256
293
  * The write {@link GuardHelpers.editDocument} performs. Every arm is a real
257
294
  * distinct client write the guard seam vets under that action — `create` is
@@ -281,6 +318,7 @@ declare type EngineScope = {
281
318
  client: TestClient;
282
319
  tag: string;
283
320
  workflowResource: WorkflowResource;
321
+ resourceClients?: ResourceClientResolver;
284
322
  executionContext: DeclaredExecutionContext;
285
323
  };
286
324
 
@@ -306,13 +344,28 @@ declare type EngineWrappers = {
306
344
  guardsForDefinition: (definition: string) => Promise<MutationGuardDoc[]>;
307
345
  };
308
346
 
347
+ /** The slice of a bench {@link startedInstanceFixture} drives — structural,
348
+ * so the fixture module needs no import of the bench itself. */
349
+ declare interface FixtureBench {
350
+ deployDefinitions: (args: {
351
+ expectedMinReaderModel: 2;
352
+ definitions: WorkflowDefinition[];
353
+ }) => Promise<unknown>;
354
+ startInstance: (args: {
355
+ definition: string;
356
+ instanceId?: string;
357
+ }) => Promise<{
358
+ instance: WorkflowInstance;
359
+ }>;
360
+ }
361
+
309
362
  declare type GuardHelpers = {
310
363
  /** All deployed guard docs ({@link MutationGuardDoc}) in the store. */
311
364
  listGuards: () => Promise<MutationGuardDoc[]>;
312
365
  /**
313
366
  * Guards that would deny an arbitrary edit to `documentId` right now (i.e.
314
- * active locks). Probes by changing every non-system field; a lifted guard
315
- * (predicate `"true"`) returns nothing.
367
+ * active locks). Probes by changing every non-system field; a guard whose
368
+ * predicate is `"true"` returns nothing.
316
369
  */
317
370
  activeGuardsForDocument: (
318
371
  documentId: string,
@@ -358,11 +411,13 @@ declare type QueryHelpers = {
358
411
  ) => Promise<T>;
359
412
  /**
360
413
  * Snapshot-aware GROQ for `instanceId` — runs against the same
361
- * in-memory view the engine's filters see. Reserved params (`$self`,
362
- * `$subject`, `$parent`, `$ancestors`, `$now`, `$stage`) auto-bound
363
- * in GDR URI form. Use to ask "what does the engine see for this
364
- * workflow?" useful for filter debugging, UIs that mirror engine
365
- * state, etc.
414
+ * in-memory view the engine's filters see, with the caller-free
415
+ * rendered scope auto-bound: every always-bound condition var,
416
+ * `$fields` with the open stage's overlay merged, `$assigned` at its
417
+ * caller-free `false`, and the author's pre-evaluated `$<predicate>`
418
+ * booleans, ids in GDR URI form. Use to ask "what does the engine see
419
+ * for this workflow?" — useful for filter debugging, UIs that mirror
420
+ * engine state, etc.
366
421
  */
367
422
  queryInScope: <T = unknown>({
368
423
  instanceId,
@@ -406,10 +461,10 @@ declare type ReadHelpers = {
406
461
  */
407
462
  children: (args: ChildrenArgs) => Promise<WorkflowInstance[]>;
408
463
  /**
409
- * All workflow instances whose `doc.ref` field entry named
410
- * `entryName` (default `"subject"`) matches the given doc id. Useful
411
- * for "what workflow is running on this document?" questions. Pass
412
- * `entryName` for workflows whose subject entry uses a different name.
464
+ * All workflow instances whose single-ref field entry (`subject` kind or
465
+ * `doc.ref`) named `entryName` (default `"subject"`) matches the given doc
466
+ * id. Useful for "what workflow is running on this document?" questions.
467
+ * Pass `entryName` for workflows whose subject entry uses a different name.
413
468
  */
414
469
  instancesForSubject: (
415
470
  subjectRef: string,
@@ -437,12 +492,20 @@ declare type ReadHelpers = {
437
492
  /**
438
493
  * The startable half of {@link ReadHelpers.instancesForDocument}: the
439
494
  * 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.
495
+ * candidate document — startable, the `subject`-kind entry accepts its
496
+ * `_type`, `start.filter` (browse-time-pure) passes. All matches, name
497
+ * ascending.
442
498
  */
443
499
  definitionsForDocument: (
444
500
  args: DefinitionsForDocumentArgs,
445
501
  ) => Promise<DeployedDefinition[]>;
502
+ /**
503
+ * Pre-flight `startInstance`'s gates for a definition + candidate
504
+ * `initialFields`: the `start.allowed` verdict (with its insight) plus the
505
+ * still-missing required inputs — the read a start surface renders before
506
+ * committing.
507
+ */
508
+ evaluateStart: (args: EvaluateStartArgs) => Promise<StartEvaluation>;
446
509
  /** Snapshot of all documents in the store — for inspecting the world. */
447
510
  snapshot: () => readonly StoreDocument[];
448
511
  };
@@ -459,6 +522,20 @@ export declare function releaseField(
459
522
  },
460
523
  ): InitialFieldValue;
461
524
 
525
+ /**
526
+ * A REAL, minimal started instance — deployed and started through the bench's
527
+ * actual verbs, parked forever on its first stage. The fixture for consumers
528
+ * (adapter tests, validation tests) that need a persisted-shape-valid
529
+ * instance document without caring about its workflow: hand-crafted instance
530
+ * stubs do not pass the engine's read parse.
531
+ */
532
+ export declare function startedInstanceFixture(
533
+ bench: FixtureBench,
534
+ opts?: {
535
+ instanceId?: string;
536
+ },
537
+ ): Promise<WorkflowInstance>;
538
+
462
539
  /** A document as stored in the bench's fake lake. */
463
540
  declare type StoreDocument = {
464
541
  _id: string;
@@ -467,9 +544,12 @@ declare type StoreDocument = {
467
544
  };
468
545
 
469
546
  /**
470
- * The conventional workflow-scope `subject` doc.ref entry, keyed by a bare
471
- * doc id in the bench's default resource. `type` defaults to `'document'`;
472
- * pass the subject's schema type when a filter or projection reads it.
547
+ * A workflow-scope `subject`-KIND entry value, keyed by a bare doc id in the
548
+ * bench's default resource what a definition declaring the first-class
549
+ * `subject` kind expects at start (applicability and document pickers key on
550
+ * that kind). `type` defaults to `'document'`; pass the subject's schema
551
+ * type when a filter or projection reads it. For a plain `doc.ref` input,
552
+ * use {@link docRefField}.
473
553
  */
474
554
  export declare function subjectField(
475
555
  documentId: string,
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
- import { guardsForResource, denyingGuards, MutationGuardDeniedError, workflow, instancesQuery, WORKFLOW_INSTANCE_TYPE, tagScopeFilter, effectOutputsMap, contextMap, createEngine, releaseRef, gdrRef, validateTag, EXECUTION_KINDS } from "@sanity/workflow-engine";
1
+ import { guardsForResource, denyingGuards, MutationGuardDeniedError, workflow, instancesQuery, WORKFLOW_INSTANCE_TYPE, tagScopeFilter, effectOutputsMap, contextMap, createEngine, gdrRef, releaseRef, validateTag, EXECUTION_KINDS, resourceFromParsed, sameResource, datasetResourceParts } from "@sanity/workflow-engine";
2
2
 
3
3
  import { createTestClient, defaultAclPath, isDraftId, isVersionId } from "@sanity-labs/client-fake-for-test";
4
4
 
5
+ import { defineWorkflow } from "@sanity/workflow-engine/define";
6
+
5
7
  const DEFAULT_WORKFLOW_RESOURCE = {
6
8
  type: "dataset",
7
9
  id: "test.test"
@@ -300,7 +302,7 @@ function createReadHelpers(scope) {
300
302
  ...engineScope,
301
303
  ...args
302
304
  }),
303
- instancesForSubject: async (subjectRef, entryName = "subject") => client.fetch(`*[_type == "${WORKFLOW_INSTANCE_TYPE}"\n && fields[_type == "doc.ref" && name == $entry][0].value.id == $ref\n && ${tagScopeFilter()}] | order(startedAt asc)`, {
305
+ instancesForSubject: async (subjectRef, entryName = "subject") => client.fetch(`*[_type == "${WORKFLOW_INSTANCE_TYPE}"\n && fields[_type in ["doc.ref", "subject"] && name == $entry][0].value.id == $ref\n && ${tagScopeFilter()}] | order(startedAt asc)`, {
304
306
  ref: subjectRef,
305
307
  entry: entryName,
306
308
  tag: tag
@@ -323,6 +325,13 @@ function createReadHelpers(scope) {
323
325
  clock: clock
324
326
  } : {}
325
327
  }),
328
+ evaluateStart: args => workflow.evaluateStart({
329
+ ...engineScope,
330
+ ...args,
331
+ ...clock !== void 0 ? {
332
+ clock: clock
333
+ } : {}
334
+ }),
326
335
  snapshot: () => client.snapshot()
327
336
  };
328
337
  }
@@ -331,6 +340,9 @@ function createBenchEngine(bench, overrides = {}) {
331
340
  return createEngine({
332
341
  client: bench.client,
333
342
  workflowResource: bench.workflowResource,
343
+ ...bench.resourceClients !== void 0 ? {
344
+ resourceClients: bench.resourceClients
345
+ } : {},
334
346
  tag: bench.tag,
335
347
  clock: bench.now,
336
348
  executionContext: bench.executionContext,
@@ -340,7 +352,7 @@ function createBenchEngine(bench, overrides = {}) {
340
352
 
341
353
  function subjectField(documentId, opts) {
342
354
  return {
343
- type: "doc.ref",
355
+ type: "subject",
344
356
  name: opts?.name ?? "subject",
345
357
  value: gdrRef({
346
358
  res: DEFAULT_WORKFLOW_RESOURCE,
@@ -350,6 +362,18 @@ function subjectField(documentId, opts) {
350
362
  };
351
363
  }
352
364
 
365
+ function docRefField(documentId, opts) {
366
+ return {
367
+ type: "doc.ref",
368
+ name: opts.name,
369
+ value: gdrRef({
370
+ res: DEFAULT_WORKFLOW_RESOURCE,
371
+ documentId: documentId,
372
+ type: opts.type ?? "document"
373
+ })
374
+ };
375
+ }
376
+
353
377
  function releaseField(releaseName, opts) {
354
378
  return {
355
379
  type: "release.ref",
@@ -361,6 +385,35 @@ function releaseField(releaseName, opts) {
361
385
  };
362
386
  }
363
387
 
388
+ async function startedInstanceFixture(bench, opts) {
389
+ const flow = defineWorkflow({
390
+ name: "fixture-flow",
391
+ title: "Fixture flow",
392
+ initialStage: "review",
393
+ stages: [ {
394
+ name: "review",
395
+ transitions: [ {
396
+ name: "go",
397
+ to: "done",
398
+ when: "false"
399
+ } ]
400
+ }, {
401
+ name: "done"
402
+ } ]
403
+ });
404
+ await bench.deployDefinitions({
405
+ expectedMinReaderModel: 2,
406
+ definitions: [ flow ]
407
+ });
408
+ const {instance: instance} = await bench.startInstance({
409
+ definition: flow.name,
410
+ ...opts?.instanceId !== void 0 ? {
411
+ instanceId: opts.instanceId
412
+ } : {}
413
+ });
414
+ return instance;
415
+ }
416
+
364
417
  function createBench(options = {}) {
365
418
  const tag = options.tag ?? BENCH_TAG;
366
419
  validateTag(tag);
@@ -372,10 +425,16 @@ function createBench(options = {}) {
372
425
  documents: options.documents
373
426
  } : {},
374
427
  defaultAccess: access
375
- }), identities = createIdentityDirectory(client), scope = {
428
+ }), identities = createIdentityDirectory(client), resourceClients = benchResourceClients({
429
+ client: client,
430
+ options: options
431
+ }), scope = {
376
432
  client: client,
377
433
  tag: tag,
378
434
  workflowResource: workflowResource,
435
+ ...resourceClients !== void 0 ? {
436
+ resourceClients: resourceClients
437
+ } : {},
379
438
  executionContext: options.executionContext ?? {
380
439
  kind: EXECUTION_KINDS.test
381
440
  }
@@ -391,10 +450,7 @@ function createBench(options = {}) {
391
450
  clock: clock
392
451
  });
393
452
  return {
394
- client: client,
395
- tag: tag,
396
- workflowResource: workflowResource,
397
- executionContext: scope.executionContext,
453
+ ...scope,
398
454
  access: access,
399
455
  currentUser: access.actor,
400
456
  grants: access.grants ?? [],
@@ -429,10 +485,21 @@ function createBench(options = {}) {
429
485
  };
430
486
  }
431
487
 
488
+ function benchResourceClients(args) {
489
+ const {client: client} = args, {serveResources: serveResources, resourceClients: resourceClients} = args.options;
490
+ if (serveResources === void 0 || serveResources.length === 0) return resourceClients;
491
+ const nonDataset = serveResources.find(served => served.type !== "dataset");
492
+ if (nonDataset !== void 0) throw new Error(`createBench: serveResources can only serve dataset siblings of the bench store — got ${nonDataset.type} "${nonDataset.id}". Pass resourceClients for other resource types.`);
493
+ return parsed => {
494
+ const target = resourceFromParsed(parsed);
495
+ return serveResources.some(served => sameResource(served, target)) ? client.withConfig(datasetResourceParts(target.id)) : resourceClients?.(parsed);
496
+ };
497
+ }
498
+
432
499
  function assertIsoInstant(iso) {
433
500
  const ms = Date.parse(iso);
434
501
  if (Number.isNaN(ms)) throw new Error(`bench clock: expected an ISO-8601 instant, got ${JSON.stringify(iso)}`);
435
502
  return new Date(ms).toISOString();
436
503
  }
437
504
 
438
- export { ALL_ACCESS, ALL_ACCESS_USER, BENCH_TAG, DEFAULT_WORKFLOW_RESOURCE, WILDCARD_GRANTS, createBench, createBenchEngine, releaseField, subjectField };
505
+ export { ALL_ACCESS, ALL_ACCESS_USER, BENCH_TAG, DEFAULT_WORKFLOW_RESOURCE, WILDCARD_GRANTS, createBench, createBenchEngine, docRefField, releaseField, startedInstanceFixture, subjectField };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/workflow-engine-test",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "In-memory test bench (createBench) for @sanity/workflow-engine.",
5
5
  "keywords": [
6
6
  "sanity",
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@sanity-labs/client-fake-for-test": "^0.8.0",
46
- "@sanity/workflow-engine": "0.16.0"
46
+ "@sanity/workflow-engine": "0.18.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@sanity/client": "^7.22.1",