@openparachute/hub 0.5.13-rc.13 → 0.5.13-rc.21
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__/api-modules-ops.test.ts +257 -4
- package/src/__tests__/api-modules.test.ts +90 -0
- package/src/__tests__/cli.test.ts +13 -0
- package/src/__tests__/hub-server.test.ts +10 -13
- package/src/__tests__/install.test.ts +259 -24
- package/src/__tests__/lifecycle.test.ts +90 -13
- package/src/__tests__/module-manifest.test.ts +19 -3
- package/src/__tests__/post-install.test.ts +0 -2
- package/src/__tests__/scope-registry.test.ts +9 -9
- package/src/__tests__/services-manifest.test.ts +456 -43
- package/src/__tests__/setup-wizard.test.ts +228 -0
- package/src/__tests__/status.test.ts +4 -4
- package/src/__tests__/upgrade.test.ts +362 -3
- package/src/api-modules-ops.ts +79 -7
- package/src/api-modules.ts +97 -1
- package/src/cli.ts +50 -4
- package/src/commands/install.ts +108 -6
- package/src/commands/lifecycle.ts +20 -0
- package/src/commands/upgrade.ts +213 -27
- package/src/help.ts +54 -17
- package/src/hub-server.ts +5 -0
- package/src/hub.ts +71 -0
- package/src/module-manifest.ts +22 -17
- package/src/service-spec.ts +44 -60
- package/src/services-manifest.ts +163 -3
- package/src/setup-wizard.ts +205 -12
- package/web/ui/dist/assets/index-5Mj6FqPg.css +1 -0
- package/web/ui/dist/assets/{index-D63mUkVX.js → index-BqjySZ_7.js} +12 -12
- package/web/ui/dist/index.html +2 -2
- package/web/ui/dist/assets/index-DliViliP.css +0 -1
package/package.json
CHANGED
|
@@ -70,6 +70,14 @@ function postReq(path: string, headers: Record<string, string>): Request {
|
|
|
70
70
|
return new Request(`http://localhost${path}`, { method: "POST", headers });
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
function postReqJson(path: string, headers: Record<string, string>, body: unknown): Request {
|
|
74
|
+
return new Request(`http://localhost${path}`, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { ...headers, "content-type": "application/json" },
|
|
77
|
+
body: JSON.stringify(body),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
73
81
|
function getReq(path: string, headers: Record<string, string>): Request {
|
|
74
82
|
return new Request(`http://localhost${path}`, { method: "GET", headers });
|
|
75
83
|
}
|
|
@@ -347,6 +355,222 @@ describe("POST /api/modules/:short/install", () => {
|
|
|
347
355
|
expect(calls).not.toContainEqual(["bun", "add", "-g", "@openparachute/vault@rc"]);
|
|
348
356
|
});
|
|
349
357
|
|
|
358
|
+
// hub#337 — per-request channel in body + PARACHUTE_INSTALL_CHANNEL env var.
|
|
359
|
+
// Precedence: body.channel > PARACHUTE_INSTALL_CHANNEL env > hub_settings row > "latest".
|
|
360
|
+
|
|
361
|
+
test("body { channel: 'rc' } overrides the hub_settings row (hub#337)", async () => {
|
|
362
|
+
// SPA-driven "install X at rc" affordance: per-call override that
|
|
363
|
+
// doesn't flip the cluster-wide toggle.
|
|
364
|
+
setModuleInstallChannel(h.db, "latest");
|
|
365
|
+
const { supervisor } = makeIdleSupervisor();
|
|
366
|
+
const { run, calls } = alwaysOkRun();
|
|
367
|
+
const bearer = await mintBearer(h, [API_MODULES_OPS_REQUIRED_SCOPE]);
|
|
368
|
+
const res = await handleInstall(
|
|
369
|
+
postReqJson(
|
|
370
|
+
"/api/modules/vault/install",
|
|
371
|
+
{ authorization: `Bearer ${bearer}` },
|
|
372
|
+
{ channel: "rc" },
|
|
373
|
+
),
|
|
374
|
+
"vault",
|
|
375
|
+
{
|
|
376
|
+
db: h.db,
|
|
377
|
+
issuer: ISSUER,
|
|
378
|
+
manifestPath: h.manifestPath,
|
|
379
|
+
configDir: h.dir,
|
|
380
|
+
supervisor,
|
|
381
|
+
run,
|
|
382
|
+
},
|
|
383
|
+
);
|
|
384
|
+
expect(res.status).toBe(202);
|
|
385
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
386
|
+
expect(calls).toContainEqual(["bun", "add", "-g", "@openparachute/vault@rc"]);
|
|
387
|
+
expect(calls).not.toContainEqual(["bun", "add", "-g", "@openparachute/vault@latest"]);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("body { channel: 'latest' } overrides hub_settings.module_install_channel = rc (hub#337)", async () => {
|
|
391
|
+
setModuleInstallChannel(h.db, "rc");
|
|
392
|
+
const { supervisor } = makeIdleSupervisor();
|
|
393
|
+
const { run, calls } = alwaysOkRun();
|
|
394
|
+
const bearer = await mintBearer(h, [API_MODULES_OPS_REQUIRED_SCOPE]);
|
|
395
|
+
await handleInstall(
|
|
396
|
+
postReqJson(
|
|
397
|
+
"/api/modules/vault/install",
|
|
398
|
+
{ authorization: `Bearer ${bearer}` },
|
|
399
|
+
{ channel: "latest" },
|
|
400
|
+
),
|
|
401
|
+
"vault",
|
|
402
|
+
{
|
|
403
|
+
db: h.db,
|
|
404
|
+
issuer: ISSUER,
|
|
405
|
+
manifestPath: h.manifestPath,
|
|
406
|
+
configDir: h.dir,
|
|
407
|
+
supervisor,
|
|
408
|
+
run,
|
|
409
|
+
},
|
|
410
|
+
);
|
|
411
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
412
|
+
expect(calls).toContainEqual(["bun", "add", "-g", "@openparachute/vault@latest"]);
|
|
413
|
+
expect(calls).not.toContainEqual(["bun", "add", "-g", "@openparachute/vault@rc"]);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("body { channel: 'banana' } returns 400 invalid_channel (hub#337)", async () => {
|
|
417
|
+
// Operator-typed garbage in the SPA → don't silently fall through to
|
|
418
|
+
// the default; surface the typo immediately.
|
|
419
|
+
const { supervisor } = makeIdleSupervisor();
|
|
420
|
+
const { run } = alwaysOkRun();
|
|
421
|
+
const bearer = await mintBearer(h, [API_MODULES_OPS_REQUIRED_SCOPE]);
|
|
422
|
+
const res = await handleInstall(
|
|
423
|
+
postReqJson(
|
|
424
|
+
"/api/modules/vault/install",
|
|
425
|
+
{ authorization: `Bearer ${bearer}` },
|
|
426
|
+
{ channel: "banana" },
|
|
427
|
+
),
|
|
428
|
+
"vault",
|
|
429
|
+
{
|
|
430
|
+
db: h.db,
|
|
431
|
+
issuer: ISSUER,
|
|
432
|
+
manifestPath: h.manifestPath,
|
|
433
|
+
configDir: h.dir,
|
|
434
|
+
supervisor,
|
|
435
|
+
run,
|
|
436
|
+
},
|
|
437
|
+
);
|
|
438
|
+
expect(res.status).toBe(400);
|
|
439
|
+
const body = (await res.json()) as { error: string; error_description: string };
|
|
440
|
+
expect(body.error).toBe("invalid_channel");
|
|
441
|
+
expect(body.error_description).toMatch(/banana/);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("missing body / empty body falls through to hub_settings channel (back-compat)", async () => {
|
|
445
|
+
// Pre-hub#337 callers don't send a JSON body. The existing SPA paths
|
|
446
|
+
// (and the first-boot wizard) keep working unchanged.
|
|
447
|
+
setModuleInstallChannel(h.db, "rc");
|
|
448
|
+
const { supervisor } = makeIdleSupervisor();
|
|
449
|
+
const { run, calls } = alwaysOkRun();
|
|
450
|
+
const bearer = await mintBearer(h, [API_MODULES_OPS_REQUIRED_SCOPE]);
|
|
451
|
+
await handleInstall(
|
|
452
|
+
postReq("/api/modules/vault/install", { authorization: `Bearer ${bearer}` }),
|
|
453
|
+
"vault",
|
|
454
|
+
{
|
|
455
|
+
db: h.db,
|
|
456
|
+
issuer: ISSUER,
|
|
457
|
+
manifestPath: h.manifestPath,
|
|
458
|
+
configDir: h.dir,
|
|
459
|
+
supervisor,
|
|
460
|
+
run,
|
|
461
|
+
},
|
|
462
|
+
);
|
|
463
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
464
|
+
expect(calls).toContainEqual(["bun", "add", "-g", "@openparachute/vault@rc"]);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test("PARACHUTE_INSTALL_CHANNEL env overrides hub_settings.module_install_channel (hub#337)", async () => {
|
|
468
|
+
// The Render-deploy cascade shape: the platform sets the env var to
|
|
469
|
+
// `rc`, hub's API path picks it up over the DB-stored default. Lets
|
|
470
|
+
// an operator-toggle override that the platform-team hasn't pinned
|
|
471
|
+
// still work via the SPA toggle below it — but with the env in
|
|
472
|
+
// play, the env wins.
|
|
473
|
+
setModuleInstallChannel(h.db, "latest");
|
|
474
|
+
const prior = process.env.PARACHUTE_INSTALL_CHANNEL;
|
|
475
|
+
process.env.PARACHUTE_INSTALL_CHANNEL = "rc";
|
|
476
|
+
try {
|
|
477
|
+
const { supervisor } = makeIdleSupervisor();
|
|
478
|
+
const { run, calls } = alwaysOkRun();
|
|
479
|
+
const bearer = await mintBearer(h, [API_MODULES_OPS_REQUIRED_SCOPE]);
|
|
480
|
+
await handleInstall(
|
|
481
|
+
postReq("/api/modules/vault/install", { authorization: `Bearer ${bearer}` }),
|
|
482
|
+
"vault",
|
|
483
|
+
{
|
|
484
|
+
db: h.db,
|
|
485
|
+
issuer: ISSUER,
|
|
486
|
+
manifestPath: h.manifestPath,
|
|
487
|
+
configDir: h.dir,
|
|
488
|
+
supervisor,
|
|
489
|
+
run,
|
|
490
|
+
},
|
|
491
|
+
);
|
|
492
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
493
|
+
expect(calls).toContainEqual(["bun", "add", "-g", "@openparachute/vault@rc"]);
|
|
494
|
+
expect(calls).not.toContainEqual(["bun", "add", "-g", "@openparachute/vault@latest"]);
|
|
495
|
+
} finally {
|
|
496
|
+
// Bun's process.env supports the `[key]: undefined` shape
|
|
497
|
+
// (biome's noDelete rule preferred this over `delete`).
|
|
498
|
+
if (prior === undefined) process.env.PARACHUTE_INSTALL_CHANNEL = undefined;
|
|
499
|
+
else process.env.PARACHUTE_INSTALL_CHANNEL = prior;
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("body channel wins over PARACHUTE_INSTALL_CHANNEL env (hub#337)", async () => {
|
|
504
|
+
// Per-request override beats the platform default — the SPA's
|
|
505
|
+
// "install this one at latest even though the cluster's on rc" path.
|
|
506
|
+
setModuleInstallChannel(h.db, "latest");
|
|
507
|
+
const prior = process.env.PARACHUTE_INSTALL_CHANNEL;
|
|
508
|
+
process.env.PARACHUTE_INSTALL_CHANNEL = "rc";
|
|
509
|
+
try {
|
|
510
|
+
const { supervisor } = makeIdleSupervisor();
|
|
511
|
+
const { run, calls } = alwaysOkRun();
|
|
512
|
+
const bearer = await mintBearer(h, [API_MODULES_OPS_REQUIRED_SCOPE]);
|
|
513
|
+
await handleInstall(
|
|
514
|
+
postReqJson(
|
|
515
|
+
"/api/modules/vault/install",
|
|
516
|
+
{ authorization: `Bearer ${bearer}` },
|
|
517
|
+
{ channel: "latest" },
|
|
518
|
+
),
|
|
519
|
+
"vault",
|
|
520
|
+
{
|
|
521
|
+
db: h.db,
|
|
522
|
+
issuer: ISSUER,
|
|
523
|
+
manifestPath: h.manifestPath,
|
|
524
|
+
configDir: h.dir,
|
|
525
|
+
supervisor,
|
|
526
|
+
run,
|
|
527
|
+
},
|
|
528
|
+
);
|
|
529
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
530
|
+
expect(calls).toContainEqual(["bun", "add", "-g", "@openparachute/vault@latest"]);
|
|
531
|
+
expect(calls).not.toContainEqual(["bun", "add", "-g", "@openparachute/vault@rc"]);
|
|
532
|
+
} finally {
|
|
533
|
+
// Bun's process.env supports the `[key]: undefined` shape
|
|
534
|
+
// (biome's noDelete rule preferred this over `delete`).
|
|
535
|
+
if (prior === undefined) process.env.PARACHUTE_INSTALL_CHANNEL = undefined;
|
|
536
|
+
else process.env.PARACHUTE_INSTALL_CHANNEL = prior;
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test("garbage PARACHUTE_INSTALL_CHANNEL env falls back to hub_settings (no crash)", async () => {
|
|
541
|
+
// Operator typo at the platform layer shouldn't crash installs.
|
|
542
|
+
// Warns + falls through to the DB-stored channel.
|
|
543
|
+
setModuleInstallChannel(h.db, "rc");
|
|
544
|
+
const prior = process.env.PARACHUTE_INSTALL_CHANNEL;
|
|
545
|
+
process.env.PARACHUTE_INSTALL_CHANNEL = "banana";
|
|
546
|
+
try {
|
|
547
|
+
const { supervisor } = makeIdleSupervisor();
|
|
548
|
+
const { run, calls } = alwaysOkRun();
|
|
549
|
+
const bearer = await mintBearer(h, [API_MODULES_OPS_REQUIRED_SCOPE]);
|
|
550
|
+
const res = await handleInstall(
|
|
551
|
+
postReq("/api/modules/vault/install", { authorization: `Bearer ${bearer}` }),
|
|
552
|
+
"vault",
|
|
553
|
+
{
|
|
554
|
+
db: h.db,
|
|
555
|
+
issuer: ISSUER,
|
|
556
|
+
manifestPath: h.manifestPath,
|
|
557
|
+
configDir: h.dir,
|
|
558
|
+
supervisor,
|
|
559
|
+
run,
|
|
560
|
+
},
|
|
561
|
+
);
|
|
562
|
+
expect(res.status).toBe(202);
|
|
563
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
564
|
+
// Falls back to the DB-stored rc, not "@latest".
|
|
565
|
+
expect(calls).toContainEqual(["bun", "add", "-g", "@openparachute/vault@rc"]);
|
|
566
|
+
} finally {
|
|
567
|
+
// Bun's process.env supports the `[key]: undefined` shape
|
|
568
|
+
// (biome's noDelete rule preferred this over `delete`).
|
|
569
|
+
if (prior === undefined) process.env.PARACHUTE_INSTALL_CHANNEL = undefined;
|
|
570
|
+
else process.env.PARACHUTE_INSTALL_CHANNEL = prior;
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
|
|
350
574
|
test("failed bun-add surfaces failed status on the operation", async () => {
|
|
351
575
|
const { supervisor } = makeIdleSupervisor();
|
|
352
576
|
// Run returns 1 + findGlobalInstall returns null = real failure.
|
|
@@ -490,6 +714,39 @@ describe("POST /api/modules/:short/upgrade", () => {
|
|
|
490
714
|
expect(calls).not.toContainEqual(["bun", "add", "-g", "@openparachute/vault@latest"]);
|
|
491
715
|
});
|
|
492
716
|
|
|
717
|
+
test("PARACHUTE_INSTALL_CHANNEL env cascades to upgrade too (hub#339 symmetry)", async () => {
|
|
718
|
+
// The Render-deploy operator sets PARACHUTE_INSTALL_CHANNEL=rc cluster-
|
|
719
|
+
// wide expecting BOTH install and upgrade through the admin SPA to
|
|
720
|
+
// honor it. Asymmetry between the two paths would surprise them.
|
|
721
|
+
setModuleInstallChannel(h.db, "latest"); // DB says latest
|
|
722
|
+
const prior = process.env.PARACHUTE_INSTALL_CHANNEL;
|
|
723
|
+
process.env.PARACHUTE_INSTALL_CHANNEL = "rc"; // env says rc — should win
|
|
724
|
+
try {
|
|
725
|
+
const { supervisor } = makeIdleSupervisor();
|
|
726
|
+
await supervisor.start({ short: "vault", cmd: ["parachute-vault", "serve"] });
|
|
727
|
+
const { run, calls } = alwaysOkRun();
|
|
728
|
+
const bearer = await mintBearer(h, [API_MODULES_OPS_REQUIRED_SCOPE]);
|
|
729
|
+
await handleUpgrade(
|
|
730
|
+
postReq("/api/modules/vault/upgrade", { authorization: `Bearer ${bearer}` }),
|
|
731
|
+
"vault",
|
|
732
|
+
{
|
|
733
|
+
db: h.db,
|
|
734
|
+
issuer: ISSUER,
|
|
735
|
+
manifestPath: h.manifestPath,
|
|
736
|
+
configDir: h.dir,
|
|
737
|
+
supervisor,
|
|
738
|
+
run,
|
|
739
|
+
},
|
|
740
|
+
);
|
|
741
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
742
|
+
expect(calls).toContainEqual(["bun", "add", "-g", "@openparachute/vault@rc"]);
|
|
743
|
+
expect(calls).not.toContainEqual(["bun", "add", "-g", "@openparachute/vault@latest"]);
|
|
744
|
+
} finally {
|
|
745
|
+
if (prior === undefined) process.env.PARACHUTE_INSTALL_CHANNEL = undefined;
|
|
746
|
+
else process.env.PARACHUTE_INSTALL_CHANNEL = prior;
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
|
|
493
750
|
test("fails with 'try install first' when module is installed but never supervised", async () => {
|
|
494
751
|
// Module has a services.json row (e.g. seeded by `parachute install`
|
|
495
752
|
// pre-supervisor era) but the supervisor never spawned it.
|
|
@@ -680,7 +937,6 @@ describe("well-known regen after module ops", () => {
|
|
|
680
937
|
manifest: {
|
|
681
938
|
name: string;
|
|
682
939
|
manifestName: string;
|
|
683
|
-
kind: "api" | "frontend" | "tool";
|
|
684
940
|
port: number;
|
|
685
941
|
paths: string[];
|
|
686
942
|
health: string;
|
|
@@ -708,7 +964,6 @@ describe("well-known regen after module ops", () => {
|
|
|
708
964
|
const install = fakeInstall("@openparachute/vault", {
|
|
709
965
|
name: "vault",
|
|
710
966
|
manifestName: "parachute-vault",
|
|
711
|
-
kind: "api",
|
|
712
967
|
port: 1940,
|
|
713
968
|
paths: ["/vault/default"],
|
|
714
969
|
health: "/vault/default/health",
|
|
@@ -812,7 +1067,6 @@ describe("well-known regen after module ops", () => {
|
|
|
812
1067
|
const install = fakeInstall("@openparachute/vault", {
|
|
813
1068
|
name: "vault",
|
|
814
1069
|
manifestName: "parachute-vault",
|
|
815
|
-
kind: "api",
|
|
816
1070
|
port: 1940,
|
|
817
1071
|
paths: ["/vault/default"],
|
|
818
1072
|
health: "/vault/default/health",
|
|
@@ -902,7 +1156,6 @@ describe("well-known regen after module ops", () => {
|
|
|
902
1156
|
const install = fakeInstall("@openparachute/vault", {
|
|
903
1157
|
name: "vault",
|
|
904
1158
|
manifestName: "parachute-vault",
|
|
905
|
-
kind: "api",
|
|
906
1159
|
port: 1940,
|
|
907
1160
|
paths: ["/vault/default"],
|
|
908
1161
|
health: "/vault/default/health",
|
|
@@ -315,6 +315,96 @@ describe("GET /api/modules", () => {
|
|
|
315
315
|
expect(body.supervisor_available).toBe(true);
|
|
316
316
|
});
|
|
317
317
|
|
|
318
|
+
test("populates management_url from a relative managementUrl + module mount (hub#342)", async () => {
|
|
319
|
+
// Vault declares `managementUrl: "/admin"` in its module.json — hub
|
|
320
|
+
// resolves that against the entry's mount path (`/vault/default`)
|
|
321
|
+
// to produce the absolute admin URL the SPA's "Open" button targets.
|
|
322
|
+
writeManifest(h.manifestPath, [
|
|
323
|
+
{
|
|
324
|
+
name: "parachute-vault",
|
|
325
|
+
port: 1940,
|
|
326
|
+
paths: ["/vault/default"],
|
|
327
|
+
health: "/vault/default/health",
|
|
328
|
+
version: "0.4.5",
|
|
329
|
+
installDir: "/install/dir/vault",
|
|
330
|
+
},
|
|
331
|
+
]);
|
|
332
|
+
const bearer = await mintBearer(h, [API_MODULES_REQUIRED_SCOPE]);
|
|
333
|
+
const res = await handleApiModules(getReq({ authorization: `Bearer ${bearer}` }), {
|
|
334
|
+
db: h.db,
|
|
335
|
+
issuer: ISSUER,
|
|
336
|
+
manifestPath: h.manifestPath,
|
|
337
|
+
fetchLatestVersion: async () => null,
|
|
338
|
+
readModuleManifest: async (installDir) => {
|
|
339
|
+
// Return a minimal module.json with managementUrl set. Cast the
|
|
340
|
+
// shape via `as unknown as ...` because the test only exercises
|
|
341
|
+
// the consumer-side resolver, not the validator (which lives in
|
|
342
|
+
// module-manifest.ts and has its own test suite).
|
|
343
|
+
if (installDir === "/install/dir/vault") {
|
|
344
|
+
return {
|
|
345
|
+
name: "parachute-vault",
|
|
346
|
+
manifestName: "parachute-vault",
|
|
347
|
+
displayName: "Vault",
|
|
348
|
+
tagline: "",
|
|
349
|
+
port: 1940,
|
|
350
|
+
paths: ["/vault/default"],
|
|
351
|
+
health: "/health",
|
|
352
|
+
managementUrl: "/admin",
|
|
353
|
+
} as unknown as Awaited<
|
|
354
|
+
ReturnType<typeof import("../module-manifest.ts").readModuleManifest>
|
|
355
|
+
>;
|
|
356
|
+
}
|
|
357
|
+
return null;
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
expect(res.status).toBe(200);
|
|
361
|
+
const body = (await res.json()) as {
|
|
362
|
+
modules: Array<{ short: string; management_url: string | null }>;
|
|
363
|
+
};
|
|
364
|
+
const vault = body.modules.find((m) => m.short === "vault");
|
|
365
|
+
expect(vault?.management_url).toBe("/vault/default/admin");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("management_url is null when the module declares neither managementUrl nor uiUrl (hub#342)", async () => {
|
|
369
|
+
// Scribe + runner today: no managementUrl declared yet. The SPA's
|
|
370
|
+
// "Open" button renders disabled with a follow-up tooltip in that
|
|
371
|
+
// case — null on the wire is the canonical signal.
|
|
372
|
+
writeManifest(h.manifestPath, [
|
|
373
|
+
{
|
|
374
|
+
name: "parachute-scribe",
|
|
375
|
+
port: 1942,
|
|
376
|
+
paths: ["/scribe"],
|
|
377
|
+
health: "/scribe/health",
|
|
378
|
+
version: "0.1.0",
|
|
379
|
+
installDir: "/install/dir/scribe",
|
|
380
|
+
},
|
|
381
|
+
]);
|
|
382
|
+
const bearer = await mintBearer(h, [API_MODULES_REQUIRED_SCOPE]);
|
|
383
|
+
const res = await handleApiModules(getReq({ authorization: `Bearer ${bearer}` }), {
|
|
384
|
+
db: h.db,
|
|
385
|
+
issuer: ISSUER,
|
|
386
|
+
manifestPath: h.manifestPath,
|
|
387
|
+
fetchLatestVersion: async () => null,
|
|
388
|
+
readModuleManifest: async () =>
|
|
389
|
+
({
|
|
390
|
+
name: "parachute-scribe",
|
|
391
|
+
manifestName: "parachute-scribe",
|
|
392
|
+
displayName: "Scribe",
|
|
393
|
+
tagline: "",
|
|
394
|
+
port: 1942,
|
|
395
|
+
paths: ["/scribe"],
|
|
396
|
+
health: "/health",
|
|
397
|
+
}) as unknown as Awaited<
|
|
398
|
+
ReturnType<typeof import("../module-manifest.ts").readModuleManifest>
|
|
399
|
+
>,
|
|
400
|
+
});
|
|
401
|
+
const body = (await res.json()) as {
|
|
402
|
+
modules: Array<{ short: string; management_url: string | null }>;
|
|
403
|
+
};
|
|
404
|
+
const scribe = body.modules.find((m) => m.short === "scribe");
|
|
405
|
+
expect(scribe?.management_url).toBeNull();
|
|
406
|
+
});
|
|
407
|
+
|
|
318
408
|
test("npm probe failure → latest_version is null but response still 200", async () => {
|
|
319
409
|
// The whole point of the probe-is-opportunistic posture: a flaky
|
|
320
410
|
// npm registry must not break the page render. The UI handles
|
|
@@ -64,6 +64,19 @@ describe("cli", () => {
|
|
|
64
64
|
expect(stderr).toMatch(/usage: parachute install/);
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
+
test("install --channel without a value exits 1 (hub#337)", async () => {
|
|
68
|
+
const { code, stderr } = await runCli(["install", "vault", "--channel"]);
|
|
69
|
+
expect(code).toBe(1);
|
|
70
|
+
expect(stderr).toMatch(/--channel requires a value/);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("install --channel with an invalid value exits 1 (hub#337)", async () => {
|
|
74
|
+
const { code, stderr } = await runCli(["install", "vault", "--channel", "banana"]);
|
|
75
|
+
expect(code).toBe(1);
|
|
76
|
+
expect(stderr).toMatch(/--channel must be "rc" or "latest"/);
|
|
77
|
+
expect(stderr).toMatch(/banana/);
|
|
78
|
+
});
|
|
79
|
+
|
|
67
80
|
test("unknown command exits 1", async () => {
|
|
68
81
|
const { code, stderr } = await runCli(["wat"]);
|
|
69
82
|
expect(code).toBe(1);
|
|
@@ -243,7 +243,6 @@ describe("hubFetch routing", () => {
|
|
|
243
243
|
readModuleManifest: async () => ({
|
|
244
244
|
name: "vault",
|
|
245
245
|
manifestName: "parachute-vault",
|
|
246
|
-
kind: "api",
|
|
247
246
|
port: 1940,
|
|
248
247
|
paths: ["/vault/default"],
|
|
249
248
|
health: "/health",
|
|
@@ -298,7 +297,6 @@ describe("hubFetch routing", () => {
|
|
|
298
297
|
readModuleManifest: async () => ({
|
|
299
298
|
name: "notes",
|
|
300
299
|
manifestName: "parachute-notes",
|
|
301
|
-
kind: "frontend",
|
|
302
300
|
port: 5173,
|
|
303
301
|
paths: ["/notes"],
|
|
304
302
|
health: "/health",
|
|
@@ -335,7 +333,6 @@ describe("hubFetch routing", () => {
|
|
|
335
333
|
readModuleManifest: async () => ({
|
|
336
334
|
name: "notes",
|
|
337
335
|
manifestName: "parachute-notes",
|
|
338
|
-
kind: "frontend",
|
|
339
336
|
port: 5173,
|
|
340
337
|
paths: ["/notes"],
|
|
341
338
|
health: "/health",
|
|
@@ -362,7 +359,6 @@ describe("hubFetch routing", () => {
|
|
|
362
359
|
readModuleManifest: async () => ({
|
|
363
360
|
name: "vault",
|
|
364
361
|
manifestName: "parachute-vault",
|
|
365
|
-
kind: "api",
|
|
366
362
|
port: 1940,
|
|
367
363
|
paths: ["/vault/default"],
|
|
368
364
|
health: "/health",
|
|
@@ -1940,19 +1936,20 @@ describe("hubFetch /<svc>/* generic proxy dispatch (#182)", () => {
|
|
|
1940
1936
|
}
|
|
1941
1937
|
});
|
|
1942
1938
|
|
|
1943
|
-
test("routes a deep /
|
|
1944
|
-
//
|
|
1939
|
+
test("routes a deep /someapp/api/health to the matching upstream", async () => {
|
|
1940
|
+
// A generic third-party module registers `/someapp`; deeper paths
|
|
1941
|
+
// route by prefix.
|
|
1945
1942
|
const h = makeHarness();
|
|
1946
|
-
const upstream = startUpstream("
|
|
1943
|
+
const upstream = startUpstream("someapp");
|
|
1947
1944
|
try {
|
|
1948
1945
|
writeManifest(
|
|
1949
1946
|
{
|
|
1950
1947
|
services: [
|
|
1951
1948
|
{
|
|
1952
|
-
name: "
|
|
1949
|
+
name: "someapp",
|
|
1953
1950
|
port: upstream.port,
|
|
1954
|
-
paths: ["/
|
|
1955
|
-
health: "/
|
|
1951
|
+
paths: ["/someapp"],
|
|
1952
|
+
health: "/someapp/api/health",
|
|
1956
1953
|
version: "0.1.0",
|
|
1957
1954
|
},
|
|
1958
1955
|
],
|
|
@@ -1960,11 +1957,11 @@ describe("hubFetch /<svc>/* generic proxy dispatch (#182)", () => {
|
|
|
1960
1957
|
h.manifestPath,
|
|
1961
1958
|
);
|
|
1962
1959
|
const fetcher = hubFetch(h.dir, { manifestPath: h.manifestPath });
|
|
1963
|
-
const res = await fetcher(req("/
|
|
1960
|
+
const res = await fetcher(req("/someapp/api/health?probe=1"));
|
|
1964
1961
|
expect(res.status).toBe(200);
|
|
1965
1962
|
const body = (await res.json()) as { tag: string; pathname: string; search: string };
|
|
1966
|
-
expect(body.tag).toBe("
|
|
1967
|
-
expect(body.pathname).toBe("/
|
|
1963
|
+
expect(body.tag).toBe("someapp");
|
|
1964
|
+
expect(body.pathname).toBe("/someapp/api/health");
|
|
1968
1965
|
expect(body.search).toBe("?probe=1");
|
|
1969
1966
|
} finally {
|
|
1970
1967
|
upstream.stop();
|