@sanity/workflow-engine-test 0.9.0 → 0.10.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
@@ -1,9 +1,16 @@
1
+ import type { AbortInstanceArgs } from "@sanity/workflow-engine";
1
2
  import { ActivityStatus } from "@sanity/workflow-engine";
2
3
  import { Actor } from "@sanity/workflow-engine";
4
+ import { CandidateDocument } from "@sanity/workflow-engine";
5
+ import { Clock } from "@sanity/workflow-engine";
3
6
  import type { CompleteEffectArgs } from "@sanity/workflow-engine";
7
+ import { CreateEngineArgs } from "@sanity/workflow-engine";
8
+ import type { DeclaredExecutionContext } from "@sanity/workflow-engine";
4
9
  import { DeployDefinitionsArgs } from "@sanity/workflow-engine";
5
10
  import { DeployDefinitionsResult } from "@sanity/workflow-engine";
11
+ import { DeployedDefinition } from "@sanity/workflow-engine";
6
12
  import type { EditFieldArgs } from "@sanity/workflow-engine";
13
+ import { Engine } from "@sanity/workflow-engine";
7
14
  import type { EvaluateArgs } from "@sanity/workflow-engine";
8
15
  import type { FireActionArgs } from "@sanity/workflow-engine";
9
16
  import { GdrUri } from "@sanity/workflow-engine";
@@ -14,23 +21,31 @@ import { MutationGuardDoc } from "@sanity/workflow-engine";
14
21
  import type { OperationArgs } from "@sanity/workflow-engine";
15
22
  import { OperationResult } from "@sanity/workflow-engine";
16
23
  import { PendingEffect } from "@sanity/workflow-engine";
24
+ import type { SetStageArgs } from "@sanity/workflow-engine";
17
25
  import type { StartInstanceArgs } from "@sanity/workflow-engine";
18
26
  import type { WorkflowAccess } from "@sanity/workflow-engine";
27
+ import { WorkflowDefinitionInput } from "@sanity/workflow-engine";
19
28
  import { WorkflowEvaluation } from "@sanity/workflow-engine";
20
29
  import { WorkflowInstance } from "@sanity/workflow-engine";
21
30
  import type { WorkflowResource } from "@sanity/workflow-engine";
22
31
 
23
32
  declare type Action = ReleaseAction | VersionAction;
24
33
 
34
+ /** Options for `action()`. Only `tag` is meaningful to the fake. */
35
+ declare interface ActionOptions {
36
+ /** Request tag, recorded on the {@link CallRecord} (`null` = explicit suppression). */
37
+ tag?: string | null;
38
+ }
39
+
25
40
  declare interface ActionResult {
26
41
  transactionId: string;
27
42
  }
28
43
 
29
44
  /**
30
- * The bench's default access state — wildcard user + wildcard
31
- * grants. The bench injects this as the `access` override on every
32
- * engine call so the underlying TestClient (which has no token, so
33
- * no `/users/me` resolution path) never needs to do auth resolution.
45
+ * The bench's default identity — wildcard user + wildcard grants,
46
+ * registered as the default token on the bench's fake client so the
47
+ * engine token-resolves it (`/users/me` + the ACL path) exactly like
48
+ * production.
34
49
  *
35
50
  * Production code never sees this; pass `createBench({ access })` to
36
51
  * test denial paths.
@@ -55,56 +70,70 @@ export declare type Bench = BenchState &
55
70
  QueryHelpers &
56
71
  ComposedHelpers;
57
72
 
58
- declare type BenchCompleteEffectArgs = Omit<CompleteEffectArgs, "access"> & {
59
- access?: WorkflowAccess;
60
- /** Shortcut for `access.actor`. */
61
- actor?: Actor;
62
- };
73
+ /** Default engine-scope partition tag for benches that don't pass one. */
74
+ export declare const BENCH_TAG = "bench";
63
75
 
64
- declare type BenchEditFieldArgs = Omit<EditFieldArgs, "access"> & {
65
- access?: WorkflowAccess;
66
- /** Shortcut for `access.actor`. */
67
- actor?: Actor;
68
- /** Shortcut for `access.grants`. */
69
- grants?: Grant[];
70
- };
76
+ declare type BenchAbortInstanceArgs = BenchArgs<AbortInstanceArgs>;
71
77
 
72
- declare type BenchEvaluateArgs = Omit<EvaluateArgs, "access"> & {
78
+ /**
79
+ * A bench verb's args: the engine verb's args plus the bench's per-call
80
+ * identity shortcuts — the composed pair runs the verb through a
81
+ * token-scoped sibling client.
82
+ */
83
+ declare type BenchArgs<T> = T & {
73
84
  access?: WorkflowAccess;
74
85
  /** Shortcut for `access.actor`. */
75
86
  actor?: Actor;
76
- /** Shortcut for `access.grants`. */
77
- grants?: Grant[];
78
87
  };
79
88
 
80
- declare type BenchFireActionArgs = Omit<FireActionArgs, "access"> & {
81
- access?: WorkflowAccess;
82
- /** Shortcut for `access.actor`. */
83
- actor?: Actor;
89
+ /** {@link BenchArgs} for the verbs that also take the `grants` shortcut. */
90
+ declare type BenchArgsWithGrants<T> = BenchArgs<T> & {
84
91
  /** Shortcut for `access.grants`. */
85
92
  grants?: Grant[];
86
93
  };
87
94
 
88
- declare type BenchStartInstanceArgs = Omit<StartInstanceArgs, "access"> & {
89
- access?: WorkflowAccess;
90
- /** Shortcut for `access.actor`. */
91
- actor?: Actor;
92
- };
95
+ declare type BenchCompleteEffectArgs = BenchArgs<CompleteEffectArgs>;
96
+
97
+ declare type BenchEditFieldArgs = BenchArgsWithGrants<EditFieldArgs>;
98
+
99
+ declare type BenchEvaluateArgs = BenchArgsWithGrants<EvaluateArgs>;
100
+
101
+ declare type BenchFireActionArgs = BenchArgsWithGrants<FireActionArgs>;
102
+
103
+ declare type BenchSetStageArgs = BenchArgs<SetStageArgs>;
104
+
105
+ declare type BenchStartInstanceArgs = BenchArgs<StartInstanceArgs>;
93
106
 
94
107
  /** Base bench state, distinct from the engine/read/guard/query helper groups. */
95
108
  declare interface BenchState {
96
109
  /** The fake Sanity client. Passed automatically by bench wrappers. */
97
110
  client: TestClient;
111
+ /** The engine-scope partition tag every wrapped call runs under. */
112
+ tag: string;
113
+ /** The workflow resource every wrapped call targets. */
114
+ workflowResource: WorkflowResource;
115
+ /** The declared execution context stamped on every wrapped call's history
116
+ * entries (default `{kind: 'test'}`) — inherited by {@link createBenchEngine}. */
117
+ executionContext: DeclaredExecutionContext;
98
118
  /**
99
- * The access state (actor + grants) the bench injects as the
100
- * `access` override on every wrapped engine call. Defaults to
101
- * `ALL_ACCESS`.
119
+ * The bench's default identity (actor + grants) registered as the
120
+ * default token on the fake client. Defaults to `ALL_ACCESS`.
102
121
  */
103
122
  access: WorkflowAccess;
104
123
  /** Convenience: `access.actor`. */
105
124
  currentUser: Actor;
106
125
  /** Convenience: `access.grants ?? []`. */
107
126
  grants: Grant[];
127
+ /**
128
+ * A token-bound sibling client acting as `access` — the bench's ONE
129
+ * identity-registration seam. Registers the pair on the fake client's
130
+ * family-shared registry (idempotent, cached per identity; omitted
131
+ * grants default to wildcard) and returns the sibling whose
132
+ * `/users/me` + ACL path serve it — the engine token-resolves it like
133
+ * production. Bind it to {@link createBenchEngine} to act as another
134
+ * user end-to-end; never hand-roll `addIdentity` + `withConfig`.
135
+ */
136
+ clientFor: (access: WorkflowAccess) => TestClient;
108
137
  /** Add documents to the store after construction. */
109
138
  seedDocuments: (documents: StoreDocument[]) => void;
110
139
  /**
@@ -129,11 +158,7 @@ declare interface BenchState {
129
158
  advance: (ms: number) => void;
130
159
  }
131
160
 
132
- declare type BenchTickArgs = Omit<OperationArgs, "access"> & {
133
- access?: WorkflowAccess;
134
- /** Shortcut for `access.actor`. */
135
- actor?: Actor;
136
- };
161
+ declare type BenchTickArgs = BenchArgs<OperationArgs>;
137
162
 
138
163
  /**
139
164
  * One recorded invocation of a client method — the spy layer behind
@@ -151,6 +176,8 @@ declare interface CallRecord {
151
176
  result?: unknown;
152
177
  /** Thrown or rejected value, set if the call failed. */
153
178
  error?: unknown;
179
+ /** Request tag from the call's options, when one was passed (`null` = explicit suppression). */
180
+ tag?: string | null;
154
181
  projectId: string;
155
182
  dataset: string;
156
183
  }
@@ -177,14 +204,28 @@ declare interface ComposedHelpers {
177
204
  */
178
205
  export declare function createBench(options?: CreateBenchOptions): Bench;
179
206
 
207
+ /**
208
+ * Build an out-of-band {@link Engine} on a bench's client, inheriting the
209
+ * bench's tag, workflow resource, and clock (so `setNow` / `advance` govern
210
+ * the engine's `$now` too). Use this when a test needs an engine of its own —
211
+ * custom effect handlers, racing two drainers — rather than the wrappers the
212
+ * bench exposes. `overrides` win over every inherited value.
213
+ */
214
+ export declare function createBenchEngine(
215
+ bench: EngineScope & {
216
+ now: Clock;
217
+ },
218
+ overrides?: Partial<CreateEngineArgs>,
219
+ ): Engine;
220
+
180
221
  export declare interface CreateBenchOptions {
181
222
  /** Subject documents seeded into the store. */
182
223
  documents?: StoreDocument[];
183
224
  /**
184
- * Access state (actor + grants) injected as the engine `access`
185
- * override on every wrapped call. Defaults to `ALL_ACCESS`
186
- * wildcard user + wildcard grants. Pass a restrictive value to
187
- * test denial paths.
225
+ * The bench's default identity (actor + grants), registered as the
226
+ * default token on the fake client the engine token-resolves it like
227
+ * production. Defaults to `ALL_ACCESS` wildcard user + wildcard
228
+ * grants. Pass a restrictive value to test denial paths.
188
229
  *
189
230
  * Mutually exclusive with the `currentUser` / `grants` shortcuts
190
231
  * (which compose into the same shape internally).
@@ -195,28 +236,7 @@ export declare interface CreateBenchOptions {
195
236
  /** Shortcut for `access.grants`. Composed with `currentUser`. */
196
237
  grants?: Grant[];
197
238
  /**
198
- * Opt-in: simulate Sanity's hard write-gate inside the TestClient.
199
- *
200
- * When set, the bench's underlying TestClient:
201
- * - Exposes `client.request({ url })` serving `/users/me` + the
202
- * configured ACL path, so the engine can resolve `WorkflowAccess`
203
- * from "the token" exactly like production.
204
- * - Rejects writes the grants don't permit with a Sanity-style
205
- * `WriteAccessDeniedError` — the same 403 a real lake would
206
- * return.
207
- *
208
- * Use this to demonstrate what would happen if the engine's
209
- * soft-gate were bypassed (or disagreed with) — the lake is still
210
- * authoritative. The bench's `access` field is the engine-side
211
- * override; `accessControl` is the lake-side enforcement. They
212
- * usually carry the same `actor` + `grants` but can be made to
213
- * disagree to test edge cases.
214
- *
215
- * Mutually exclusive with passing a pre-built `client`.
216
- */
217
- accessControl?: TestClientAccessControl;
218
- /**
219
- * Engine-scope environment partition. Defaults to `"bench"`. Pass
239
+ * Engine-scope environment partition. Defaults to {@link BENCH_TAG}. Pass
220
240
  * different tags to two benches sharing a `client` to assert partition
221
241
  * isolation; pass the same tag to assert visibility through a shared
222
242
  * partition.
@@ -246,6 +266,12 @@ export declare interface CreateBenchOptions {
246
266
  * start on the wall clock (`advance` / `setNow` can freeze it later).
247
267
  */
248
268
  now?: string;
269
+ /**
270
+ * Declared execution context stamped (with the inferred runtime) on every
271
+ * history entry the bench's engine calls append. Defaults to
272
+ * `{ kind: "test" }`; override to exercise provenance rendering.
273
+ */
274
+ executionContext?: DeclaredExecutionContext;
249
275
  }
250
276
 
251
277
  declare interface CurrentUser {
@@ -278,10 +304,33 @@ declare interface DocumentBody {
278
304
 
279
305
  declare type DocumentValuePermission = "create" | "read" | "update";
280
306
 
281
- /** Engine-call wrappers that inject the bench-owned client + tag + access. */
307
+ /**
308
+ * How {@link GuardHelpers.editDocument} mutates the document. Each present
309
+ * key becomes one patch op, applied in `set` → `unset` → `insert` order in a
310
+ * single commit; the shapes mirror the client's patch builder.
311
+ */
312
+ export declare type EditDocumentPatch = {
313
+ set?: Record<string, unknown>;
314
+ unset?: string[];
315
+ insert?: {
316
+ position: "before" | "after" | "replace";
317
+ anchor: string;
318
+ items: unknown[];
319
+ };
320
+ };
321
+
322
+ /** The bench-owned engine call context injected into every wrapped verb. */
323
+ declare type EngineScope = {
324
+ client: TestClient;
325
+ tag: string;
326
+ workflowResource: WorkflowResource;
327
+ executionContext: DeclaredExecutionContext;
328
+ };
329
+
330
+ /** Engine-call wrappers that inject the bench-owned client + tag + identity. */
282
331
  declare type EngineWrappers = {
283
- deployDefinitions: (
284
- args: DeployDefinitionsArgs,
332
+ deployDefinitions: <T extends WorkflowDefinitionInput<T>>(
333
+ args: DeployDefinitionsArgs<T>,
285
334
  ) => Promise<DeployDefinitionsResult>;
286
335
  startInstance: (args: BenchStartInstanceArgs) => Promise<OperationResult>;
287
336
  fireAction: (args: BenchFireActionArgs) => Promise<OperationResult>;
@@ -289,6 +338,10 @@ declare type EngineWrappers = {
289
338
  completeEffect: (args: BenchCompleteEffectArgs) => Promise<OperationResult>;
290
339
  tick: (args: BenchTickArgs) => Promise<OperationResult>;
291
340
  evaluate: (args: BenchEvaluateArgs) => Promise<WorkflowEvaluation>;
341
+ /** Admin override — bypass filters/transitions and force the stage. */
342
+ setStage: (args: BenchSetStageArgs) => Promise<OperationResult>;
343
+ /** Admin override — hard-stop an in-flight instance where it stands. */
344
+ abortInstance: (args: BenchAbortInstanceArgs) => Promise<OperationResult>;
292
345
  guardsForInstance: (instanceId: string) => Promise<MutationGuardDoc[]>;
293
346
  guardsForDefinition: (definition: string) => Promise<MutationGuardDoc[]>;
294
347
  };
@@ -369,8 +422,12 @@ declare interface FetchOptions {
369
422
  * raw response. Ignored when `filterResponse` is not `false`.
370
423
  */
371
424
  returnQuery?: boolean;
372
- /** Accepted for signature parity; not simulated. */
373
- tag?: string;
425
+ /**
426
+ * Request tag, recorded on the {@link CallRecord} for assertions.
427
+ * `null` mirrors `@sanity/client`'s explicit-suppression form and is
428
+ * recorded as-is. No other behavior is simulated.
429
+ */
430
+ tag?: string | null;
374
431
  /** Accepted for signature parity; not simulated. */
375
432
  signal?: AbortSignal;
376
433
  }
@@ -397,11 +454,13 @@ declare type GuardHelpers = {
397
454
  * client gates the write against deployed guards (incl. `identity()`) and
398
455
  * throws `MutationGuardDeniedError` on denial — the lake's intended
399
456
  * rejection, simulated by the bench. `as` routes the write through a
400
- * token-scoped sibling so `identity()` resolves to that actor.
457
+ * token-scoped sibling so `identity()` resolves to that actor. A `patch`
458
+ * ({@link EditDocumentPatch}) is required for edits and rejected for
459
+ * `action: 'delete'` — the combination is contradictory input.
401
460
  */
402
461
  editDocument: (opts: {
403
462
  documentId: string;
404
- patch: Record<string, unknown>;
463
+ patch?: EditDocumentPatch;
405
464
  action?: MutationGuardAction;
406
465
  as?: Actor;
407
466
  }) => Promise<StoreDocument>;
@@ -423,6 +482,8 @@ declare interface MutationLogEntry {
423
482
  projectId: string;
424
483
  dataset: string;
425
484
  mutations: TxnMutation[];
485
+ /** Request tag the commit carried, when one was passed (`null` = explicit suppression). */
486
+ tag?: string | null;
426
487
  }
427
488
 
428
489
  /** The operation recorded per affected document, matching `@sanity/client`. */
@@ -450,8 +511,21 @@ declare interface MutationOptions {
450
511
  visibility?: "sync" | "async" | "deferred";
451
512
  /** Accepted for signature parity; not simulated. */
452
513
  autoGenerateArrayKeys?: boolean;
514
+ /**
515
+ * Request tag, recorded on the {@link CallRecord} and the
516
+ * {@link MutationLogEntry} for assertions. `null` mirrors
517
+ * `@sanity/client`'s explicit-suppression form and is recorded as-is.
518
+ * No other behavior is simulated.
519
+ */
520
+ tag?: string | null;
521
+ }
522
+
523
+ /** Options for `PatchHandle.commit()`. Only `tag` is meaningful to the fake. */
524
+ declare interface PatchCommitOptions {
525
+ /** Request tag, recorded on the mutation log entry (`null` = explicit suppression). */
526
+ tag?: string | null;
453
527
  /** Accepted for signature parity; not simulated. */
454
- tag?: string;
528
+ visibility?: "sync" | "async" | "deferred";
455
529
  }
456
530
 
457
531
  declare interface PatchHandle {
@@ -466,7 +540,7 @@ declare interface PatchHandle {
466
540
  items: unknown[],
467
541
  ) => PatchHandle;
468
542
  ifRevisionId: (rev: string) => PatchHandle;
469
- commit: () => Promise<SanityDocument>;
543
+ commit: (options?: PatchCommitOptions) => Promise<SanityDocument>;
470
544
  /** Test-only introspection — not on real Patch */
471
545
  toJSON: () => {
472
546
  id: string;
@@ -560,9 +634,9 @@ declare type ReadHelpers = {
560
634
  * Spawned children of `parentInstanceId`. If `activity` is provided,
561
635
  * only children spawned by that specific activity on the parent.
562
636
  *
563
- * Walks `history.spawned` entries on the parent — this is the durable
564
- * record. (The per-activity `spawnedInstances` field only lives on the
565
- * open StageEntry's activities; history survives transitions.)
637
+ * Walks `history.spawned` entries on the parent — the durable record
638
+ * (the workflow-scope registry tracks the same children, keyed for
639
+ * adoption rather than enumeration).
566
640
  *
567
641
  * Returns children ordered by `startedAt` ascending.
568
642
  */
@@ -597,6 +671,15 @@ declare type ReadHelpers = {
597
671
  * non-reactive content forwarder needs. Sorted by `startedAt` ascending.
598
672
  */
599
673
  instancesForDocument: (document: GdrUri) => Promise<WorkflowInstance[]>;
674
+ /**
675
+ * The startable half of {@link ReadHelpers.instancesForDocument}: the
676
+ * latest deployed version of every definition applicable to the LOADED
677
+ * candidate document — startable, a required subject entry accepts its
678
+ * `_type`, `applicableWhen` passes. All matches, name ascending.
679
+ */
680
+ definitionsForDocument: (
681
+ document: CandidateDocument,
682
+ ) => Promise<DeployedDefinition[]>;
600
683
  /** Snapshot of all documents in the store — for inspecting the world. */
601
684
  snapshot: () => readonly StoreDocument[];
602
685
  };
@@ -782,22 +865,18 @@ declare interface TestClient {
782
865
  options?: FetchOptions,
783
866
  ) => Promise<(T | null)[]>;
784
867
  /**
785
- * Sanity-style HTTP request hook. Present iff the client was
786
- * created with `accessControl`. The engine's access resolver pokes
787
- * `/users/me` and the configured ACL path through this — without it
788
- * the engine throws on any verb that needs an actor (which is the
789
- * cue for the bench to supply an `access` override).
790
- *
791
- * Test cases that want to exercise the engine's token-resolution
792
- * path should configure `accessControl`; cases that want bench-style
793
- * always-allow should leave it off and pass `access` through the
794
- * bench.
868
+ * Sanity-style HTTP request hook. Present iff the handle has an
869
+ * acting identity — created with `accessControl`, or bound to a
870
+ * registered token (`identities` + `token`). The engine's access
871
+ * resolver pokes `/users/me` and the configured ACL path through
872
+ * this without it the engine throws on any verb that needs an
873
+ * actor.
795
874
  */
796
875
  request?: <T>(opts: {
797
876
  url?: string;
798
877
  uri?: string;
799
878
  signal?: AbortSignal;
800
- tag?: string;
879
+ tag?: string | null;
801
880
  }) => Promise<T>;
802
881
  create: <T extends SanityDocumentLike>(
803
882
  doc: T,
@@ -835,7 +914,10 @@ declare interface TestClient {
835
914
  * Returns a synthetic `transactionId`. Multi-action arrays are applied
836
915
  * atomically — any failure rolls back the batch.
837
916
  */
838
- action: (action: Action | Action[]) => Promise<ActionResult>;
917
+ action: (
918
+ action: Action | Action[],
919
+ options?: ActionOptions,
920
+ ) => Promise<ActionResult>;
839
921
  /** Helpers mirroring `@sanity/client`'s `client.releases.*` surface. */
840
922
  releases: ReleasesHandle;
841
923
  /** Helper for `sanity.action.document.version.create`. */
@@ -849,11 +931,15 @@ declare interface TestClient {
849
931
  releaseId: string;
850
932
  publishedId: string;
851
933
  }) => Promise<ActionResult>;
852
- /** Current `(projectId, dataset)` and active perspective. */
934
+ /**
935
+ * Current `(projectId, dataset)` and active perspective. `token` is
936
+ * present iff the handle is bound to a registered identity.
937
+ */
853
938
  config: () => {
854
939
  projectId: string;
855
940
  dataset: string;
856
941
  perspective: Perspective;
942
+ token?: string;
857
943
  };
858
944
  /**
859
945
  * Return a new client handle pointing at a different `(projectId, dataset)`
@@ -870,8 +956,18 @@ declare interface TestClient {
870
956
  * Rebind the acting identity (and its grants) on a sibling that shares
871
957
  * the same store — the test analogue of a token-scoped client. Lets two
872
958
  * actors write the same dataset, e.g. for `identity()`-based guards.
959
+ * Mutually exclusive with `token`.
873
960
  */
874
961
  accessControl: TestClientAccessControl;
962
+ /**
963
+ * Bind the sibling to a token registered in the family's identity
964
+ * registry (`createTestClient({ identities })` / `addIdentity`): its
965
+ * `/users/me` serves that identity's user, its ACL path serves that
966
+ * identity's grants, and reads/writes are gated through them — the
967
+ * token determines identity, exactly like production. Unknown tokens
968
+ * throw. Mutually exclusive with `accessControl`.
969
+ */
970
+ token: string;
875
971
  }>,
876
972
  ) => TestClient;
877
973
  /** Replace the current dataset with the given documents. */
@@ -888,9 +984,21 @@ declare interface TestClient {
888
984
  * Simulate `count` concurrent edits to `id` in the current dataset: the
889
985
  * next `count` revision-guarded mutations (those passing `ifRevisionId`)
890
986
  * to that document throw a 409-shaped conflict, as if another client
891
- * committed between the caller's read and write. Lets a test exercise
892
- * optimistic-locking retry paths deterministically, without timing or
893
- * interleaving. Mutations that don't pass `ifRevisionId` are unaffected.
987
+ * committed between the caller's read and write. Mutations that don't
988
+ * pass `ifRevisionId` are unaffected.
989
+ *
990
+ * Reach for this when the code under test reads a document's `_rev` and
991
+ * commits with `ifRevisionId` internally — so you can't stage the race
992
+ * by mutating the document first (the code would just re-read the new
993
+ * revision and its commit would match). The counter is consumed one per
994
+ * guarded commit, so `count` lets a test drive a recover-after-N-losses
995
+ * path or exhaust a retry budget, deterministically and without relying
996
+ * on promise interleaving.
997
+ *
998
+ * Note: this forces the conflict *outcome* — it short-circuits the
999
+ * revision comparison rather than changing the stored `_rev`. The code
1000
+ * under test sees the identical 409 error a real lost race produces; the
1001
+ * stored revision is left untouched.
894
1002
  */
895
1003
  simulateConcurrentEdit: (id: string, count?: number) => void;
896
1004
  /** List `(projectId, dataset)` pairs known in this registry. */
@@ -908,6 +1016,16 @@ declare interface TestClient {
908
1016
  stub: (name: string, handler: unknown) => void;
909
1017
  /** The currently-registered stubs, keyed by member name. */
910
1018
  stubs: () => Record<string, unknown>;
1019
+ /**
1020
+ * Register (or upsert) a token→identity entry in the family's shared
1021
+ * registry. Siblings already bound to `token` resolve lazily, so a
1022
+ * re-registration takes effect on them immediately. Registering an
1023
+ * identity does not bind it — pass `token` at `createTestClient` or
1024
+ * `withConfig({ token })` to act as it.
1025
+ */
1026
+ addIdentity: (token: string, identity: TestClientIdentity) => void;
1027
+ /** Snapshot of the registered token→identity entries. */
1028
+ identities: () => Record<string, TestClientIdentity>;
911
1029
  /**
912
1030
  * Every mutation committed through this client's registry, flattened
913
1031
  * and in order — `create` / `createOrReplace` / `createIfNotExists` /
@@ -952,6 +1070,20 @@ declare interface TestClientAccessControl {
952
1070
  aclPath?: string;
953
1071
  }
954
1072
 
1073
+ /**
1074
+ * One entry in the token→identity registry (`createTestClient({ identities })`).
1075
+ * A handle bound to the entry's token behaves exactly like one configured
1076
+ * with `accessControl: { currentUser: user, grants }` — `/users/me` serves
1077
+ * `user`, the default ACL path serves `grants`, and reads/writes are gated
1078
+ * through them.
1079
+ */
1080
+ declare interface TestClientIdentity {
1081
+ /** What `/users/me` returns for clients bound to this token. */
1082
+ user: CurrentUser;
1083
+ /** Grants served at the ACL path and enforced on reads/writes. */
1084
+ grants: Grant_2[];
1085
+ }
1086
+
955
1087
  /**
956
1088
  * In-memory document store backing a TestClient. Closure-scoped — created
957
1089
  * fresh per test, no global state.
@@ -1007,6 +1139,12 @@ declare interface TxnCommitOptions {
1007
1139
  returnFirst?: boolean;
1008
1140
  /** Override the synthetic transaction id (mirrors `client.transaction(undefined, { transactionId })`). */
1009
1141
  transactionId?: string;
1142
+ /**
1143
+ * Request tag, recorded on the mutation log entry for assertions.
1144
+ * `null` mirrors `@sanity/client`'s explicit-suppression form and is
1145
+ * recorded as-is. No other behavior is simulated.
1146
+ */
1147
+ tag?: string | null;
1010
1148
  }
1011
1149
 
1012
1150
  declare type TxnCommitResult =