@indigoai-us/hq-cli 5.12.4 → 5.13.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.
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="217ceadb-01cd-5778-9cc0-6f6d895d8d67")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b75c8495-7e0e-53f8-a0d2-c92a993e6492")}catch(e){}}();
3
3
  import chalk from "chalk";
4
4
  import * as readline from "node:readline";
5
5
  import { spawn } from "node:child_process";
@@ -467,7 +467,6 @@ export function registerSecretsCommand(program) {
467
467
  .option("--expires <duration>", "Token expiry duration (e.g. 24h, 2d, 30m)", "24h")
468
468
  .action(async (name, opts) => {
469
469
  try {
470
- rejectIfPersonal(secrets.opts(), "generate-link");
471
470
  if (!SECRET_NAME_PATTERN.test(name)) {
472
471
  console.error(chalk.red(`Invalid secret name '${name}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_API_KEY or DEV/MY_KEY)`));
473
472
  process.exit(1);
@@ -713,4 +712,4 @@ export function registerSecretsCommand(program) {
713
712
  });
714
713
  }
715
714
  //# sourceMappingURL=secrets.js.map
716
- //# debugId=217ceadb-01cd-5778-9cc0-6f6d895d8d67
715
+ //# debugId=b75c8495-7e0e-53f8-a0d2-c92a993e6492
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * HQ CLI - Module management, package management, and cloud sync for HQ
4
4
  */
5
5
 
6
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="efa927a6-3cad-5122-928e-f7b055b533e8")}catch(e){}}();
6
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="56a9367e-c800-55df-9c55-44274965648e")}catch(e){}}();
7
7
  import { Command } from "commander";
8
8
  import { initSentry, Sentry } from "./sentry.js";
9
9
  import { registerAddCommand } from "./commands/add.js";
@@ -31,6 +31,15 @@ import { registerMembersCommand } from "./commands/members.js";
31
31
  import { registerFeedbackCommand } from "./commands/feedback.js";
32
32
  import { sanitizeArgv } from "./utils/feedback-diagnostics.js";
33
33
  import { maybeWarnNewVersion, refreshVersionCache, } from "./utils/version-check.js";
34
+ // Swallow EPIPE when a downstream reader (e.g. `source <(…)`, `| head`) closes the pipe early.
35
+ const onPipeError = (err) => {
36
+ if (err.code === "EPIPE") {
37
+ process.exit(0);
38
+ }
39
+ throw err;
40
+ };
41
+ process.stdout.on("error", onPipeError);
42
+ process.stderr.on("error", onPipeError);
34
43
  initSentry();
35
44
  maybeWarnNewVersion();
36
45
  const program = new Command();
@@ -110,4 +119,4 @@ registerFeedbackCommand(program);
110
119
  }
111
120
  })();
112
121
  //# sourceMappingURL=index.js.map
113
- //# debugId=efa927a6-3cad-5122-928e-f7b055b533e8
122
+ //# debugId=56a9367e-c800-55df-9c55-44274965648e
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.12.4",
3
+ "version": "5.13.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -0,0 +1,381 @@
1
+ /**
2
+ * Unit tests for `pushAll` — the orchestrator behind `hq sync push --all`.
3
+ *
4
+ * Mirrors the DI layout used in `cloud.pull-all.test.ts`: pushAll takes a
5
+ * `VaultClient`-shaped adapter and a `share`-shaped function as deps, so
6
+ * the tests can exercise the membership + canonical-person fanout without
7
+ * touching the network or the real share() implementation.
8
+ *
9
+ * Personal paths are computed via the real `computePersonalVaultPaths` so
10
+ * we verify the hqRoot scope on disk. Each test that exercises personal
11
+ * uses an isolated tmpdir with known top-level entries.
12
+ */
13
+
14
+ import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
15
+ import * as fs from "fs";
16
+ import * as os from "os";
17
+ import * as path from "path";
18
+
19
+ import {
20
+ pushAll,
21
+ type PushAllDeps,
22
+ type PullAllVaultClient,
23
+ type ShareCallOptions,
24
+ type ShareCallResult,
25
+ } from "./cloud.js";
26
+
27
+ // ── Helpers ─────────────────────────────────────────────────────────────────
28
+
29
+ interface FakeMembership {
30
+ companyUid: string;
31
+ }
32
+
33
+ interface FakeEntity {
34
+ uid: string;
35
+ type: "person" | "company";
36
+ slug: string;
37
+ bucketName?: string;
38
+ createdAt: string;
39
+ }
40
+
41
+ function makeVaultClient(opts: {
42
+ memberships?: FakeMembership[];
43
+ persons?: FakeEntity[];
44
+ entitiesBySlug?: Record<string, { slug: string; name?: string }>;
45
+ failGetEntityFor?: Set<string>;
46
+ }): PullAllVaultClient {
47
+ return {
48
+ listMyMemberships: vi
49
+ .fn<[], Promise<FakeMembership[]>>()
50
+ .mockResolvedValue(opts.memberships ?? []),
51
+ listPersonEntities: vi
52
+ .fn<[], Promise<FakeEntity[]>>()
53
+ .mockResolvedValue(opts.persons ?? []),
54
+ getEntity: vi
55
+ .fn<[string], Promise<{ slug?: string; name?: string } | null>>()
56
+ .mockImplementation(async (uid: string) => {
57
+ if (opts.failGetEntityFor?.has(uid)) {
58
+ throw new Error(`fake getEntity failure for ${uid}`);
59
+ }
60
+ return opts.entitiesBySlug?.[uid] ?? null;
61
+ }),
62
+ };
63
+ }
64
+
65
+ function makeShareSpy(
66
+ perCallOverrides: Record<string, Partial<ShareCallResult> | Error> = {},
67
+ ): {
68
+ fn: PushAllDeps["share"];
69
+ calls: ShareCallOptions[];
70
+ } {
71
+ const calls: ShareCallOptions[] = [];
72
+ const fn: PushAllDeps["share"] = vi
73
+ .fn<[ShareCallOptions], Promise<ShareCallResult>>()
74
+ .mockImplementation(async (options: ShareCallOptions) => {
75
+ calls.push(options);
76
+ const override = perCallOverrides[options.company];
77
+ if (override instanceof Error) {
78
+ throw override;
79
+ }
80
+ return {
81
+ filesUploaded: 0,
82
+ bytesUploaded: 0,
83
+ filesSkipped: 0,
84
+ filesDeleted: 0,
85
+ conflictPaths: [],
86
+ aborted: false,
87
+ ...override,
88
+ };
89
+ });
90
+ return { fn, calls };
91
+ }
92
+
93
+ // Create a tmpdir hqRoot with a known set of top-level entries so the
94
+ // personal-vault path computation produces a stable expected output.
95
+ let tmpHqRoots: string[] = [];
96
+
97
+ function makeHqRoot(topLevelEntries: string[]): string {
98
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "hq-pushall-test-"));
99
+ for (const name of topLevelEntries) {
100
+ fs.mkdirSync(path.join(root, name), { recursive: true });
101
+ }
102
+ tmpHqRoots.push(root);
103
+ return root;
104
+ }
105
+
106
+ beforeEach(() => {
107
+ tmpHqRoots = [];
108
+ });
109
+
110
+ afterEach(() => {
111
+ for (const root of tmpHqRoots) {
112
+ try {
113
+ fs.rmSync(root, { recursive: true, force: true });
114
+ } catch {
115
+ // Best-effort cleanup.
116
+ }
117
+ }
118
+ });
119
+
120
+ // ── 1. Happy path ───────────────────────────────────────────────────────────
121
+
122
+ describe("pushAll", () => {
123
+ it("pushes every membership plus the canonical person entity", async () => {
124
+ const hqRoot = makeHqRoot([
125
+ ".git",
126
+ "companies",
127
+ "repos",
128
+ "workspace",
129
+ ".claude",
130
+ "knowledge",
131
+ "personal",
132
+ ]);
133
+ const vaultClient = makeVaultClient({
134
+ memberships: [
135
+ { companyUid: "cmp_acme" },
136
+ { companyUid: "cmp_globex" },
137
+ ],
138
+ persons: [
139
+ {
140
+ uid: "psn_alice",
141
+ type: "person",
142
+ slug: "alice",
143
+ bucketName: "hq-vault-psn-alice",
144
+ createdAt: "2026-01-01T00:00:00Z",
145
+ },
146
+ ],
147
+ entitiesBySlug: {
148
+ cmp_acme: { slug: "acme", name: "Acme" },
149
+ cmp_globex: { slug: "globex", name: "Globex" },
150
+ },
151
+ });
152
+ const share = makeShareSpy({
153
+ cmp_acme: { filesUploaded: 4, bytesUploaded: 400 },
154
+ cmp_globex: { filesUploaded: 1, bytesUploaded: 50 },
155
+ psn_alice: { filesUploaded: 9, bytesUploaded: 900 },
156
+ });
157
+
158
+ const result = await pushAll(
159
+ { hqRoot, onConflict: "abort" },
160
+ { vaultClient, share: share.fn },
161
+ );
162
+
163
+ expect(share.calls).toHaveLength(3);
164
+ expect(share.calls[0]).toMatchObject({
165
+ company: "cmp_acme",
166
+ hqRoot,
167
+ paths: [path.join(hqRoot, "companies", "acme")],
168
+ onConflict: "abort",
169
+ skipUnchanged: true,
170
+ propagateDeletes: true,
171
+ });
172
+ expect(share.calls[1]).toMatchObject({
173
+ company: "cmp_globex",
174
+ paths: [path.join(hqRoot, "companies", "globex")],
175
+ });
176
+ expect(share.calls[2]).toMatchObject({
177
+ company: "psn_alice",
178
+ personalMode: true,
179
+ journalSlug: "personal",
180
+ skipUnchanged: true,
181
+ propagateDeletes: true,
182
+ });
183
+ // Personal paths excludes .git/companies/repos/workspace, keeps the rest.
184
+ expect(new Set(share.calls[2].paths)).toEqual(
185
+ new Set([
186
+ path.join(hqRoot, ".claude"),
187
+ path.join(hqRoot, "knowledge"),
188
+ path.join(hqRoot, "personal"),
189
+ ]),
190
+ );
191
+
192
+ expect(result.attempted).toBe(3);
193
+ expect(result.filesUploaded).toBe(14);
194
+ expect(result.bytesUploaded).toBe(1350);
195
+ expect(result.errors).toEqual([]);
196
+ expect(result.perCompany.map((row) => row.slug)).toEqual([
197
+ "acme",
198
+ "globex",
199
+ "personal",
200
+ ]);
201
+ });
202
+
203
+ // ── 2. Conflict + message passthrough ─────────────────────────────────────
204
+
205
+ it("forwards --on-conflict and --message to every share() call", async () => {
206
+ const hqRoot = makeHqRoot(["companies", ".claude"]);
207
+ const vaultClient = makeVaultClient({
208
+ memberships: [{ companyUid: "cmp_acme" }],
209
+ persons: [
210
+ {
211
+ uid: "psn_alice",
212
+ type: "person",
213
+ slug: "alice",
214
+ createdAt: "2026-01-01T00:00:00Z",
215
+ },
216
+ ],
217
+ });
218
+ const share = makeShareSpy();
219
+
220
+ await pushAll(
221
+ { hqRoot, onConflict: "overwrite", message: "test broadcast" },
222
+ { vaultClient, share: share.fn },
223
+ );
224
+
225
+ for (const call of share.calls) {
226
+ expect(call.onConflict).toBe("overwrite");
227
+ expect(call.message).toBe("test broadcast");
228
+ expect(call.skipUnchanged).toBe(true);
229
+ expect(call.propagateDeletes).toBe(true);
230
+ }
231
+ });
232
+
233
+ // ── 3. Per-company error isolation ────────────────────────────────────────
234
+
235
+ it("continues past a per-company share failure and reports it in errors[]", async () => {
236
+ const hqRoot = makeHqRoot(["companies"]);
237
+ const vaultClient = makeVaultClient({
238
+ memberships: [
239
+ { companyUid: "cmp_acme" },
240
+ { companyUid: "cmp_globex" },
241
+ ],
242
+ persons: [],
243
+ entitiesBySlug: {
244
+ cmp_acme: { slug: "acme" },
245
+ cmp_globex: { slug: "globex" },
246
+ },
247
+ });
248
+ const share = makeShareSpy({
249
+ cmp_globex: new Error("STS denied"),
250
+ });
251
+
252
+ const result = await pushAll(
253
+ { hqRoot },
254
+ { vaultClient, share: share.fn },
255
+ );
256
+
257
+ expect(share.calls.map((c) => c.company)).toEqual([
258
+ "cmp_acme",
259
+ "cmp_globex",
260
+ ]);
261
+ expect(result.attempted).toBe(2);
262
+ expect(result.errors).toEqual([
263
+ { company: "globex", message: "STS denied" },
264
+ ]);
265
+ const globexRow = result.perCompany.find((r) => r.slug === "globex");
266
+ expect(globexRow?.error).toBe("STS denied");
267
+ expect(globexRow?.result).toBeUndefined();
268
+ });
269
+
270
+ // ── 4. No person entity ────────────────────────────────────────────────────
271
+
272
+ it("omits the personal target when listPersonEntities returns []", async () => {
273
+ const hqRoot = makeHqRoot(["companies"]);
274
+ const vaultClient = makeVaultClient({
275
+ memberships: [{ companyUid: "cmp_acme" }],
276
+ persons: [],
277
+ entitiesBySlug: { cmp_acme: { slug: "acme" } },
278
+ });
279
+ const share = makeShareSpy();
280
+
281
+ const result = await pushAll(
282
+ { hqRoot },
283
+ { vaultClient, share: share.fn },
284
+ );
285
+
286
+ expect(share.calls).toHaveLength(1);
287
+ expect(share.calls[0].company).toBe("cmp_acme");
288
+ expect(share.calls[0].personalMode).toBeUndefined();
289
+ expect(result.perCompany.map((r) => r.slug)).toEqual(["acme"]);
290
+ });
291
+
292
+ // ── 5. No memberships and no person entity ────────────────────────────────
293
+
294
+ it("returns cleanly with zero attempts when there is nothing to push", async () => {
295
+ const hqRoot = makeHqRoot([]);
296
+ const vaultClient = makeVaultClient({});
297
+ const share = makeShareSpy();
298
+
299
+ const result = await pushAll(
300
+ { hqRoot },
301
+ { vaultClient, share: share.fn },
302
+ );
303
+
304
+ expect(share.calls).toEqual([]);
305
+ expect(result).toEqual({
306
+ attempted: 0,
307
+ filesUploaded: 0,
308
+ bytesUploaded: 0,
309
+ filesDeleted: 0,
310
+ errors: [],
311
+ perCompany: [],
312
+ });
313
+ });
314
+
315
+ // ── 6. Canonical-person tiebreak ──────────────────────────────────────────
316
+
317
+ it("picks the oldest person entity, breaking ties by uid", async () => {
318
+ const hqRoot = makeHqRoot(["docs"]);
319
+ const vaultClient = makeVaultClient({
320
+ memberships: [],
321
+ persons: [
322
+ {
323
+ uid: "psn_zzz",
324
+ type: "person",
325
+ slug: "alice-zzz",
326
+ createdAt: "2026-01-01T00:00:00Z",
327
+ },
328
+ {
329
+ uid: "psn_aaa",
330
+ type: "person",
331
+ slug: "alice-aaa",
332
+ createdAt: "2026-01-01T00:00:00Z",
333
+ },
334
+ {
335
+ uid: "psn_old",
336
+ type: "person",
337
+ slug: "alice-old",
338
+ createdAt: "2025-06-01T00:00:00Z",
339
+ },
340
+ ],
341
+ });
342
+ const share = makeShareSpy();
343
+
344
+ await pushAll({ hqRoot }, { vaultClient, share: share.fn });
345
+
346
+ expect(share.calls).toHaveLength(1);
347
+ expect(share.calls[0].company).toBe("psn_old");
348
+ expect(share.calls[0].personalMode).toBe(true);
349
+ expect(share.calls[0].journalSlug).toBe("personal");
350
+ });
351
+
352
+ // ── 7. Aggregate counts include deletes ───────────────────────────────────
353
+
354
+ it("aggregates filesDeleted across every successful target", async () => {
355
+ const hqRoot = makeHqRoot(["companies", "docs"]);
356
+ const vaultClient = makeVaultClient({
357
+ memberships: [{ companyUid: "cmp_acme" }],
358
+ persons: [
359
+ {
360
+ uid: "psn_alice",
361
+ type: "person",
362
+ slug: "alice",
363
+ createdAt: "2026-01-01T00:00:00Z",
364
+ },
365
+ ],
366
+ entitiesBySlug: { cmp_acme: { slug: "acme" } },
367
+ });
368
+ const share = makeShareSpy({
369
+ cmp_acme: { filesUploaded: 2, filesDeleted: 1 },
370
+ psn_alice: { filesUploaded: 1, filesDeleted: 3 },
371
+ });
372
+
373
+ const result = await pushAll(
374
+ { hqRoot },
375
+ { vaultClient, share: share.fn },
376
+ );
377
+
378
+ expect(result.filesUploaded).toBe(3);
379
+ expect(result.filesDeleted).toBe(4);
380
+ });
381
+ });
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Unit tests for the two small helpers that underpin the new `hq sync`
3
+ * routing flags:
4
+ *
5
+ * - `assertSingleSelector`: refuses ambiguous combinations of --all,
6
+ * --personal, and --company.
7
+ * - `resolveCanonicalPersonUid`: looks up the caller's canonical person
8
+ * entity and surfaces a clean error when none exists.
9
+ */
10
+
11
+ import { describe, expect, it, vi } from "vitest";
12
+
13
+ import {
14
+ assertSingleSelector,
15
+ resolveCanonicalPersonUid,
16
+ type PullAllVaultClient,
17
+ } from "./cloud.js";
18
+
19
+ // ── assertSingleSelector ────────────────────────────────────────────────────
20
+
21
+ describe("assertSingleSelector", () => {
22
+ it("accepts no selectors (= use active company)", () => {
23
+ expect(() => assertSingleSelector({}, "pull")).not.toThrow();
24
+ });
25
+
26
+ it.each([
27
+ ["all only", { all: true }],
28
+ ["personal only", { personal: true }],
29
+ ["company only", { company: "acme" }],
30
+ ])("accepts a single selector: %s", (_label, opts) => {
31
+ expect(() => assertSingleSelector(opts, "push")).not.toThrow();
32
+ });
33
+
34
+ it.each([
35
+ [
36
+ "all + personal",
37
+ { all: true, personal: true },
38
+ ["--all", "--personal"],
39
+ ],
40
+ [
41
+ "all + company",
42
+ { all: true, company: "acme" },
43
+ ["--all", "--company acme"],
44
+ ],
45
+ [
46
+ "personal + company",
47
+ { personal: true, company: "acme" },
48
+ ["--personal", "--company acme"],
49
+ ],
50
+ [
51
+ "all + personal + company",
52
+ { all: true, personal: true, company: "acme" },
53
+ ["--all", "--personal", "--company acme"],
54
+ ],
55
+ ])(
56
+ "throws on conflicting selectors: %s",
57
+ (_label, opts, mentioned: string[]) => {
58
+ let caught: Error | null = null;
59
+ try {
60
+ assertSingleSelector(opts, "now");
61
+ } catch (err) {
62
+ caught = err as Error;
63
+ }
64
+ expect(caught).not.toBeNull();
65
+ expect(caught?.message).toMatch(/at most one of --all, --personal, --company/);
66
+ for (const flag of mentioned) {
67
+ expect(caught?.message).toContain(flag);
68
+ }
69
+ },
70
+ );
71
+
72
+ it("interpolates the command name in the error message", () => {
73
+ expect(() =>
74
+ assertSingleSelector({ all: true, personal: true }, "push"),
75
+ ).toThrow(/`hq sync push`/);
76
+ });
77
+ });
78
+
79
+ // ── resolveCanonicalPersonUid ──────────────────────────────────────────────
80
+
81
+ function makeClient(persons: Array<{
82
+ uid: string;
83
+ type: "person" | "company";
84
+ slug: string;
85
+ createdAt: string;
86
+ }>): PullAllVaultClient {
87
+ return {
88
+ listMyMemberships: vi.fn().mockResolvedValue([]),
89
+ listPersonEntities: vi.fn().mockResolvedValue(persons),
90
+ getEntity: vi.fn().mockResolvedValue(null),
91
+ };
92
+ }
93
+
94
+ describe("resolveCanonicalPersonUid", () => {
95
+ it("returns the UID of the canonical (oldest) person entity", async () => {
96
+ const client = makeClient([
97
+ {
98
+ uid: "psn_new",
99
+ type: "person",
100
+ slug: "alice",
101
+ createdAt: "2026-04-01T00:00:00Z",
102
+ },
103
+ {
104
+ uid: "psn_old",
105
+ type: "person",
106
+ slug: "alice-old",
107
+ createdAt: "2025-06-01T00:00:00Z",
108
+ },
109
+ ]);
110
+
111
+ await expect(resolveCanonicalPersonUid(client)).resolves.toBe("psn_old");
112
+ });
113
+
114
+ it("throws a clean error when no person entity is present", async () => {
115
+ const client = makeClient([]);
116
+ await expect(resolveCanonicalPersonUid(client)).rejects.toThrow(
117
+ /No personal vault found/,
118
+ );
119
+ });
120
+
121
+ it("breaks createdAt ties by uid lex order (deterministic)", async () => {
122
+ const client = makeClient([
123
+ {
124
+ uid: "psn_zzz",
125
+ type: "person",
126
+ slug: "alice-zzz",
127
+ createdAt: "2026-01-01T00:00:00Z",
128
+ },
129
+ {
130
+ uid: "psn_aaa",
131
+ type: "person",
132
+ slug: "alice-aaa",
133
+ createdAt: "2026-01-01T00:00:00Z",
134
+ },
135
+ ]);
136
+
137
+ await expect(resolveCanonicalPersonUid(client)).resolves.toBe("psn_aaa");
138
+ });
139
+
140
+ it("ignores non-person entities returned by the client", async () => {
141
+ const client: PullAllVaultClient = {
142
+ listMyMemberships: vi.fn().mockResolvedValue([]),
143
+ listPersonEntities: vi.fn().mockResolvedValue([
144
+ // Belt-and-braces: callers today pass entity.listByType("person"),
145
+ // but a future caller passing a mixed list shouldn't silently pick a
146
+ // company entity.
147
+ { uid: "cmp_acme", type: "company", slug: "acme", createdAt: "2024-01-01T00:00:00Z" },
148
+ { uid: "psn_alice", type: "person", slug: "alice", createdAt: "2026-01-01T00:00:00Z" },
149
+ ]),
150
+ getEntity: vi.fn().mockResolvedValue(null),
151
+ };
152
+
153
+ await expect(resolveCanonicalPersonUid(client)).resolves.toBe("psn_alice");
154
+ });
155
+ });