@sanity/workflow-engine-test 0.11.0 → 0.12.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,35 @@
1
1
  # @sanity/workflow-engine-test
2
2
 
3
+ ## 0.12.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 092a0d4: **BREAKING:** runtime-supplied refs written into field state are gated on the deployment's declared resource surface.
8
+ - 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.
9
+ - 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.
10
+ - 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.
11
+ - `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.
12
+ - The CLI wires no `resourceClients`, so refs supplied at runtime through it (`start --field`, `fire-action` ref params) admit the workflow resource only.
13
+
14
+ - 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).
15
+ - 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.
16
+
17
+ ### Patch Changes
18
+
19
+ - 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.
20
+ - Updated dependencies [f9389e5]
21
+ - Updated dependencies [d0c62ea]
22
+ - Updated dependencies [c3eed2e]
23
+ - Updated dependencies [df4bd80]
24
+ - Updated dependencies [092a0d4]
25
+ - Updated dependencies [5a1a9fe]
26
+ - Updated dependencies [e3a7ba2]
27
+ - Updated dependencies [30fed9e]
28
+ - Updated dependencies [1321ba5]
29
+ - Updated dependencies [e683875]
30
+ - Updated dependencies [a8ace4d]
31
+ - @sanity/workflow-engine@0.17.0
32
+
3
33
  ## 0.11.0
4
34
 
5
35
  ### 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,10 +30,10 @@ 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'})
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",
@@ -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,
@@ -365,6 +375,34 @@ function releaseField(releaseName, opts) {
365
375
  };
366
376
  }
367
377
 
378
+ async function startedInstanceFixture(bench, opts) {
379
+ const flow = define.defineWorkflow({
380
+ name: "fixture-flow",
381
+ title: "Fixture flow",
382
+ initialStage: "review",
383
+ stages: [ {
384
+ name: "review",
385
+ transitions: [ {
386
+ name: "go",
387
+ to: "done",
388
+ when: "false"
389
+ } ]
390
+ }, {
391
+ name: "done"
392
+ } ]
393
+ });
394
+ await bench.deployDefinitions({
395
+ definitions: [ flow ]
396
+ });
397
+ const {instance: instance} = await bench.startInstance({
398
+ definition: flow.name,
399
+ ...opts?.instanceId !== void 0 ? {
400
+ instanceId: opts.instanceId
401
+ } : {}
402
+ });
403
+ return instance;
404
+ }
405
+
368
406
  function createBench(options = {}) {
369
407
  const tag = options.tag ?? BENCH_TAG;
370
408
  workflowEngine.validateTag(tag);
@@ -376,10 +414,16 @@ function createBench(options = {}) {
376
414
  documents: options.documents
377
415
  } : {},
378
416
  defaultAccess: access
379
- }), identities = createIdentityDirectory(client), scope = {
417
+ }), identities = createIdentityDirectory(client), resourceClients = benchResourceClients({
418
+ client: client,
419
+ options: options
420
+ }), scope = {
380
421
  client: client,
381
422
  tag: tag,
382
423
  workflowResource: workflowResource,
424
+ ...resourceClients !== void 0 ? {
425
+ resourceClients: resourceClients
426
+ } : {},
383
427
  executionContext: options.executionContext ?? {
384
428
  kind: workflowEngine.EXECUTION_KINDS.test
385
429
  }
@@ -395,10 +439,7 @@ function createBench(options = {}) {
395
439
  clock: clock
396
440
  });
397
441
  return {
398
- client: client,
399
- tag: tag,
400
- workflowResource: workflowResource,
401
- executionContext: scope.executionContext,
442
+ ...scope,
402
443
  access: access,
403
444
  currentUser: access.actor,
404
445
  grants: access.grants ?? [],
@@ -433,6 +474,17 @@ function createBench(options = {}) {
433
474
  };
434
475
  }
435
476
 
477
+ function benchResourceClients(args) {
478
+ const {client: client} = args, {serveResources: serveResources, resourceClients: resourceClients} = args.options;
479
+ if (serveResources === void 0 || serveResources.length === 0) return resourceClients;
480
+ const nonDataset = serveResources.find(served => served.type !== "dataset");
481
+ 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.`);
482
+ return parsed => {
483
+ const target = workflowEngine.resourceFromParsed(parsed);
484
+ return serveResources.some(served => workflowEngine.sameResource(served, target)) ? client.withConfig(workflowEngine.datasetResourceParts(target.id)) : resourceClients?.(parsed);
485
+ };
486
+ }
487
+
436
488
  function assertIsoInstant(iso) {
437
489
  const ms = Date.parse(iso);
438
490
  if (Number.isNaN(ms)) throw new Error(`bench clock: expected an ISO-8601 instant, got ${JSON.stringify(iso)}`);
@@ -455,4 +507,6 @@ exports.createBenchEngine = createBenchEngine;
455
507
 
456
508
  exports.releaseField = releaseField;
457
509
 
510
+ exports.startedInstanceFixture = startedInstanceFixture;
511
+
458
512
  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
@@ -281,6 +305,7 @@ declare type EngineScope = {
281
305
  client: TestClient;
282
306
  tag: string;
283
307
  workflowResource: WorkflowResource;
308
+ resourceClients?: ResourceClientResolver;
284
309
  executionContext: DeclaredExecutionContext;
285
310
  };
286
311
 
@@ -306,6 +331,20 @@ declare type EngineWrappers = {
306
331
  guardsForDefinition: (definition: string) => Promise<MutationGuardDoc[]>;
307
332
  };
308
333
 
334
+ /** The slice of a bench {@link startedInstanceFixture} drives — structural,
335
+ * so the fixture module needs no import of the bench itself. */
336
+ declare interface FixtureBench {
337
+ deployDefinitions: (args: {
338
+ definitions: WorkflowDefinition[];
339
+ }) => Promise<unknown>;
340
+ startInstance: (args: {
341
+ definition: string;
342
+ instanceId?: string;
343
+ }) => Promise<{
344
+ instance: WorkflowInstance;
345
+ }>;
346
+ }
347
+
309
348
  declare type GuardHelpers = {
310
349
  /** All deployed guard docs ({@link MutationGuardDoc}) in the store. */
311
350
  listGuards: () => Promise<MutationGuardDoc[]>;
@@ -358,11 +397,13 @@ declare type QueryHelpers = {
358
397
  ) => Promise<T>;
359
398
  /**
360
399
  * 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.
400
+ * in-memory view the engine's filters see, with the caller-free
401
+ * rendered scope auto-bound: every always-bound condition var,
402
+ * `$fields` with the open stage's overlay merged, `$assigned` at its
403
+ * caller-free `false`, and the author's pre-evaluated `$<predicate>`
404
+ * booleans, ids in GDR URI form. Use to ask "what does the engine see
405
+ * for this workflow?" — useful for filter debugging, UIs that mirror
406
+ * engine state, etc.
366
407
  */
367
408
  queryInScope: <T = unknown>({
368
409
  instanceId,
@@ -438,11 +479,19 @@ declare type ReadHelpers = {
438
479
  * The startable half of {@link ReadHelpers.instancesForDocument}: the
439
480
  * latest deployed version of every definition applicable to the LOADED
440
481
  * candidate document — startable, a required subject entry accepts its
441
- * `_type`, `start.filter` passes. All matches, name ascending.
482
+ * `_type`, `start.filter` (browse-time-pure) passes. All matches, name
483
+ * ascending.
442
484
  */
443
485
  definitionsForDocument: (
444
486
  args: DefinitionsForDocumentArgs,
445
487
  ) => Promise<DeployedDefinition[]>;
488
+ /**
489
+ * Pre-flight `startInstance`'s gates for a definition + candidate
490
+ * `initialFields`: the `start.allowed` verdict (with its insight) plus the
491
+ * still-missing required inputs — the read a start surface renders before
492
+ * committing.
493
+ */
494
+ evaluateStart: (args: EvaluateStartArgs) => Promise<StartEvaluation>;
446
495
  /** Snapshot of all documents in the store — for inspecting the world. */
447
496
  snapshot: () => readonly StoreDocument[];
448
497
  };
@@ -459,6 +508,20 @@ export declare function releaseField(
459
508
  },
460
509
  ): InitialFieldValue;
461
510
 
511
+ /**
512
+ * A REAL, minimal started instance — deployed and started through the bench's
513
+ * actual verbs, parked forever on its first stage. The fixture for consumers
514
+ * (adapter tests, validation tests) that need a persisted-shape-valid
515
+ * instance document without caring about its workflow: hand-crafted instance
516
+ * stubs do not pass the engine's read parse.
517
+ */
518
+ export declare function startedInstanceFixture(
519
+ bench: FixtureBench,
520
+ opts?: {
521
+ instanceId?: string;
522
+ },
523
+ ): Promise<WorkflowInstance>;
524
+
462
525
  /** A document as stored in the bench's fake lake. */
463
526
  declare type StoreDocument = {
464
527
  _id: 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
@@ -281,6 +305,7 @@ declare type EngineScope = {
281
305
  client: TestClient;
282
306
  tag: string;
283
307
  workflowResource: WorkflowResource;
308
+ resourceClients?: ResourceClientResolver;
284
309
  executionContext: DeclaredExecutionContext;
285
310
  };
286
311
 
@@ -306,6 +331,20 @@ declare type EngineWrappers = {
306
331
  guardsForDefinition: (definition: string) => Promise<MutationGuardDoc[]>;
307
332
  };
308
333
 
334
+ /** The slice of a bench {@link startedInstanceFixture} drives — structural,
335
+ * so the fixture module needs no import of the bench itself. */
336
+ declare interface FixtureBench {
337
+ deployDefinitions: (args: {
338
+ definitions: WorkflowDefinition[];
339
+ }) => Promise<unknown>;
340
+ startInstance: (args: {
341
+ definition: string;
342
+ instanceId?: string;
343
+ }) => Promise<{
344
+ instance: WorkflowInstance;
345
+ }>;
346
+ }
347
+
309
348
  declare type GuardHelpers = {
310
349
  /** All deployed guard docs ({@link MutationGuardDoc}) in the store. */
311
350
  listGuards: () => Promise<MutationGuardDoc[]>;
@@ -358,11 +397,13 @@ declare type QueryHelpers = {
358
397
  ) => Promise<T>;
359
398
  /**
360
399
  * 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.
400
+ * in-memory view the engine's filters see, with the caller-free
401
+ * rendered scope auto-bound: every always-bound condition var,
402
+ * `$fields` with the open stage's overlay merged, `$assigned` at its
403
+ * caller-free `false`, and the author's pre-evaluated `$<predicate>`
404
+ * booleans, ids in GDR URI form. Use to ask "what does the engine see
405
+ * for this workflow?" — useful for filter debugging, UIs that mirror
406
+ * engine state, etc.
366
407
  */
367
408
  queryInScope: <T = unknown>({
368
409
  instanceId,
@@ -438,11 +479,19 @@ declare type ReadHelpers = {
438
479
  * The startable half of {@link ReadHelpers.instancesForDocument}: the
439
480
  * latest deployed version of every definition applicable to the LOADED
440
481
  * candidate document — startable, a required subject entry accepts its
441
- * `_type`, `start.filter` passes. All matches, name ascending.
482
+ * `_type`, `start.filter` (browse-time-pure) passes. All matches, name
483
+ * ascending.
442
484
  */
443
485
  definitionsForDocument: (
444
486
  args: DefinitionsForDocumentArgs,
445
487
  ) => Promise<DeployedDefinition[]>;
488
+ /**
489
+ * Pre-flight `startInstance`'s gates for a definition + candidate
490
+ * `initialFields`: the `start.allowed` verdict (with its insight) plus the
491
+ * still-missing required inputs — the read a start surface renders before
492
+ * committing.
493
+ */
494
+ evaluateStart: (args: EvaluateStartArgs) => Promise<StartEvaluation>;
446
495
  /** Snapshot of all documents in the store — for inspecting the world. */
447
496
  snapshot: () => readonly StoreDocument[];
448
497
  };
@@ -459,6 +508,20 @@ export declare function releaseField(
459
508
  },
460
509
  ): InitialFieldValue;
461
510
 
511
+ /**
512
+ * A REAL, minimal started instance — deployed and started through the bench's
513
+ * actual verbs, parked forever on its first stage. The fixture for consumers
514
+ * (adapter tests, validation tests) that need a persisted-shape-valid
515
+ * instance document without caring about its workflow: hand-crafted instance
516
+ * stubs do not pass the engine's read parse.
517
+ */
518
+ export declare function startedInstanceFixture(
519
+ bench: FixtureBench,
520
+ opts?: {
521
+ instanceId?: string;
522
+ },
523
+ ): Promise<WorkflowInstance>;
524
+
462
525
  /** A document as stored in the bench's fake lake. */
463
526
  declare type StoreDocument = {
464
527
  _id: 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, releaseRef, gdrRef, 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"
@@ -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,
@@ -361,6 +373,34 @@ function releaseField(releaseName, opts) {
361
373
  };
362
374
  }
363
375
 
376
+ async function startedInstanceFixture(bench, opts) {
377
+ const flow = defineWorkflow({
378
+ name: "fixture-flow",
379
+ title: "Fixture flow",
380
+ initialStage: "review",
381
+ stages: [ {
382
+ name: "review",
383
+ transitions: [ {
384
+ name: "go",
385
+ to: "done",
386
+ when: "false"
387
+ } ]
388
+ }, {
389
+ name: "done"
390
+ } ]
391
+ });
392
+ await bench.deployDefinitions({
393
+ definitions: [ flow ]
394
+ });
395
+ const {instance: instance} = await bench.startInstance({
396
+ definition: flow.name,
397
+ ...opts?.instanceId !== void 0 ? {
398
+ instanceId: opts.instanceId
399
+ } : {}
400
+ });
401
+ return instance;
402
+ }
403
+
364
404
  function createBench(options = {}) {
365
405
  const tag = options.tag ?? BENCH_TAG;
366
406
  validateTag(tag);
@@ -372,10 +412,16 @@ function createBench(options = {}) {
372
412
  documents: options.documents
373
413
  } : {},
374
414
  defaultAccess: access
375
- }), identities = createIdentityDirectory(client), scope = {
415
+ }), identities = createIdentityDirectory(client), resourceClients = benchResourceClients({
416
+ client: client,
417
+ options: options
418
+ }), scope = {
376
419
  client: client,
377
420
  tag: tag,
378
421
  workflowResource: workflowResource,
422
+ ...resourceClients !== void 0 ? {
423
+ resourceClients: resourceClients
424
+ } : {},
379
425
  executionContext: options.executionContext ?? {
380
426
  kind: EXECUTION_KINDS.test
381
427
  }
@@ -391,10 +437,7 @@ function createBench(options = {}) {
391
437
  clock: clock
392
438
  });
393
439
  return {
394
- client: client,
395
- tag: tag,
396
- workflowResource: workflowResource,
397
- executionContext: scope.executionContext,
440
+ ...scope,
398
441
  access: access,
399
442
  currentUser: access.actor,
400
443
  grants: access.grants ?? [],
@@ -429,10 +472,21 @@ function createBench(options = {}) {
429
472
  };
430
473
  }
431
474
 
475
+ function benchResourceClients(args) {
476
+ const {client: client} = args, {serveResources: serveResources, resourceClients: resourceClients} = args.options;
477
+ if (serveResources === void 0 || serveResources.length === 0) return resourceClients;
478
+ const nonDataset = serveResources.find(served => served.type !== "dataset");
479
+ 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.`);
480
+ return parsed => {
481
+ const target = resourceFromParsed(parsed);
482
+ return serveResources.some(served => sameResource(served, target)) ? client.withConfig(datasetResourceParts(target.id)) : resourceClients?.(parsed);
483
+ };
484
+ }
485
+
432
486
  function assertIsoInstant(iso) {
433
487
  const ms = Date.parse(iso);
434
488
  if (Number.isNaN(ms)) throw new Error(`bench clock: expected an ISO-8601 instant, got ${JSON.stringify(iso)}`);
435
489
  return new Date(ms).toISOString();
436
490
  }
437
491
 
438
- export { ALL_ACCESS, ALL_ACCESS_USER, BENCH_TAG, DEFAULT_WORKFLOW_RESOURCE, WILDCARD_GRANTS, createBench, createBenchEngine, releaseField, subjectField };
492
+ export { ALL_ACCESS, ALL_ACCESS_USER, BENCH_TAG, DEFAULT_WORKFLOW_RESOURCE, WILDCARD_GRANTS, createBench, createBenchEngine, 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.12.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.17.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@sanity/client": "^7.22.1",