@sanity/workflow-engine-test 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -13,13 +13,17 @@ import type { OperationArgs } from "@sanity/workflow-engine";
13
13
  import { PendingEffect } from "@sanity/workflow-engine";
14
14
  import type { StartInstanceArgs } from "@sanity/workflow-engine";
15
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
16
  import type { WorkflowAccess } from "@sanity/workflow-engine";
19
17
  import { WorkflowEvaluation } from "@sanity/workflow-engine";
20
18
  import { WorkflowInstance } from "@sanity/workflow-engine";
21
19
  import type { WorkflowResource } from "@sanity/workflow-engine";
22
20
 
21
+ declare type Action = ReleaseAction | VersionAction;
22
+
23
+ declare interface ActionResult {
24
+ transactionId: string;
25
+ }
26
+
23
27
  /**
24
28
  * The bench's default access state — wildcard user + wildcard
25
29
  * grants. The bench injects this as the `access` override on every
@@ -102,6 +106,26 @@ declare interface BenchState {
102
106
  grants: Grant[];
103
107
  /** Add documents to the store after construction. */
104
108
  seedDocuments: (documents: StoreDocument[]) => void;
109
+ /**
110
+ * Current bench clock reading (ISO). Wall-clock until {@link Bench.setNow}
111
+ * or {@link Bench.advance} freezes it. Handy for computing deadlines
112
+ * relative to frozen time in a test.
113
+ */
114
+ now: () => string;
115
+ /**
116
+ * Freeze the bench clock at an explicit ISO instant. Every subsequent
117
+ * engine call (`startInstance` / `fireAction` / `tick` / `completeEffect`
118
+ * / `evaluate`) sees this as `$now` and stamps it on history, so
119
+ * time-based `completeWhen` / `failWhen` / SLA predicates are
120
+ * deterministic.
121
+ */
122
+ setNow: (iso: string) => void;
123
+ /**
124
+ * Move the bench clock forward by `ms`, freezing it at
125
+ * `current + ms`. Re-evaluate by calling {@link Bench.tick} — `advance`
126
+ * itself never writes, so the timeline only progresses when you tick.
127
+ */
128
+ advance: (ms: number) => void;
105
129
  }
106
130
 
107
131
  declare type BenchTickArgs = Omit<
@@ -113,6 +137,26 @@ declare type BenchTickArgs = Omit<
113
137
  actor?: Actor;
114
138
  };
115
139
 
140
+ /**
141
+ * One recorded invocation of a client method — the spy layer behind
142
+ * `client.calls()` / `client.callCount()`. `result` holds the resolved
143
+ * value (awaited, for async methods); `error` holds the thrown/rejected
144
+ * value. Builder-returning methods (`patch`, `transaction`) record the
145
+ * builder as `result`; their eventual writes show up in the mutation log.
146
+ */
147
+ declare interface CallRecord {
148
+ /** Method name; namespace methods are dotted, e.g. `releases.create`. */
149
+ method: string;
150
+ /** Arguments as passed (stored by reference, like `vi.fn`). */
151
+ args: unknown[];
152
+ /** Resolved/returned value, set once the call settles successfully. */
153
+ result?: unknown;
154
+ /** Thrown or rejected value, set if the call failed. */
155
+ error?: unknown;
156
+ projectId: string;
157
+ dataset: string;
158
+ }
159
+
116
160
  /**
117
161
  * Create a fresh bench. By default the bench is fully isolated — two
118
162
  * `createBench()` calls have no shared state. Pass `client` + `tags`
@@ -181,11 +225,45 @@ export declare interface CreateBenchOptions {
181
225
  * URIs read as `dataset:test:test:<docId>`.
182
226
  */
183
227
  workflowResource?: WorkflowResource;
228
+ /**
229
+ * Freeze the bench clock at this ISO instant from construction, so
230
+ * `$now` and every engine timestamp are deterministic. Equivalent to
231
+ * calling {@link Bench.setNow} immediately after `createBench`. Omit to
232
+ * start on the wall clock (`advance` / `setNow` can freeze it later).
233
+ */
234
+ now?: string;
235
+ }
236
+
237
+ declare interface CurrentUser {
238
+ id: string;
239
+ name?: string;
240
+ email?: string;
241
+ /** @deprecated singular `role` on legacy responses */
242
+ role?: string;
243
+ roles?: {
244
+ name: string;
245
+ title?: string;
246
+ description?: string;
247
+ }[];
184
248
  }
185
249
 
186
250
  /** Default workflow resource for benches that don't pass one. */
187
251
  export declare const DEFAULT_WORKFLOW_RESOURCE: WorkflowResource;
188
252
 
253
+ /**
254
+ * Document body accepted by `version.create` / `createVersion`. The id
255
+ * is derived from `releaseId + publishedId` so callers may omit `_id`
256
+ * here. `version.replace` requires `_id` since it identifies the doc
257
+ * to replace.
258
+ */
259
+ declare interface DocumentBody {
260
+ _id?: string;
261
+ _type: string;
262
+ [field: string]: unknown;
263
+ }
264
+
265
+ declare type DocumentValuePermission = "create" | "read" | "update";
266
+
189
267
  /** Engine-call wrappers that inject the bench-owned client + tags + access. */
190
268
  declare type EngineWrappers = {
191
269
  deployDefinitions: (
@@ -196,8 +274,96 @@ declare type EngineWrappers = {
196
274
  completeEffect: (args: BenchCompleteEffectArgs) => Promise<DispatchResult>;
197
275
  tick: (args: BenchTickArgs) => Promise<DispatchResult>;
198
276
  evaluate: (args: BenchEvaluateArgs) => Promise<WorkflowEvaluation>;
277
+ guardsForInstance: (instanceId: string) => Promise<MutationGuardDoc[]>;
199
278
  };
200
279
 
280
+ /**
281
+ * A single mutation in the raw array form accepted by `mutate()` — the
282
+ * same shape `@sanity/client` accepts. The inline `patch` form mirrors
283
+ * the real client's keyed patch object.
284
+ */
285
+ declare type FakeMutation =
286
+ | {
287
+ create: SanityDocumentLike;
288
+ }
289
+ | {
290
+ createOrReplace: SanityDocumentLike;
291
+ }
292
+ | {
293
+ createIfNotExists: SanityDocumentLike;
294
+ }
295
+ | {
296
+ delete: {
297
+ id: string;
298
+ };
299
+ }
300
+ | {
301
+ patch: {
302
+ id: string;
303
+ ifRevisionID?: string;
304
+ set?: Record<string, unknown>;
305
+ setIfMissing?: Record<string, unknown>;
306
+ unset?: string[];
307
+ inc?: Record<string, number>;
308
+ dec?: Record<string, number>;
309
+ insert?: {
310
+ before?: string;
311
+ after?: string;
312
+ replace?: string;
313
+ items: unknown[];
314
+ };
315
+ };
316
+ };
317
+
318
+ /**
319
+ * Data-plane subset of `@sanity/client`'s surface, with multi-dataset support.
320
+ *
321
+ * One `createTestClient()` call creates a registry of stores keyed by
322
+ * `(projectId, dataset)`. The returned client points at one of them.
323
+ * `withConfig({ projectId, dataset })` returns a new handle into the same
324
+ * registry but pointing at a different store — useful for workflows where
325
+ * definitions live in one dataset and instances live in another.
326
+ *
327
+ * Two separate `createTestClient()` calls have separate registries, so
328
+ * tests stay isolated from each other.
329
+ *
330
+ * Methods not on the real client:
331
+ *
332
+ * - `seed`, `add`, `reset`, `snapshot`, `store` — bulk-load helpers,
333
+ * scoped to the current dataset.
334
+ * - `simulateConcurrentEdit()` — force the next N revision-guarded writes
335
+ * to a document to 409, for optimistic-locking retry tests.
336
+ * - `datasets()` — lists known dataset keys in this registry.
337
+ *
338
+ * Methods deliberately omitted:
339
+ *
340
+ * - Project / agent / asset / live / listen APIs.
341
+ */
342
+ declare interface FetchOptions {
343
+ /** Override the client's configured perspective for this read. */
344
+ perspective?: Perspective;
345
+ /**
346
+ * When `false`, resolve to a {@link RawQueryResponse} (`{ query, ms,
347
+ * result }`) instead of the bare result — mirrors `@sanity/client`.
348
+ * Defaults to `true`.
349
+ */
350
+ filterResponse?: boolean;
351
+ /**
352
+ * With `filterResponse: false`, set to `false` to omit `query` from the
353
+ * raw response. Ignored when `filterResponse` is not `false`.
354
+ */
355
+ returnQuery?: boolean;
356
+ /** Accepted for signature parity; not simulated. */
357
+ tag?: string;
358
+ /** Accepted for signature parity; not simulated. */
359
+ signal?: AbortSignal;
360
+ }
361
+
362
+ declare interface Grant_2 {
363
+ filter: string;
364
+ permissions: DocumentValuePermission[];
365
+ }
366
+
201
367
  declare type GuardHelpers = {
202
368
  /** All deployed `temp.system.guard` docs in the store. */
203
369
  listGuards: () => Promise<MutationGuardDoc[]>;
@@ -211,10 +377,11 @@ declare type GuardHelpers = {
211
377
  as?: Actor,
212
378
  ) => Promise<MutationGuardDoc[]>;
213
379
  /**
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.
380
+ * Write to a document through the client's enforced write seam — the bench
381
+ * client gates the write against deployed guards (incl. `identity()`) and
382
+ * throws `MutationGuardDeniedError` on denial, exactly as the lake would.
383
+ * `as` routes the write through a token-scoped sibling so `identity()`
384
+ * resolves to that actor.
218
385
  */
219
386
  editDocument: (
220
387
  documentId: string,
@@ -226,6 +393,110 @@ declare type GuardHelpers = {
226
393
  ) => Promise<StoreDocument>;
227
394
  };
228
395
 
396
+ /** Result of a mutation when ids are requested for all affected documents. */
397
+ declare interface MultipleMutationResult {
398
+ transactionId: string;
399
+ documentIds: string[];
400
+ results: {
401
+ id: string;
402
+ operation: MutationOperation;
403
+ }[];
404
+ }
405
+
406
+ /** Append-only record of one committed transaction (a single mutation is a 1-entry transaction). */
407
+ declare interface MutationLogEntry {
408
+ transactionId: string;
409
+ projectId: string;
410
+ dataset: string;
411
+ mutations: TxnMutation[];
412
+ }
413
+
414
+ /** The operation recorded per affected document, matching `@sanity/client`. */
415
+ declare type MutationOperation = "create" | "update" | "delete" | "none";
416
+
417
+ declare interface MutationOptions {
418
+ ifRevisionId?: string;
419
+ /**
420
+ * Shape the resolved value, mirroring `@sanity/client`:
421
+ * - default → the mutated document
422
+ * - `returnDocuments: false` → an id-shaped mutation result
423
+ * ({@link SingleMutationResult} / {@link MultipleMutationResult})
424
+ * - `returnFirst: false` with documents → an array
425
+ *
426
+ * The static return type reflects the default (the document); pass a
427
+ * type argument or cast when requesting an id-shaped result.
428
+ */
429
+ returnDocuments?: boolean;
430
+ returnFirst?: boolean;
431
+ /** Override the synthetic transaction id used in the result and log. */
432
+ transactionId?: string;
433
+ /** When `true`, validate + shape the result but don't persist. */
434
+ dryRun?: boolean;
435
+ /** Accepted for signature parity; not simulated. */
436
+ visibility?: "sync" | "async" | "deferred";
437
+ /** Accepted for signature parity; not simulated. */
438
+ autoGenerateArrayKeys?: boolean;
439
+ /** Accepted for signature parity; not simulated. */
440
+ tag?: string;
441
+ }
442
+
443
+ declare interface PatchHandle {
444
+ set: (props: Record<string, unknown>) => PatchHandle;
445
+ setIfMissing: (props: Record<string, unknown>) => PatchHandle;
446
+ unset: (paths: string[]) => PatchHandle;
447
+ inc: (props: Record<string, number>) => PatchHandle;
448
+ dec: (props: Record<string, number>) => PatchHandle;
449
+ insert: (
450
+ position: "before" | "after" | "replace",
451
+ anchor: string,
452
+ items: unknown[],
453
+ ) => PatchHandle;
454
+ ifRevisionId: (rev: string) => PatchHandle;
455
+ commit: () => Promise<SanityDocument>;
456
+ /** Test-only introspection — not on real Patch */
457
+ toJSON: () => {
458
+ id: string;
459
+ ops: PatchOp[];
460
+ ifRev?: string;
461
+ };
462
+ }
463
+
464
+ /**
465
+ * Patch operations the test client supports.
466
+ *
467
+ * Subset of `@sanity/client`'s PatchOperations. Each entry is one
468
+ * mutation step applied in order at commit time.
469
+ */
470
+ declare type PatchOp =
471
+ | {
472
+ kind: "set";
473
+ props: Record<string, unknown>;
474
+ }
475
+ | {
476
+ kind: "setIfMissing";
477
+ props: Record<string, unknown>;
478
+ }
479
+ | {
480
+ kind: "unset";
481
+ paths: string[];
482
+ }
483
+ | {
484
+ kind: "inc";
485
+ props: Record<string, number>;
486
+ }
487
+ | {
488
+ kind: "dec";
489
+ props: Record<string, number>;
490
+ }
491
+ | {
492
+ kind: "insert";
493
+ position: "before" | "after" | "replace";
494
+ anchor: string;
495
+ items: unknown[];
496
+ };
497
+
498
+ declare type Perspective = "raw" | "published" | "drafts" | string[];
499
+
229
500
  declare type QueryHelpers = {
230
501
  /**
231
502
  * Run an arbitrary GROQ query through `workflow.query`. The query MUST
@@ -310,6 +581,134 @@ declare type ReadHelpers = {
310
581
  snapshot: () => readonly StoreDocument[];
311
582
  };
312
583
 
584
+ declare type ReleaseAction =
585
+ | {
586
+ actionType: "sanity.action.release.create";
587
+ releaseId: string;
588
+ metadata?: Partial<ReleaseMetadata>;
589
+ }
590
+ | {
591
+ actionType: "sanity.action.release.edit";
592
+ releaseId: string;
593
+ patch: {
594
+ set?: Record<string, unknown>;
595
+ unset?: string[];
596
+ };
597
+ }
598
+ | {
599
+ actionType: "sanity.action.release.schedule";
600
+ releaseId: string;
601
+ publishAt: string;
602
+ }
603
+ | {
604
+ actionType: "sanity.action.release.unschedule";
605
+ releaseId: string;
606
+ }
607
+ | {
608
+ actionType: "sanity.action.release.publish";
609
+ releaseId: string;
610
+ }
611
+ | {
612
+ actionType: "sanity.action.release.archive";
613
+ releaseId: string;
614
+ }
615
+ | {
616
+ actionType: "sanity.action.release.unarchive";
617
+ releaseId: string;
618
+ }
619
+ | {
620
+ actionType: "sanity.action.release.delete";
621
+ releaseId: string;
622
+ };
623
+
624
+ declare interface ReleaseDocument extends SanityDocument {
625
+ _type: "system.release";
626
+ name: string;
627
+ state: ReleaseState;
628
+ metadata: ReleaseMetadata;
629
+ publishAt?: string;
630
+ }
631
+
632
+ declare interface ReleaseMetadata {
633
+ title?: string;
634
+ description?: string;
635
+ releaseType?: ReleaseType;
636
+ intendedPublishAt?: string;
637
+ }
638
+
639
+ declare interface ReleasesHandle {
640
+ create: (params: {
641
+ releaseId: string;
642
+ metadata?: Partial<ReleaseMetadata>;
643
+ }) => Promise<
644
+ ActionResult & {
645
+ releaseId: string;
646
+ metadata: ReleaseMetadata;
647
+ }
648
+ >;
649
+ edit: (params: {
650
+ releaseId: string;
651
+ patch: {
652
+ set?: Record<string, unknown>;
653
+ unset?: string[];
654
+ };
655
+ }) => Promise<ActionResult>;
656
+ get: (params: { releaseId: string }) => Promise<ReleaseDocument | undefined>;
657
+ fetchDocuments: (params: { releaseId: string }) => Promise<SanityDocument[]>;
658
+ schedule: (params: {
659
+ releaseId: string;
660
+ publishAt: string;
661
+ }) => Promise<ActionResult>;
662
+ unschedule: (params: { releaseId: string }) => Promise<ActionResult>;
663
+ publish: (params: { releaseId: string }) => Promise<ActionResult>;
664
+ archive: (params: { releaseId: string }) => Promise<ActionResult>;
665
+ unarchive: (params: { releaseId: string }) => Promise<ActionResult>;
666
+ delete: (params: { releaseId: string }) => Promise<ActionResult>;
667
+ }
668
+
669
+ declare type ReleaseState = "active" | "scheduled" | "published" | "archived";
670
+
671
+ declare type ReleaseType = "asap" | "scheduled" | "undecided";
672
+
673
+ /** @public */
674
+ declare interface SanityDocument {
675
+ _id: string;
676
+ _type: string;
677
+ _createdAt: string;
678
+ _updatedAt: string;
679
+ _rev: string;
680
+ [key: string]: unknown;
681
+ }
682
+
683
+ /**
684
+ * Similar to `SanityDocument` but only requires the `_id` and `_type`
685
+ *
686
+ * @see SanityDocument
687
+ *
688
+ * @public
689
+ */
690
+ declare interface SanityDocumentLike {
691
+ _id: string;
692
+ _type: string;
693
+ _createdAt?: string;
694
+ _updatedAt?: string;
695
+ _rev?: string;
696
+ _system?: {
697
+ delete?: boolean;
698
+ };
699
+ [key: string]: unknown;
700
+ }
701
+
702
+ /** Result of a mutation when `returnFirst` resolves true and ids are requested. */
703
+ declare interface SingleMutationResult {
704
+ transactionId: string;
705
+ documentId: string;
706
+ results: {
707
+ id: string;
708
+ operation: MutationOperation;
709
+ }[];
710
+ }
711
+
313
712
  /** A document as stored in the bench's fake lake. */
314
713
  declare type StoreDocument = {
315
714
  _id: string;
@@ -317,6 +716,312 @@ declare type StoreDocument = {
317
716
  [key: string]: unknown;
318
717
  };
319
718
 
719
+ declare interface SystemFields {
720
+ _rev: string;
721
+ _createdAt: string;
722
+ _updatedAt: string;
723
+ }
724
+
725
+ declare interface TestClient {
726
+ fetch: <T = unknown>(
727
+ query: string,
728
+ params?: Record<string, unknown>,
729
+ options?: FetchOptions,
730
+ ) => Promise<T>;
731
+ getDocument: <T = SanityDocument>(
732
+ id: string,
733
+ options?: FetchOptions,
734
+ ) => Promise<T | undefined>;
735
+ getDocuments: <T = SanityDocument>(
736
+ ids: string[],
737
+ options?: FetchOptions,
738
+ ) => Promise<(T | null)[]>;
739
+ /**
740
+ * Sanity-style HTTP request hook. Present iff the client was
741
+ * created with `accessControl`. The engine's access resolver pokes
742
+ * `/users/me` and the configured ACL path through this — without it
743
+ * the engine throws on any verb that needs an actor (which is the
744
+ * cue for the bench to supply an `access` override).
745
+ *
746
+ * Test cases that want to exercise the engine's token-resolution
747
+ * path should configure `accessControl`; cases that want bench-style
748
+ * always-allow should leave it off and pass `access` through the
749
+ * bench.
750
+ */
751
+ request?: <T>(opts: {
752
+ url?: string;
753
+ uri?: string;
754
+ signal?: AbortSignal;
755
+ tag?: string;
756
+ }) => Promise<T>;
757
+ create: <T extends SanityDocumentLike>(
758
+ doc: T,
759
+ options?: MutationOptions,
760
+ ) => Promise<T & SystemFields>;
761
+ createOrReplace: <T extends SanityDocumentLike>(
762
+ doc: T,
763
+ options?: MutationOptions,
764
+ ) => Promise<T & SystemFields>;
765
+ createIfNotExists: <T extends SanityDocumentLike>(
766
+ doc: T,
767
+ options?: MutationOptions,
768
+ ) => Promise<T & SystemFields>;
769
+ delete: (
770
+ id: string,
771
+ options?: MutationOptions,
772
+ ) => Promise<{
773
+ _id: string;
774
+ } | null>;
775
+ patch: (documentId: string) => PatchHandle;
776
+ transaction: () => TransactionHandle;
777
+ /**
778
+ * Apply a batch of mutations in the raw array form (or a `patch()` /
779
+ * `transaction()` builder) — mirrors `@sanity/client`'s `mutate()`.
780
+ * Atomic: any failure rolls the whole batch back. The resolved shape
781
+ * follows `options.returnDocuments` / `returnFirst` exactly like the
782
+ * real client (default → {@link MultipleMutationResult}).
783
+ */
784
+ mutate: (
785
+ operations: FakeMutation[] | PatchHandle | TransactionHandle,
786
+ options?: TxnCommitOptions,
787
+ ) => Promise<TxnCommitResult>;
788
+ /**
789
+ * Dispatch one or more release / version actions against the store.
790
+ * Returns a synthetic `transactionId`. Multi-action arrays are applied
791
+ * atomically — any failure rolls back the batch.
792
+ */
793
+ action: (action: Action | Action[]) => Promise<ActionResult>;
794
+ /** Helpers mirroring `@sanity/client`'s `client.releases.*` surface. */
795
+ releases: ReleasesHandle;
796
+ /** Helper for `sanity.action.document.version.create`. */
797
+ createVersion: (params: {
798
+ releaseId: string;
799
+ publishedId: string;
800
+ document: DocumentBody;
801
+ }) => Promise<ActionResult>;
802
+ /** Helper for `sanity.action.document.version.unpublish`. */
803
+ unpublishVersion: (params: {
804
+ releaseId: string;
805
+ publishedId: string;
806
+ }) => Promise<ActionResult>;
807
+ /** Current `(projectId, dataset)` and active perspective. */
808
+ config: () => {
809
+ projectId: string;
810
+ dataset: string;
811
+ perspective: Perspective;
812
+ };
813
+ /**
814
+ * Return a new client handle pointing at a different `(projectId, dataset)`
815
+ * within the SAME backing registry. Either field can be omitted to keep
816
+ * the current value. Two clients produced by `withConfig` see each
817
+ * other's writes if they point at the same `(projectId, dataset)`.
818
+ */
819
+ withConfig: (
820
+ config: Partial<{
821
+ projectId: string;
822
+ dataset: string;
823
+ perspective: Perspective;
824
+ /**
825
+ * Rebind the acting identity (and its grants) on a sibling that shares
826
+ * the same store — the test analogue of a token-scoped client. Lets two
827
+ * actors write the same dataset, e.g. for `identity()`-based guards.
828
+ */
829
+ accessControl: TestClientAccessControl;
830
+ }>,
831
+ ) => TestClient;
832
+ /** Replace the current dataset with the given documents. */
833
+ seed: (documents: SanityDocumentLike[]) => void;
834
+ /** Add documents to the current dataset without dropping existing ones. */
835
+ add: (documents: SanityDocumentLike[]) => void;
836
+ /** Drop all documents in the current dataset. */
837
+ reset: () => void;
838
+ /** Read-only snapshot of all documents in the current dataset. */
839
+ snapshot: () => readonly SanityDocument[];
840
+ /** Direct store access for the current dataset. */
841
+ store: () => TestStore;
842
+ /**
843
+ * Simulate `count` concurrent edits to `id` in the current dataset: the
844
+ * next `count` revision-guarded mutations (those passing `ifRevisionId`)
845
+ * to that document throw a 409-shaped conflict, as if another client
846
+ * committed between the caller's read and write. Lets a test exercise
847
+ * optimistic-locking retry paths deterministically, without timing or
848
+ * interleaving. Mutations that don't pass `ifRevisionId` are unaffected.
849
+ */
850
+ simulateConcurrentEdit: (id: string, count?: number) => void;
851
+ /** List `(projectId, dataset)` pairs known in this registry. */
852
+ datasets: () => {
853
+ projectId: string;
854
+ dataset: string;
855
+ }[];
856
+ /**
857
+ * Register a handler for a `@sanity/client` member the fake doesn't
858
+ * implement (e.g. `listen`, `assets`). Accessing that member on the
859
+ * client then returns the handler instead of throwing the
860
+ * "not implemented" guard error. Shared across sibling `withConfig`
861
+ * handles. Stubs fill gaps only — they don't shadow implemented methods.
862
+ */
863
+ stub: (name: string, handler: unknown) => void;
864
+ /** The currently-registered stubs, keyed by member name. */
865
+ stubs: () => Record<string, unknown>;
866
+ /**
867
+ * Every mutation committed through this client's registry, flattened
868
+ * and in order — `create` / `createOrReplace` / `createIfNotExists` /
869
+ * `delete` / `patch`, whether issued directly, via `transaction()`, or
870
+ * via `mutate()`. The spy hook for "assert exactly what writes my code
871
+ * submitted".
872
+ */
873
+ mutations: () => TxnMutation[];
874
+ /**
875
+ * Every committed transaction, in order. A direct mutation is recorded
876
+ * as a one-mutation transaction. Each entry carries its synthetic
877
+ * `transactionId` and the `(projectId, dataset)` it targeted.
878
+ */
879
+ transactions: () => MutationLogEntry[];
880
+ /** Clear the recorded mutation / transaction log. */
881
+ clearLog: () => void;
882
+ /**
883
+ * Every recorded method call, in order — the spy layer for "was `fetch`
884
+ * called, how many times, and with what params". Records the
885
+ * `@sanity/client`-surface methods (`fetch`, `getDocument`, `create`,
886
+ * `patch`, `mutate`, `releases.*`, …), not the test-only helpers.
887
+ * Pass a method name to filter (e.g. `calls("fetch")`); namespace
888
+ * methods are dotted (`calls("releases.create")`). Shared across
889
+ * `withConfig` siblings.
890
+ */
891
+ calls: (method?: string) => CallRecord[];
892
+ /** Number of recorded calls, optionally filtered by method name. */
893
+ callCount: (method?: string) => number;
894
+ /** Clear the recorded call log. */
895
+ clearCalls: () => void;
896
+ }
897
+
898
+ declare interface TestClientAccessControl {
899
+ /** What `client.request({ url: "/users/me" })` returns. */
900
+ currentUser: CurrentUser;
901
+ /** What `client.request({ url: aclPath })` returns. */
902
+ grants: Grant_2[];
903
+ /**
904
+ * URL path the engine's access resolver hits to fetch grants.
905
+ * Defaults to `/projects/<projectId>/datasets/<dataset>/acl`.
906
+ */
907
+ aclPath?: string;
908
+ }
909
+
910
+ /**
911
+ * In-memory document store backing a TestClient. Closure-scoped — created
912
+ * fresh per test, no global state.
913
+ *
914
+ * The shape mirrors what `groq-js` evaluates against: an array of
915
+ * documents. Internally we keep a Map by `_id` for O(1) lookup, and
916
+ * project to an array on each query.
917
+ */
918
+ declare interface TestStore {
919
+ /** Replace the entire dataset with the given documents. Existing docs are dropped. */
920
+ seed: (documents: SanityDocumentLike[]) => void;
921
+ /** Add documents without dropping existing ones. Conflicts (same _id) overwrite. */
922
+ add: (documents: SanityDocumentLike[]) => void;
923
+ /** Remove all documents. */
924
+ reset: () => void;
925
+ /** All documents as an array, in insertion order. */
926
+ all: () => SanityDocument[];
927
+ /** Get one document by _id, or undefined. */
928
+ get: (id: string) => SanityDocument | undefined;
929
+ /** Insert a fully-formed document by _id. Replaces if it already exists. */
930
+ put: (doc: SanityDocument) => void;
931
+ /** Remove a document by _id. Returns the removed doc or undefined. */
932
+ remove: (id: string) => SanityDocument | undefined;
933
+ /** Snapshot of all docs, useful for assertions. */
934
+ snapshot: () => readonly SanityDocument[];
935
+ }
936
+
937
+ declare interface TransactionHandle {
938
+ create: (doc: SanityDocumentLike) => TransactionHandle;
939
+ createOrReplace: (doc: SanityDocumentLike) => TransactionHandle;
940
+ createIfNotExists: (doc: SanityDocumentLike) => TransactionHandle;
941
+ delete: (id: string) => TransactionHandle;
942
+ /** Accept either a Patch builder (drained at commit) or an inline doc id + ops. */
943
+ patch: (
944
+ documentIdOrPatch: string | PatchHandle,
945
+ ops?: PatchOp[],
946
+ ifRev?: string,
947
+ ) => TransactionHandle;
948
+ commit: (options?: TxnCommitOptions) => Promise<TxnCommitResult>;
949
+ /** Test-only introspection — not on real Transaction. */
950
+ toJSON: () => TxnMutation[];
951
+ }
952
+
953
+ declare interface TxnCommitOptions {
954
+ /**
955
+ * Shape the resolved value (mirrors `@sanity/client`):
956
+ * - default (both unset) → {@link MultipleMutationResult}
957
+ * - `returnDocuments: true` → the resulting document(s)
958
+ * - `returnFirst: true` with `returnDocuments` → a single document
959
+ * - `returnDocuments: false` → an id-shaped mutation result
960
+ */
961
+ returnDocuments?: boolean;
962
+ returnFirst?: boolean;
963
+ /** Override the synthetic transaction id (mirrors `client.transaction(undefined, { transactionId })`). */
964
+ transactionId?: string;
965
+ }
966
+
967
+ declare type TxnCommitResult =
968
+ | MultipleMutationResult
969
+ | SingleMutationResult
970
+ | SanityDocument
971
+ | SanityDocument[];
972
+
973
+ /**
974
+ * A transaction collects mutations and applies them atomically at commit
975
+ * time. Mutations are applied in declaration order; if any one fails, the
976
+ * whole transaction throws and no writes land in the store.
977
+ *
978
+ * Subset of `@sanity/client`'s Transaction. Same shape, narrower surface.
979
+ */
980
+ declare type TxnMutation =
981
+ | {
982
+ kind: "create";
983
+ doc: SanityDocumentLike;
984
+ }
985
+ | {
986
+ kind: "createOrReplace";
987
+ doc: SanityDocumentLike;
988
+ }
989
+ | {
990
+ kind: "createIfNotExists";
991
+ doc: SanityDocumentLike;
992
+ }
993
+ | {
994
+ kind: "delete";
995
+ id: string;
996
+ }
997
+ | {
998
+ kind: "patch";
999
+ documentId: string;
1000
+ ops: PatchOp[];
1001
+ ifRev?: string;
1002
+ };
1003
+
1004
+ declare type VersionAction =
1005
+ | {
1006
+ actionType: "sanity.action.document.version.create";
1007
+ publishedId: string;
1008
+ document: DocumentBody;
1009
+ releaseId?: string;
1010
+ }
1011
+ | {
1012
+ actionType: "sanity.action.document.version.replace";
1013
+ document: SanityDocumentLike;
1014
+ }
1015
+ | {
1016
+ actionType: "sanity.action.document.version.discard";
1017
+ versionId: string;
1018
+ }
1019
+ | {
1020
+ actionType: "sanity.action.document.version.unpublish";
1021
+ versionId: string;
1022
+ publishedId: string;
1023
+ };
1024
+
320
1025
  /**
321
1026
  * The default grants used when tests don't specify any. A single
322
1027
  * permissive grant matches every document and confers all permissions.