@sanity-labs/client-fake-for-test 0.5.0 → 0.7.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,5 +1,11 @@
1
1
  export declare type Action = ReleaseAction | VersionAction;
2
2
 
3
+ /** Options for `action()`. Only `tag` is meaningful to the fake. */
4
+ export declare interface ActionOptions {
5
+ /** Request tag, recorded on the {@link CallRecord} (`null` = explicit suppression). */
6
+ tag?: string | null;
7
+ }
8
+
3
9
  export declare interface ActionResult {
4
10
  transactionId: string;
5
11
  }
@@ -51,6 +57,8 @@ export declare interface CallRecord {
51
57
  result?: unknown;
52
58
  /** Thrown or rejected value, set if the call failed. */
53
59
  error?: unknown;
60
+ /** Request tag from the call's options, when one was passed (`null` = explicit suppression). */
61
+ tag?: string | null;
54
62
  projectId: string;
55
63
  dataset: string;
56
64
  }
@@ -110,6 +118,37 @@ export declare interface CreateTestClientOptions {
110
118
  * unconditionally — the legacy behavior.
111
119
  */
112
120
  accessControl?: TestClientAccessControl;
121
+ /**
122
+ * Token→identity registry, shared by the whole `withConfig` family:
123
+ * multiple identities over one store, keyed by the token a handle is
124
+ * bound to — the token determines identity, exactly like production.
125
+ *
126
+ * ```ts
127
+ * const client = createTestClient({
128
+ * token: "t-admin",
129
+ * identities: {
130
+ * "t-admin": { user: { id: "admin", roles: [{ name: "administrator" }] }, grants: WILDCARD },
131
+ * "t-bob": { user: { id: "bob", roles: [{ name: "editor" }] }, grants: READ_ONLY },
132
+ * },
133
+ * });
134
+ * const bob = client.withConfig({ token: "t-bob" });
135
+ * // same store; bob's /users/me → bob; bob's ACL path → bob's grants
136
+ * ```
137
+ *
138
+ * A token-bound handle behaves exactly like one configured with
139
+ * `accessControl` derived from its identity (identity serving + grant
140
+ * gating; the ACL path defaults per `(projectId, dataset)`). Register
141
+ * more identities later with `addIdentity`.
142
+ */
143
+ identities?: Record<string, TestClientIdentity>;
144
+ /**
145
+ * Token the root handle is bound to — must name an entry in
146
+ * `identities` (unknown tokens throw). Omit to leave the root handle
147
+ * identity-free (a bare factory for token-bound siblings): its
148
+ * `request` is undefined and its reads/writes are ungated. Mutually
149
+ * exclusive with `accessControl`.
150
+ */
151
+ token?: string;
113
152
  /**
114
153
  * Pluggable write interceptor — runs on each write through the document
115
154
  * mutation API (create/patch/delete/transaction; not the release/version
@@ -290,8 +329,12 @@ export declare interface FetchOptions {
290
329
  * raw response. Ignored when `filterResponse` is not `false`.
291
330
  */
292
331
  returnQuery?: boolean;
293
- /** Accepted for signature parity; not simulated. */
294
- tag?: string;
332
+ /**
333
+ * Request tag, recorded on the {@link CallRecord} for assertions.
334
+ * `null` mirrors `@sanity/client`'s explicit-suppression form and is
335
+ * recorded as-is. No other behavior is simulated.
336
+ */
337
+ tag?: string | null;
295
338
  /** Accepted for signature parity; not simulated. */
296
339
  signal?: AbortSignal;
297
340
  }
@@ -398,6 +441,8 @@ export declare interface MutationLogEntry {
398
441
  projectId: string;
399
442
  dataset: string;
400
443
  mutations: TxnMutation[];
444
+ /** Request tag the commit carried, when one was passed (`null` = explicit suppression). */
445
+ tag?: string | null;
401
446
  }
402
447
 
403
448
  /** The operation recorded per affected document, matching `@sanity/client`. */
@@ -425,8 +470,21 @@ export declare interface MutationOptions {
425
470
  visibility?: "sync" | "async" | "deferred";
426
471
  /** Accepted for signature parity; not simulated. */
427
472
  autoGenerateArrayKeys?: boolean;
473
+ /**
474
+ * Request tag, recorded on the {@link CallRecord} and the
475
+ * {@link MutationLogEntry} for assertions. `null` mirrors
476
+ * `@sanity/client`'s explicit-suppression form and is recorded as-is.
477
+ * No other behavior is simulated.
478
+ */
479
+ tag?: string | null;
480
+ }
481
+
482
+ /** Options for `PatchHandle.commit()`. Only `tag` is meaningful to the fake. */
483
+ export declare interface PatchCommitOptions {
484
+ /** Request tag, recorded on the mutation log entry (`null` = explicit suppression). */
485
+ tag?: string | null;
428
486
  /** Accepted for signature parity; not simulated. */
429
- tag?: string;
487
+ visibility?: "sync" | "async" | "deferred";
430
488
  }
431
489
 
432
490
  export declare interface PatchExecutor {
@@ -434,6 +492,7 @@ export declare interface PatchExecutor {
434
492
  documentId: string,
435
493
  ops: PatchOp[],
436
494
  ifRev: string | undefined,
495
+ options: PatchCommitOptions | undefined,
437
496
  ) => Promise<SanityDocument>;
438
497
  }
439
498
 
@@ -449,7 +508,7 @@ export declare interface PatchHandle {
449
508
  items: unknown[],
450
509
  ) => PatchHandle;
451
510
  ifRevisionId: (rev: string) => PatchHandle;
452
- commit: () => Promise<SanityDocument>;
511
+ commit: (options?: PatchCommitOptions) => Promise<SanityDocument>;
453
512
  /** Test-only introspection — not on real Patch */
454
513
  toJSON: () => {
455
514
  id: string;
@@ -659,7 +718,7 @@ export declare const SUPPORTED_SURFACE: readonly [
659
718
  "releases",
660
719
  "createVersion",
661
720
  "unpublishVersion",
662
- "request (only with accessControl)",
721
+ "request (only with accessControl or a bound token)",
663
722
  "config",
664
723
  "withConfig",
665
724
  ];
@@ -685,22 +744,18 @@ export declare interface TestClient {
685
744
  options?: FetchOptions,
686
745
  ) => Promise<(T | null)[]>;
687
746
  /**
688
- * Sanity-style HTTP request hook. Present iff the client was
689
- * created with `accessControl`. The engine's access resolver pokes
690
- * `/users/me` and the configured ACL path through this — without it
691
- * the engine throws on any verb that needs an actor (which is the
692
- * cue for the bench to supply an `access` override).
693
- *
694
- * Test cases that want to exercise the engine's token-resolution
695
- * path should configure `accessControl`; cases that want bench-style
696
- * always-allow should leave it off and pass `access` through the
697
- * bench.
747
+ * Sanity-style HTTP request hook. Present iff the handle has an
748
+ * acting identity — created with `accessControl`, or bound to a
749
+ * registered token (`identities` + `token`). The engine's access
750
+ * resolver pokes `/users/me` and the configured ACL path through
751
+ * this — without it the engine throws on any verb that needs an
752
+ * actor.
698
753
  */
699
754
  request?: <T>(opts: {
700
755
  url?: string;
701
756
  uri?: string;
702
757
  signal?: AbortSignal;
703
- tag?: string;
758
+ tag?: string | null;
704
759
  }) => Promise<T>;
705
760
  create: <T extends SanityDocumentLike>(
706
761
  doc: T,
@@ -738,7 +793,10 @@ export declare interface TestClient {
738
793
  * Returns a synthetic `transactionId`. Multi-action arrays are applied
739
794
  * atomically — any failure rolls back the batch.
740
795
  */
741
- action: (action: Action | Action[]) => Promise<ActionResult>;
796
+ action: (
797
+ action: Action | Action[],
798
+ options?: ActionOptions,
799
+ ) => Promise<ActionResult>;
742
800
  /** Helpers mirroring `@sanity/client`'s `client.releases.*` surface. */
743
801
  releases: ReleasesHandle;
744
802
  /** Helper for `sanity.action.document.version.create`. */
@@ -752,11 +810,15 @@ export declare interface TestClient {
752
810
  releaseId: string;
753
811
  publishedId: string;
754
812
  }) => Promise<ActionResult>;
755
- /** Current `(projectId, dataset)` and active perspective. */
813
+ /**
814
+ * Current `(projectId, dataset)` and active perspective. `token` is
815
+ * present iff the handle is bound to a registered identity.
816
+ */
756
817
  config: () => {
757
818
  projectId: string;
758
819
  dataset: string;
759
820
  perspective: Perspective;
821
+ token?: string;
760
822
  };
761
823
  /**
762
824
  * Return a new client handle pointing at a different `(projectId, dataset)`
@@ -773,8 +835,18 @@ export declare interface TestClient {
773
835
  * Rebind the acting identity (and its grants) on a sibling that shares
774
836
  * the same store — the test analogue of a token-scoped client. Lets two
775
837
  * actors write the same dataset, e.g. for `identity()`-based guards.
838
+ * Mutually exclusive with `token`.
776
839
  */
777
840
  accessControl: TestClientAccessControl;
841
+ /**
842
+ * Bind the sibling to a token registered in the family's identity
843
+ * registry (`createTestClient({ identities })` / `addIdentity`): its
844
+ * `/users/me` serves that identity's user, its ACL path serves that
845
+ * identity's grants, and reads/writes are gated through them — the
846
+ * token determines identity, exactly like production. Unknown tokens
847
+ * throw. Mutually exclusive with `accessControl`.
848
+ */
849
+ token: string;
778
850
  }>,
779
851
  ) => TestClient;
780
852
  /** Replace the current dataset with the given documents. */
@@ -791,9 +863,21 @@ export declare interface TestClient {
791
863
  * Simulate `count` concurrent edits to `id` in the current dataset: the
792
864
  * next `count` revision-guarded mutations (those passing `ifRevisionId`)
793
865
  * to that document throw a 409-shaped conflict, as if another client
794
- * committed between the caller's read and write. Lets a test exercise
795
- * optimistic-locking retry paths deterministically, without timing or
796
- * interleaving. Mutations that don't pass `ifRevisionId` are unaffected.
866
+ * committed between the caller's read and write. Mutations that don't
867
+ * pass `ifRevisionId` are unaffected.
868
+ *
869
+ * Reach for this when the code under test reads a document's `_rev` and
870
+ * commits with `ifRevisionId` internally — so you can't stage the race
871
+ * by mutating the document first (the code would just re-read the new
872
+ * revision and its commit would match). The counter is consumed one per
873
+ * guarded commit, so `count` lets a test drive a recover-after-N-losses
874
+ * path or exhaust a retry budget, deterministically and without relying
875
+ * on promise interleaving.
876
+ *
877
+ * Note: this forces the conflict *outcome* — it short-circuits the
878
+ * revision comparison rather than changing the stored `_rev`. The code
879
+ * under test sees the identical 409 error a real lost race produces; the
880
+ * stored revision is left untouched.
797
881
  */
798
882
  simulateConcurrentEdit: (id: string, count?: number) => void;
799
883
  /** List `(projectId, dataset)` pairs known in this registry. */
@@ -811,6 +895,16 @@ export declare interface TestClient {
811
895
  stub: (name: string, handler: unknown) => void;
812
896
  /** The currently-registered stubs, keyed by member name. */
813
897
  stubs: () => Record<string, unknown>;
898
+ /**
899
+ * Register (or upsert) a token→identity entry in the family's shared
900
+ * registry. Siblings already bound to `token` resolve lazily, so a
901
+ * re-registration takes effect on them immediately. Registering an
902
+ * identity does not bind it — pass `token` at `createTestClient` or
903
+ * `withConfig({ token })` to act as it.
904
+ */
905
+ addIdentity: (token: string, identity: TestClientIdentity) => void;
906
+ /** Snapshot of the registered token→identity entries. */
907
+ identities: () => Record<string, TestClientIdentity>;
814
908
  /**
815
909
  * Every mutation committed through this client's registry, flattened
816
910
  * and in order — `create` / `createOrReplace` / `createIfNotExists` /
@@ -855,6 +949,20 @@ export declare interface TestClientAccessControl {
855
949
  aclPath?: string;
856
950
  }
857
951
 
952
+ /**
953
+ * One entry in the token→identity registry (`createTestClient({ identities })`).
954
+ * A handle bound to the entry's token behaves exactly like one configured
955
+ * with `accessControl: { currentUser: user, grants }` — `/users/me` serves
956
+ * `user`, the default ACL path serves `grants`, and reads/writes are gated
957
+ * through them.
958
+ */
959
+ export declare interface TestClientIdentity {
960
+ /** What `/users/me` returns for clients bound to this token. */
961
+ user: CurrentUser;
962
+ /** Grants served at the ACL path and enforced on reads/writes. */
963
+ grants: Grant[];
964
+ }
965
+
858
966
  /**
859
967
  * In-memory document store backing a TestClient. Closure-scoped — created
860
968
  * fresh per test, no global state.
@@ -910,6 +1018,12 @@ export declare interface TxnCommitOptions {
910
1018
  returnFirst?: boolean;
911
1019
  /** Override the synthetic transaction id (mirrors `client.transaction(undefined, { transactionId })`). */
912
1020
  transactionId?: string;
1021
+ /**
1022
+ * Request tag, recorded on the mutation log entry for assertions.
1023
+ * `null` mirrors `@sanity/client`'s explicit-suppression form and is
1024
+ * recorded as-is. No other behavior is simulated.
1025
+ */
1026
+ tag?: string | null;
913
1027
  }
914
1028
 
915
1029
  export declare type TxnCommitResult =
package/dist/index.d.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  export declare type Action = ReleaseAction | VersionAction;
2
2
 
3
+ /** Options for `action()`. Only `tag` is meaningful to the fake. */
4
+ export declare interface ActionOptions {
5
+ /** Request tag, recorded on the {@link CallRecord} (`null` = explicit suppression). */
6
+ tag?: string | null;
7
+ }
8
+
3
9
  export declare interface ActionResult {
4
10
  transactionId: string;
5
11
  }
@@ -51,6 +57,8 @@ export declare interface CallRecord {
51
57
  result?: unknown;
52
58
  /** Thrown or rejected value, set if the call failed. */
53
59
  error?: unknown;
60
+ /** Request tag from the call's options, when one was passed (`null` = explicit suppression). */
61
+ tag?: string | null;
54
62
  projectId: string;
55
63
  dataset: string;
56
64
  }
@@ -110,6 +118,37 @@ export declare interface CreateTestClientOptions {
110
118
  * unconditionally — the legacy behavior.
111
119
  */
112
120
  accessControl?: TestClientAccessControl;
121
+ /**
122
+ * Token→identity registry, shared by the whole `withConfig` family:
123
+ * multiple identities over one store, keyed by the token a handle is
124
+ * bound to — the token determines identity, exactly like production.
125
+ *
126
+ * ```ts
127
+ * const client = createTestClient({
128
+ * token: "t-admin",
129
+ * identities: {
130
+ * "t-admin": { user: { id: "admin", roles: [{ name: "administrator" }] }, grants: WILDCARD },
131
+ * "t-bob": { user: { id: "bob", roles: [{ name: "editor" }] }, grants: READ_ONLY },
132
+ * },
133
+ * });
134
+ * const bob = client.withConfig({ token: "t-bob" });
135
+ * // same store; bob's /users/me → bob; bob's ACL path → bob's grants
136
+ * ```
137
+ *
138
+ * A token-bound handle behaves exactly like one configured with
139
+ * `accessControl` derived from its identity (identity serving + grant
140
+ * gating; the ACL path defaults per `(projectId, dataset)`). Register
141
+ * more identities later with `addIdentity`.
142
+ */
143
+ identities?: Record<string, TestClientIdentity>;
144
+ /**
145
+ * Token the root handle is bound to — must name an entry in
146
+ * `identities` (unknown tokens throw). Omit to leave the root handle
147
+ * identity-free (a bare factory for token-bound siblings): its
148
+ * `request` is undefined and its reads/writes are ungated. Mutually
149
+ * exclusive with `accessControl`.
150
+ */
151
+ token?: string;
113
152
  /**
114
153
  * Pluggable write interceptor — runs on each write through the document
115
154
  * mutation API (create/patch/delete/transaction; not the release/version
@@ -290,8 +329,12 @@ export declare interface FetchOptions {
290
329
  * raw response. Ignored when `filterResponse` is not `false`.
291
330
  */
292
331
  returnQuery?: boolean;
293
- /** Accepted for signature parity; not simulated. */
294
- tag?: string;
332
+ /**
333
+ * Request tag, recorded on the {@link CallRecord} for assertions.
334
+ * `null` mirrors `@sanity/client`'s explicit-suppression form and is
335
+ * recorded as-is. No other behavior is simulated.
336
+ */
337
+ tag?: string | null;
295
338
  /** Accepted for signature parity; not simulated. */
296
339
  signal?: AbortSignal;
297
340
  }
@@ -398,6 +441,8 @@ export declare interface MutationLogEntry {
398
441
  projectId: string;
399
442
  dataset: string;
400
443
  mutations: TxnMutation[];
444
+ /** Request tag the commit carried, when one was passed (`null` = explicit suppression). */
445
+ tag?: string | null;
401
446
  }
402
447
 
403
448
  /** The operation recorded per affected document, matching `@sanity/client`. */
@@ -425,8 +470,21 @@ export declare interface MutationOptions {
425
470
  visibility?: "sync" | "async" | "deferred";
426
471
  /** Accepted for signature parity; not simulated. */
427
472
  autoGenerateArrayKeys?: boolean;
473
+ /**
474
+ * Request tag, recorded on the {@link CallRecord} and the
475
+ * {@link MutationLogEntry} for assertions. `null` mirrors
476
+ * `@sanity/client`'s explicit-suppression form and is recorded as-is.
477
+ * No other behavior is simulated.
478
+ */
479
+ tag?: string | null;
480
+ }
481
+
482
+ /** Options for `PatchHandle.commit()`. Only `tag` is meaningful to the fake. */
483
+ export declare interface PatchCommitOptions {
484
+ /** Request tag, recorded on the mutation log entry (`null` = explicit suppression). */
485
+ tag?: string | null;
428
486
  /** Accepted for signature parity; not simulated. */
429
- tag?: string;
487
+ visibility?: "sync" | "async" | "deferred";
430
488
  }
431
489
 
432
490
  export declare interface PatchExecutor {
@@ -434,6 +492,7 @@ export declare interface PatchExecutor {
434
492
  documentId: string,
435
493
  ops: PatchOp[],
436
494
  ifRev: string | undefined,
495
+ options: PatchCommitOptions | undefined,
437
496
  ) => Promise<SanityDocument>;
438
497
  }
439
498
 
@@ -449,7 +508,7 @@ export declare interface PatchHandle {
449
508
  items: unknown[],
450
509
  ) => PatchHandle;
451
510
  ifRevisionId: (rev: string) => PatchHandle;
452
- commit: () => Promise<SanityDocument>;
511
+ commit: (options?: PatchCommitOptions) => Promise<SanityDocument>;
453
512
  /** Test-only introspection — not on real Patch */
454
513
  toJSON: () => {
455
514
  id: string;
@@ -659,7 +718,7 @@ export declare const SUPPORTED_SURFACE: readonly [
659
718
  "releases",
660
719
  "createVersion",
661
720
  "unpublishVersion",
662
- "request (only with accessControl)",
721
+ "request (only with accessControl or a bound token)",
663
722
  "config",
664
723
  "withConfig",
665
724
  ];
@@ -685,22 +744,18 @@ export declare interface TestClient {
685
744
  options?: FetchOptions,
686
745
  ) => Promise<(T | null)[]>;
687
746
  /**
688
- * Sanity-style HTTP request hook. Present iff the client was
689
- * created with `accessControl`. The engine's access resolver pokes
690
- * `/users/me` and the configured ACL path through this — without it
691
- * the engine throws on any verb that needs an actor (which is the
692
- * cue for the bench to supply an `access` override).
693
- *
694
- * Test cases that want to exercise the engine's token-resolution
695
- * path should configure `accessControl`; cases that want bench-style
696
- * always-allow should leave it off and pass `access` through the
697
- * bench.
747
+ * Sanity-style HTTP request hook. Present iff the handle has an
748
+ * acting identity — created with `accessControl`, or bound to a
749
+ * registered token (`identities` + `token`). The engine's access
750
+ * resolver pokes `/users/me` and the configured ACL path through
751
+ * this — without it the engine throws on any verb that needs an
752
+ * actor.
698
753
  */
699
754
  request?: <T>(opts: {
700
755
  url?: string;
701
756
  uri?: string;
702
757
  signal?: AbortSignal;
703
- tag?: string;
758
+ tag?: string | null;
704
759
  }) => Promise<T>;
705
760
  create: <T extends SanityDocumentLike>(
706
761
  doc: T,
@@ -738,7 +793,10 @@ export declare interface TestClient {
738
793
  * Returns a synthetic `transactionId`. Multi-action arrays are applied
739
794
  * atomically — any failure rolls back the batch.
740
795
  */
741
- action: (action: Action | Action[]) => Promise<ActionResult>;
796
+ action: (
797
+ action: Action | Action[],
798
+ options?: ActionOptions,
799
+ ) => Promise<ActionResult>;
742
800
  /** Helpers mirroring `@sanity/client`'s `client.releases.*` surface. */
743
801
  releases: ReleasesHandle;
744
802
  /** Helper for `sanity.action.document.version.create`. */
@@ -752,11 +810,15 @@ export declare interface TestClient {
752
810
  releaseId: string;
753
811
  publishedId: string;
754
812
  }) => Promise<ActionResult>;
755
- /** Current `(projectId, dataset)` and active perspective. */
813
+ /**
814
+ * Current `(projectId, dataset)` and active perspective. `token` is
815
+ * present iff the handle is bound to a registered identity.
816
+ */
756
817
  config: () => {
757
818
  projectId: string;
758
819
  dataset: string;
759
820
  perspective: Perspective;
821
+ token?: string;
760
822
  };
761
823
  /**
762
824
  * Return a new client handle pointing at a different `(projectId, dataset)`
@@ -773,8 +835,18 @@ export declare interface TestClient {
773
835
  * Rebind the acting identity (and its grants) on a sibling that shares
774
836
  * the same store — the test analogue of a token-scoped client. Lets two
775
837
  * actors write the same dataset, e.g. for `identity()`-based guards.
838
+ * Mutually exclusive with `token`.
776
839
  */
777
840
  accessControl: TestClientAccessControl;
841
+ /**
842
+ * Bind the sibling to a token registered in the family's identity
843
+ * registry (`createTestClient({ identities })` / `addIdentity`): its
844
+ * `/users/me` serves that identity's user, its ACL path serves that
845
+ * identity's grants, and reads/writes are gated through them — the
846
+ * token determines identity, exactly like production. Unknown tokens
847
+ * throw. Mutually exclusive with `accessControl`.
848
+ */
849
+ token: string;
778
850
  }>,
779
851
  ) => TestClient;
780
852
  /** Replace the current dataset with the given documents. */
@@ -791,9 +863,21 @@ export declare interface TestClient {
791
863
  * Simulate `count` concurrent edits to `id` in the current dataset: the
792
864
  * next `count` revision-guarded mutations (those passing `ifRevisionId`)
793
865
  * to that document throw a 409-shaped conflict, as if another client
794
- * committed between the caller's read and write. Lets a test exercise
795
- * optimistic-locking retry paths deterministically, without timing or
796
- * interleaving. Mutations that don't pass `ifRevisionId` are unaffected.
866
+ * committed between the caller's read and write. Mutations that don't
867
+ * pass `ifRevisionId` are unaffected.
868
+ *
869
+ * Reach for this when the code under test reads a document's `_rev` and
870
+ * commits with `ifRevisionId` internally — so you can't stage the race
871
+ * by mutating the document first (the code would just re-read the new
872
+ * revision and its commit would match). The counter is consumed one per
873
+ * guarded commit, so `count` lets a test drive a recover-after-N-losses
874
+ * path or exhaust a retry budget, deterministically and without relying
875
+ * on promise interleaving.
876
+ *
877
+ * Note: this forces the conflict *outcome* — it short-circuits the
878
+ * revision comparison rather than changing the stored `_rev`. The code
879
+ * under test sees the identical 409 error a real lost race produces; the
880
+ * stored revision is left untouched.
797
881
  */
798
882
  simulateConcurrentEdit: (id: string, count?: number) => void;
799
883
  /** List `(projectId, dataset)` pairs known in this registry. */
@@ -811,6 +895,16 @@ export declare interface TestClient {
811
895
  stub: (name: string, handler: unknown) => void;
812
896
  /** The currently-registered stubs, keyed by member name. */
813
897
  stubs: () => Record<string, unknown>;
898
+ /**
899
+ * Register (or upsert) a token→identity entry in the family's shared
900
+ * registry. Siblings already bound to `token` resolve lazily, so a
901
+ * re-registration takes effect on them immediately. Registering an
902
+ * identity does not bind it — pass `token` at `createTestClient` or
903
+ * `withConfig({ token })` to act as it.
904
+ */
905
+ addIdentity: (token: string, identity: TestClientIdentity) => void;
906
+ /** Snapshot of the registered token→identity entries. */
907
+ identities: () => Record<string, TestClientIdentity>;
814
908
  /**
815
909
  * Every mutation committed through this client's registry, flattened
816
910
  * and in order — `create` / `createOrReplace` / `createIfNotExists` /
@@ -855,6 +949,20 @@ export declare interface TestClientAccessControl {
855
949
  aclPath?: string;
856
950
  }
857
951
 
952
+ /**
953
+ * One entry in the token→identity registry (`createTestClient({ identities })`).
954
+ * A handle bound to the entry's token behaves exactly like one configured
955
+ * with `accessControl: { currentUser: user, grants }` — `/users/me` serves
956
+ * `user`, the default ACL path serves `grants`, and reads/writes are gated
957
+ * through them.
958
+ */
959
+ export declare interface TestClientIdentity {
960
+ /** What `/users/me` returns for clients bound to this token. */
961
+ user: CurrentUser;
962
+ /** Grants served at the ACL path and enforced on reads/writes. */
963
+ grants: Grant[];
964
+ }
965
+
858
966
  /**
859
967
  * In-memory document store backing a TestClient. Closure-scoped — created
860
968
  * fresh per test, no global state.
@@ -910,6 +1018,12 @@ export declare interface TxnCommitOptions {
910
1018
  returnFirst?: boolean;
911
1019
  /** Override the synthetic transaction id (mirrors `client.transaction(undefined, { transactionId })`). */
912
1020
  transactionId?: string;
1021
+ /**
1022
+ * Request tag, recorded on the mutation log entry for assertions.
1023
+ * `null` mirrors `@sanity/client`'s explicit-suppression form and is
1024
+ * recorded as-is. No other behavior is simulated.
1025
+ */
1026
+ tag?: string | null;
913
1027
  }
914
1028
 
915
1029
  export declare type TxnCommitResult =