@openparachute/hub 0.5.10 → 0.5.12-rc.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openparachute/hub",
3
- "version": "0.5.10",
3
+ "version": "0.5.12-rc.2",
4
4
  "description": "parachute — the local hub for the Parachute ecosystem (discovery, ports, lifecycle, soon OAuth).",
5
5
  "license": "AGPL-3.0",
6
6
  "publishConfig": {
@@ -1,5 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import {
@@ -656,3 +656,285 @@ describe("GET /api/modules/operations/:id", () => {
656
656
  expect(res.status).toBe(404);
657
657
  });
658
658
  });
659
+
660
+ /**
661
+ * Well-known regen + installDir stamping coverage. These exercise the
662
+ * fix for the "newly installed module doesn't appear on discovery" bug:
663
+ * the live HTTP build at `/.well-known/parachute.json` reads each
664
+ * module's `installDir/.parachute/module.json` to find `uiUrl` (which
665
+ * the discovery page needs to render a tile). Without an installDir
666
+ * stamp post-install, the resolver skips the entry. We assert both
667
+ * the disk regen lands and the row carries installDir afterwards.
668
+ */
669
+ describe("well-known regen after module ops", () => {
670
+ let h: Harness;
671
+ beforeEach(async () => {
672
+ h = await makeHarness();
673
+ _resetOperationsRegistryForTests();
674
+ });
675
+ afterEach(() => h.cleanup());
676
+
677
+ /** Stub findGlobalInstall + readModuleManifest pair for in-memory installs. */
678
+ function fakeInstall(
679
+ pkg: string,
680
+ manifest: {
681
+ name: string;
682
+ manifestName: string;
683
+ kind: "api" | "frontend" | "tool";
684
+ port: number;
685
+ paths: string[];
686
+ health: string;
687
+ uiUrl?: string;
688
+ displayName?: string;
689
+ },
690
+ ): {
691
+ findGlobalInstall: (p: string) => string | null;
692
+ readModuleManifest: (dir: string) => Promise<typeof manifest | null>;
693
+ installDir: string;
694
+ } {
695
+ const installDir = join(h.dir, "fake-install", ...pkg.split("/"));
696
+ const pkgJson = join(installDir, "package.json");
697
+ return {
698
+ findGlobalInstall: (p) => (p === pkg ? pkgJson : null),
699
+ readModuleManifest: async (dir) => (dir === installDir ? manifest : null),
700
+ installDir,
701
+ };
702
+ }
703
+
704
+ test("runInstall happy path: regenerates well-known + stamps installDir", async () => {
705
+ const { supervisor } = makeIdleSupervisor();
706
+ const { run } = alwaysOkRun();
707
+ const wkPath = join(h.dir, "well-known.json");
708
+ const install = fakeInstall("@openparachute/vault", {
709
+ name: "vault",
710
+ manifestName: "parachute-vault",
711
+ kind: "api",
712
+ port: 1940,
713
+ paths: ["/vault/default"],
714
+ health: "/vault/default/health",
715
+ });
716
+ const bearer = await mintBearer(h, [API_MODULES_OPS_REQUIRED_SCOPE]);
717
+ const deps = {
718
+ db: h.db,
719
+ issuer: ISSUER,
720
+ manifestPath: h.manifestPath,
721
+ configDir: h.dir,
722
+ supervisor,
723
+ run,
724
+ findGlobalInstall: install.findGlobalInstall,
725
+ readModuleManifest: install.readModuleManifest,
726
+ wellKnownPath: wkPath,
727
+ };
728
+ const res = await handleInstall(
729
+ postReq("/api/modules/vault/install", { authorization: `Bearer ${bearer}` }),
730
+ "vault",
731
+ deps,
732
+ );
733
+ expect(res.status).toBe(202);
734
+ await new Promise((r) => setTimeout(r, 10));
735
+
736
+ // installDir landed on the row (so the live well-known build's
737
+ // `uiUrl` resolver can find the module's manifest).
738
+ const manifest = JSON.parse(readFileSync(h.manifestPath, "utf8")) as {
739
+ services: Array<{ name: string; installDir?: string }>;
740
+ };
741
+ const vaultRow = manifest.services.find((s) => s.name === "parachute-vault");
742
+ expect(vaultRow?.installDir).toBe(install.installDir);
743
+
744
+ // The on-disk well-known doc reflects the new module.
745
+ const doc = JSON.parse(readFileSync(wkPath, "utf8")) as {
746
+ services: Array<{ name: string; version: string }>;
747
+ vaults: Array<{ name: string }>;
748
+ };
749
+ expect(doc.services.some((s) => s.name === "parachute-vault")).toBe(true);
750
+ expect(doc.vaults.some((v) => v.name === "default")).toBe(true);
751
+ });
752
+
753
+ test("runInstall failure: bun add fails -> no well-known regen (no partial state)", async () => {
754
+ const { supervisor } = makeIdleSupervisor();
755
+ const wkPath = join(h.dir, "well-known.json");
756
+ const bearer = await mintBearer(h, [API_MODULES_OPS_REQUIRED_SCOPE]);
757
+ const deps = {
758
+ db: h.db,
759
+ issuer: ISSUER,
760
+ manifestPath: h.manifestPath,
761
+ configDir: h.dir,
762
+ supervisor,
763
+ run: async () => 1,
764
+ findGlobalInstall: () => null,
765
+ wellKnownPath: wkPath,
766
+ };
767
+ const res = await handleInstall(
768
+ postReq("/api/modules/vault/install", { authorization: `Bearer ${bearer}` }),
769
+ "vault",
770
+ deps,
771
+ );
772
+ expect(res.status).toBe(202);
773
+ const body = (await res.json()) as { operation_id: string };
774
+ await new Promise((r) => setTimeout(r, 10));
775
+
776
+ // Operation failed.
777
+ const opRes = await handleOperationGet(
778
+ getReq(`/api/modules/operations/${body.operation_id}`, {
779
+ authorization: `Bearer ${bearer}`,
780
+ }),
781
+ body.operation_id,
782
+ deps,
783
+ );
784
+ const op = (await opRes.json()) as { status: string; log: string[] };
785
+ expect(op.status).toBe("failed");
786
+
787
+ // No well-known doc was written — the regen step only runs after a
788
+ // successful spawn, not on failure paths.
789
+ expect(existsSync(wkPath)).toBe(false);
790
+ // And the operation log carries no regen line (defensive — confirms
791
+ // the early-return short-circuit, not just an absent file).
792
+ expect(op.log.join(" ")).not.toMatch(/regenerated/);
793
+ });
794
+
795
+ test("runUpgrade regenerates well-known with the new version on the row", async () => {
796
+ // Seed services.json with an existing vault row at the old version,
797
+ // and make the supervisor's restart return a state (success path).
798
+ writeManifest(h.manifestPath, [
799
+ {
800
+ name: "parachute-vault",
801
+ port: 1940,
802
+ paths: ["/vault/default"],
803
+ health: "/vault/default/health",
804
+ version: "0.4.5",
805
+ },
806
+ ]);
807
+ const { supervisor } = makeIdleSupervisor();
808
+ await supervisor.start({ short: "vault", cmd: ["parachute-vault", "serve"] });
809
+
810
+ const { run } = alwaysOkRun();
811
+ const wkPath = join(h.dir, "well-known.json");
812
+ const install = fakeInstall("@openparachute/vault", {
813
+ name: "vault",
814
+ manifestName: "parachute-vault",
815
+ kind: "api",
816
+ port: 1940,
817
+ paths: ["/vault/default"],
818
+ health: "/vault/default/health",
819
+ });
820
+ const bearer = await mintBearer(h, [API_MODULES_OPS_REQUIRED_SCOPE]);
821
+ const deps = {
822
+ db: h.db,
823
+ issuer: ISSUER,
824
+ manifestPath: h.manifestPath,
825
+ configDir: h.dir,
826
+ supervisor,
827
+ run,
828
+ findGlobalInstall: install.findGlobalInstall,
829
+ readModuleManifest: install.readModuleManifest,
830
+ wellKnownPath: wkPath,
831
+ };
832
+ const res = await handleUpgrade(
833
+ postReq("/api/modules/vault/upgrade", { authorization: `Bearer ${bearer}` }),
834
+ "vault",
835
+ deps,
836
+ );
837
+ expect(res.status).toBe(202);
838
+ await new Promise((r) => setTimeout(r, 10));
839
+
840
+ const doc = JSON.parse(readFileSync(wkPath, "utf8")) as {
841
+ services: Array<{ name: string; version: string }>;
842
+ };
843
+ const row = doc.services.find((s) => s.name === "parachute-vault");
844
+ expect(row?.version).toBe("0.4.5");
845
+ });
846
+
847
+ test("runUninstall regenerates well-known without the removed module", async () => {
848
+ writeManifest(h.manifestPath, [
849
+ {
850
+ name: "parachute-vault",
851
+ port: 1940,
852
+ paths: ["/vault/default"],
853
+ health: "/vault/default/health",
854
+ version: "0.4.5",
855
+ },
856
+ {
857
+ name: "parachute-notes",
858
+ port: 1942,
859
+ paths: ["/notes"],
860
+ health: "/notes/health",
861
+ version: "0.4.0",
862
+ },
863
+ ]);
864
+ const { supervisor } = makeIdleSupervisor();
865
+ const { run } = alwaysOkRun();
866
+ const wkPath = join(h.dir, "well-known.json");
867
+ const bearer = await mintBearer(h, [API_MODULES_OPS_REQUIRED_SCOPE]);
868
+ const res = await handleUninstall(
869
+ postReq("/api/modules/vault/uninstall", { authorization: `Bearer ${bearer}` }),
870
+ "vault",
871
+ {
872
+ db: h.db,
873
+ issuer: ISSUER,
874
+ manifestPath: h.manifestPath,
875
+ configDir: h.dir,
876
+ supervisor,
877
+ run,
878
+ wellKnownPath: wkPath,
879
+ },
880
+ );
881
+ expect(res.status).toBe(200);
882
+ const body = (await res.json()) as { log: string[] };
883
+ expect(body.log.join(" ")).toMatch(/regenerated/);
884
+
885
+ const doc = JSON.parse(readFileSync(wkPath, "utf8")) as {
886
+ services: Array<{ name: string }>;
887
+ vaults: Array<{ name: string }>;
888
+ };
889
+ // Vault gone, notes still present.
890
+ expect(doc.services.some((s) => s.name === "parachute-vault")).toBe(false);
891
+ expect(doc.vaults.some((v) => v.name === "default")).toBe(false);
892
+ expect(doc.services.some((s) => s.name === "parachute-notes")).toBe(true);
893
+ });
894
+
895
+ test("well-known regen is idempotent across two consecutive install ops", async () => {
896
+ // Two installs in a row of the same module produce the same on-disk
897
+ // doc — no drift from the regen path itself (e.g. extra entries,
898
+ // duplicated rows, non-deterministic ordering).
899
+ const { supervisor } = makeIdleSupervisor();
900
+ const { run } = alwaysOkRun();
901
+ const wkPath = join(h.dir, "well-known.json");
902
+ const install = fakeInstall("@openparachute/vault", {
903
+ name: "vault",
904
+ manifestName: "parachute-vault",
905
+ kind: "api",
906
+ port: 1940,
907
+ paths: ["/vault/default"],
908
+ health: "/vault/default/health",
909
+ });
910
+ const bearer = await mintBearer(h, [API_MODULES_OPS_REQUIRED_SCOPE]);
911
+ const deps = {
912
+ db: h.db,
913
+ issuer: ISSUER,
914
+ manifestPath: h.manifestPath,
915
+ configDir: h.dir,
916
+ supervisor,
917
+ run,
918
+ findGlobalInstall: install.findGlobalInstall,
919
+ readModuleManifest: install.readModuleManifest,
920
+ wellKnownPath: wkPath,
921
+ };
922
+ await handleInstall(
923
+ postReq("/api/modules/vault/install", { authorization: `Bearer ${bearer}` }),
924
+ "vault",
925
+ deps,
926
+ );
927
+ await new Promise((r) => setTimeout(r, 10));
928
+ const first = readFileSync(wkPath, "utf8");
929
+
930
+ await handleInstall(
931
+ postReq("/api/modules/vault/install", { authorization: `Bearer ${bearer}` }),
932
+ "vault",
933
+ deps,
934
+ );
935
+ await new Promise((r) => setTimeout(r, 10));
936
+ const second = readFileSync(wkPath, "utf8");
937
+
938
+ expect(second).toBe(first);
939
+ });
940
+ });