@openparachute/hub 0.6.4-rc.3 → 0.6.4-rc.5
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/package.json +1 -1
- package/src/__tests__/account-home-ui.test.ts +303 -110
- package/src/__tests__/account-mirror.test.ts +156 -0
- package/src/__tests__/api-account.test.ts +51 -1
- package/src/__tests__/grants.test.ts +98 -8
- package/src/account-home-ui.ts +399 -257
- package/src/account-mirror.ts +126 -0
- package/src/account-vault-token.ts +2 -0
- package/src/api-account.ts +57 -0
- package/src/grants.ts +25 -0
package/package.json
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* tests pin the load-bearing shape:
|
|
5
5
|
*
|
|
6
6
|
* - Assigned-vault branch: Notes CTA href encodes the hub+vault URL,
|
|
7
|
-
* vault name shows in the body,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* vault name shows in the body, the backup-state line surfaces, and a
|
|
8
|
+
* "build your own UI" hint links the surface-build starter. The connect
|
|
9
|
+
* instructions live ONCE in the onboarding checklist (not duplicated in
|
|
10
|
+
* the vault card); token-minting is gone from /account (OAuth-first);
|
|
11
|
+
* a single "Advanced vault settings ↗" deep-link covers advanced needs.
|
|
10
12
|
* - Admin (no assigned vault) branch: link to /admin/ visible.
|
|
11
13
|
* - Defensive third branch (non-admin + no vault): "ask the operator"
|
|
12
14
|
* copy renders.
|
|
@@ -44,35 +46,9 @@ describe("renderAccountHome", () => {
|
|
|
44
46
|
expect(html).toContain(`https://notes.parachute.computer/add?url=${encodedVaultUrl}`);
|
|
45
47
|
expect(html).toContain('target="_blank"');
|
|
46
48
|
expect(html).toContain('rel="noopener"');
|
|
47
|
-
// The friend-connect surface: MCP endpoint + `claude mcp add` command,
|
|
48
|
-
// each with a copy button. OAuth path (no token in the command).
|
|
49
|
-
expect(html).toContain(`${HUB_ORIGIN}/vault/alice/mcp`);
|
|
50
|
-
expect(html).toContain(
|
|
51
|
-
`claude mcp add --transport http parachute-alice ${HUB_ORIGIN}/vault/alice/mcp`,
|
|
52
|
-
);
|
|
53
|
-
expect(html).toContain('data-testid="copy-mcp-endpoint"');
|
|
54
|
-
expect(html).toContain('data-testid="copy-mcp-add-command"');
|
|
55
|
-
// The connect command must NOT embed a token — the OAuth path needs none.
|
|
56
|
-
expect(html).not.toContain("--header");
|
|
57
|
-
expect(html).not.toContain("Authorization: Bearer");
|
|
58
49
|
// Copy-button progressive-enhancement script is present.
|
|
59
50
|
expect(html).toContain("navigator.clipboard");
|
|
60
|
-
//
|
|
61
|
-
// rather than MCP jargon up top.
|
|
62
|
-
expect(html).toContain('data-testid="connect-ai-heading"');
|
|
63
|
-
expect(html).toContain("Connect your AI");
|
|
64
|
-
// BOTH connect methods render as distinct, labelled blocks.
|
|
65
|
-
expect(html).toContain('data-testid="connect-method-claude-code"');
|
|
66
|
-
expect(html).toContain("Claude Code");
|
|
67
|
-
expect(html).toContain('data-testid="connect-method-claude-ai"');
|
|
68
|
-
expect(html).toContain("Claude.ai");
|
|
69
|
-
// The Claude.ai path mirrors the install.njk canonical phrasing
|
|
70
|
-
// (Settings → Connectors → Add custom connector, paste the endpoint).
|
|
71
|
-
expect(html).toContain("Connectors");
|
|
72
|
-
expect(html).toContain("Add custom connector");
|
|
73
|
-
// A brief "any other MCP client" line is present (no bloat — just one).
|
|
74
|
-
expect(html).toContain('data-testid="connect-any-client-hint"');
|
|
75
|
-
// Notes CTA still present, now framed as the browser-UI option.
|
|
51
|
+
// Notes CTA present, framed as the browser-UI option.
|
|
76
52
|
expect(html).toContain('data-testid="open-notes-cta"');
|
|
77
53
|
// Import-notes CTA deep-links to the Notes-UI /import route for the same
|
|
78
54
|
// vault, mirroring the Open-Notes target-resolution (same hosted origin,
|
|
@@ -81,6 +57,78 @@ describe("renderAccountHome", () => {
|
|
|
81
57
|
expect(html).toContain('data-testid="import-notes-cta"');
|
|
82
58
|
});
|
|
83
59
|
|
|
60
|
+
test("dedup — the full connect block lives ONCE (in the checklist), NOT in the vault card", () => {
|
|
61
|
+
const html = renderAccountHome({
|
|
62
|
+
username: "alice",
|
|
63
|
+
assignedVaults: ["alice"],
|
|
64
|
+
passwordChanged: true,
|
|
65
|
+
hubOrigin: HUB_ORIGIN,
|
|
66
|
+
isFirstAdmin: false,
|
|
67
|
+
csrfToken: CSRF,
|
|
68
|
+
twoFactorEnabled: false,
|
|
69
|
+
connectedVault: false,
|
|
70
|
+
});
|
|
71
|
+
// The checklist owns "Connect your AI" — the endpoint + the `claude mcp add`
|
|
72
|
+
// command (OAuth, no token) appear there.
|
|
73
|
+
expect(html).toContain('data-testid="onboarding-mcp-endpoint"');
|
|
74
|
+
expect(html).toContain('data-testid="onboarding-mcp-add-command"');
|
|
75
|
+
expect(html).toContain(`${HUB_ORIGIN}/vault/alice/mcp`);
|
|
76
|
+
// The vault card must NOT repeat the connect instructions. None of the
|
|
77
|
+
// old mcp-connect block markers may appear — no duplicated connect surface.
|
|
78
|
+
expect(html).not.toContain('data-testid="mcp-connect"');
|
|
79
|
+
expect(html).not.toContain('data-testid="connect-ai-heading"');
|
|
80
|
+
expect(html).not.toContain('data-testid="connect-method-claude-code"');
|
|
81
|
+
expect(html).not.toContain('data-testid="connect-method-claude-ai"');
|
|
82
|
+
expect(html).not.toContain('data-testid="connect-any-client-hint"');
|
|
83
|
+
// The connect instructions live only in the checklist, never duplicated in
|
|
84
|
+
// the vault card: split the page at the vault card and assert the endpoint
|
|
85
|
+
// doesn't reappear in that slice.
|
|
86
|
+
const cardIdx = html.indexOf('data-testid="vault-card"');
|
|
87
|
+
expect(cardIdx).toBeGreaterThan(-1);
|
|
88
|
+
const vaultCardSlice = html.slice(cardIdx);
|
|
89
|
+
expect(vaultCardSlice).not.toContain(`${HUB_ORIGIN}/vault/alice/mcp`);
|
|
90
|
+
expect(vaultCardSlice).not.toContain("claude mcp add");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("token-mint — the mint affordance is gone from /account (OAuth-first)", () => {
|
|
94
|
+
const html = renderAccountHome({
|
|
95
|
+
username: "alice",
|
|
96
|
+
assignedVaults: ["alice"],
|
|
97
|
+
passwordChanged: true,
|
|
98
|
+
hubOrigin: HUB_ORIGIN,
|
|
99
|
+
isFirstAdmin: false,
|
|
100
|
+
csrfToken: CSRF,
|
|
101
|
+
twoFactorEnabled: false,
|
|
102
|
+
mintableVerbs: { alice: ["read", "write", "admin"] },
|
|
103
|
+
});
|
|
104
|
+
// No token-mint <details> block, no mint form, no verb radios — minting a
|
|
105
|
+
// header-auth token is a script/advanced concern that lives in the SPA.
|
|
106
|
+
expect(html).not.toContain('data-testid="token-mint"');
|
|
107
|
+
expect(html).not.toContain('data-testid="mint-form"');
|
|
108
|
+
expect(html).not.toContain('data-testid="mint-verb-read"');
|
|
109
|
+
expect(html).not.toContain('data-testid="mint-verb-admin"');
|
|
110
|
+
expect(html).not.toContain("Mint an access token");
|
|
111
|
+
// The single advanced entry point covers the advanced needs.
|
|
112
|
+
expect(html).toContain('data-testid="vault-admin-button"');
|
|
113
|
+
expect(html).toContain("Advanced vault settings");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("build-your-own-UI — Notes is one way; the hint links the surface-build starter", () => {
|
|
117
|
+
const html = renderAccountHome({
|
|
118
|
+
username: "alice",
|
|
119
|
+
assignedVaults: ["alice"],
|
|
120
|
+
passwordChanged: true,
|
|
121
|
+
hubOrigin: HUB_ORIGIN,
|
|
122
|
+
isFirstAdmin: false,
|
|
123
|
+
csrfToken: CSRF,
|
|
124
|
+
twoFactorEnabled: false,
|
|
125
|
+
});
|
|
126
|
+
expect(html).toContain('data-testid="build-your-own-ui"');
|
|
127
|
+
expect(html).toContain('data-testid="build-your-own-ui-link"');
|
|
128
|
+
expect(html).toContain("https://parachute.computer/onboarding/surface-build/");
|
|
129
|
+
expect(html).toContain("build you a custom UI");
|
|
130
|
+
});
|
|
131
|
+
|
|
84
132
|
test("assigned-vault branch — import-notes CTA gated alongside open-notes (no dead link)", () => {
|
|
85
133
|
// Both CTAs render together for an assigned vault and are absent together
|
|
86
134
|
// in the no-vault branches — so we never surface an Import link that points
|
|
@@ -175,7 +223,7 @@ describe("renderAccountHome", () => {
|
|
|
175
223
|
expect(html).not.toContain('data-testid="mcp-connect"');
|
|
176
224
|
});
|
|
177
225
|
|
|
178
|
-
test("get-started card — links to the two onboarding prompts, placed
|
|
226
|
+
test("get-started card — links to the two onboarding prompts, placed AFTER the vault card", () => {
|
|
179
227
|
const html = renderAccountHome({
|
|
180
228
|
username: "alice",
|
|
181
229
|
assignedVaults: ["alice"],
|
|
@@ -195,8 +243,10 @@ describe("renderAccountHome", () => {
|
|
|
195
243
|
expect(html).toContain('data-testid="starter-surface-build"');
|
|
196
244
|
// External links open safely.
|
|
197
245
|
expect(html).toContain('rel="noopener"');
|
|
198
|
-
//
|
|
199
|
-
|
|
246
|
+
// Connect-before-prompts: the prompts are only useful once connected, so
|
|
247
|
+
// they now sit AFTER the vault card in document order (and after the
|
|
248
|
+
// onboarding checklist, which leads the page).
|
|
249
|
+
expect(html.indexOf('data-testid="get-started-card"')).toBeGreaterThan(
|
|
200
250
|
html.indexOf('data-testid="vault-card"'),
|
|
201
251
|
);
|
|
202
252
|
});
|
|
@@ -231,8 +281,78 @@ describe("renderAccountHome", () => {
|
|
|
231
281
|
expect(noVault).toContain('data-testid="no-vault-card"');
|
|
232
282
|
});
|
|
233
283
|
|
|
234
|
-
test("
|
|
235
|
-
const
|
|
284
|
+
test("backup state — renders the backup line + omits when no mirror entry", () => {
|
|
285
|
+
const backedUp = renderAccountHome({
|
|
286
|
+
username: "alice",
|
|
287
|
+
assignedVaults: ["alice"],
|
|
288
|
+
passwordChanged: true,
|
|
289
|
+
hubOrigin: HUB_ORIGIN,
|
|
290
|
+
isFirstAdmin: false,
|
|
291
|
+
csrfToken: CSRF,
|
|
292
|
+
twoFactorEnabled: false,
|
|
293
|
+
mintableVerbs: { alice: ["read", "write", "admin"] },
|
|
294
|
+
mirrorLines: { alice: "Backed up — full version history" },
|
|
295
|
+
});
|
|
296
|
+
// The warm, plain-language backup line surfaces on the tile.
|
|
297
|
+
expect(backedUp).toContain('data-testid="backup-state-line"');
|
|
298
|
+
expect(backedUp).toContain("Backed up — full version history");
|
|
299
|
+
// Not pushing yet → a "Back up to GitHub ↗" action (reuses the
|
|
300
|
+
// vault-admin-token deep-link, gated on admin).
|
|
301
|
+
expect(backedUp).toContain('data-testid="backup-github-button"');
|
|
302
|
+
expect(backedUp).toContain("Back up to GitHub");
|
|
303
|
+
expect(backedUp).toContain("/account/vault-admin-token/alice");
|
|
304
|
+
|
|
305
|
+
// GitHub variant: when pushing, the line says so and the action is dropped.
|
|
306
|
+
// Suppression is gated on the `mirrorPushing` boolean (NOT the line string).
|
|
307
|
+
const pushing = renderAccountHome({
|
|
308
|
+
username: "alice",
|
|
309
|
+
assignedVaults: ["alice"],
|
|
310
|
+
passwordChanged: true,
|
|
311
|
+
hubOrigin: HUB_ORIGIN,
|
|
312
|
+
isFirstAdmin: false,
|
|
313
|
+
csrfToken: CSRF,
|
|
314
|
+
twoFactorEnabled: false,
|
|
315
|
+
mintableVerbs: { alice: ["read", "write", "admin"] },
|
|
316
|
+
mirrorLines: { alice: "Backed up — version history + GitHub" },
|
|
317
|
+
mirrorPushing: { alice: true },
|
|
318
|
+
});
|
|
319
|
+
expect(pushing).toContain("version history + GitHub");
|
|
320
|
+
expect(pushing).not.toContain('data-testid="backup-github-button"');
|
|
321
|
+
|
|
322
|
+
// Omitted silently when the mirror fetch returned nothing (no entry).
|
|
323
|
+
const noMirror = renderAccountHome({
|
|
324
|
+
username: "alice",
|
|
325
|
+
assignedVaults: ["alice"],
|
|
326
|
+
passwordChanged: true,
|
|
327
|
+
hubOrigin: HUB_ORIGIN,
|
|
328
|
+
isFirstAdmin: false,
|
|
329
|
+
csrfToken: CSRF,
|
|
330
|
+
twoFactorEnabled: false,
|
|
331
|
+
mintableVerbs: { alice: ["read", "write", "admin"] },
|
|
332
|
+
});
|
|
333
|
+
expect(noMirror).not.toContain('data-testid="backup-state-line"');
|
|
334
|
+
expect(noMirror).not.toContain('data-testid="vault-backup"');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("backup action — gated on the mirrorPushing boolean, not the line string", () => {
|
|
338
|
+
// Regression guard: the "Back up to GitHub ↗" suppression must follow the
|
|
339
|
+
// proper `mirrorPushing` boolean, NOT substring-match the display line.
|
|
340
|
+
// (a) line mentions "GitHub" but mirrorPushing is false → action STILL shows.
|
|
341
|
+
const lineLies = renderAccountHome({
|
|
342
|
+
username: "alice",
|
|
343
|
+
assignedVaults: ["alice"],
|
|
344
|
+
passwordChanged: true,
|
|
345
|
+
hubOrigin: HUB_ORIGIN,
|
|
346
|
+
isFirstAdmin: false,
|
|
347
|
+
csrfToken: CSRF,
|
|
348
|
+
twoFactorEnabled: false,
|
|
349
|
+
mintableVerbs: { alice: ["read", "write", "admin"] },
|
|
350
|
+
mirrorLines: { alice: "Backed up — full version history (GitHub disabled)" },
|
|
351
|
+
mirrorPushing: { alice: false },
|
|
352
|
+
});
|
|
353
|
+
expect(lineLies).toContain('data-testid="backup-github-button"');
|
|
354
|
+
// (b) mirrorPushing true but the line lacks "GitHub" → action suppressed.
|
|
355
|
+
const boolWins = renderAccountHome({
|
|
236
356
|
username: "alice",
|
|
237
357
|
assignedVaults: ["alice"],
|
|
238
358
|
passwordChanged: true,
|
|
@@ -240,14 +360,11 @@ describe("renderAccountHome", () => {
|
|
|
240
360
|
isFirstAdmin: false,
|
|
241
361
|
csrfToken: CSRF,
|
|
242
362
|
twoFactorEnabled: false,
|
|
363
|
+
mintableVerbs: { alice: ["read", "write", "admin"] },
|
|
364
|
+
mirrorLines: { alice: "Backed up — full version history" },
|
|
365
|
+
mirrorPushing: { alice: true },
|
|
243
366
|
});
|
|
244
|
-
|
|
245
|
-
// a friend who only knows that word can find the right place to paste.
|
|
246
|
-
// Assert the NEW hint string specifically — a bare toContain("connector")
|
|
247
|
-
// was already satisfied pre-PR by the Claude.ai "Connectors" block, so it
|
|
248
|
-
// wouldn't catch a regression that drops this bridging copy.
|
|
249
|
-
expect(html).toContain('data-testid="connect-any-client-hint"');
|
|
250
|
-
expect(html).toContain('call these "connectors."');
|
|
367
|
+
expect(boolWins).not.toContain('data-testid="backup-github-button"');
|
|
251
368
|
});
|
|
252
369
|
|
|
253
370
|
test("account card — security actions collapse into a secondary <details>", () => {
|
|
@@ -345,18 +462,13 @@ describe("renderAccountHome", () => {
|
|
|
345
462
|
const familyEncoded = encodeURIComponent(`${HUB_ORIGIN}/vault/family`);
|
|
346
463
|
expect(html).toContain(`https://notes.parachute.computer/add?url=${personalEncoded}`);
|
|
347
464
|
expect(html).toContain(`https://notes.parachute.computer/add?url=${familyEncoded}`);
|
|
348
|
-
//
|
|
349
|
-
expect(html).
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
);
|
|
354
|
-
|
|
355
|
-
`claude mcp add --transport http parachute-family ${HUB_ORIGIN}/vault/family/mcp`,
|
|
356
|
-
);
|
|
357
|
-
// Two tiles → two copy-endpoint buttons.
|
|
358
|
-
expect(html.split('data-testid="copy-mcp-endpoint"').length - 1).toBe(2);
|
|
359
|
-
// The copy script is emitted once at the section level, not per-tile.
|
|
465
|
+
// Two tiles → two Open-Notes CTAs.
|
|
466
|
+
expect(html.split('data-testid="open-notes-cta"').length - 1).toBe(2);
|
|
467
|
+
// The vault card does NOT repeat the connect block — that lives in the
|
|
468
|
+
// checklist (which uses the first/primary vault only).
|
|
469
|
+
expect(html).not.toContain('data-testid="mcp-connect"');
|
|
470
|
+
expect(html).not.toContain(`${HUB_ORIGIN}/vault/family/mcp`);
|
|
471
|
+
// The copy script is emitted once at the page level, not per-tile.
|
|
360
472
|
expect(html.split("<script>").length - 1).toBe(1);
|
|
361
473
|
});
|
|
362
474
|
|
|
@@ -394,9 +506,9 @@ describe("renderAccountHome", () => {
|
|
|
394
506
|
expect(html).toContain("parachute-<vault>");
|
|
395
507
|
});
|
|
396
508
|
|
|
397
|
-
// ---
|
|
509
|
+
// --- single advanced entry point (gated on the admin verb) ---------------
|
|
398
510
|
|
|
399
|
-
test("
|
|
511
|
+
test("advanced settings link — present + gated on the admin verb", () => {
|
|
400
512
|
const html = renderAccountHome({
|
|
401
513
|
username: "alice",
|
|
402
514
|
assignedVaults: ["work"],
|
|
@@ -405,28 +517,22 @@ describe("renderAccountHome", () => {
|
|
|
405
517
|
isFirstAdmin: false,
|
|
406
518
|
csrfToken: CSRF,
|
|
407
519
|
twoFactorEnabled: false,
|
|
408
|
-
mintableVerbs: { work: ["read", "write"] },
|
|
520
|
+
mintableVerbs: { work: ["read", "write", "admin"] },
|
|
409
521
|
});
|
|
410
|
-
//
|
|
411
|
-
expect(html).toContain('data-testid="
|
|
412
|
-
expect(html).toContain("
|
|
413
|
-
expect(html).toContain("
|
|
414
|
-
|
|
415
|
-
expect(html).toContain('data-testid="mint-verb-read"');
|
|
416
|
-
expect(html).toContain('data-testid="mint-verb-write"');
|
|
417
|
-
// Form POSTs to the per-vault endpoint with the CSRF token embedded.
|
|
418
|
-
expect(html).toContain('action="/account/vault-token/work"');
|
|
522
|
+
// One clearly-labelled "Advanced vault settings ↗" link → the SPA deep-link.
|
|
523
|
+
expect(html).toContain('data-testid="vault-admin-form"');
|
|
524
|
+
expect(html).toContain('data-testid="vault-admin-button"');
|
|
525
|
+
expect(html).toContain("Advanced vault settings");
|
|
526
|
+
expect(html).toContain('action="/account/vault-admin-token/work"');
|
|
419
527
|
expect(html).toContain('method="POST"');
|
|
420
|
-
expect(html).toContain('data-testid="mint-form"');
|
|
421
528
|
expect(html).toContain(CSRF);
|
|
422
|
-
//
|
|
423
|
-
expect(html).toContain("
|
|
529
|
+
// The old dual-purpose "Configure / back up this vault" label is gone.
|
|
530
|
+
expect(html).not.toContain("Configure / back up this vault");
|
|
424
531
|
});
|
|
425
532
|
|
|
426
|
-
test("
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
// never surface a write radio (the server would reject it anyway).
|
|
533
|
+
test("advanced settings link — absent when the user lacks the admin verb", () => {
|
|
534
|
+
// A read/write-only assignment must not surface the admin deep-link (the
|
|
535
|
+
// POST handler would 403 the admin token mint).
|
|
430
536
|
const html = renderAccountHome({
|
|
431
537
|
username: "alice",
|
|
432
538
|
assignedVaults: ["work"],
|
|
@@ -435,31 +541,10 @@ describe("renderAccountHome", () => {
|
|
|
435
541
|
isFirstAdmin: false,
|
|
436
542
|
csrfToken: CSRF,
|
|
437
543
|
twoFactorEnabled: false,
|
|
438
|
-
mintableVerbs: { work: ["read"] },
|
|
439
|
-
});
|
|
440
|
-
expect(html).toContain('data-testid="mint-verb-read"');
|
|
441
|
-
expect(html).not.toContain('data-testid="mint-verb-write"');
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
test("mint affordance — offers the admin verb when the user holds it", () => {
|
|
445
|
-
// 2026-05-30: assigned users hold read/write/admin on their vault, so the
|
|
446
|
-
// mint form offers admin (the live `vaultVerbsForUserVault` returns it).
|
|
447
|
-
const html = renderAccountHome({
|
|
448
|
-
username: "alice",
|
|
449
|
-
assignedVaults: ["work"],
|
|
450
|
-
passwordChanged: true,
|
|
451
|
-
hubOrigin: HUB_ORIGIN,
|
|
452
|
-
isFirstAdmin: false,
|
|
453
|
-
csrfToken: CSRF,
|
|
454
|
-
twoFactorEnabled: false,
|
|
455
|
-
mintableVerbs: { work: ["read", "write", "admin"] },
|
|
544
|
+
mintableVerbs: { work: ["read", "write"] },
|
|
456
545
|
});
|
|
457
|
-
expect(html).toContain('
|
|
458
|
-
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
test("mint affordance — absent when no mintable verbs (admin / no-vault / unmapped role)", () => {
|
|
462
|
-
// Admin branch: no tiles at all, so no mint block.
|
|
546
|
+
expect(html).not.toContain('data-testid="vault-admin-button"');
|
|
547
|
+
// Admin branch: no tiles at all, so no advanced link.
|
|
463
548
|
const admin = renderAccountHome({
|
|
464
549
|
username: "admin",
|
|
465
550
|
assignedVaults: [],
|
|
@@ -469,19 +554,7 @@ describe("renderAccountHome", () => {
|
|
|
469
554
|
csrfToken: CSRF,
|
|
470
555
|
twoFactorEnabled: false,
|
|
471
556
|
});
|
|
472
|
-
expect(admin).not.toContain('data-testid="
|
|
473
|
-
// Assigned vault but empty verb list (fail-closed unknown role) → no block.
|
|
474
|
-
const empty = renderAccountHome({
|
|
475
|
-
username: "alice",
|
|
476
|
-
assignedVaults: ["work"],
|
|
477
|
-
passwordChanged: true,
|
|
478
|
-
hubOrigin: HUB_ORIGIN,
|
|
479
|
-
isFirstAdmin: false,
|
|
480
|
-
csrfToken: CSRF,
|
|
481
|
-
twoFactorEnabled: false,
|
|
482
|
-
mintableVerbs: { work: [] },
|
|
483
|
-
});
|
|
484
|
-
expect(empty).not.toContain('data-testid="token-mint"');
|
|
557
|
+
expect(admin).not.toContain('data-testid="vault-admin-button"');
|
|
485
558
|
});
|
|
486
559
|
|
|
487
560
|
test("minted-token banner — shows the token once with a save-it warning, no revoke claim", () => {
|
|
@@ -529,4 +602,124 @@ describe("renderAccountHome", () => {
|
|
|
529
602
|
// Error render must NOT also show a token.
|
|
530
603
|
expect(html).not.toContain('data-testid="minted-token-banner"');
|
|
531
604
|
});
|
|
605
|
+
|
|
606
|
+
// --- first-run onboarding checklist --------------------------------------
|
|
607
|
+
|
|
608
|
+
test("onboarding checklist — renders 3 steps with the correct /mcp endpoint (not connected)", () => {
|
|
609
|
+
const html = renderAccountHome({
|
|
610
|
+
username: "alice",
|
|
611
|
+
assignedVaults: ["alice"],
|
|
612
|
+
passwordChanged: true,
|
|
613
|
+
hubOrigin: HUB_ORIGIN,
|
|
614
|
+
isFirstAdmin: false,
|
|
615
|
+
csrfToken: CSRF,
|
|
616
|
+
twoFactorEnabled: false,
|
|
617
|
+
connectedVault: false,
|
|
618
|
+
});
|
|
619
|
+
expect(html).toContain('data-testid="onboarding-checklist"');
|
|
620
|
+
expect(html).toContain('data-connected="false"');
|
|
621
|
+
// All three numbered steps render.
|
|
622
|
+
expect(html).toContain('data-testid="onboarding-step-1"');
|
|
623
|
+
expect(html).toContain('data-testid="onboarding-step-2"');
|
|
624
|
+
expect(html).toContain('data-testid="onboarding-step-3"');
|
|
625
|
+
expect(html).toContain("Your account is ready");
|
|
626
|
+
expect(html).toContain("Connect your AI");
|
|
627
|
+
expect(html).toContain("Set up your vault");
|
|
628
|
+
// Step ② shows the canonical /vault/<name>/mcp endpoint inline — the /mcp
|
|
629
|
+
// suffix is load-bearing (only it returns the WWW-Authenticate header).
|
|
630
|
+
expect(html).toContain(`${HUB_ORIGIN}/vault/alice/mcp`);
|
|
631
|
+
expect(html).toMatch(/data-testid="onboarding-mcp-endpoint">[^<]*\/vault\/alice\/mcp</);
|
|
632
|
+
// Both connect methods are inline in step ②.
|
|
633
|
+
expect(html).toContain('data-testid="onboarding-mcp-add-command"');
|
|
634
|
+
expect(html).toContain("Add custom connector");
|
|
635
|
+
expect(html).toContain("claude mcp add");
|
|
636
|
+
// Step ③ links the vault-setup starter prompt.
|
|
637
|
+
expect(html).toContain('data-testid="onboarding-vault-setup-link"');
|
|
638
|
+
expect(html).toContain("https://parachute.computer/onboarding/vault-setup/");
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
test("onboarding checklist — condenses to 'you're connected' when a grant exists", () => {
|
|
642
|
+
const html = renderAccountHome({
|
|
643
|
+
username: "alice",
|
|
644
|
+
assignedVaults: ["alice"],
|
|
645
|
+
passwordChanged: true,
|
|
646
|
+
hubOrigin: HUB_ORIGIN,
|
|
647
|
+
isFirstAdmin: false,
|
|
648
|
+
csrfToken: CSRF,
|
|
649
|
+
twoFactorEnabled: false,
|
|
650
|
+
connectedVault: true,
|
|
651
|
+
});
|
|
652
|
+
// Still the same section, but in its condensed done-state.
|
|
653
|
+
expect(html).toContain('data-testid="onboarding-checklist"');
|
|
654
|
+
expect(html).toContain('data-connected="true"');
|
|
655
|
+
expect(html).toContain('data-testid="onboarding-done-line"');
|
|
656
|
+
expect(html).toContain("You're connected");
|
|
657
|
+
// The full 3-step list is gone (no nagging) — but the vault card below
|
|
658
|
+
// remains the working surface.
|
|
659
|
+
expect(html).not.toContain('data-testid="onboarding-step-2"');
|
|
660
|
+
expect(html).toContain('data-testid="vault-card"');
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
test("onboarding checklist — leads the page: BEFORE the vault card and the starter prompts", () => {
|
|
664
|
+
const html = renderAccountHome({
|
|
665
|
+
username: "alice",
|
|
666
|
+
assignedVaults: ["alice"],
|
|
667
|
+
passwordChanged: true,
|
|
668
|
+
hubOrigin: HUB_ORIGIN,
|
|
669
|
+
isFirstAdmin: false,
|
|
670
|
+
csrfToken: CSRF,
|
|
671
|
+
twoFactorEnabled: false,
|
|
672
|
+
connectedVault: false,
|
|
673
|
+
});
|
|
674
|
+
const checklistIdx = html.indexOf('data-testid="onboarding-checklist"');
|
|
675
|
+
const vaultIdx = html.indexOf('data-testid="vault-card"');
|
|
676
|
+
const promptsIdx = html.indexOf('data-testid="get-started-card"');
|
|
677
|
+
// Net first-run order: checklist (connect) → vault details → prompts.
|
|
678
|
+
expect(checklistIdx).toBeGreaterThanOrEqual(0);
|
|
679
|
+
expect(checklistIdx).toBeLessThan(vaultIdx);
|
|
680
|
+
expect(vaultIdx).toBeLessThan(promptsIdx);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
test("onboarding checklist — absent on the admin and no-vault branches", () => {
|
|
684
|
+
const admin = renderAccountHome({
|
|
685
|
+
username: "admin",
|
|
686
|
+
assignedVaults: [],
|
|
687
|
+
passwordChanged: true,
|
|
688
|
+
hubOrigin: HUB_ORIGIN,
|
|
689
|
+
isFirstAdmin: true,
|
|
690
|
+
csrfToken: CSRF,
|
|
691
|
+
twoFactorEnabled: false,
|
|
692
|
+
});
|
|
693
|
+
expect(admin).not.toContain('data-testid="onboarding-checklist"');
|
|
694
|
+
|
|
695
|
+
const noVault = renderAccountHome({
|
|
696
|
+
username: "ghost",
|
|
697
|
+
assignedVaults: [],
|
|
698
|
+
passwordChanged: true,
|
|
699
|
+
hubOrigin: HUB_ORIGIN,
|
|
700
|
+
isFirstAdmin: false,
|
|
701
|
+
csrfToken: CSRF,
|
|
702
|
+
twoFactorEnabled: false,
|
|
703
|
+
});
|
|
704
|
+
expect(noVault).not.toContain('data-testid="onboarding-checklist"');
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
test("onboarding checklist — multi-vault uses the first vault for the connect step", () => {
|
|
708
|
+
const html = renderAccountHome({
|
|
709
|
+
username: "alice",
|
|
710
|
+
assignedVaults: ["personal", "family"],
|
|
711
|
+
passwordChanged: true,
|
|
712
|
+
hubOrigin: HUB_ORIGIN,
|
|
713
|
+
isFirstAdmin: false,
|
|
714
|
+
csrfToken: CSRF,
|
|
715
|
+
twoFactorEnabled: false,
|
|
716
|
+
connectedVault: false,
|
|
717
|
+
});
|
|
718
|
+
// The checklist's connect step references the first/primary vault; the
|
|
719
|
+
// per-vault tiles below still list every vault (by name + Notes CTA), but
|
|
720
|
+
// no longer repeat the connect endpoint (dedup — connect lives only here).
|
|
721
|
+
expect(html).toMatch(/data-testid="onboarding-mcp-endpoint">[^<]*\/vault\/personal\/mcp</);
|
|
722
|
+
expect(html).toContain("<strong>family</strong>"); // second vault still has a tile
|
|
723
|
+
expect(html).not.toContain(`${HUB_ORIGIN}/vault/family/mcp`); // no duplicated connect
|
|
724
|
+
});
|
|
532
725
|
});
|