@openparachute/hub 0.5.14-rc.1 → 0.5.14-rc.3
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 +29 -6
- package/src/__tests__/admin-vault-admin-token.test.ts +2 -2
- package/src/__tests__/api-account.test.ts +2 -2
- package/src/__tests__/api-modules-config.test.ts +16 -10
- package/src/__tests__/api-modules-ops.test.ts +97 -0
- package/src/__tests__/api-modules.test.ts +92 -75
- package/src/__tests__/api-users.test.ts +191 -9
- package/src/__tests__/hub-db.test.ts +194 -29
- package/src/__tests__/oauth-handlers.test.ts +722 -28
- package/src/__tests__/serve.test.ts +9 -9
- package/src/__tests__/setup-wizard.test.ts +412 -47
- package/src/__tests__/users.test.ts +135 -9
- package/src/account-home-ui.ts +57 -27
- package/src/api-account.ts +1 -1
- package/src/api-modules-ops.ts +52 -16
- package/src/api-modules.ts +29 -12
- package/src/api-users.ts +303 -51
- package/src/bun-link.ts +55 -0
- package/src/commands/install.ts +8 -21
- package/src/hub-db.ts +42 -0
- package/src/hub-server.ts +19 -0
- package/src/oauth-handlers.ts +278 -173
- package/src/oauth-ui.ts +18 -2
- package/src/setup-wizard.ts +227 -140
- package/src/users.ts +195 -29
- package/web/ui/dist/assets/index-tRmPbbC7.js +61 -0
- package/web/ui/dist/index.html +1 -1
- package/web/ui/dist/assets/index-Qf56GsGm.js +0 -61
package/package.json
CHANGED
|
@@ -22,7 +22,7 @@ describe("renderAccountHome", () => {
|
|
|
22
22
|
test("assigned-vault branch — Notes CTA carries the encoded hub+vault URL", () => {
|
|
23
23
|
const html = renderAccountHome({
|
|
24
24
|
username: "alice",
|
|
25
|
-
|
|
25
|
+
assignedVaults: ["alice"],
|
|
26
26
|
passwordChanged: true,
|
|
27
27
|
hubOrigin: HUB_ORIGIN,
|
|
28
28
|
isFirstAdmin: false,
|
|
@@ -49,7 +49,7 @@ describe("renderAccountHome", () => {
|
|
|
49
49
|
// renderer must produce a clean `/vault/<name>` join either way.
|
|
50
50
|
const html = renderAccountHome({
|
|
51
51
|
username: "alice",
|
|
52
|
-
|
|
52
|
+
assignedVaults: ["alice"],
|
|
53
53
|
passwordChanged: true,
|
|
54
54
|
hubOrigin: `${HUB_ORIGIN}/`,
|
|
55
55
|
isFirstAdmin: false,
|
|
@@ -65,7 +65,7 @@ describe("renderAccountHome", () => {
|
|
|
65
65
|
test("admin branch — null assignedVault + isFirstAdmin renders an /admin/ link", () => {
|
|
66
66
|
const html = renderAccountHome({
|
|
67
67
|
username: "admin",
|
|
68
|
-
|
|
68
|
+
assignedVaults: [],
|
|
69
69
|
passwordChanged: true,
|
|
70
70
|
hubOrigin: HUB_ORIGIN,
|
|
71
71
|
isFirstAdmin: true,
|
|
@@ -85,7 +85,7 @@ describe("renderAccountHome", () => {
|
|
|
85
85
|
// state via hand-edit or migration race.
|
|
86
86
|
const html = renderAccountHome({
|
|
87
87
|
username: "ghost",
|
|
88
|
-
|
|
88
|
+
assignedVaults: [],
|
|
89
89
|
passwordChanged: true,
|
|
90
90
|
hubOrigin: HUB_ORIGIN,
|
|
91
91
|
isFirstAdmin: false,
|
|
@@ -102,7 +102,7 @@ describe("renderAccountHome", () => {
|
|
|
102
102
|
test("account card — change-password link and sign-out form are present", () => {
|
|
103
103
|
const html = renderAccountHome({
|
|
104
104
|
username: "alice",
|
|
105
|
-
|
|
105
|
+
assignedVaults: ["alice"],
|
|
106
106
|
passwordChanged: true,
|
|
107
107
|
hubOrigin: HUB_ORIGIN,
|
|
108
108
|
isFirstAdmin: false,
|
|
@@ -120,6 +120,29 @@ describe("renderAccountHome", () => {
|
|
|
120
120
|
expect(html).toContain("<code>alice</code>");
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
+
test("multi-vault branch — renders one tile per assigned vault (Phase 2 PR 2)", () => {
|
|
124
|
+
const html = renderAccountHome({
|
|
125
|
+
username: "alice",
|
|
126
|
+
assignedVaults: ["personal", "family"],
|
|
127
|
+
passwordChanged: true,
|
|
128
|
+
hubOrigin: HUB_ORIGIN,
|
|
129
|
+
isFirstAdmin: false,
|
|
130
|
+
csrfToken: CSRF,
|
|
131
|
+
});
|
|
132
|
+
// Plural heading.
|
|
133
|
+
expect(html).toContain("Your vaults");
|
|
134
|
+
// Each vault name appears.
|
|
135
|
+
expect(html).toContain("<strong>personal</strong>");
|
|
136
|
+
expect(html).toContain("<strong>family</strong>");
|
|
137
|
+
// One CTA per vault with the right encoded URL.
|
|
138
|
+
const personalEncoded = encodeURIComponent(`${HUB_ORIGIN}/vault/personal`);
|
|
139
|
+
const familyEncoded = encodeURIComponent(`${HUB_ORIGIN}/vault/family`);
|
|
140
|
+
expect(html).toContain(`https://notes.parachute.computer/add?url=${personalEncoded}`);
|
|
141
|
+
expect(html).toContain(`https://notes.parachute.computer/add?url=${familyEncoded}`);
|
|
142
|
+
// Hub origin block appears once at the section level, not per tile.
|
|
143
|
+
expect(html.split(`<code>${HUB_ORIGIN}</code>`).length - 1).toBe(1);
|
|
144
|
+
});
|
|
145
|
+
|
|
123
146
|
test("escapes hostile content in username and vault name", () => {
|
|
124
147
|
// Defense-in-depth: usernames pass validateUsername (lowercase alnum
|
|
125
148
|
// + `_-`), so HTML metacharacters won't normally make it through. But
|
|
@@ -127,7 +150,7 @@ describe("renderAccountHome", () => {
|
|
|
127
150
|
// escape is load-bearing if the validator ever loosens.
|
|
128
151
|
const html = renderAccountHome({
|
|
129
152
|
username: "<script>",
|
|
130
|
-
|
|
153
|
+
assignedVaults: ["<vault>"],
|
|
131
154
|
passwordChanged: true,
|
|
132
155
|
hubOrigin: HUB_ORIGIN,
|
|
133
156
|
isFirstAdmin: false,
|
|
@@ -160,7 +160,7 @@ describe("handleVaultAdminToken", () => {
|
|
|
160
160
|
// whole reason this gate exists.
|
|
161
161
|
await createUser(harness.db, "operator", "hunter2-admin");
|
|
162
162
|
const friend = await createUser(harness.db, "friend", "hunter2-friend", {
|
|
163
|
-
|
|
163
|
+
assignedVaults: ["work"],
|
|
164
164
|
allowMulti: true,
|
|
165
165
|
});
|
|
166
166
|
const session = createSession(harness.db, { userId: friend.id });
|
|
@@ -182,7 +182,7 @@ describe("handleVaultAdminToken", () => {
|
|
|
182
182
|
// happy path when there are friends in the DB.
|
|
183
183
|
const admin = await createUser(harness.db, "operator", "hunter2-admin");
|
|
184
184
|
await createUser(harness.db, "friend", "hunter2-friend", {
|
|
185
|
-
|
|
185
|
+
assignedVaults: ["work"],
|
|
186
186
|
allowMulti: true,
|
|
187
187
|
});
|
|
188
188
|
const session = createSession(harness.db, { userId: admin.id });
|
|
@@ -757,7 +757,7 @@ describe("handleAccountHomeGet", () => {
|
|
|
757
757
|
const friend = await createUser(harness.db, "alice", "alice-passphrase", {
|
|
758
758
|
allowMulti: true,
|
|
759
759
|
passwordChanged: true,
|
|
760
|
-
|
|
760
|
+
assignedVaults: ["alice"],
|
|
761
761
|
});
|
|
762
762
|
const session = createSession(harness.db, { userId: friend.id });
|
|
763
763
|
const cookie = buildSessionCookie(session.id, Math.floor(SESSION_TTL_MS / 1000));
|
|
@@ -774,7 +774,7 @@ describe("handleAccountHomeGet", () => {
|
|
|
774
774
|
expect(html).toContain(`https://notes.parachute.computer/add?url=${encoded}`);
|
|
775
775
|
});
|
|
776
776
|
|
|
777
|
-
test("200 + admin branch when the first-admin signs in (
|
|
777
|
+
test("200 + admin branch when the first-admin signs in (no vault assignments)", async () => {
|
|
778
778
|
// The first-created user with no vault pin is the admin posture.
|
|
779
779
|
const admin = await createUser(harness.db, "admin", "admin-passphrase", {
|
|
780
780
|
passwordChanged: true,
|
|
@@ -23,6 +23,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
23
23
|
import { tmpdir } from "node:os";
|
|
24
24
|
import { join } from "node:path";
|
|
25
25
|
import { decodeJwt } from "jose";
|
|
26
|
+
import type { CuratedModuleShort } from "../api-modules.ts";
|
|
26
27
|
import {
|
|
27
28
|
API_MODULES_CONFIG_REQUIRED_SCOPE,
|
|
28
29
|
MODULE_CONFIG_PROXY_CLIENT_ID,
|
|
@@ -152,14 +153,19 @@ describe("parseModulesConfigPath", () => {
|
|
|
152
153
|
});
|
|
153
154
|
});
|
|
154
155
|
|
|
155
|
-
test("matches vault and
|
|
156
|
+
test("matches vault and scribe (curated modules)", () => {
|
|
156
157
|
expect(parseModulesConfigPath("/api/modules/vault/config")?.short).toBe("vault");
|
|
157
|
-
expect(parseModulesConfigPath("/api/modules/
|
|
158
|
+
expect(parseModulesConfigPath("/api/modules/scribe/config/schema")?.short).toBe("scribe");
|
|
158
159
|
});
|
|
159
160
|
|
|
160
161
|
test("rejects unknown short (non-curated)", () => {
|
|
161
162
|
expect(parseModulesConfigPath("/api/modules/unknown/config")).toBeUndefined();
|
|
162
163
|
expect(parseModulesConfigPath("/api/modules/channel/config")).toBeUndefined();
|
|
164
|
+
// Curated list trimmed 2026-05-27: notes / runner / surface are no
|
|
165
|
+
// longer curated and reject at the parse boundary.
|
|
166
|
+
expect(parseModulesConfigPath("/api/modules/notes/config")).toBeUndefined();
|
|
167
|
+
expect(parseModulesConfigPath("/api/modules/runner/config")).toBeUndefined();
|
|
168
|
+
expect(parseModulesConfigPath("/api/modules/surface/config")).toBeUndefined();
|
|
163
169
|
});
|
|
164
170
|
|
|
165
171
|
test("rejects non-config suffix shapes", () => {
|
|
@@ -302,7 +308,7 @@ describe("handleApiModulesConfig — FALLBACK retirement (hub#310)", () => {
|
|
|
302
308
|
makeReq("/api/modules/runner/config/schema", {
|
|
303
309
|
headers: { authorization: `Bearer ${bearer}` },
|
|
304
310
|
}),
|
|
305
|
-
{ short: "runner", suffix: "schema" },
|
|
311
|
+
{ short: "runner" as CuratedModuleShort, suffix: "schema" },
|
|
306
312
|
{ db: h.db, issuer: ISSUER, manifestPath: h.manifestPath },
|
|
307
313
|
);
|
|
308
314
|
expect(res.status).toBe(404);
|
|
@@ -365,7 +371,7 @@ describe("handleApiModulesConfig — FALLBACK retirement (hub#310)", () => {
|
|
|
365
371
|
makeReq("/api/modules/runner/config/schema", {
|
|
366
372
|
headers: { authorization: `Bearer ${bearer}` },
|
|
367
373
|
}),
|
|
368
|
-
{ short: "runner", suffix: "schema" },
|
|
374
|
+
{ short: "runner" as CuratedModuleShort, suffix: "schema" },
|
|
369
375
|
{
|
|
370
376
|
db: h.db,
|
|
371
377
|
issuer: ISSUER,
|
|
@@ -614,7 +620,7 @@ describe("handleApiModulesConfig — stripPrefix=false (notes-shape)", () => {
|
|
|
614
620
|
makeReq("/api/modules/notes/config/schema", {
|
|
615
621
|
headers: { authorization: `Bearer ${bearer}` },
|
|
616
622
|
}),
|
|
617
|
-
{ short: "notes", suffix: "schema" },
|
|
623
|
+
{ short: "notes" as CuratedModuleShort, suffix: "schema" },
|
|
618
624
|
{
|
|
619
625
|
db: h.db,
|
|
620
626
|
issuer: ISSUER,
|
|
@@ -668,7 +674,7 @@ describe("handleApiModulesConfig — hostsBareParachute (hub#307)", () => {
|
|
|
668
674
|
makeReq("/api/modules/runner/config/schema", {
|
|
669
675
|
headers: { authorization: `Bearer ${bearer}` },
|
|
670
676
|
}),
|
|
671
|
-
{ short: "runner", suffix: "schema" },
|
|
677
|
+
{ short: "runner" as CuratedModuleShort, suffix: "schema" },
|
|
672
678
|
{
|
|
673
679
|
db: h.db,
|
|
674
680
|
issuer: ISSUER,
|
|
@@ -700,7 +706,7 @@ describe("handleApiModulesConfig — hostsBareParachute (hub#307)", () => {
|
|
|
700
706
|
makeReq("/api/modules/runner/config", {
|
|
701
707
|
headers: { authorization: `Bearer ${bearer}` },
|
|
702
708
|
}),
|
|
703
|
-
{ short: "runner", suffix: "" },
|
|
709
|
+
{ short: "runner" as CuratedModuleShort, suffix: "" },
|
|
704
710
|
{
|
|
705
711
|
db: h.db,
|
|
706
712
|
issuer: ISSUER,
|
|
@@ -733,7 +739,7 @@ describe("handleApiModulesConfig — hostsBareParachute (hub#307)", () => {
|
|
|
733
739
|
},
|
|
734
740
|
body: JSON.stringify({ intervalSeconds: 120 }),
|
|
735
741
|
}),
|
|
736
|
-
{ short: "runner", suffix: "" },
|
|
742
|
+
{ short: "runner" as CuratedModuleShort, suffix: "" },
|
|
737
743
|
{
|
|
738
744
|
db: h.db,
|
|
739
745
|
issuer: ISSUER,
|
|
@@ -760,7 +766,7 @@ describe("handleApiModulesConfig — hostsBareParachute (hub#307)", () => {
|
|
|
760
766
|
makeReq("/api/modules/runner/config", {
|
|
761
767
|
headers: { authorization: `Bearer ${bearer}` },
|
|
762
768
|
}),
|
|
763
|
-
{ short: "runner", suffix: "" },
|
|
769
|
+
{ short: "runner" as CuratedModuleShort, suffix: "" },
|
|
764
770
|
{
|
|
765
771
|
db: h.db,
|
|
766
772
|
issuer: ISSUER,
|
|
@@ -863,7 +869,7 @@ describe("handleApiModulesConfig — hostsBareParachute (hub#307)", () => {
|
|
|
863
869
|
makeReq("/api/modules/runner/config/schema", {
|
|
864
870
|
headers: { authorization: `Bearer ${bearer}` },
|
|
865
871
|
}),
|
|
866
|
-
{ short: "runner", suffix: "schema" },
|
|
872
|
+
{ short: "runner" as CuratedModuleShort, suffix: "schema" },
|
|
867
873
|
{
|
|
868
874
|
db: h.db,
|
|
869
875
|
issuer: ISSUER,
|
|
@@ -126,6 +126,19 @@ function alwaysOkRun(): {
|
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Test default for `isLinked`: assume the package is NOT bun-linked so
|
|
131
|
+
* `runInstall` exercises the `bun add -g` path (which the stubbed runner
|
|
132
|
+
* captures into `calls`). The production default at
|
|
133
|
+
* `src/bun-link.ts` reads the contributor's real `~/.bun/install/global/`
|
|
134
|
+
* symlinks; on Aaron's machine vault/scribe/notes/hub are all linked
|
|
135
|
+
* (the canonical local-dev shape — smoke 2026-05-27 finding 1 caps the
|
|
136
|
+
* fix). Tests asserting "bun add WAS called" must opt out of that leakage
|
|
137
|
+
* by passing this stub. Tests specifically exercising the skip path use
|
|
138
|
+
* an inline `isLinked: () => true` or a per-pkg discriminator.
|
|
139
|
+
*/
|
|
140
|
+
const TEST_DEFAULT_NOT_LINKED = (_pkg: string): boolean => false;
|
|
141
|
+
|
|
129
142
|
describe("parseModulesPath", () => {
|
|
130
143
|
test("recognizes curated short + action", () => {
|
|
131
144
|
expect(parseModulesPath("/api/modules/vault/install")).toEqual({
|
|
@@ -231,6 +244,7 @@ describe("POST /api/modules/:short/install", () => {
|
|
|
231
244
|
configDir: h.dir,
|
|
232
245
|
supervisor,
|
|
233
246
|
run,
|
|
247
|
+
isLinked: TEST_DEFAULT_NOT_LINKED,
|
|
234
248
|
},
|
|
235
249
|
);
|
|
236
250
|
expect(res.status).toBe(202);
|
|
@@ -280,6 +294,7 @@ describe("POST /api/modules/:short/install", () => {
|
|
|
280
294
|
configDir: h.dir,
|
|
281
295
|
supervisor,
|
|
282
296
|
run,
|
|
297
|
+
isLinked: TEST_DEFAULT_NOT_LINKED,
|
|
283
298
|
},
|
|
284
299
|
);
|
|
285
300
|
expect(res.status).toBe(202);
|
|
@@ -301,6 +316,7 @@ describe("POST /api/modules/:short/install", () => {
|
|
|
301
316
|
configDir: h.dir,
|
|
302
317
|
supervisor,
|
|
303
318
|
run,
|
|
319
|
+
isLinked: TEST_DEFAULT_NOT_LINKED,
|
|
304
320
|
},
|
|
305
321
|
);
|
|
306
322
|
const op = (await opRes.json()) as { status: string };
|
|
@@ -324,6 +340,7 @@ describe("POST /api/modules/:short/install", () => {
|
|
|
324
340
|
configDir: h.dir,
|
|
325
341
|
supervisor,
|
|
326
342
|
run,
|
|
343
|
+
isLinked: TEST_DEFAULT_NOT_LINKED,
|
|
327
344
|
},
|
|
328
345
|
);
|
|
329
346
|
expect(res.status).toBe(202);
|
|
@@ -348,6 +365,7 @@ describe("POST /api/modules/:short/install", () => {
|
|
|
348
365
|
configDir: h.dir,
|
|
349
366
|
supervisor,
|
|
350
367
|
run,
|
|
368
|
+
isLinked: TEST_DEFAULT_NOT_LINKED,
|
|
351
369
|
},
|
|
352
370
|
);
|
|
353
371
|
await new Promise((r) => setTimeout(r, 10));
|
|
@@ -379,6 +397,7 @@ describe("POST /api/modules/:short/install", () => {
|
|
|
379
397
|
configDir: h.dir,
|
|
380
398
|
supervisor,
|
|
381
399
|
run,
|
|
400
|
+
isLinked: TEST_DEFAULT_NOT_LINKED,
|
|
382
401
|
},
|
|
383
402
|
);
|
|
384
403
|
expect(res.status).toBe(202);
|
|
@@ -406,6 +425,7 @@ describe("POST /api/modules/:short/install", () => {
|
|
|
406
425
|
configDir: h.dir,
|
|
407
426
|
supervisor,
|
|
408
427
|
run,
|
|
428
|
+
isLinked: TEST_DEFAULT_NOT_LINKED,
|
|
409
429
|
},
|
|
410
430
|
);
|
|
411
431
|
await new Promise((r) => setTimeout(r, 10));
|
|
@@ -433,6 +453,7 @@ describe("POST /api/modules/:short/install", () => {
|
|
|
433
453
|
configDir: h.dir,
|
|
434
454
|
supervisor,
|
|
435
455
|
run,
|
|
456
|
+
isLinked: TEST_DEFAULT_NOT_LINKED,
|
|
436
457
|
},
|
|
437
458
|
);
|
|
438
459
|
expect(res.status).toBe(400);
|
|
@@ -458,6 +479,7 @@ describe("POST /api/modules/:short/install", () => {
|
|
|
458
479
|
configDir: h.dir,
|
|
459
480
|
supervisor,
|
|
460
481
|
run,
|
|
482
|
+
isLinked: TEST_DEFAULT_NOT_LINKED,
|
|
461
483
|
},
|
|
462
484
|
);
|
|
463
485
|
await new Promise((r) => setTimeout(r, 10));
|
|
@@ -487,6 +509,7 @@ describe("POST /api/modules/:short/install", () => {
|
|
|
487
509
|
configDir: h.dir,
|
|
488
510
|
supervisor,
|
|
489
511
|
run,
|
|
512
|
+
isLinked: TEST_DEFAULT_NOT_LINKED,
|
|
490
513
|
},
|
|
491
514
|
);
|
|
492
515
|
await new Promise((r) => setTimeout(r, 10));
|
|
@@ -524,6 +547,7 @@ describe("POST /api/modules/:short/install", () => {
|
|
|
524
547
|
configDir: h.dir,
|
|
525
548
|
supervisor,
|
|
526
549
|
run,
|
|
550
|
+
isLinked: TEST_DEFAULT_NOT_LINKED,
|
|
527
551
|
},
|
|
528
552
|
);
|
|
529
553
|
await new Promise((r) => setTimeout(r, 10));
|
|
@@ -557,6 +581,7 @@ describe("POST /api/modules/:short/install", () => {
|
|
|
557
581
|
configDir: h.dir,
|
|
558
582
|
supervisor,
|
|
559
583
|
run,
|
|
584
|
+
isLinked: TEST_DEFAULT_NOT_LINKED,
|
|
560
585
|
},
|
|
561
586
|
);
|
|
562
587
|
expect(res.status).toBe(202);
|
|
@@ -583,6 +608,7 @@ describe("POST /api/modules/:short/install", () => {
|
|
|
583
608
|
supervisor,
|
|
584
609
|
run: async () => 1,
|
|
585
610
|
findGlobalInstall: () => null,
|
|
611
|
+
isLinked: TEST_DEFAULT_NOT_LINKED,
|
|
586
612
|
};
|
|
587
613
|
const res = await handleInstall(
|
|
588
614
|
postReq("/api/modules/vault/install", { authorization: `Bearer ${bearer}` }),
|
|
@@ -602,6 +628,71 @@ describe("POST /api/modules/:short/install", () => {
|
|
|
602
628
|
expect(op.status).toBe("failed");
|
|
603
629
|
expect(op.error).toMatch(/bun add -g exited 1/);
|
|
604
630
|
});
|
|
631
|
+
|
|
632
|
+
test("skips bun add -g when package is already bun-linked (smoke 2026-05-27 finding 1)", async () => {
|
|
633
|
+
// Smoke finding 1: the wizard's parallel install path was unconditionally
|
|
634
|
+
// invoking `bun add -g <pkg>` even when the package was already linked
|
|
635
|
+
// via `bun link <abspath>` (the standard local-dev shape). At best a
|
|
636
|
+
// wasted ~3s npm round-trip per install; at worst the global bun.lock
|
|
637
|
+
// had unrelated noise and the install failed outright, taking the
|
|
638
|
+
// wizard's vault step with it. Fix: mirror the CLI install path's
|
|
639
|
+
// `isLinked` short-circuit. Regression guard.
|
|
640
|
+
const { supervisor, spawns } = makeIdleSupervisor();
|
|
641
|
+
const { run, calls } = alwaysOkRun();
|
|
642
|
+
const bearer = await mintBearer(h, [API_MODULES_OPS_REQUIRED_SCOPE]);
|
|
643
|
+
const res = await handleInstall(
|
|
644
|
+
postReq("/api/modules/vault/install", { authorization: `Bearer ${bearer}` }),
|
|
645
|
+
"vault",
|
|
646
|
+
{
|
|
647
|
+
db: h.db,
|
|
648
|
+
issuer: ISSUER,
|
|
649
|
+
manifestPath: h.manifestPath,
|
|
650
|
+
configDir: h.dir,
|
|
651
|
+
supervisor,
|
|
652
|
+
run,
|
|
653
|
+
// The bug shape: package IS linked locally. Without the
|
|
654
|
+
// short-circuit, runInstall would still call bun add -g.
|
|
655
|
+
isLinked: (pkg) => pkg === "@openparachute/vault",
|
|
656
|
+
},
|
|
657
|
+
);
|
|
658
|
+
expect(res.status).toBe(202);
|
|
659
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
660
|
+
|
|
661
|
+
// The fix: bun add -g was NOT invoked for the linked package.
|
|
662
|
+
expect(calls).not.toContainEqual(["bun", "add", "-g", "@openparachute/vault@latest"]);
|
|
663
|
+
// Downstream of the skip, the seed + spawn still happen — the install
|
|
664
|
+
// op completes successfully against the locally-linked checkout.
|
|
665
|
+
const manifest = JSON.parse(readFileSync(h.manifestPath, "utf8")) as {
|
|
666
|
+
services: Array<{ name: string }>;
|
|
667
|
+
};
|
|
668
|
+
expect(manifest.services.some((s) => s.name === "parachute-vault")).toBe(true);
|
|
669
|
+
expect(spawns.find((s) => s.short === "vault")?.cmd).toEqual(["parachute-vault", "serve"]);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test("still runs bun add -g when package is NOT bun-linked", async () => {
|
|
673
|
+
// Companion to the above — confirms the short-circuit doesn't
|
|
674
|
+
// unconditionally skip. On a friend's fresh machine (no bun link),
|
|
675
|
+
// bun add -g IS what installs the package from npm. `isLinked: () => false`
|
|
676
|
+
// is the production default behavior for a non-linked package.
|
|
677
|
+
const { supervisor } = makeIdleSupervisor();
|
|
678
|
+
const { run, calls } = alwaysOkRun();
|
|
679
|
+
const bearer = await mintBearer(h, [API_MODULES_OPS_REQUIRED_SCOPE]);
|
|
680
|
+
await handleInstall(
|
|
681
|
+
postReq("/api/modules/vault/install", { authorization: `Bearer ${bearer}` }),
|
|
682
|
+
"vault",
|
|
683
|
+
{
|
|
684
|
+
db: h.db,
|
|
685
|
+
issuer: ISSUER,
|
|
686
|
+
manifestPath: h.manifestPath,
|
|
687
|
+
configDir: h.dir,
|
|
688
|
+
supervisor,
|
|
689
|
+
run,
|
|
690
|
+
isLinked: () => false,
|
|
691
|
+
},
|
|
692
|
+
);
|
|
693
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
694
|
+
expect(calls).toContainEqual(["bun", "add", "-g", "@openparachute/vault@latest"]);
|
|
695
|
+
});
|
|
605
696
|
});
|
|
606
697
|
|
|
607
698
|
describe("POST /api/modules/:short/restart", () => {
|
|
@@ -682,6 +773,7 @@ describe("POST /api/modules/:short/upgrade", () => {
|
|
|
682
773
|
configDir: h.dir,
|
|
683
774
|
supervisor,
|
|
684
775
|
run,
|
|
776
|
+
isLinked: TEST_DEFAULT_NOT_LINKED,
|
|
685
777
|
},
|
|
686
778
|
);
|
|
687
779
|
expect(res.status).toBe(202);
|
|
@@ -706,6 +798,7 @@ describe("POST /api/modules/:short/upgrade", () => {
|
|
|
706
798
|
configDir: h.dir,
|
|
707
799
|
supervisor,
|
|
708
800
|
run,
|
|
801
|
+
isLinked: TEST_DEFAULT_NOT_LINKED,
|
|
709
802
|
},
|
|
710
803
|
);
|
|
711
804
|
expect(res.status).toBe(202);
|
|
@@ -736,6 +829,7 @@ describe("POST /api/modules/:short/upgrade", () => {
|
|
|
736
829
|
configDir: h.dir,
|
|
737
830
|
supervisor,
|
|
738
831
|
run,
|
|
832
|
+
isLinked: TEST_DEFAULT_NOT_LINKED,
|
|
739
833
|
},
|
|
740
834
|
);
|
|
741
835
|
await new Promise((r) => setTimeout(r, 10));
|
|
@@ -846,6 +940,7 @@ describe("POST /api/modules/:short/uninstall", () => {
|
|
|
846
940
|
configDir: h.dir,
|
|
847
941
|
supervisor,
|
|
848
942
|
run,
|
|
943
|
+
isLinked: TEST_DEFAULT_NOT_LINKED,
|
|
849
944
|
},
|
|
850
945
|
);
|
|
851
946
|
expect(res.status).toBe(200);
|
|
@@ -878,6 +973,7 @@ describe("POST /api/modules/:short/uninstall", () => {
|
|
|
878
973
|
configDir: h.dir,
|
|
879
974
|
supervisor,
|
|
880
975
|
run,
|
|
976
|
+
isLinked: TEST_DEFAULT_NOT_LINKED,
|
|
881
977
|
},
|
|
882
978
|
);
|
|
883
979
|
expect(res.status).toBe(200);
|
|
@@ -1099,6 +1195,7 @@ describe("well-known regen after module ops", () => {
|
|
|
1099
1195
|
supervisor,
|
|
1100
1196
|
run: async () => 1,
|
|
1101
1197
|
findGlobalInstall: () => null,
|
|
1198
|
+
isLinked: TEST_DEFAULT_NOT_LINKED,
|
|
1102
1199
|
wellKnownPath: wkPath,
|
|
1103
1200
|
};
|
|
1104
1201
|
const res = await handleInstall(
|