Sanity Library Reference Docs
    Preparing search index...

    Module @sanity/workflow-engine-test - v0.32.0

    @sanity/workflow-engine-test

    In-memory test bench (createBench) for @sanity/workflow-engine. Wraps the engine and an in-memory Sanity client so workflow tests stay free of client wiring and actor boilerplate.

    Status: 0.x, public. Intended for testing @sanity/workflow-engine.

    npm install -D @sanity/workflow-engine @sanity/workflow-engine-test
    

    createBench() builds an isolated bench: the real engine, a fake in-memory Sanity client, an all-access default actor, and a bench-owned clock. Every wrapped verb injects the bench's client, tag, workflow resource, access default, clock, and cross-resource resolver (serveResources sugar for same-store dataset siblings, raw resourceClients for anything else), so a test reads as the workflow it exercises:

    • Runtime verbsdeployDefinitions, deleteDefinition, startInstance, fireAction, editField, completeEffect (and completePendingEffect, addressed by effect name), tick, evaluate.
    • Instance-admin overridessetStage, abortInstance. Same injection, so a forced move into a deadline-gated stage evaluates $now at bench time, not wall-clock.
    • Reads and probesgetInstance, currentStage, activityStatus, listPendingEffects, findPendingEffects, children, instancesForSubject, instancesByStage, instancesForDocument, definitionsForDocument, evaluateStart, query, plus the guard helpers below. Helpers sharing an engine verb's name take the engine's own args objects and throw its typed errors — bench reads and engine reads cannot drift.
    const bench = createBench({now: '2026-01-01T00:00:00Z'})
    await bench.deployDefinitions({expectedMinReaderModel: 4, definitions: [reviewWorkflow]})

    const {instance} = await bench.startInstance({
    definition: 'review-workflow',
    initialFields: [subjectField('article-1')],
    })
    await bench.fireAction({instanceId: instance._id, activity: 'review', action: 'approve'})
    expect(await bench.currentStage(instance._id)).toBe('approved')

    // Admin overrides ride the same defaults:
    await bench.abortInstance({instanceId: instance._id, reason: 'hard stop'})

    subjectField(docId) and releaseField(name) build the runtime initial-field entries for the bench's default resource, so tests never hand-roll GDR URI strings. For denial paths, pass a restrictive currentUser / grants / attributes (or a full access) at construction, or override per call with the actor / grants / attributes shortcuts or an explicit access. The package re-exports WriteAccessDeniedError for assertions against those access-control denials.

    Time is bench-owned. createBench({now}) starts frozen; setNow / advance move it. Every wrapped verb evaluates $now and stamps history at bench time, so deadline predicates are tested by advancing the clock and ticking — never by waiting.

    Some tests need an engine of their own — custom effect handlers, racing two drainers, a custom clock. createBenchEngine(bench, overrides?) builds one on the bench's client, inheriting its tag, workflow resource, and clock (overrides win). The bench exposes tag and workflowResource for raw workflow.* calls, and the default tag ships as the BENCH_TAG constant.

    A workflow stage can deploy mutation guardstemp.system.guard documents that lock the documents it governs while the stage is active, and are deleted when it exits. The lake does not enforce this doc type yet: in production, guard enforcement is the engine's own optimistic evaluation (advisory verdicts and engine-side write denials), and the lake ACL is the only hard boundary. The bench installs the intended lake-side contract at its client's write seam, so tests exercise the rejection semantics guards are designed to have once the lake enforces them. There are two distinct things you test, and the bench gives you a helper for each:

    • Write-time rejection (simulated)bench.editDocument({documentId, patch}) writes through the bench client (patch is a {set?, unset?, insert?} spec), and a write a deployed guard denies throws the package's re-exported GuardDeniedError — the rejection the lake is designed to apply to any client once guard enforcement ships; the bench reproduces it on the client it mints. action: 'delete' deletes, 'publish' creates or updates the published document and deletes its draft, and 'unpublish' creates the draft and deletes the published document. Author guards with publish / unpublish; the engine compiles those lifecycle actions to the underlying create, update, and delete mutations. Only hand-seeded Lake guard documents use those operations directly. The helper resolves to the post-write document, or undefined once a delete/unpublish removed it.
    • Read-time preflightbench.activeGuardsForDocument(id) answers "would a write be denied right now?" without writing. This is the check a UI uses to disable a button or show a lock ahead of time. It's a pure read over the same evaluation path as the bench's write rejection, so its verdict matches what a bench write would actually do — and it works against any client.

    bench.listGuards() returns the deployed guard docs for inspection.

    import {createBench, GuardDeniedError} from '@sanity/workflow-engine-test'

    const bench = createBench({documents: [{_id: 'drafts.doc-1', _type: 'article', body: 'b'}]})
    // …deploy a definition whose current stage locks `drafts.doc-1`, start an instance…

    // Preflight: is the doc locked for this user right now?
    const active = await bench.activeGuardsForDocument('drafts.doc-1')

    // Rejection: a guard-violating write throws (bench-simulated lake contract).
    await expect(
    bench.editDocument({documentId: 'drafts.doc-1', patch: {set: {body: 'edited'}}}),
    ).rejects.toBeInstanceOf(GuardDeniedError)

    To share one store across benches with the write seam intact, pass another bench's already-wired client: createBench({client: other.client, tag}). The mutationGuard rides along, so the second bench denies the same locks. Only a bare hand-built createTestClient() opts out of write rejection — the read-time preflight needs no wiring either way.

    MIT

    Classes

    GuardDeniedError
    WriteAccessDeniedError

    Interfaces

    BenchAbortInstanceArgs
    BenchCompleteEffectArgs
    BenchDeleteDefinitionArgs
    BenchEditFieldArgs
    BenchEvaluateArgs
    BenchFireActionArgs
    BenchResetActivityArgs
    BenchSetStageArgs
    BenchStartInstanceArgs
    BenchState
    BenchTickArgs
    ComposedHelpers
    CreateBenchOptions
    FixtureBench

    Type Aliases

    Bench
    EditDocumentAction
    EditDocumentPatch
    EngineScope
    EngineWrappers
    GuardHelpers
    QueryHelpers
    ReadHelpers
    StoreDocument

    Variables

    ALL_ACCESS
    ALL_ACCESS_USER
    BENCH_TAG
    DEFAULT_WORKFLOW_RESOURCE
    WILDCARD_GRANTS

    Functions

    createBench
    createBenchEngine
    docRefField
    releaseField
    startedInstanceFixture
    subjectField