@sanity/workflow-engine-test 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sanity, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # @sanity/workflow-engine-test
2
+
3
+ In-memory test bench (`createBench`) for
4
+ [`@sanity/workflow-engine`](../workflow-engine). Wraps the engine and an
5
+ in-memory Sanity client so workflow tests stay free of client wiring and actor
6
+ boilerplate.
7
+
8
+ > **Status:** 0.x, internal. Restricted-access package.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install -D @sanity/workflow-engine-test
14
+ ```
15
+
16
+ ## License
17
+
18
+ [MIT](./LICENSE)
package/dist/index.cjs ADDED
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: !0 });
3
+ var workflowEngine = require("@sanity/workflow-engine"), clientFakeForTest = require("@sanity-labs/client-fake-for-test");
4
+ const DEFAULT_WORKFLOW_RESOURCE = {
5
+ type: "dataset",
6
+ id: "test.test"
7
+ }, ALL_ACCESS_USER = {
8
+ kind: "user",
9
+ id: "bench-user",
10
+ roles: ["*"]
11
+ }, WILDCARD_GRANTS = [
12
+ { filter: "true", permissions: ["create", "read", "update"] }
13
+ ], ALL_ACCESS = {
14
+ actor: ALL_ACCESS_USER,
15
+ grants: WILDCARD_GRANTS
16
+ };
17
+ function resolveBenchClient(options) {
18
+ if (options.accessControl !== void 0 && options.client !== void 0)
19
+ throw new Error(
20
+ "createBench: `accessControl` and `client` are mutually exclusive. Either let the bench build a TestClient configured with the supplied accessControl, or build the client yourself with `createTestClient({ accessControl })` and pass it in."
21
+ );
22
+ const client = options.client ?? clientFakeForTest.createTestClient(
23
+ options.accessControl !== void 0 ? { accessControl: options.accessControl } : {}
24
+ );
25
+ return options.documents && client.add(options.documents), client;
26
+ }
27
+ function resolveBenchAccess(options) {
28
+ return options.access ?? {
29
+ actor: options.currentUser ?? ALL_ACCESS_USER,
30
+ grants: options.grants ?? WILDCARD_GRANTS
31
+ };
32
+ }
33
+ function accessFor(access, args, canTokenResolve) {
34
+ if (args.access !== void 0) return args.access;
35
+ if (args.actor === void 0 && args.grants === void 0) return access;
36
+ const inheritedGrants = canTokenResolve ? void 0 : access.grants, grants = args.grants ?? inheritedGrants;
37
+ return {
38
+ actor: args.actor ?? access.actor,
39
+ ...grants !== void 0 ? { grants } : {}
40
+ };
41
+ }
42
+ function stripAccessShortcuts(args) {
43
+ const rest = { ...args };
44
+ return delete rest.access, delete rest.actor, delete rest.grants, rest;
45
+ }
46
+ function createEngineWrappers(scope, access) {
47
+ const canTokenResolve = "request" in scope.client, withAccess = (args) => ({
48
+ ...scope,
49
+ access: accessFor(access, args, canTokenResolve),
50
+ ...stripAccessShortcuts(args)
51
+ });
52
+ return {
53
+ deployDefinitions: (args) => workflowEngine.workflow.deployDefinitions({ ...scope, ...args }),
54
+ startInstance: (args) => workflowEngine.workflow.startInstance(withAccess(args)),
55
+ fireAction: (args) => workflowEngine.workflow.fireAction(withAccess(args)),
56
+ completeEffect: (args) => workflowEngine.workflow.completeEffect(withAccess(args)),
57
+ tick: (args) => workflowEngine.workflow.tick(withAccess(args)),
58
+ evaluate: (args) => workflowEngine.workflow.evaluate(withAccess(args))
59
+ };
60
+ }
61
+ function createGuardHelpers(client) {
62
+ const fetchGuards = () => client.fetch('*[_type == "temp.system.guard"]'), guardDocRef = (documentId, before) => ({
63
+ id: documentId,
64
+ ...before?._type ? { type: before._type } : {}
65
+ });
66
+ return {
67
+ listGuards: () => client.fetch('*[_type == "temp.system.guard"] | order(_id asc)'),
68
+ activeGuardsForDocument: async (documentId, as) => {
69
+ const guards = await fetchGuards(), before = await client.getDocument(documentId), after = { ...before ?? { _id: documentId, _type: "unknown" } };
70
+ for (const key of Object.keys(after))
71
+ key.startsWith("_") || (after[key] = { __guardProbe: !0 });
72
+ return after.__guardProbe = { __guardProbe: !0 }, workflowEngine.denyingGuards({
73
+ guards,
74
+ doc: guardDocRef(documentId, before),
75
+ context: {
76
+ before: before ?? null,
77
+ after,
78
+ action: "update",
79
+ ...as ? { identity: as.id } : {}
80
+ }
81
+ });
82
+ },
83
+ editDocument: async (documentId, patch, editOptions) => {
84
+ const action = editOptions?.action ?? "update", as = editOptions?.as, before = await client.getDocument(documentId), after = action === "delete" ? null : { ...before ?? { _id: documentId, _type: "unknown" }, ...patch }, guards = await fetchGuards(), denied = await workflowEngine.denyingGuards({
85
+ guards,
86
+ doc: guardDocRef(documentId, before),
87
+ context: {
88
+ before: before ?? null,
89
+ after,
90
+ action,
91
+ ...as ? { identity: as.id } : {}
92
+ }
93
+ });
94
+ if (denied.length > 0)
95
+ throw new workflowEngine.MutationGuardDeniedError({
96
+ documentId,
97
+ action,
98
+ denied: denied.map((g) => ({ guardId: g._id, ...g.name ? { name: g.name } : {} }))
99
+ });
100
+ return await client.patch(documentId).set(patch).commit(), await client.getDocument(documentId);
101
+ }
102
+ };
103
+ }
104
+ function createQueryHelpers(scope) {
105
+ const withParams = (params) => params !== void 0 ? { params } : {};
106
+ return {
107
+ query: (groq, params) => workflowEngine.workflow.query({ ...scope, groq, ...withParams(params) }),
108
+ queryInScope: (instanceId, groq, params) => workflowEngine.workflow.queryInScope({ ...scope, instanceId, groq, ...withParams(params) })
109
+ };
110
+ }
111
+ function createReadHelpers(scope) {
112
+ const { client, tags } = scope, intersectsTags = (docTags) => docTags !== void 0 && tags.some((t) => docTags.includes(t)), getInstance = async (instanceId) => {
113
+ const doc = await client.getDocument(instanceId);
114
+ if (!doc) throw new Error(`Workflow instance ${instanceId} not found`);
115
+ if (!intersectsTags(doc.tags))
116
+ throw new Error(`Workflow instance ${instanceId} not visible to this engine (tag mismatch)`);
117
+ return doc;
118
+ };
119
+ return {
120
+ getInstance,
121
+ currentStage: async (instanceId) => (await getInstance(instanceId)).currentStageId,
122
+ pendingEffects: async (instanceId) => (await getInstance(instanceId)).pendingEffects,
123
+ taskStatus: async (instanceId, taskId) => {
124
+ const instance = await getInstance(instanceId);
125
+ return instance.stages.find(
126
+ (s) => s.id === instance.currentStageId && s.exitedAt === void 0
127
+ )?.tasks.find((t) => t.id === taskId)?.status;
128
+ },
129
+ effectsContextMap: async (instanceId) => {
130
+ const instance = await getInstance(instanceId), map = {};
131
+ for (const entry of instance.effectsContext) map[entry.id] = entry.value;
132
+ return map;
133
+ },
134
+ children: async (parentInstanceId, taskId) => workflowEngine.workflow.children({
135
+ ...scope,
136
+ instanceId: parentInstanceId,
137
+ ...taskId !== void 0 ? { taskId } : {}
138
+ }),
139
+ instancesForSubject: async (subjectRef, slotId = "subject") => (
140
+ // Subjects live on a workflow-scope `state[id == $slot]` doc.ref
141
+ // slot. Match instances whose slot's value.id equals the supplied
142
+ // ref. The slot id is conventionally "subject" but workflows may
143
+ // name it otherwise.
144
+ client.fetch(
145
+ `*[_type == "workflow.instance"
146
+ && state[_type == "workflow.state.doc.ref" && id == $slot][0].value.id == $ref
147
+ && count(tags[@ in $engineTags]) > 0] | order(startedAt asc)`,
148
+ { ref: subjectRef, slot: slotId, engineTags: tags }
149
+ )
150
+ ),
151
+ instancesByStage: async (workflowId, stageId, queryOptions) => {
152
+ const filter = queryOptions?.openOnly ? " && completedAt == null" : "";
153
+ return client.fetch(
154
+ `*[_type == "workflow.instance" && workflowId == $wf && currentStageId == $stage && count(tags[@ in $engineTags]) > 0${filter}]
155
+ | order(startedAt asc)`,
156
+ { wf: workflowId, stage: stageId, engineTags: tags }
157
+ );
158
+ },
159
+ snapshot: () => client.snapshot()
160
+ };
161
+ }
162
+ function createBench(options = {}) {
163
+ const tags = options.tags ?? ["bench"];
164
+ workflowEngine.validateTags(tags);
165
+ const workflowResource = options.workflowResource ?? DEFAULT_WORKFLOW_RESOURCE, client = resolveBenchClient(options), scope = { client, tags, workflowResource }, access = resolveBenchAccess(options);
166
+ return {
167
+ client,
168
+ access,
169
+ currentUser: access.actor,
170
+ grants: access.grants ?? [],
171
+ seedDocuments: (documents) => client.add(documents),
172
+ ...createEngineWrappers(scope, access),
173
+ ...createReadHelpers(scope),
174
+ ...createGuardHelpers(client),
175
+ ...createQueryHelpers(scope)
176
+ };
177
+ }
178
+ exports.ALL_ACCESS = ALL_ACCESS;
179
+ exports.ALL_ACCESS_USER = ALL_ACCESS_USER;
180
+ exports.DEFAULT_WORKFLOW_RESOURCE = DEFAULT_WORKFLOW_RESOURCE;
181
+ exports.WILDCARD_GRANTS = WILDCARD_GRANTS;
182
+ exports.createBench = createBench;
183
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../src/constants.ts","../src/access.ts","../src/engine-wrappers.ts","../src/guard-helpers.ts","../src/query-helpers.ts","../src/read-helpers.ts","../src/index.ts"],"sourcesContent":["import type {Actor, Grant, WorkflowAccess, WorkflowResource} from '@sanity/workflow-engine'\n\n/** Default workflow resource for benches that don't pass one. */\nexport const DEFAULT_WORKFLOW_RESOURCE: WorkflowResource = {\n type: 'dataset',\n id: 'test.test',\n}\n\n/**\n * The default actor used when tests don't specify one. Has the\n * conventional `\"*\"` wildcard role — `Action.roles` checks treat\n * `\"*\"` as matching any required role, and `Task.assignees` of\n * `{kind: \"role\"}` are satisfied by it too. Production code should\n * never see this id.\n */\nexport const ALL_ACCESS_USER: Actor = {\n kind: 'user',\n id: 'bench-user',\n roles: ['*'],\n}\n\n/**\n * The default grants used when tests don't specify any. A single\n * permissive grant matches every document and confers all permissions.\n */\nexport const WILDCARD_GRANTS: Grant[] = [\n {filter: 'true', permissions: ['create', 'read', 'update']},\n]\n\n/**\n * The bench's default access state — wildcard user + wildcard\n * grants. The bench injects this as the `access` override on every\n * engine call so the underlying TestClient (which has no token, so\n * no `/users/me` resolution path) never needs to do auth resolution.\n *\n * Production code never sees this; pass `createBench({ access })` to\n * test denial paths.\n */\nexport const ALL_ACCESS: WorkflowAccess = {\n actor: ALL_ACCESS_USER,\n grants: WILDCARD_GRANTS,\n}\n","import type {TestClient, TestClientAccessControl} from '@sanity-labs/client-fake-for-test'\nimport {createTestClient} from '@sanity-labs/client-fake-for-test'\nimport type {Actor, Grant, WorkflowAccess} from '@sanity/workflow-engine'\n\nimport {ALL_ACCESS_USER, WILDCARD_GRANTS} from './constants.ts'\nimport type {AccessShortcuts, StoreDocument} from './types.ts'\n\n/**\n * Build the TestClient the bench operates against, enforcing the\n * `client` / `accessControl` mutual-exclusion contract: callers either\n * supply their own client or have the bench mint one (optionally wired\n * with lake-side access control), never both.\n */\nexport function resolveBenchClient(options: {\n client?: TestClient\n accessControl?: TestClientAccessControl\n documents?: StoreDocument[]\n}): TestClient {\n if (options.accessControl !== undefined && options.client !== undefined) {\n throw new Error(\n 'createBench: `accessControl` and `client` are mutually exclusive. Either let the bench ' +\n 'build a TestClient configured with the supplied accessControl, or build the client ' +\n 'yourself with `createTestClient({ accessControl })` and pass it in.',\n )\n }\n const client =\n options.client ??\n createTestClient(\n options.accessControl !== undefined ? {accessControl: options.accessControl} : {},\n )\n if (options.documents) client.add(options.documents)\n return client\n}\n\n/**\n * Compose the bench-wide access default. Precedence: the canonical\n * `access` shape, then the `currentUser` / `grants` shortcuts, then\n * {@link ALL_ACCESS_USER} + {@link WILDCARD_GRANTS}.\n */\nexport function resolveBenchAccess(options: {\n access?: WorkflowAccess\n currentUser?: Actor\n grants?: Grant[]\n}): WorkflowAccess {\n return (\n options.access ?? {\n actor: options.currentUser ?? ALL_ACCESS_USER,\n grants: options.grants ?? WILDCARD_GRANTS,\n }\n )\n}\n\n/**\n * Resolve the per-call access override against the bench-wide default.\n * A call may tighten or replace access via the canonical `{ access }`\n * field or the `{ actor, grants }` shortcuts.\n *\n * `canTokenResolve` is true when the bench's client was built with\n * `accessControl` — i.e. its `request` hook can resolve grants from\n * the ACL path. In that case an actor-only override (no grants) must\n * NOT inherit the bench's default grants: doing so would gate the\n * per-call actor with the default's (e.g. wildcard) grants instead of\n * that actor's real grants, silently false-greening denial tests. The\n * engine treats verb `access` as `WorkflowAccessOverride`, so the bench\n * hands it `{ actor }` alone and lets the engine token-resolve grants\n * (via the call's `grantsFromPath`) or skip the gate — both faithful to\n * production. Tokenless default benches keep inheriting the default\n * grants, since there is no other source.\n */\nexport function accessFor(\n access: WorkflowAccess,\n args: AccessShortcuts,\n canTokenResolve: boolean,\n): WorkflowAccess {\n if (args.access !== undefined) return args.access\n if (args.actor === undefined && args.grants === undefined) return access\n const inheritedGrants = canTokenResolve ? undefined : access.grants\n const grants = args.grants ?? inheritedGrants\n return {\n actor: args.actor ?? access.actor,\n ...(grants !== undefined ? {grants} : {}),\n }\n}\n\n/**\n * Strip the bench-only access shortcut fields before spreading the rest\n * of a call's args into an engine call, so the composed `access` is the\n * single source of truth.\n */\nexport function stripAccessShortcuts<T extends AccessShortcuts>(\n args: T,\n): Omit<T, 'access' | 'actor' | 'grants'> {\n const rest = {...args}\n delete rest.access\n delete rest.actor\n delete rest.grants\n return rest\n}\n","import {\n workflow,\n type DeployDefinitionsArgs,\n type DeployDefinitionsResult,\n type DispatchResult,\n type WorkflowAccess,\n type WorkflowEvaluation,\n type WorkflowInstance,\n} from '@sanity/workflow-engine'\n\nimport {accessFor, stripAccessShortcuts} from './access.ts'\nimport type {\n AccessShortcuts,\n BenchCompleteEffectArgs,\n BenchEvaluateArgs,\n BenchFireActionArgs,\n BenchStartInstanceArgs,\n BenchTickArgs,\n EngineScope,\n} from './types.ts'\n\n/** Engine-call wrappers that inject the bench-owned client + tags + access. */\nexport type EngineWrappers = {\n deployDefinitions: (\n args: Omit<DeployDefinitionsArgs, 'client' | 'tags' | 'workflowResource'>,\n ) => Promise<DeployDefinitionsResult>\n startInstance: (args: BenchStartInstanceArgs) => Promise<WorkflowInstance>\n fireAction: (args: BenchFireActionArgs) => Promise<DispatchResult>\n completeEffect: (args: BenchCompleteEffectArgs) => Promise<DispatchResult>\n tick: (args: BenchTickArgs) => Promise<DispatchResult>\n evaluate: (args: BenchEvaluateArgs) => Promise<WorkflowEvaluation>\n}\n\n/**\n * Build the engine-call wrappers. Each access-aware wrapper composes the\n * call's `access` from the bench default plus any per-call override, then\n * spreads the remaining args into the engine with the bench-owned scope.\n */\nexport function createEngineWrappers(scope: EngineScope, access: WorkflowAccess): EngineWrappers {\n // The fake client only exposes an own `request` hook when built with\n // `accessControl`; that hook is what lets the engine token-resolve\n // grants from the ACL path. A plain bench client has no such hook.\n const canTokenResolve = 'request' in scope.client\n const withAccess = <T extends AccessShortcuts>(args: T) => ({\n ...scope,\n access: accessFor(access, args, canTokenResolve),\n ...stripAccessShortcuts(args),\n })\n return {\n deployDefinitions: (args) => workflow.deployDefinitions({...scope, ...args}),\n startInstance: (args) => workflow.startInstance(withAccess(args)),\n fireAction: (args) => workflow.fireAction(withAccess(args)),\n completeEffect: (args) => workflow.completeEffect(withAccess(args)),\n tick: (args) => workflow.tick(withAccess(args)),\n evaluate: (args) => workflow.evaluate(withAccess(args)),\n }\n}\n","import type {TestClient} from '@sanity-labs/client-fake-for-test'\nimport {\n denyingGuards,\n MutationGuardDeniedError,\n type Actor,\n type MutationGuardAction,\n type MutationGuardDoc,\n} from '@sanity/workflow-engine'\n\nimport type {StoreDocument} from './types.ts'\n\nexport type GuardHelpers = {\n /** All deployed `temp.system.guard` docs in the store. */\n listGuards: () => Promise<MutationGuardDoc[]>\n\n /**\n * Guards that would deny an arbitrary edit to `documentId` right now (i.e.\n * active locks). Probes by changing every non-system field; a lifted guard\n * (predicate `\"true\"`) returns nothing.\n */\n activeGuardsForDocument: (documentId: string, as?: Actor) => Promise<MutationGuardDoc[]>\n\n /**\n * Attempt a guarded write to a document — the bench's stand-in for the lake's\n * mutation path. Evaluates deployed guards (incl. `identity()` = the actor)\n * against the before/after image and throws `MutationGuardDeniedError` if any\n * deny; otherwise applies the patch. OPTIMISTIC — not the lake enforcing.\n */\n editDocument: (\n documentId: string,\n patch: Record<string, unknown>,\n options?: {action?: MutationGuardAction; as?: Actor},\n ) => Promise<StoreDocument>\n}\n\n/**\n * Build the optimistic, bench-enforced lake mutation-guard helpers. These\n * stand in for the lake's mutation path: they evaluate deployed guards\n * against a before/after image rather than the lake enforcing.\n */\nexport function createGuardHelpers(client: TestClient): GuardHelpers {\n const fetchGuards = () => client.fetch<MutationGuardDoc[]>(`*[_type == \"temp.system.guard\"]`)\n\n const guardDocRef = (documentId: string, before: StoreDocument | null | undefined) => ({\n id: documentId,\n ...(before?._type ? {type: before._type} : {}),\n })\n\n return {\n listGuards: () =>\n client.fetch<MutationGuardDoc[]>(`*[_type == \"temp.system.guard\"] | order(_id asc)`),\n\n activeGuardsForDocument: async (documentId, as) => {\n const guards = await fetchGuards()\n const before = await client.getDocument<StoreDocument>(documentId)\n const after: Record<string, unknown> = {...(before ?? {_id: documentId, _type: 'unknown'})}\n for (const key of Object.keys(after)) {\n if (!key.startsWith('_')) after[key] = {__guardProbe: true}\n }\n after.__guardProbe = {__guardProbe: true}\n return denyingGuards({\n guards,\n doc: guardDocRef(documentId, before),\n context: {\n before: before ?? null,\n after: after as {_id: string; _type: string},\n action: 'update',\n ...(as ? {identity: as.id} : {}),\n },\n })\n },\n\n editDocument: async (documentId, patch, editOptions) => {\n const action = editOptions?.action ?? 'update'\n const as = editOptions?.as\n const before = await client.getDocument<StoreDocument>(documentId)\n const after =\n action === 'delete' ? null : {...(before ?? {_id: documentId, _type: 'unknown'}), ...patch}\n const guards = await fetchGuards()\n const denied = await denyingGuards({\n guards,\n doc: guardDocRef(documentId, before),\n context: {\n before: before ?? null,\n after,\n action,\n ...(as ? {identity: as.id} : {}),\n },\n })\n if (denied.length > 0) {\n throw new MutationGuardDeniedError({\n documentId,\n action,\n denied: denied.map((g) => ({guardId: g._id, ...(g.name ? {name: g.name} : {})})),\n })\n }\n await client.patch(documentId).set(patch).commit()\n return (await client.getDocument(documentId)) as StoreDocument\n },\n }\n}\n","import {workflow} from '@sanity/workflow-engine'\n\nimport type {EngineScope} from './types.ts'\n\nexport type QueryHelpers = {\n /**\n * Run an arbitrary GROQ query through `workflow.query`. The query MUST\n * filter on the auto-injected `$engineTags` param (e.g.\n * `*[... && count(tags[@ in $engineTags]) > 0]`) or it throws — the\n * engine refuses to run a query that could read across tenant tags.\n * Caller is responsible for narrowing the result.\n */\n query: <T = unknown>(groq: string, params?: Record<string, unknown>) => Promise<T>\n\n /**\n * Snapshot-aware GROQ for `instanceId` — runs against the same\n * in-memory view the engine's filters see. Reserved params (`$self`,\n * `$subject`, `$parent`, `$ancestors`, `$now`, `$stage`) auto-bound\n * in GDR URI form. Use to ask \"what does the engine see for this\n * workflow?\" — useful for filter debugging, UIs that mirror engine\n * state, etc.\n */\n queryInScope: <T = unknown>(\n instanceId: string,\n groq: string,\n params?: Record<string, unknown>,\n ) => Promise<T>\n}\n\n/** Build the GROQ passthrough helpers, both tag-scoped via the bench scope. */\nexport function createQueryHelpers(scope: EngineScope): QueryHelpers {\n const withParams = (params?: Record<string, unknown>) => (params !== undefined ? {params} : {})\n return {\n query: <T = unknown>(groq: string, params?: Record<string, unknown>) =>\n workflow.query<T>({...scope, groq, ...withParams(params)}),\n\n queryInScope: <T = unknown>(\n instanceId: string,\n groq: string,\n params?: Record<string, unknown>,\n ) => workflow.queryInScope<T>({...scope, instanceId, groq, ...withParams(params)}),\n }\n}\n","import {\n workflow,\n type EffectsContextEntry,\n type PendingEffect,\n type TaskStatus,\n type WorkflowInstance,\n} from '@sanity/workflow-engine'\n\nimport type {EngineScope, StoreDocument} from './types.ts'\n\nexport type ReadHelpers = {\n /** Read the workflow instance by id. Throws if missing. */\n getInstance: (instanceId: string) => Promise<WorkflowInstance>\n\n /** Current stage id of an instance. */\n currentStage: (instanceId: string) => Promise<string>\n\n /** Pending effects on an instance. */\n pendingEffects: (instanceId: string) => Promise<PendingEffect[]>\n\n /** Status of a single task on an instance, or undefined if missing. */\n taskStatus: (instanceId: string, taskId: string) => Promise<TaskStatus | undefined>\n\n /** EffectsContext entries on an instance, keyed by `key`. */\n effectsContextMap: (instanceId: string) => Promise<Record<string, EffectsContextEntry['value']>>\n\n /**\n * Spawned children of `parentInstanceId`. If `taskId` is provided,\n * only children spawned by that specific task on the parent.\n *\n * Walks `history.spawned` entries on the parent — this is the durable\n * record. (The per-task `spawnedInstances` field on `stages[id == $stage && !defined(exitedAt)][0].tasks[]`\n * gets wiped when the parent transitions past the spawning stage,\n * since `taskStatus` is rebuilt on every transition.)\n *\n * Returns children ordered by `startedAt` ascending.\n */\n children: (parentInstanceId: string, taskId?: string) => Promise<WorkflowInstance[]>\n\n /**\n * All workflow instances whose `workflow.state.doc.ref` slot named\n * `slotId` (default `\"subject\"`) matches the given doc id. Useful\n * for \"what workflow is running on this document?\" questions. Pass\n * `slotId` for workflows whose subject slot uses a different id.\n */\n instancesForSubject: (subjectRef: string, slotId?: string) => Promise<WorkflowInstance[]>\n\n /**\n * All instances of `workflowId` currently at `stageId`. Includes\n * completed instances (`completedAt != null`) by default; set\n * `{ openOnly: true }` to filter to in-flight only.\n */\n instancesByStage: (\n workflowId: string,\n stageId: string,\n options?: {openOnly?: boolean},\n ) => Promise<WorkflowInstance[]>\n\n /** Snapshot of all documents in the store — for inspecting the world. */\n snapshot: () => readonly StoreDocument[]\n}\n\n/**\n * Build the read-side DX helpers. {@link ReadHelpers.getInstance} enforces\n * tag-scoped visibility so every derived read (`currentStage`, `taskStatus`,\n * …) inherits the same isolation the engine's filters apply.\n */\nexport function createReadHelpers(scope: EngineScope): ReadHelpers {\n const {client, tags} = scope\n\n const intersectsTags = (docTags: string[] | undefined): boolean =>\n docTags !== undefined && tags.some((t) => docTags.includes(t))\n\n const getInstance = async (instanceId: string): Promise<WorkflowInstance> => {\n const doc = await client.getDocument<WorkflowInstance>(instanceId)\n if (!doc) throw new Error(`Workflow instance ${instanceId} not found`)\n if (!intersectsTags(doc.tags)) {\n throw new Error(`Workflow instance ${instanceId} not visible to this engine (tag mismatch)`)\n }\n return doc\n }\n\n return {\n getInstance,\n\n currentStage: async (instanceId) => (await getInstance(instanceId)).currentStageId,\n\n pendingEffects: async (instanceId) => (await getInstance(instanceId)).pendingEffects,\n\n taskStatus: async (instanceId, taskId) => {\n const instance = await getInstance(instanceId)\n const stage = instance.stages.find(\n (s) => s.id === instance.currentStageId && s.exitedAt === undefined,\n )\n return stage?.tasks.find((t) => t.id === taskId)?.status\n },\n\n effectsContextMap: async (instanceId) => {\n const instance = await getInstance(instanceId)\n const map: Record<string, EffectsContextEntry['value']> = {}\n for (const entry of instance.effectsContext) map[entry.id] = entry.value\n return map\n },\n\n children: async (parentInstanceId, taskId) =>\n workflow.children({\n ...scope,\n instanceId: parentInstanceId,\n ...(taskId !== undefined ? {taskId} : {}),\n }),\n\n instancesForSubject: async (subjectRef, slotId = 'subject') =>\n // Subjects live on a workflow-scope `state[id == $slot]` doc.ref\n // slot. Match instances whose slot's value.id equals the supplied\n // ref. The slot id is conventionally \"subject\" but workflows may\n // name it otherwise.\n client.fetch<WorkflowInstance[]>(\n `*[_type == \"workflow.instance\"\n && state[_type == \"workflow.state.doc.ref\" && id == $slot][0].value.id == $ref\n && count(tags[@ in $engineTags]) > 0] | order(startedAt asc)`,\n {ref: subjectRef, slot: slotId, engineTags: tags},\n ),\n\n instancesByStage: async (workflowId, stageId, queryOptions) => {\n const filter = queryOptions?.openOnly ? ' && completedAt == null' : ''\n return client.fetch<WorkflowInstance[]>(\n `*[_type == \"workflow.instance\" && workflowId == $wf && currentStageId == $stage && count(tags[@ in $engineTags]) > 0${filter}]\n | order(startedAt asc)`,\n {wf: workflowId, stage: stageId, engineTags: tags},\n )\n },\n\n snapshot: () => client.snapshot() as readonly StoreDocument[],\n }\n}\n","/**\n * The bench — a dry-run test harness wrapping `@sanity-labs/client-fake-for-test`.\n *\n * The bench is the **only** place in the package that fabricates an\n * actor identity. The engine itself requires an explicit `actor` on\n * every call; the bench provides a default `ALL_ACCESS_USER` so tests\n * don't have to thread one manually, and exposes wrapper methods that\n * inject `bench.currentUser` + `bench.grants` for every engine call.\n *\n * ```\n * const bench = createBench();\n * await bench.deployDefinitions({ definitions: [definition] });\n * const inst = await bench.startInstance({ workflowId: \"...\", subject: {...} });\n * await bench.fireAction({ instanceId: inst._id, taskId: \"...\", action: \"...\" });\n * expect(await bench.currentStage(inst._id)).toBe(\"ready\");\n * ```\n *\n * For denial-path tests, pass an explicit `currentUser` and/or `grants`:\n *\n * ```\n * const bench = createBench({\n * currentUser: { kind: \"user\", id: \"viewer\", roles: [\"viewer\"] },\n * grants: [{ filter: '_type == \"workflow.instance\"', permissions: [\"read\"] }],\n * });\n * await expect(bench.fireAction({...})).rejects.toBeInstanceOf(ActionDisabledError);\n * ```\n */\n\nimport type {TestClient, TestClientAccessControl} from '@sanity-labs/client-fake-for-test'\nimport {validateTags} from '@sanity/workflow-engine'\nimport type {Actor, Grant, WorkflowAccess, WorkflowResource} from '@sanity/workflow-engine'\n\nimport {resolveBenchAccess, resolveBenchClient} from './access.ts'\nimport {DEFAULT_WORKFLOW_RESOURCE} from './constants.ts'\nimport {createEngineWrappers, type EngineWrappers} from './engine-wrappers.ts'\nimport {createGuardHelpers, type GuardHelpers} from './guard-helpers.ts'\nimport {createQueryHelpers, type QueryHelpers} from './query-helpers.ts'\nimport {createReadHelpers, type ReadHelpers} from './read-helpers.ts'\nimport type {StoreDocument} from './types.ts'\n\nexport {\n ALL_ACCESS,\n ALL_ACCESS_USER,\n DEFAULT_WORKFLOW_RESOURCE,\n WILDCARD_GRANTS,\n} from './constants.ts'\n\nexport interface CreateBenchOptions {\n /** Subject documents seeded into the store. */\n documents?: StoreDocument[]\n /**\n * Access state (actor + grants) injected as the engine `access`\n * override on every wrapped call. Defaults to `ALL_ACCESS` —\n * wildcard user + wildcard grants. Pass a restrictive value to\n * test denial paths.\n *\n * Mutually exclusive with the `currentUser` / `grants` shortcuts\n * (which compose into the same shape internally).\n */\n access?: WorkflowAccess\n /** Shortcut for `access.actor`. Composed with `grants`. */\n currentUser?: Actor\n /** Shortcut for `access.grants`. Composed with `currentUser`. */\n grants?: Grant[]\n /**\n * Opt-in: simulate Sanity's hard write-gate inside the TestClient.\n *\n * When set, the bench's underlying TestClient:\n * - Exposes `client.request({ url })` serving `/users/me` + the\n * configured ACL path, so the engine can resolve `WorkflowAccess`\n * from \"the token\" exactly like production.\n * - Rejects writes the grants don't permit with a Sanity-style\n * `WriteAccessDeniedError` — the same 403 a real lake would\n * return.\n *\n * Use this to demonstrate what would happen if the engine's\n * soft-gate were bypassed (or disagreed with) — the lake is still\n * authoritative. The bench's `access` field is the engine-side\n * override; `accessControl` is the lake-side enforcement. They\n * usually carry the same `actor` + `grants` but can be made to\n * disagree to test edge cases.\n *\n * Mutually exclusive with passing a pre-built `client`.\n */\n accessControl?: TestClientAccessControl\n /**\n * Engine-scope tags. Defaults to `[\"bench\"]`. Pass disjoint sets to two\n * benches sharing a `client` to assert multi-tenant isolation; pass an\n * overlapping set to assert visibility through a shared tag.\n *\n * Tags must be ASCII lowercase + digits + dashes, no leading dash,\n * no dots — validated at construction.\n */\n tags?: string[]\n /**\n * Shared TestClient. If omitted, the bench creates its own — two\n * benches without a shared client are completely isolated, regardless\n * of tags. Pass the same client to two benches to test tag-scoped\n * visibility across a shared store.\n */\n client?: TestClient\n /**\n * Workflow resource the bench's engine operates against. Mirrors\n * `@sanity/client`'s `ClientConfigResource`. Defaults to\n * `{ type: \"dataset\", id: \"test.test\" }` so all bench-minted GDR\n * URIs read as `dataset:test:test:<docId>`.\n */\n workflowResource?: WorkflowResource\n}\n\n/** Base bench state, distinct from the engine/read/guard/query helper groups. */\ninterface BenchState {\n /** The fake Sanity client. Passed automatically by bench wrappers. */\n client: TestClient\n /**\n * The access state (actor + grants) the bench injects as the\n * `access` override on every wrapped engine call. Defaults to\n * `ALL_ACCESS`.\n */\n access: WorkflowAccess\n /** Convenience: `access.actor`. */\n currentUser: Actor\n /** Convenience: `access.grants ?? []`. */\n grants: Grant[]\n\n /** Add documents to the store after construction. */\n seedDocuments: (documents: StoreDocument[]) => void\n}\n\nexport type Bench = BenchState & EngineWrappers & ReadHelpers & GuardHelpers & QueryHelpers\n\n/**\n * Create a fresh bench. By default the bench is fully isolated — two\n * `createBench()` calls have no shared state. Pass `client` + `tags`\n * to two benches to share a store and exercise tag scoping.\n */\nexport function createBench(options: CreateBenchOptions = {}): Bench {\n const tags = options.tags ?? ['bench']\n validateTags(tags)\n const workflowResource = options.workflowResource ?? DEFAULT_WORKFLOW_RESOURCE\n const client = resolveBenchClient(options)\n const scope = {client, tags, workflowResource}\n\n const access = resolveBenchAccess(options)\n\n return {\n client,\n access,\n currentUser: access.actor,\n grants: access.grants ?? [],\n\n seedDocuments: (documents) => client.add(documents),\n\n ...createEngineWrappers(scope, access),\n ...createReadHelpers(scope),\n ...createGuardHelpers(client),\n ...createQueryHelpers(scope),\n }\n}\n"],"names":["createTestClient","workflow","denyingGuards","MutationGuardDeniedError","validateTags"],"mappings":"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"}
@@ -0,0 +1,326 @@
1
+ import { Actor } from "@sanity/workflow-engine";
2
+ import type { CompleteEffectArgs } from "@sanity/workflow-engine";
3
+ import { DeployDefinitionsArgs } from "@sanity/workflow-engine";
4
+ import { DeployDefinitionsResult } from "@sanity/workflow-engine";
5
+ import { DispatchResult } from "@sanity/workflow-engine";
6
+ import { EffectsContextEntry } from "@sanity/workflow-engine";
7
+ import type { EvaluateArgs } from "@sanity/workflow-engine";
8
+ import type { FireActionArgs } from "@sanity/workflow-engine";
9
+ import type { Grant } from "@sanity/workflow-engine";
10
+ import { MutationGuardAction } from "@sanity/workflow-engine";
11
+ import { MutationGuardDoc } from "@sanity/workflow-engine";
12
+ import type { OperationArgs } from "@sanity/workflow-engine";
13
+ import { PendingEffect } from "@sanity/workflow-engine";
14
+ import type { StartInstanceArgs } from "@sanity/workflow-engine";
15
+ import { TaskStatus } from "@sanity/workflow-engine";
16
+ import type { TestClient } from "@sanity-labs/client-fake-for-test";
17
+ import type { TestClientAccessControl } from "@sanity-labs/client-fake-for-test";
18
+ import type { WorkflowAccess } from "@sanity/workflow-engine";
19
+ import { WorkflowEvaluation } from "@sanity/workflow-engine";
20
+ import { WorkflowInstance } from "@sanity/workflow-engine";
21
+ import type { WorkflowResource } from "@sanity/workflow-engine";
22
+
23
+ /**
24
+ * The bench's default access state — wildcard user + wildcard
25
+ * grants. The bench injects this as the `access` override on every
26
+ * engine call so the underlying TestClient (which has no token, so
27
+ * no `/users/me` resolution path) never needs to do auth resolution.
28
+ *
29
+ * Production code never sees this; pass `createBench({ access })` to
30
+ * test denial paths.
31
+ */
32
+ export declare const ALL_ACCESS: WorkflowAccess;
33
+
34
+ /**
35
+ * The default actor used when tests don't specify one. Has the
36
+ * conventional `"*"` wildcard role — `Action.roles` checks treat
37
+ * `"*"` as matching any required role, and `Task.assignees` of
38
+ * `{kind: "role"}` are satisfied by it too. Production code should
39
+ * never see this id.
40
+ */
41
+ export declare const ALL_ACCESS_USER: Actor;
42
+
43
+ export declare type Bench = BenchState &
44
+ EngineWrappers &
45
+ ReadHelpers &
46
+ GuardHelpers &
47
+ QueryHelpers;
48
+
49
+ declare type BenchCompleteEffectArgs = Omit<
50
+ CompleteEffectArgs,
51
+ "client" | "tags" | "workflowResource" | "access"
52
+ > & {
53
+ access?: WorkflowAccess;
54
+ /** Shortcut for `access.actor`. */
55
+ actor?: Actor;
56
+ };
57
+
58
+ declare type BenchEvaluateArgs = Omit<
59
+ EvaluateArgs,
60
+ "client" | "tags" | "workflowResource" | "access"
61
+ > & {
62
+ access?: WorkflowAccess;
63
+ /** Shortcut for `access.actor`. */
64
+ actor?: Actor;
65
+ /** Shortcut for `access.grants`. */
66
+ grants?: Grant[];
67
+ };
68
+
69
+ declare type BenchFireActionArgs = Omit<
70
+ FireActionArgs,
71
+ "client" | "tags" | "workflowResource" | "access"
72
+ > & {
73
+ access?: WorkflowAccess;
74
+ /** Shortcut for `access.actor`. */
75
+ actor?: Actor;
76
+ /** Shortcut for `access.grants`. */
77
+ grants?: Grant[];
78
+ };
79
+
80
+ declare type BenchStartInstanceArgs = Omit<
81
+ StartInstanceArgs,
82
+ "client" | "tags" | "workflowResource" | "access"
83
+ > & {
84
+ access?: WorkflowAccess;
85
+ /** Shortcut for `access.actor`. */
86
+ actor?: Actor;
87
+ };
88
+
89
+ /** Base bench state, distinct from the engine/read/guard/query helper groups. */
90
+ declare interface BenchState {
91
+ /** The fake Sanity client. Passed automatically by bench wrappers. */
92
+ client: TestClient;
93
+ /**
94
+ * The access state (actor + grants) the bench injects as the
95
+ * `access` override on every wrapped engine call. Defaults to
96
+ * `ALL_ACCESS`.
97
+ */
98
+ access: WorkflowAccess;
99
+ /** Convenience: `access.actor`. */
100
+ currentUser: Actor;
101
+ /** Convenience: `access.grants ?? []`. */
102
+ grants: Grant[];
103
+ /** Add documents to the store after construction. */
104
+ seedDocuments: (documents: StoreDocument[]) => void;
105
+ }
106
+
107
+ declare type BenchTickArgs = Omit<
108
+ OperationArgs,
109
+ "client" | "tags" | "workflowResource" | "access"
110
+ > & {
111
+ access?: WorkflowAccess;
112
+ /** Shortcut for `access.actor`. */
113
+ actor?: Actor;
114
+ };
115
+
116
+ /**
117
+ * Create a fresh bench. By default the bench is fully isolated — two
118
+ * `createBench()` calls have no shared state. Pass `client` + `tags`
119
+ * to two benches to share a store and exercise tag scoping.
120
+ */
121
+ export declare function createBench(options?: CreateBenchOptions): Bench;
122
+
123
+ export declare interface CreateBenchOptions {
124
+ /** Subject documents seeded into the store. */
125
+ documents?: StoreDocument[];
126
+ /**
127
+ * Access state (actor + grants) injected as the engine `access`
128
+ * override on every wrapped call. Defaults to `ALL_ACCESS` —
129
+ * wildcard user + wildcard grants. Pass a restrictive value to
130
+ * test denial paths.
131
+ *
132
+ * Mutually exclusive with the `currentUser` / `grants` shortcuts
133
+ * (which compose into the same shape internally).
134
+ */
135
+ access?: WorkflowAccess;
136
+ /** Shortcut for `access.actor`. Composed with `grants`. */
137
+ currentUser?: Actor;
138
+ /** Shortcut for `access.grants`. Composed with `currentUser`. */
139
+ grants?: Grant[];
140
+ /**
141
+ * Opt-in: simulate Sanity's hard write-gate inside the TestClient.
142
+ *
143
+ * When set, the bench's underlying TestClient:
144
+ * - Exposes `client.request({ url })` serving `/users/me` + the
145
+ * configured ACL path, so the engine can resolve `WorkflowAccess`
146
+ * from "the token" exactly like production.
147
+ * - Rejects writes the grants don't permit with a Sanity-style
148
+ * `WriteAccessDeniedError` — the same 403 a real lake would
149
+ * return.
150
+ *
151
+ * Use this to demonstrate what would happen if the engine's
152
+ * soft-gate were bypassed (or disagreed with) — the lake is still
153
+ * authoritative. The bench's `access` field is the engine-side
154
+ * override; `accessControl` is the lake-side enforcement. They
155
+ * usually carry the same `actor` + `grants` but can be made to
156
+ * disagree to test edge cases.
157
+ *
158
+ * Mutually exclusive with passing a pre-built `client`.
159
+ */
160
+ accessControl?: TestClientAccessControl;
161
+ /**
162
+ * Engine-scope tags. Defaults to `["bench"]`. Pass disjoint sets to two
163
+ * benches sharing a `client` to assert multi-tenant isolation; pass an
164
+ * overlapping set to assert visibility through a shared tag.
165
+ *
166
+ * Tags must be ASCII lowercase + digits + dashes, no leading dash,
167
+ * no dots — validated at construction.
168
+ */
169
+ tags?: string[];
170
+ /**
171
+ * Shared TestClient. If omitted, the bench creates its own — two
172
+ * benches without a shared client are completely isolated, regardless
173
+ * of tags. Pass the same client to two benches to test tag-scoped
174
+ * visibility across a shared store.
175
+ */
176
+ client?: TestClient;
177
+ /**
178
+ * Workflow resource the bench's engine operates against. Mirrors
179
+ * `@sanity/client`'s `ClientConfigResource`. Defaults to
180
+ * `{ type: "dataset", id: "test.test" }` so all bench-minted GDR
181
+ * URIs read as `dataset:test:test:<docId>`.
182
+ */
183
+ workflowResource?: WorkflowResource;
184
+ }
185
+
186
+ /** Default workflow resource for benches that don't pass one. */
187
+ export declare const DEFAULT_WORKFLOW_RESOURCE: WorkflowResource;
188
+
189
+ /** Engine-call wrappers that inject the bench-owned client + tags + access. */
190
+ declare type EngineWrappers = {
191
+ deployDefinitions: (
192
+ args: Omit<DeployDefinitionsArgs, "client" | "tags" | "workflowResource">,
193
+ ) => Promise<DeployDefinitionsResult>;
194
+ startInstance: (args: BenchStartInstanceArgs) => Promise<WorkflowInstance>;
195
+ fireAction: (args: BenchFireActionArgs) => Promise<DispatchResult>;
196
+ completeEffect: (args: BenchCompleteEffectArgs) => Promise<DispatchResult>;
197
+ tick: (args: BenchTickArgs) => Promise<DispatchResult>;
198
+ evaluate: (args: BenchEvaluateArgs) => Promise<WorkflowEvaluation>;
199
+ };
200
+
201
+ declare type GuardHelpers = {
202
+ /** All deployed `temp.system.guard` docs in the store. */
203
+ listGuards: () => Promise<MutationGuardDoc[]>;
204
+ /**
205
+ * Guards that would deny an arbitrary edit to `documentId` right now (i.e.
206
+ * active locks). Probes by changing every non-system field; a lifted guard
207
+ * (predicate `"true"`) returns nothing.
208
+ */
209
+ activeGuardsForDocument: (
210
+ documentId: string,
211
+ as?: Actor,
212
+ ) => Promise<MutationGuardDoc[]>;
213
+ /**
214
+ * Attempt a guarded write to a document — the bench's stand-in for the lake's
215
+ * mutation path. Evaluates deployed guards (incl. `identity()` = the actor)
216
+ * against the before/after image and throws `MutationGuardDeniedError` if any
217
+ * deny; otherwise applies the patch. OPTIMISTIC — not the lake enforcing.
218
+ */
219
+ editDocument: (
220
+ documentId: string,
221
+ patch: Record<string, unknown>,
222
+ options?: {
223
+ action?: MutationGuardAction;
224
+ as?: Actor;
225
+ },
226
+ ) => Promise<StoreDocument>;
227
+ };
228
+
229
+ declare type QueryHelpers = {
230
+ /**
231
+ * Run an arbitrary GROQ query through `workflow.query`. The query MUST
232
+ * filter on the auto-injected `$engineTags` param (e.g.
233
+ * `*[... && count(tags[@ in $engineTags]) > 0]`) or it throws — the
234
+ * engine refuses to run a query that could read across tenant tags.
235
+ * Caller is responsible for narrowing the result.
236
+ */
237
+ query: <T = unknown>(
238
+ groq: string,
239
+ params?: Record<string, unknown>,
240
+ ) => Promise<T>;
241
+ /**
242
+ * Snapshot-aware GROQ for `instanceId` — runs against the same
243
+ * in-memory view the engine's filters see. Reserved params (`$self`,
244
+ * `$subject`, `$parent`, `$ancestors`, `$now`, `$stage`) auto-bound
245
+ * in GDR URI form. Use to ask "what does the engine see for this
246
+ * workflow?" — useful for filter debugging, UIs that mirror engine
247
+ * state, etc.
248
+ */
249
+ queryInScope: <T = unknown>(
250
+ instanceId: string,
251
+ groq: string,
252
+ params?: Record<string, unknown>,
253
+ ) => Promise<T>;
254
+ };
255
+
256
+ declare type ReadHelpers = {
257
+ /** Read the workflow instance by id. Throws if missing. */
258
+ getInstance: (instanceId: string) => Promise<WorkflowInstance>;
259
+ /** Current stage id of an instance. */
260
+ currentStage: (instanceId: string) => Promise<string>;
261
+ /** Pending effects on an instance. */
262
+ pendingEffects: (instanceId: string) => Promise<PendingEffect[]>;
263
+ /** Status of a single task on an instance, or undefined if missing. */
264
+ taskStatus: (
265
+ instanceId: string,
266
+ taskId: string,
267
+ ) => Promise<TaskStatus | undefined>;
268
+ /** EffectsContext entries on an instance, keyed by `key`. */
269
+ effectsContextMap: (
270
+ instanceId: string,
271
+ ) => Promise<Record<string, EffectsContextEntry["value"]>>;
272
+ /**
273
+ * Spawned children of `parentInstanceId`. If `taskId` is provided,
274
+ * only children spawned by that specific task on the parent.
275
+ *
276
+ * Walks `history.spawned` entries on the parent — this is the durable
277
+ * record. (The per-task `spawnedInstances` field on `stages[id == $stage && !defined(exitedAt)][0].tasks[]`
278
+ * gets wiped when the parent transitions past the spawning stage,
279
+ * since `taskStatus` is rebuilt on every transition.)
280
+ *
281
+ * Returns children ordered by `startedAt` ascending.
282
+ */
283
+ children: (
284
+ parentInstanceId: string,
285
+ taskId?: string,
286
+ ) => Promise<WorkflowInstance[]>;
287
+ /**
288
+ * All workflow instances whose `workflow.state.doc.ref` slot named
289
+ * `slotId` (default `"subject"`) matches the given doc id. Useful
290
+ * for "what workflow is running on this document?" questions. Pass
291
+ * `slotId` for workflows whose subject slot uses a different id.
292
+ */
293
+ instancesForSubject: (
294
+ subjectRef: string,
295
+ slotId?: string,
296
+ ) => Promise<WorkflowInstance[]>;
297
+ /**
298
+ * All instances of `workflowId` currently at `stageId`. Includes
299
+ * completed instances (`completedAt != null`) by default; set
300
+ * `{ openOnly: true }` to filter to in-flight only.
301
+ */
302
+ instancesByStage: (
303
+ workflowId: string,
304
+ stageId: string,
305
+ options?: {
306
+ openOnly?: boolean;
307
+ },
308
+ ) => Promise<WorkflowInstance[]>;
309
+ /** Snapshot of all documents in the store — for inspecting the world. */
310
+ snapshot: () => readonly StoreDocument[];
311
+ };
312
+
313
+ /** A document as stored in the bench's fake lake. */
314
+ declare type StoreDocument = {
315
+ _id: string;
316
+ _type: string;
317
+ [key: string]: unknown;
318
+ };
319
+
320
+ /**
321
+ * The default grants used when tests don't specify any. A single
322
+ * permissive grant matches every document and confers all permissions.
323
+ */
324
+ export declare const WILDCARD_GRANTS: Grant[];
325
+
326
+ export {};
@@ -0,0 +1,326 @@
1
+ import { Actor } from "@sanity/workflow-engine";
2
+ import type { CompleteEffectArgs } from "@sanity/workflow-engine";
3
+ import { DeployDefinitionsArgs } from "@sanity/workflow-engine";
4
+ import { DeployDefinitionsResult } from "@sanity/workflow-engine";
5
+ import { DispatchResult } from "@sanity/workflow-engine";
6
+ import { EffectsContextEntry } from "@sanity/workflow-engine";
7
+ import type { EvaluateArgs } from "@sanity/workflow-engine";
8
+ import type { FireActionArgs } from "@sanity/workflow-engine";
9
+ import type { Grant } from "@sanity/workflow-engine";
10
+ import { MutationGuardAction } from "@sanity/workflow-engine";
11
+ import { MutationGuardDoc } from "@sanity/workflow-engine";
12
+ import type { OperationArgs } from "@sanity/workflow-engine";
13
+ import { PendingEffect } from "@sanity/workflow-engine";
14
+ import type { StartInstanceArgs } from "@sanity/workflow-engine";
15
+ import { TaskStatus } from "@sanity/workflow-engine";
16
+ import type { TestClient } from "@sanity-labs/client-fake-for-test";
17
+ import type { TestClientAccessControl } from "@sanity-labs/client-fake-for-test";
18
+ import type { WorkflowAccess } from "@sanity/workflow-engine";
19
+ import { WorkflowEvaluation } from "@sanity/workflow-engine";
20
+ import { WorkflowInstance } from "@sanity/workflow-engine";
21
+ import type { WorkflowResource } from "@sanity/workflow-engine";
22
+
23
+ /**
24
+ * The bench's default access state — wildcard user + wildcard
25
+ * grants. The bench injects this as the `access` override on every
26
+ * engine call so the underlying TestClient (which has no token, so
27
+ * no `/users/me` resolution path) never needs to do auth resolution.
28
+ *
29
+ * Production code never sees this; pass `createBench({ access })` to
30
+ * test denial paths.
31
+ */
32
+ export declare const ALL_ACCESS: WorkflowAccess;
33
+
34
+ /**
35
+ * The default actor used when tests don't specify one. Has the
36
+ * conventional `"*"` wildcard role — `Action.roles` checks treat
37
+ * `"*"` as matching any required role, and `Task.assignees` of
38
+ * `{kind: "role"}` are satisfied by it too. Production code should
39
+ * never see this id.
40
+ */
41
+ export declare const ALL_ACCESS_USER: Actor;
42
+
43
+ export declare type Bench = BenchState &
44
+ EngineWrappers &
45
+ ReadHelpers &
46
+ GuardHelpers &
47
+ QueryHelpers;
48
+
49
+ declare type BenchCompleteEffectArgs = Omit<
50
+ CompleteEffectArgs,
51
+ "client" | "tags" | "workflowResource" | "access"
52
+ > & {
53
+ access?: WorkflowAccess;
54
+ /** Shortcut for `access.actor`. */
55
+ actor?: Actor;
56
+ };
57
+
58
+ declare type BenchEvaluateArgs = Omit<
59
+ EvaluateArgs,
60
+ "client" | "tags" | "workflowResource" | "access"
61
+ > & {
62
+ access?: WorkflowAccess;
63
+ /** Shortcut for `access.actor`. */
64
+ actor?: Actor;
65
+ /** Shortcut for `access.grants`. */
66
+ grants?: Grant[];
67
+ };
68
+
69
+ declare type BenchFireActionArgs = Omit<
70
+ FireActionArgs,
71
+ "client" | "tags" | "workflowResource" | "access"
72
+ > & {
73
+ access?: WorkflowAccess;
74
+ /** Shortcut for `access.actor`. */
75
+ actor?: Actor;
76
+ /** Shortcut for `access.grants`. */
77
+ grants?: Grant[];
78
+ };
79
+
80
+ declare type BenchStartInstanceArgs = Omit<
81
+ StartInstanceArgs,
82
+ "client" | "tags" | "workflowResource" | "access"
83
+ > & {
84
+ access?: WorkflowAccess;
85
+ /** Shortcut for `access.actor`. */
86
+ actor?: Actor;
87
+ };
88
+
89
+ /** Base bench state, distinct from the engine/read/guard/query helper groups. */
90
+ declare interface BenchState {
91
+ /** The fake Sanity client. Passed automatically by bench wrappers. */
92
+ client: TestClient;
93
+ /**
94
+ * The access state (actor + grants) the bench injects as the
95
+ * `access` override on every wrapped engine call. Defaults to
96
+ * `ALL_ACCESS`.
97
+ */
98
+ access: WorkflowAccess;
99
+ /** Convenience: `access.actor`. */
100
+ currentUser: Actor;
101
+ /** Convenience: `access.grants ?? []`. */
102
+ grants: Grant[];
103
+ /** Add documents to the store after construction. */
104
+ seedDocuments: (documents: StoreDocument[]) => void;
105
+ }
106
+
107
+ declare type BenchTickArgs = Omit<
108
+ OperationArgs,
109
+ "client" | "tags" | "workflowResource" | "access"
110
+ > & {
111
+ access?: WorkflowAccess;
112
+ /** Shortcut for `access.actor`. */
113
+ actor?: Actor;
114
+ };
115
+
116
+ /**
117
+ * Create a fresh bench. By default the bench is fully isolated — two
118
+ * `createBench()` calls have no shared state. Pass `client` + `tags`
119
+ * to two benches to share a store and exercise tag scoping.
120
+ */
121
+ export declare function createBench(options?: CreateBenchOptions): Bench;
122
+
123
+ export declare interface CreateBenchOptions {
124
+ /** Subject documents seeded into the store. */
125
+ documents?: StoreDocument[];
126
+ /**
127
+ * Access state (actor + grants) injected as the engine `access`
128
+ * override on every wrapped call. Defaults to `ALL_ACCESS` —
129
+ * wildcard user + wildcard grants. Pass a restrictive value to
130
+ * test denial paths.
131
+ *
132
+ * Mutually exclusive with the `currentUser` / `grants` shortcuts
133
+ * (which compose into the same shape internally).
134
+ */
135
+ access?: WorkflowAccess;
136
+ /** Shortcut for `access.actor`. Composed with `grants`. */
137
+ currentUser?: Actor;
138
+ /** Shortcut for `access.grants`. Composed with `currentUser`. */
139
+ grants?: Grant[];
140
+ /**
141
+ * Opt-in: simulate Sanity's hard write-gate inside the TestClient.
142
+ *
143
+ * When set, the bench's underlying TestClient:
144
+ * - Exposes `client.request({ url })` serving `/users/me` + the
145
+ * configured ACL path, so the engine can resolve `WorkflowAccess`
146
+ * from "the token" exactly like production.
147
+ * - Rejects writes the grants don't permit with a Sanity-style
148
+ * `WriteAccessDeniedError` — the same 403 a real lake would
149
+ * return.
150
+ *
151
+ * Use this to demonstrate what would happen if the engine's
152
+ * soft-gate were bypassed (or disagreed with) — the lake is still
153
+ * authoritative. The bench's `access` field is the engine-side
154
+ * override; `accessControl` is the lake-side enforcement. They
155
+ * usually carry the same `actor` + `grants` but can be made to
156
+ * disagree to test edge cases.
157
+ *
158
+ * Mutually exclusive with passing a pre-built `client`.
159
+ */
160
+ accessControl?: TestClientAccessControl;
161
+ /**
162
+ * Engine-scope tags. Defaults to `["bench"]`. Pass disjoint sets to two
163
+ * benches sharing a `client` to assert multi-tenant isolation; pass an
164
+ * overlapping set to assert visibility through a shared tag.
165
+ *
166
+ * Tags must be ASCII lowercase + digits + dashes, no leading dash,
167
+ * no dots — validated at construction.
168
+ */
169
+ tags?: string[];
170
+ /**
171
+ * Shared TestClient. If omitted, the bench creates its own — two
172
+ * benches without a shared client are completely isolated, regardless
173
+ * of tags. Pass the same client to two benches to test tag-scoped
174
+ * visibility across a shared store.
175
+ */
176
+ client?: TestClient;
177
+ /**
178
+ * Workflow resource the bench's engine operates against. Mirrors
179
+ * `@sanity/client`'s `ClientConfigResource`. Defaults to
180
+ * `{ type: "dataset", id: "test.test" }` so all bench-minted GDR
181
+ * URIs read as `dataset:test:test:<docId>`.
182
+ */
183
+ workflowResource?: WorkflowResource;
184
+ }
185
+
186
+ /** Default workflow resource for benches that don't pass one. */
187
+ export declare const DEFAULT_WORKFLOW_RESOURCE: WorkflowResource;
188
+
189
+ /** Engine-call wrappers that inject the bench-owned client + tags + access. */
190
+ declare type EngineWrappers = {
191
+ deployDefinitions: (
192
+ args: Omit<DeployDefinitionsArgs, "client" | "tags" | "workflowResource">,
193
+ ) => Promise<DeployDefinitionsResult>;
194
+ startInstance: (args: BenchStartInstanceArgs) => Promise<WorkflowInstance>;
195
+ fireAction: (args: BenchFireActionArgs) => Promise<DispatchResult>;
196
+ completeEffect: (args: BenchCompleteEffectArgs) => Promise<DispatchResult>;
197
+ tick: (args: BenchTickArgs) => Promise<DispatchResult>;
198
+ evaluate: (args: BenchEvaluateArgs) => Promise<WorkflowEvaluation>;
199
+ };
200
+
201
+ declare type GuardHelpers = {
202
+ /** All deployed `temp.system.guard` docs in the store. */
203
+ listGuards: () => Promise<MutationGuardDoc[]>;
204
+ /**
205
+ * Guards that would deny an arbitrary edit to `documentId` right now (i.e.
206
+ * active locks). Probes by changing every non-system field; a lifted guard
207
+ * (predicate `"true"`) returns nothing.
208
+ */
209
+ activeGuardsForDocument: (
210
+ documentId: string,
211
+ as?: Actor,
212
+ ) => Promise<MutationGuardDoc[]>;
213
+ /**
214
+ * Attempt a guarded write to a document — the bench's stand-in for the lake's
215
+ * mutation path. Evaluates deployed guards (incl. `identity()` = the actor)
216
+ * against the before/after image and throws `MutationGuardDeniedError` if any
217
+ * deny; otherwise applies the patch. OPTIMISTIC — not the lake enforcing.
218
+ */
219
+ editDocument: (
220
+ documentId: string,
221
+ patch: Record<string, unknown>,
222
+ options?: {
223
+ action?: MutationGuardAction;
224
+ as?: Actor;
225
+ },
226
+ ) => Promise<StoreDocument>;
227
+ };
228
+
229
+ declare type QueryHelpers = {
230
+ /**
231
+ * Run an arbitrary GROQ query through `workflow.query`. The query MUST
232
+ * filter on the auto-injected `$engineTags` param (e.g.
233
+ * `*[... && count(tags[@ in $engineTags]) > 0]`) or it throws — the
234
+ * engine refuses to run a query that could read across tenant tags.
235
+ * Caller is responsible for narrowing the result.
236
+ */
237
+ query: <T = unknown>(
238
+ groq: string,
239
+ params?: Record<string, unknown>,
240
+ ) => Promise<T>;
241
+ /**
242
+ * Snapshot-aware GROQ for `instanceId` — runs against the same
243
+ * in-memory view the engine's filters see. Reserved params (`$self`,
244
+ * `$subject`, `$parent`, `$ancestors`, `$now`, `$stage`) auto-bound
245
+ * in GDR URI form. Use to ask "what does the engine see for this
246
+ * workflow?" — useful for filter debugging, UIs that mirror engine
247
+ * state, etc.
248
+ */
249
+ queryInScope: <T = unknown>(
250
+ instanceId: string,
251
+ groq: string,
252
+ params?: Record<string, unknown>,
253
+ ) => Promise<T>;
254
+ };
255
+
256
+ declare type ReadHelpers = {
257
+ /** Read the workflow instance by id. Throws if missing. */
258
+ getInstance: (instanceId: string) => Promise<WorkflowInstance>;
259
+ /** Current stage id of an instance. */
260
+ currentStage: (instanceId: string) => Promise<string>;
261
+ /** Pending effects on an instance. */
262
+ pendingEffects: (instanceId: string) => Promise<PendingEffect[]>;
263
+ /** Status of a single task on an instance, or undefined if missing. */
264
+ taskStatus: (
265
+ instanceId: string,
266
+ taskId: string,
267
+ ) => Promise<TaskStatus | undefined>;
268
+ /** EffectsContext entries on an instance, keyed by `key`. */
269
+ effectsContextMap: (
270
+ instanceId: string,
271
+ ) => Promise<Record<string, EffectsContextEntry["value"]>>;
272
+ /**
273
+ * Spawned children of `parentInstanceId`. If `taskId` is provided,
274
+ * only children spawned by that specific task on the parent.
275
+ *
276
+ * Walks `history.spawned` entries on the parent — this is the durable
277
+ * record. (The per-task `spawnedInstances` field on `stages[id == $stage && !defined(exitedAt)][0].tasks[]`
278
+ * gets wiped when the parent transitions past the spawning stage,
279
+ * since `taskStatus` is rebuilt on every transition.)
280
+ *
281
+ * Returns children ordered by `startedAt` ascending.
282
+ */
283
+ children: (
284
+ parentInstanceId: string,
285
+ taskId?: string,
286
+ ) => Promise<WorkflowInstance[]>;
287
+ /**
288
+ * All workflow instances whose `workflow.state.doc.ref` slot named
289
+ * `slotId` (default `"subject"`) matches the given doc id. Useful
290
+ * for "what workflow is running on this document?" questions. Pass
291
+ * `slotId` for workflows whose subject slot uses a different id.
292
+ */
293
+ instancesForSubject: (
294
+ subjectRef: string,
295
+ slotId?: string,
296
+ ) => Promise<WorkflowInstance[]>;
297
+ /**
298
+ * All instances of `workflowId` currently at `stageId`. Includes
299
+ * completed instances (`completedAt != null`) by default; set
300
+ * `{ openOnly: true }` to filter to in-flight only.
301
+ */
302
+ instancesByStage: (
303
+ workflowId: string,
304
+ stageId: string,
305
+ options?: {
306
+ openOnly?: boolean;
307
+ },
308
+ ) => Promise<WorkflowInstance[]>;
309
+ /** Snapshot of all documents in the store — for inspecting the world. */
310
+ snapshot: () => readonly StoreDocument[];
311
+ };
312
+
313
+ /** A document as stored in the bench's fake lake. */
314
+ declare type StoreDocument = {
315
+ _id: string;
316
+ _type: string;
317
+ [key: string]: unknown;
318
+ };
319
+
320
+ /**
321
+ * The default grants used when tests don't specify any. A single
322
+ * permissive grant matches every document and confers all permissions.
323
+ */
324
+ export declare const WILDCARD_GRANTS: Grant[];
325
+
326
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,184 @@
1
+ import { workflow, denyingGuards, MutationGuardDeniedError, validateTags } from "@sanity/workflow-engine";
2
+ import { createTestClient } from "@sanity-labs/client-fake-for-test";
3
+ const DEFAULT_WORKFLOW_RESOURCE = {
4
+ type: "dataset",
5
+ id: "test.test"
6
+ }, ALL_ACCESS_USER = {
7
+ kind: "user",
8
+ id: "bench-user",
9
+ roles: ["*"]
10
+ }, WILDCARD_GRANTS = [
11
+ { filter: "true", permissions: ["create", "read", "update"] }
12
+ ], ALL_ACCESS = {
13
+ actor: ALL_ACCESS_USER,
14
+ grants: WILDCARD_GRANTS
15
+ };
16
+ function resolveBenchClient(options) {
17
+ if (options.accessControl !== void 0 && options.client !== void 0)
18
+ throw new Error(
19
+ "createBench: `accessControl` and `client` are mutually exclusive. Either let the bench build a TestClient configured with the supplied accessControl, or build the client yourself with `createTestClient({ accessControl })` and pass it in."
20
+ );
21
+ const client = options.client ?? createTestClient(
22
+ options.accessControl !== void 0 ? { accessControl: options.accessControl } : {}
23
+ );
24
+ return options.documents && client.add(options.documents), client;
25
+ }
26
+ function resolveBenchAccess(options) {
27
+ return options.access ?? {
28
+ actor: options.currentUser ?? ALL_ACCESS_USER,
29
+ grants: options.grants ?? WILDCARD_GRANTS
30
+ };
31
+ }
32
+ function accessFor(access, args, canTokenResolve) {
33
+ if (args.access !== void 0) return args.access;
34
+ if (args.actor === void 0 && args.grants === void 0) return access;
35
+ const inheritedGrants = canTokenResolve ? void 0 : access.grants, grants = args.grants ?? inheritedGrants;
36
+ return {
37
+ actor: args.actor ?? access.actor,
38
+ ...grants !== void 0 ? { grants } : {}
39
+ };
40
+ }
41
+ function stripAccessShortcuts(args) {
42
+ const rest = { ...args };
43
+ return delete rest.access, delete rest.actor, delete rest.grants, rest;
44
+ }
45
+ function createEngineWrappers(scope, access) {
46
+ const canTokenResolve = "request" in scope.client, withAccess = (args) => ({
47
+ ...scope,
48
+ access: accessFor(access, args, canTokenResolve),
49
+ ...stripAccessShortcuts(args)
50
+ });
51
+ return {
52
+ deployDefinitions: (args) => workflow.deployDefinitions({ ...scope, ...args }),
53
+ startInstance: (args) => workflow.startInstance(withAccess(args)),
54
+ fireAction: (args) => workflow.fireAction(withAccess(args)),
55
+ completeEffect: (args) => workflow.completeEffect(withAccess(args)),
56
+ tick: (args) => workflow.tick(withAccess(args)),
57
+ evaluate: (args) => workflow.evaluate(withAccess(args))
58
+ };
59
+ }
60
+ function createGuardHelpers(client) {
61
+ const fetchGuards = () => client.fetch('*[_type == "temp.system.guard"]'), guardDocRef = (documentId, before) => ({
62
+ id: documentId,
63
+ ...before?._type ? { type: before._type } : {}
64
+ });
65
+ return {
66
+ listGuards: () => client.fetch('*[_type == "temp.system.guard"] | order(_id asc)'),
67
+ activeGuardsForDocument: async (documentId, as) => {
68
+ const guards = await fetchGuards(), before = await client.getDocument(documentId), after = { ...before ?? { _id: documentId, _type: "unknown" } };
69
+ for (const key of Object.keys(after))
70
+ key.startsWith("_") || (after[key] = { __guardProbe: !0 });
71
+ return after.__guardProbe = { __guardProbe: !0 }, denyingGuards({
72
+ guards,
73
+ doc: guardDocRef(documentId, before),
74
+ context: {
75
+ before: before ?? null,
76
+ after,
77
+ action: "update",
78
+ ...as ? { identity: as.id } : {}
79
+ }
80
+ });
81
+ },
82
+ editDocument: async (documentId, patch, editOptions) => {
83
+ const action = editOptions?.action ?? "update", as = editOptions?.as, before = await client.getDocument(documentId), after = action === "delete" ? null : { ...before ?? { _id: documentId, _type: "unknown" }, ...patch }, guards = await fetchGuards(), denied = await denyingGuards({
84
+ guards,
85
+ doc: guardDocRef(documentId, before),
86
+ context: {
87
+ before: before ?? null,
88
+ after,
89
+ action,
90
+ ...as ? { identity: as.id } : {}
91
+ }
92
+ });
93
+ if (denied.length > 0)
94
+ throw new MutationGuardDeniedError({
95
+ documentId,
96
+ action,
97
+ denied: denied.map((g) => ({ guardId: g._id, ...g.name ? { name: g.name } : {} }))
98
+ });
99
+ return await client.patch(documentId).set(patch).commit(), await client.getDocument(documentId);
100
+ }
101
+ };
102
+ }
103
+ function createQueryHelpers(scope) {
104
+ const withParams = (params) => params !== void 0 ? { params } : {};
105
+ return {
106
+ query: (groq, params) => workflow.query({ ...scope, groq, ...withParams(params) }),
107
+ queryInScope: (instanceId, groq, params) => workflow.queryInScope({ ...scope, instanceId, groq, ...withParams(params) })
108
+ };
109
+ }
110
+ function createReadHelpers(scope) {
111
+ const { client, tags } = scope, intersectsTags = (docTags) => docTags !== void 0 && tags.some((t) => docTags.includes(t)), getInstance = async (instanceId) => {
112
+ const doc = await client.getDocument(instanceId);
113
+ if (!doc) throw new Error(`Workflow instance ${instanceId} not found`);
114
+ if (!intersectsTags(doc.tags))
115
+ throw new Error(`Workflow instance ${instanceId} not visible to this engine (tag mismatch)`);
116
+ return doc;
117
+ };
118
+ return {
119
+ getInstance,
120
+ currentStage: async (instanceId) => (await getInstance(instanceId)).currentStageId,
121
+ pendingEffects: async (instanceId) => (await getInstance(instanceId)).pendingEffects,
122
+ taskStatus: async (instanceId, taskId) => {
123
+ const instance = await getInstance(instanceId);
124
+ return instance.stages.find(
125
+ (s) => s.id === instance.currentStageId && s.exitedAt === void 0
126
+ )?.tasks.find((t) => t.id === taskId)?.status;
127
+ },
128
+ effectsContextMap: async (instanceId) => {
129
+ const instance = await getInstance(instanceId), map = {};
130
+ for (const entry of instance.effectsContext) map[entry.id] = entry.value;
131
+ return map;
132
+ },
133
+ children: async (parentInstanceId, taskId) => workflow.children({
134
+ ...scope,
135
+ instanceId: parentInstanceId,
136
+ ...taskId !== void 0 ? { taskId } : {}
137
+ }),
138
+ instancesForSubject: async (subjectRef, slotId = "subject") => (
139
+ // Subjects live on a workflow-scope `state[id == $slot]` doc.ref
140
+ // slot. Match instances whose slot's value.id equals the supplied
141
+ // ref. The slot id is conventionally "subject" but workflows may
142
+ // name it otherwise.
143
+ client.fetch(
144
+ `*[_type == "workflow.instance"
145
+ && state[_type == "workflow.state.doc.ref" && id == $slot][0].value.id == $ref
146
+ && count(tags[@ in $engineTags]) > 0] | order(startedAt asc)`,
147
+ { ref: subjectRef, slot: slotId, engineTags: tags }
148
+ )
149
+ ),
150
+ instancesByStage: async (workflowId, stageId, queryOptions) => {
151
+ const filter = queryOptions?.openOnly ? " && completedAt == null" : "";
152
+ return client.fetch(
153
+ `*[_type == "workflow.instance" && workflowId == $wf && currentStageId == $stage && count(tags[@ in $engineTags]) > 0${filter}]
154
+ | order(startedAt asc)`,
155
+ { wf: workflowId, stage: stageId, engineTags: tags }
156
+ );
157
+ },
158
+ snapshot: () => client.snapshot()
159
+ };
160
+ }
161
+ function createBench(options = {}) {
162
+ const tags = options.tags ?? ["bench"];
163
+ validateTags(tags);
164
+ const workflowResource = options.workflowResource ?? DEFAULT_WORKFLOW_RESOURCE, client = resolveBenchClient(options), scope = { client, tags, workflowResource }, access = resolveBenchAccess(options);
165
+ return {
166
+ client,
167
+ access,
168
+ currentUser: access.actor,
169
+ grants: access.grants ?? [],
170
+ seedDocuments: (documents) => client.add(documents),
171
+ ...createEngineWrappers(scope, access),
172
+ ...createReadHelpers(scope),
173
+ ...createGuardHelpers(client),
174
+ ...createQueryHelpers(scope)
175
+ };
176
+ }
177
+ export {
178
+ ALL_ACCESS,
179
+ ALL_ACCESS_USER,
180
+ DEFAULT_WORKFLOW_RESOURCE,
181
+ WILDCARD_GRANTS,
182
+ createBench
183
+ };
184
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/constants.ts","../src/access.ts","../src/engine-wrappers.ts","../src/guard-helpers.ts","../src/query-helpers.ts","../src/read-helpers.ts","../src/index.ts"],"sourcesContent":["import type {Actor, Grant, WorkflowAccess, WorkflowResource} from '@sanity/workflow-engine'\n\n/** Default workflow resource for benches that don't pass one. */\nexport const DEFAULT_WORKFLOW_RESOURCE: WorkflowResource = {\n type: 'dataset',\n id: 'test.test',\n}\n\n/**\n * The default actor used when tests don't specify one. Has the\n * conventional `\"*\"` wildcard role — `Action.roles` checks treat\n * `\"*\"` as matching any required role, and `Task.assignees` of\n * `{kind: \"role\"}` are satisfied by it too. Production code should\n * never see this id.\n */\nexport const ALL_ACCESS_USER: Actor = {\n kind: 'user',\n id: 'bench-user',\n roles: ['*'],\n}\n\n/**\n * The default grants used when tests don't specify any. A single\n * permissive grant matches every document and confers all permissions.\n */\nexport const WILDCARD_GRANTS: Grant[] = [\n {filter: 'true', permissions: ['create', 'read', 'update']},\n]\n\n/**\n * The bench's default access state — wildcard user + wildcard\n * grants. The bench injects this as the `access` override on every\n * engine call so the underlying TestClient (which has no token, so\n * no `/users/me` resolution path) never needs to do auth resolution.\n *\n * Production code never sees this; pass `createBench({ access })` to\n * test denial paths.\n */\nexport const ALL_ACCESS: WorkflowAccess = {\n actor: ALL_ACCESS_USER,\n grants: WILDCARD_GRANTS,\n}\n","import type {TestClient, TestClientAccessControl} from '@sanity-labs/client-fake-for-test'\nimport {createTestClient} from '@sanity-labs/client-fake-for-test'\nimport type {Actor, Grant, WorkflowAccess} from '@sanity/workflow-engine'\n\nimport {ALL_ACCESS_USER, WILDCARD_GRANTS} from './constants.ts'\nimport type {AccessShortcuts, StoreDocument} from './types.ts'\n\n/**\n * Build the TestClient the bench operates against, enforcing the\n * `client` / `accessControl` mutual-exclusion contract: callers either\n * supply their own client or have the bench mint one (optionally wired\n * with lake-side access control), never both.\n */\nexport function resolveBenchClient(options: {\n client?: TestClient\n accessControl?: TestClientAccessControl\n documents?: StoreDocument[]\n}): TestClient {\n if (options.accessControl !== undefined && options.client !== undefined) {\n throw new Error(\n 'createBench: `accessControl` and `client` are mutually exclusive. Either let the bench ' +\n 'build a TestClient configured with the supplied accessControl, or build the client ' +\n 'yourself with `createTestClient({ accessControl })` and pass it in.',\n )\n }\n const client =\n options.client ??\n createTestClient(\n options.accessControl !== undefined ? {accessControl: options.accessControl} : {},\n )\n if (options.documents) client.add(options.documents)\n return client\n}\n\n/**\n * Compose the bench-wide access default. Precedence: the canonical\n * `access` shape, then the `currentUser` / `grants` shortcuts, then\n * {@link ALL_ACCESS_USER} + {@link WILDCARD_GRANTS}.\n */\nexport function resolveBenchAccess(options: {\n access?: WorkflowAccess\n currentUser?: Actor\n grants?: Grant[]\n}): WorkflowAccess {\n return (\n options.access ?? {\n actor: options.currentUser ?? ALL_ACCESS_USER,\n grants: options.grants ?? WILDCARD_GRANTS,\n }\n )\n}\n\n/**\n * Resolve the per-call access override against the bench-wide default.\n * A call may tighten or replace access via the canonical `{ access }`\n * field or the `{ actor, grants }` shortcuts.\n *\n * `canTokenResolve` is true when the bench's client was built with\n * `accessControl` — i.e. its `request` hook can resolve grants from\n * the ACL path. In that case an actor-only override (no grants) must\n * NOT inherit the bench's default grants: doing so would gate the\n * per-call actor with the default's (e.g. wildcard) grants instead of\n * that actor's real grants, silently false-greening denial tests. The\n * engine treats verb `access` as `WorkflowAccessOverride`, so the bench\n * hands it `{ actor }` alone and lets the engine token-resolve grants\n * (via the call's `grantsFromPath`) or skip the gate — both faithful to\n * production. Tokenless default benches keep inheriting the default\n * grants, since there is no other source.\n */\nexport function accessFor(\n access: WorkflowAccess,\n args: AccessShortcuts,\n canTokenResolve: boolean,\n): WorkflowAccess {\n if (args.access !== undefined) return args.access\n if (args.actor === undefined && args.grants === undefined) return access\n const inheritedGrants = canTokenResolve ? undefined : access.grants\n const grants = args.grants ?? inheritedGrants\n return {\n actor: args.actor ?? access.actor,\n ...(grants !== undefined ? {grants} : {}),\n }\n}\n\n/**\n * Strip the bench-only access shortcut fields before spreading the rest\n * of a call's args into an engine call, so the composed `access` is the\n * single source of truth.\n */\nexport function stripAccessShortcuts<T extends AccessShortcuts>(\n args: T,\n): Omit<T, 'access' | 'actor' | 'grants'> {\n const rest = {...args}\n delete rest.access\n delete rest.actor\n delete rest.grants\n return rest\n}\n","import {\n workflow,\n type DeployDefinitionsArgs,\n type DeployDefinitionsResult,\n type DispatchResult,\n type WorkflowAccess,\n type WorkflowEvaluation,\n type WorkflowInstance,\n} from '@sanity/workflow-engine'\n\nimport {accessFor, stripAccessShortcuts} from './access.ts'\nimport type {\n AccessShortcuts,\n BenchCompleteEffectArgs,\n BenchEvaluateArgs,\n BenchFireActionArgs,\n BenchStartInstanceArgs,\n BenchTickArgs,\n EngineScope,\n} from './types.ts'\n\n/** Engine-call wrappers that inject the bench-owned client + tags + access. */\nexport type EngineWrappers = {\n deployDefinitions: (\n args: Omit<DeployDefinitionsArgs, 'client' | 'tags' | 'workflowResource'>,\n ) => Promise<DeployDefinitionsResult>\n startInstance: (args: BenchStartInstanceArgs) => Promise<WorkflowInstance>\n fireAction: (args: BenchFireActionArgs) => Promise<DispatchResult>\n completeEffect: (args: BenchCompleteEffectArgs) => Promise<DispatchResult>\n tick: (args: BenchTickArgs) => Promise<DispatchResult>\n evaluate: (args: BenchEvaluateArgs) => Promise<WorkflowEvaluation>\n}\n\n/**\n * Build the engine-call wrappers. Each access-aware wrapper composes the\n * call's `access` from the bench default plus any per-call override, then\n * spreads the remaining args into the engine with the bench-owned scope.\n */\nexport function createEngineWrappers(scope: EngineScope, access: WorkflowAccess): EngineWrappers {\n // The fake client only exposes an own `request` hook when built with\n // `accessControl`; that hook is what lets the engine token-resolve\n // grants from the ACL path. A plain bench client has no such hook.\n const canTokenResolve = 'request' in scope.client\n const withAccess = <T extends AccessShortcuts>(args: T) => ({\n ...scope,\n access: accessFor(access, args, canTokenResolve),\n ...stripAccessShortcuts(args),\n })\n return {\n deployDefinitions: (args) => workflow.deployDefinitions({...scope, ...args}),\n startInstance: (args) => workflow.startInstance(withAccess(args)),\n fireAction: (args) => workflow.fireAction(withAccess(args)),\n completeEffect: (args) => workflow.completeEffect(withAccess(args)),\n tick: (args) => workflow.tick(withAccess(args)),\n evaluate: (args) => workflow.evaluate(withAccess(args)),\n }\n}\n","import type {TestClient} from '@sanity-labs/client-fake-for-test'\nimport {\n denyingGuards,\n MutationGuardDeniedError,\n type Actor,\n type MutationGuardAction,\n type MutationGuardDoc,\n} from '@sanity/workflow-engine'\n\nimport type {StoreDocument} from './types.ts'\n\nexport type GuardHelpers = {\n /** All deployed `temp.system.guard` docs in the store. */\n listGuards: () => Promise<MutationGuardDoc[]>\n\n /**\n * Guards that would deny an arbitrary edit to `documentId` right now (i.e.\n * active locks). Probes by changing every non-system field; a lifted guard\n * (predicate `\"true\"`) returns nothing.\n */\n activeGuardsForDocument: (documentId: string, as?: Actor) => Promise<MutationGuardDoc[]>\n\n /**\n * Attempt a guarded write to a document — the bench's stand-in for the lake's\n * mutation path. Evaluates deployed guards (incl. `identity()` = the actor)\n * against the before/after image and throws `MutationGuardDeniedError` if any\n * deny; otherwise applies the patch. OPTIMISTIC — not the lake enforcing.\n */\n editDocument: (\n documentId: string,\n patch: Record<string, unknown>,\n options?: {action?: MutationGuardAction; as?: Actor},\n ) => Promise<StoreDocument>\n}\n\n/**\n * Build the optimistic, bench-enforced lake mutation-guard helpers. These\n * stand in for the lake's mutation path: they evaluate deployed guards\n * against a before/after image rather than the lake enforcing.\n */\nexport function createGuardHelpers(client: TestClient): GuardHelpers {\n const fetchGuards = () => client.fetch<MutationGuardDoc[]>(`*[_type == \"temp.system.guard\"]`)\n\n const guardDocRef = (documentId: string, before: StoreDocument | null | undefined) => ({\n id: documentId,\n ...(before?._type ? {type: before._type} : {}),\n })\n\n return {\n listGuards: () =>\n client.fetch<MutationGuardDoc[]>(`*[_type == \"temp.system.guard\"] | order(_id asc)`),\n\n activeGuardsForDocument: async (documentId, as) => {\n const guards = await fetchGuards()\n const before = await client.getDocument<StoreDocument>(documentId)\n const after: Record<string, unknown> = {...(before ?? {_id: documentId, _type: 'unknown'})}\n for (const key of Object.keys(after)) {\n if (!key.startsWith('_')) after[key] = {__guardProbe: true}\n }\n after.__guardProbe = {__guardProbe: true}\n return denyingGuards({\n guards,\n doc: guardDocRef(documentId, before),\n context: {\n before: before ?? null,\n after: after as {_id: string; _type: string},\n action: 'update',\n ...(as ? {identity: as.id} : {}),\n },\n })\n },\n\n editDocument: async (documentId, patch, editOptions) => {\n const action = editOptions?.action ?? 'update'\n const as = editOptions?.as\n const before = await client.getDocument<StoreDocument>(documentId)\n const after =\n action === 'delete' ? null : {...(before ?? {_id: documentId, _type: 'unknown'}), ...patch}\n const guards = await fetchGuards()\n const denied = await denyingGuards({\n guards,\n doc: guardDocRef(documentId, before),\n context: {\n before: before ?? null,\n after,\n action,\n ...(as ? {identity: as.id} : {}),\n },\n })\n if (denied.length > 0) {\n throw new MutationGuardDeniedError({\n documentId,\n action,\n denied: denied.map((g) => ({guardId: g._id, ...(g.name ? {name: g.name} : {})})),\n })\n }\n await client.patch(documentId).set(patch).commit()\n return (await client.getDocument(documentId)) as StoreDocument\n },\n }\n}\n","import {workflow} from '@sanity/workflow-engine'\n\nimport type {EngineScope} from './types.ts'\n\nexport type QueryHelpers = {\n /**\n * Run an arbitrary GROQ query through `workflow.query`. The query MUST\n * filter on the auto-injected `$engineTags` param (e.g.\n * `*[... && count(tags[@ in $engineTags]) > 0]`) or it throws — the\n * engine refuses to run a query that could read across tenant tags.\n * Caller is responsible for narrowing the result.\n */\n query: <T = unknown>(groq: string, params?: Record<string, unknown>) => Promise<T>\n\n /**\n * Snapshot-aware GROQ for `instanceId` — runs against the same\n * in-memory view the engine's filters see. Reserved params (`$self`,\n * `$subject`, `$parent`, `$ancestors`, `$now`, `$stage`) auto-bound\n * in GDR URI form. Use to ask \"what does the engine see for this\n * workflow?\" — useful for filter debugging, UIs that mirror engine\n * state, etc.\n */\n queryInScope: <T = unknown>(\n instanceId: string,\n groq: string,\n params?: Record<string, unknown>,\n ) => Promise<T>\n}\n\n/** Build the GROQ passthrough helpers, both tag-scoped via the bench scope. */\nexport function createQueryHelpers(scope: EngineScope): QueryHelpers {\n const withParams = (params?: Record<string, unknown>) => (params !== undefined ? {params} : {})\n return {\n query: <T = unknown>(groq: string, params?: Record<string, unknown>) =>\n workflow.query<T>({...scope, groq, ...withParams(params)}),\n\n queryInScope: <T = unknown>(\n instanceId: string,\n groq: string,\n params?: Record<string, unknown>,\n ) => workflow.queryInScope<T>({...scope, instanceId, groq, ...withParams(params)}),\n }\n}\n","import {\n workflow,\n type EffectsContextEntry,\n type PendingEffect,\n type TaskStatus,\n type WorkflowInstance,\n} from '@sanity/workflow-engine'\n\nimport type {EngineScope, StoreDocument} from './types.ts'\n\nexport type ReadHelpers = {\n /** Read the workflow instance by id. Throws if missing. */\n getInstance: (instanceId: string) => Promise<WorkflowInstance>\n\n /** Current stage id of an instance. */\n currentStage: (instanceId: string) => Promise<string>\n\n /** Pending effects on an instance. */\n pendingEffects: (instanceId: string) => Promise<PendingEffect[]>\n\n /** Status of a single task on an instance, or undefined if missing. */\n taskStatus: (instanceId: string, taskId: string) => Promise<TaskStatus | undefined>\n\n /** EffectsContext entries on an instance, keyed by `key`. */\n effectsContextMap: (instanceId: string) => Promise<Record<string, EffectsContextEntry['value']>>\n\n /**\n * Spawned children of `parentInstanceId`. If `taskId` is provided,\n * only children spawned by that specific task on the parent.\n *\n * Walks `history.spawned` entries on the parent — this is the durable\n * record. (The per-task `spawnedInstances` field on `stages[id == $stage && !defined(exitedAt)][0].tasks[]`\n * gets wiped when the parent transitions past the spawning stage,\n * since `taskStatus` is rebuilt on every transition.)\n *\n * Returns children ordered by `startedAt` ascending.\n */\n children: (parentInstanceId: string, taskId?: string) => Promise<WorkflowInstance[]>\n\n /**\n * All workflow instances whose `workflow.state.doc.ref` slot named\n * `slotId` (default `\"subject\"`) matches the given doc id. Useful\n * for \"what workflow is running on this document?\" questions. Pass\n * `slotId` for workflows whose subject slot uses a different id.\n */\n instancesForSubject: (subjectRef: string, slotId?: string) => Promise<WorkflowInstance[]>\n\n /**\n * All instances of `workflowId` currently at `stageId`. Includes\n * completed instances (`completedAt != null`) by default; set\n * `{ openOnly: true }` to filter to in-flight only.\n */\n instancesByStage: (\n workflowId: string,\n stageId: string,\n options?: {openOnly?: boolean},\n ) => Promise<WorkflowInstance[]>\n\n /** Snapshot of all documents in the store — for inspecting the world. */\n snapshot: () => readonly StoreDocument[]\n}\n\n/**\n * Build the read-side DX helpers. {@link ReadHelpers.getInstance} enforces\n * tag-scoped visibility so every derived read (`currentStage`, `taskStatus`,\n * …) inherits the same isolation the engine's filters apply.\n */\nexport function createReadHelpers(scope: EngineScope): ReadHelpers {\n const {client, tags} = scope\n\n const intersectsTags = (docTags: string[] | undefined): boolean =>\n docTags !== undefined && tags.some((t) => docTags.includes(t))\n\n const getInstance = async (instanceId: string): Promise<WorkflowInstance> => {\n const doc = await client.getDocument<WorkflowInstance>(instanceId)\n if (!doc) throw new Error(`Workflow instance ${instanceId} not found`)\n if (!intersectsTags(doc.tags)) {\n throw new Error(`Workflow instance ${instanceId} not visible to this engine (tag mismatch)`)\n }\n return doc\n }\n\n return {\n getInstance,\n\n currentStage: async (instanceId) => (await getInstance(instanceId)).currentStageId,\n\n pendingEffects: async (instanceId) => (await getInstance(instanceId)).pendingEffects,\n\n taskStatus: async (instanceId, taskId) => {\n const instance = await getInstance(instanceId)\n const stage = instance.stages.find(\n (s) => s.id === instance.currentStageId && s.exitedAt === undefined,\n )\n return stage?.tasks.find((t) => t.id === taskId)?.status\n },\n\n effectsContextMap: async (instanceId) => {\n const instance = await getInstance(instanceId)\n const map: Record<string, EffectsContextEntry['value']> = {}\n for (const entry of instance.effectsContext) map[entry.id] = entry.value\n return map\n },\n\n children: async (parentInstanceId, taskId) =>\n workflow.children({\n ...scope,\n instanceId: parentInstanceId,\n ...(taskId !== undefined ? {taskId} : {}),\n }),\n\n instancesForSubject: async (subjectRef, slotId = 'subject') =>\n // Subjects live on a workflow-scope `state[id == $slot]` doc.ref\n // slot. Match instances whose slot's value.id equals the supplied\n // ref. The slot id is conventionally \"subject\" but workflows may\n // name it otherwise.\n client.fetch<WorkflowInstance[]>(\n `*[_type == \"workflow.instance\"\n && state[_type == \"workflow.state.doc.ref\" && id == $slot][0].value.id == $ref\n && count(tags[@ in $engineTags]) > 0] | order(startedAt asc)`,\n {ref: subjectRef, slot: slotId, engineTags: tags},\n ),\n\n instancesByStage: async (workflowId, stageId, queryOptions) => {\n const filter = queryOptions?.openOnly ? ' && completedAt == null' : ''\n return client.fetch<WorkflowInstance[]>(\n `*[_type == \"workflow.instance\" && workflowId == $wf && currentStageId == $stage && count(tags[@ in $engineTags]) > 0${filter}]\n | order(startedAt asc)`,\n {wf: workflowId, stage: stageId, engineTags: tags},\n )\n },\n\n snapshot: () => client.snapshot() as readonly StoreDocument[],\n }\n}\n","/**\n * The bench — a dry-run test harness wrapping `@sanity-labs/client-fake-for-test`.\n *\n * The bench is the **only** place in the package that fabricates an\n * actor identity. The engine itself requires an explicit `actor` on\n * every call; the bench provides a default `ALL_ACCESS_USER` so tests\n * don't have to thread one manually, and exposes wrapper methods that\n * inject `bench.currentUser` + `bench.grants` for every engine call.\n *\n * ```\n * const bench = createBench();\n * await bench.deployDefinitions({ definitions: [definition] });\n * const inst = await bench.startInstance({ workflowId: \"...\", subject: {...} });\n * await bench.fireAction({ instanceId: inst._id, taskId: \"...\", action: \"...\" });\n * expect(await bench.currentStage(inst._id)).toBe(\"ready\");\n * ```\n *\n * For denial-path tests, pass an explicit `currentUser` and/or `grants`:\n *\n * ```\n * const bench = createBench({\n * currentUser: { kind: \"user\", id: \"viewer\", roles: [\"viewer\"] },\n * grants: [{ filter: '_type == \"workflow.instance\"', permissions: [\"read\"] }],\n * });\n * await expect(bench.fireAction({...})).rejects.toBeInstanceOf(ActionDisabledError);\n * ```\n */\n\nimport type {TestClient, TestClientAccessControl} from '@sanity-labs/client-fake-for-test'\nimport {validateTags} from '@sanity/workflow-engine'\nimport type {Actor, Grant, WorkflowAccess, WorkflowResource} from '@sanity/workflow-engine'\n\nimport {resolveBenchAccess, resolveBenchClient} from './access.ts'\nimport {DEFAULT_WORKFLOW_RESOURCE} from './constants.ts'\nimport {createEngineWrappers, type EngineWrappers} from './engine-wrappers.ts'\nimport {createGuardHelpers, type GuardHelpers} from './guard-helpers.ts'\nimport {createQueryHelpers, type QueryHelpers} from './query-helpers.ts'\nimport {createReadHelpers, type ReadHelpers} from './read-helpers.ts'\nimport type {StoreDocument} from './types.ts'\n\nexport {\n ALL_ACCESS,\n ALL_ACCESS_USER,\n DEFAULT_WORKFLOW_RESOURCE,\n WILDCARD_GRANTS,\n} from './constants.ts'\n\nexport interface CreateBenchOptions {\n /** Subject documents seeded into the store. */\n documents?: StoreDocument[]\n /**\n * Access state (actor + grants) injected as the engine `access`\n * override on every wrapped call. Defaults to `ALL_ACCESS` —\n * wildcard user + wildcard grants. Pass a restrictive value to\n * test denial paths.\n *\n * Mutually exclusive with the `currentUser` / `grants` shortcuts\n * (which compose into the same shape internally).\n */\n access?: WorkflowAccess\n /** Shortcut for `access.actor`. Composed with `grants`. */\n currentUser?: Actor\n /** Shortcut for `access.grants`. Composed with `currentUser`. */\n grants?: Grant[]\n /**\n * Opt-in: simulate Sanity's hard write-gate inside the TestClient.\n *\n * When set, the bench's underlying TestClient:\n * - Exposes `client.request({ url })` serving `/users/me` + the\n * configured ACL path, so the engine can resolve `WorkflowAccess`\n * from \"the token\" exactly like production.\n * - Rejects writes the grants don't permit with a Sanity-style\n * `WriteAccessDeniedError` — the same 403 a real lake would\n * return.\n *\n * Use this to demonstrate what would happen if the engine's\n * soft-gate were bypassed (or disagreed with) — the lake is still\n * authoritative. The bench's `access` field is the engine-side\n * override; `accessControl` is the lake-side enforcement. They\n * usually carry the same `actor` + `grants` but can be made to\n * disagree to test edge cases.\n *\n * Mutually exclusive with passing a pre-built `client`.\n */\n accessControl?: TestClientAccessControl\n /**\n * Engine-scope tags. Defaults to `[\"bench\"]`. Pass disjoint sets to two\n * benches sharing a `client` to assert multi-tenant isolation; pass an\n * overlapping set to assert visibility through a shared tag.\n *\n * Tags must be ASCII lowercase + digits + dashes, no leading dash,\n * no dots — validated at construction.\n */\n tags?: string[]\n /**\n * Shared TestClient. If omitted, the bench creates its own — two\n * benches without a shared client are completely isolated, regardless\n * of tags. Pass the same client to two benches to test tag-scoped\n * visibility across a shared store.\n */\n client?: TestClient\n /**\n * Workflow resource the bench's engine operates against. Mirrors\n * `@sanity/client`'s `ClientConfigResource`. Defaults to\n * `{ type: \"dataset\", id: \"test.test\" }` so all bench-minted GDR\n * URIs read as `dataset:test:test:<docId>`.\n */\n workflowResource?: WorkflowResource\n}\n\n/** Base bench state, distinct from the engine/read/guard/query helper groups. */\ninterface BenchState {\n /** The fake Sanity client. Passed automatically by bench wrappers. */\n client: TestClient\n /**\n * The access state (actor + grants) the bench injects as the\n * `access` override on every wrapped engine call. Defaults to\n * `ALL_ACCESS`.\n */\n access: WorkflowAccess\n /** Convenience: `access.actor`. */\n currentUser: Actor\n /** Convenience: `access.grants ?? []`. */\n grants: Grant[]\n\n /** Add documents to the store after construction. */\n seedDocuments: (documents: StoreDocument[]) => void\n}\n\nexport type Bench = BenchState & EngineWrappers & ReadHelpers & GuardHelpers & QueryHelpers\n\n/**\n * Create a fresh bench. By default the bench is fully isolated — two\n * `createBench()` calls have no shared state. Pass `client` + `tags`\n * to two benches to share a store and exercise tag scoping.\n */\nexport function createBench(options: CreateBenchOptions = {}): Bench {\n const tags = options.tags ?? ['bench']\n validateTags(tags)\n const workflowResource = options.workflowResource ?? DEFAULT_WORKFLOW_RESOURCE\n const client = resolveBenchClient(options)\n const scope = {client, tags, workflowResource}\n\n const access = resolveBenchAccess(options)\n\n return {\n client,\n access,\n currentUser: access.actor,\n grants: access.grants ?? [],\n\n seedDocuments: (documents) => client.add(documents),\n\n ...createEngineWrappers(scope, access),\n ...createReadHelpers(scope),\n ...createGuardHelpers(client),\n ...createQueryHelpers(scope),\n }\n}\n"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@sanity/workflow-engine-test",
3
+ "version": "0.0.0",
4
+ "description": "In-memory test bench (createBench) for @sanity/workflow-engine.",
5
+ "keywords": [
6
+ "sanity",
7
+ "sanity-io",
8
+ "test-bench",
9
+ "testing",
10
+ "workflow",
11
+ "workflows"
12
+ ],
13
+ "homepage": "https://github.com/sanity-io/workflows/tree/main/packages/workflow-engine-test#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/sanity-io/workflows/issues"
16
+ },
17
+ "license": "MIT",
18
+ "author": "Sanity.io <hello@sanity.io>",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/sanity-io/workflows.git",
22
+ "directory": "packages/workflow-engine-test"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "main": "./dist/index.cjs",
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "import": "./dist/index.js",
35
+ "require": "./dist/index.cjs",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "./package.json": "./package.json"
39
+ },
40
+ "publishConfig": {
41
+ "access": "restricted"
42
+ },
43
+ "dependencies": {
44
+ "@sanity-labs/client-fake-for-test": "^0.3.0",
45
+ "@sanity/workflow-engine": "0.0.0"
46
+ },
47
+ "devDependencies": {
48
+ "@sanity/pkg-utils": "^10.5.2",
49
+ "vitest": "^4.1.8"
50
+ },
51
+ "engines": {
52
+ "node": ">=20"
53
+ },
54
+ "scripts": {
55
+ "build": "pkg-utils build --clean",
56
+ "test": "vitest run",
57
+ "test:watch": "vitest",
58
+ "typecheck": "tsc --noEmit -p tsconfig.json"
59
+ }
60
+ }