@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
|
@@ -2709,3 +2709,231 @@ describe("bootstrap token gate (handleSetupAccountPost)", () => {
|
|
|
2709
2709
|
}
|
|
2710
2710
|
});
|
|
2711
2711
|
});
|
|
2712
|
+
|
|
2713
|
+
// --- hub#342 UI pass: "Start using your vault" lead tile + Use it now ---
|
|
2714
|
+
|
|
2715
|
+
describe("done screen — 'Start using your vault' tile (hub#342)", () => {
|
|
2716
|
+
let h: Harness;
|
|
2717
|
+
beforeEach(() => {
|
|
2718
|
+
h = makeHarness();
|
|
2719
|
+
_resetOperationsRegistryForTests();
|
|
2720
|
+
});
|
|
2721
|
+
afterEach(() => h.cleanup());
|
|
2722
|
+
|
|
2723
|
+
test("when only vault is installed, the lead tile links to vault admin", async () => {
|
|
2724
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
2725
|
+
try {
|
|
2726
|
+
const user = await createUser(db, "owner", "pw");
|
|
2727
|
+
writeManifest(
|
|
2728
|
+
{
|
|
2729
|
+
services: [
|
|
2730
|
+
{
|
|
2731
|
+
name: "parachute-vault",
|
|
2732
|
+
version: "0.1.0",
|
|
2733
|
+
port: 1940,
|
|
2734
|
+
paths: ["/vault/default"],
|
|
2735
|
+
health: "/health",
|
|
2736
|
+
},
|
|
2737
|
+
],
|
|
2738
|
+
},
|
|
2739
|
+
h.manifestPath,
|
|
2740
|
+
);
|
|
2741
|
+
setSetting(db, "setup_expose_mode", "localhost");
|
|
2742
|
+
const { createSession } = await import("../sessions.ts");
|
|
2743
|
+
const session = createSession(db, { userId: user.id });
|
|
2744
|
+
const res = handleSetupGet(
|
|
2745
|
+
req("/admin/setup?just_finished=1", {
|
|
2746
|
+
headers: { cookie: `${SESSION_COOKIE_NAME}=${session.id}` },
|
|
2747
|
+
}),
|
|
2748
|
+
{
|
|
2749
|
+
db,
|
|
2750
|
+
manifestPath: h.manifestPath,
|
|
2751
|
+
configDir: h.dir,
|
|
2752
|
+
issuer: "https://hub.example",
|
|
2753
|
+
registry: getDefaultOperationsRegistry(),
|
|
2754
|
+
},
|
|
2755
|
+
);
|
|
2756
|
+
const html = await res.text();
|
|
2757
|
+
// Section heading present + primary CTA points at the vault's own admin.
|
|
2758
|
+
expect(html).toContain("Start using your vault");
|
|
2759
|
+
expect(html).toContain('href="/vault/default/admin/"');
|
|
2760
|
+
// Lead tile precedes the MCP / install tiles (it's the lead).
|
|
2761
|
+
const startIdx = html.indexOf("Start using your vault");
|
|
2762
|
+
const installIdx = html.indexOf("What's next?");
|
|
2763
|
+
expect(startIdx).toBeLessThan(installIdx);
|
|
2764
|
+
} finally {
|
|
2765
|
+
db.close();
|
|
2766
|
+
}
|
|
2767
|
+
});
|
|
2768
|
+
|
|
2769
|
+
test("when app is also installed, the lead tile links to /app/notes/", async () => {
|
|
2770
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
2771
|
+
try {
|
|
2772
|
+
const user = await createUser(db, "owner", "pw");
|
|
2773
|
+
writeManifest(
|
|
2774
|
+
{
|
|
2775
|
+
services: [
|
|
2776
|
+
{
|
|
2777
|
+
name: "parachute-vault",
|
|
2778
|
+
version: "0.1.0",
|
|
2779
|
+
port: 1940,
|
|
2780
|
+
paths: ["/vault/default"],
|
|
2781
|
+
health: "/health",
|
|
2782
|
+
},
|
|
2783
|
+
{
|
|
2784
|
+
name: "parachute-app",
|
|
2785
|
+
version: "0.2.0",
|
|
2786
|
+
port: 1946,
|
|
2787
|
+
paths: ["/app"],
|
|
2788
|
+
health: "/app/healthz",
|
|
2789
|
+
},
|
|
2790
|
+
],
|
|
2791
|
+
},
|
|
2792
|
+
h.manifestPath,
|
|
2793
|
+
);
|
|
2794
|
+
setSetting(db, "setup_expose_mode", "localhost");
|
|
2795
|
+
const { createSession } = await import("../sessions.ts");
|
|
2796
|
+
const session = createSession(db, { userId: user.id });
|
|
2797
|
+
const res = handleSetupGet(
|
|
2798
|
+
req("/admin/setup?just_finished=1", {
|
|
2799
|
+
headers: { cookie: `${SESSION_COOKIE_NAME}=${session.id}` },
|
|
2800
|
+
}),
|
|
2801
|
+
{
|
|
2802
|
+
db,
|
|
2803
|
+
manifestPath: h.manifestPath,
|
|
2804
|
+
configDir: h.dir,
|
|
2805
|
+
issuer: "https://hub.example",
|
|
2806
|
+
registry: getDefaultOperationsRegistry(),
|
|
2807
|
+
},
|
|
2808
|
+
);
|
|
2809
|
+
const html = await res.text();
|
|
2810
|
+
expect(html).toContain("Start using your vault");
|
|
2811
|
+
// App installed → primary CTA links to Notes-as-UI inside App.
|
|
2812
|
+
expect(html).toContain('href="/app/notes/"');
|
|
2813
|
+
expect(html).toContain("Open Notes");
|
|
2814
|
+
} finally {
|
|
2815
|
+
db.close();
|
|
2816
|
+
}
|
|
2817
|
+
});
|
|
2818
|
+
|
|
2819
|
+
test("succeeded install op renders a 'Use it now' link pointing at the module's surface", async () => {
|
|
2820
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
2821
|
+
try {
|
|
2822
|
+
const user = await createUser(db, "owner", "pw");
|
|
2823
|
+
writeManifest(
|
|
2824
|
+
{
|
|
2825
|
+
services: [
|
|
2826
|
+
{
|
|
2827
|
+
name: "parachute-vault",
|
|
2828
|
+
version: "0.1.0",
|
|
2829
|
+
port: 1940,
|
|
2830
|
+
paths: ["/vault/default"],
|
|
2831
|
+
health: "/health",
|
|
2832
|
+
},
|
|
2833
|
+
],
|
|
2834
|
+
},
|
|
2835
|
+
h.manifestPath,
|
|
2836
|
+
);
|
|
2837
|
+
setSetting(db, "setup_expose_mode", "localhost");
|
|
2838
|
+
const reg = getDefaultOperationsRegistry();
|
|
2839
|
+
const op = reg.create("install", "app");
|
|
2840
|
+
reg.update(op.id, { status: "succeeded" }, "installed @openparachute/app");
|
|
2841
|
+
const { createSession } = await import("../sessions.ts");
|
|
2842
|
+
const session = createSession(db, { userId: user.id });
|
|
2843
|
+
const res = handleSetupGet(
|
|
2844
|
+
req(`/admin/setup?just_finished=1&op_app=${op.id}`, {
|
|
2845
|
+
headers: { cookie: `${SESSION_COOKIE_NAME}=${session.id}` },
|
|
2846
|
+
}),
|
|
2847
|
+
{
|
|
2848
|
+
db,
|
|
2849
|
+
manifestPath: h.manifestPath,
|
|
2850
|
+
configDir: h.dir,
|
|
2851
|
+
issuer: "https://hub.example",
|
|
2852
|
+
registry: reg,
|
|
2853
|
+
},
|
|
2854
|
+
);
|
|
2855
|
+
const html = await res.text();
|
|
2856
|
+
expect(html).toContain("status: succeeded");
|
|
2857
|
+
// Primary "Use it now" link goes to the app's surface; secondary
|
|
2858
|
+
// "Manage modules" link still present.
|
|
2859
|
+
expect(html).toContain(">Use it now<");
|
|
2860
|
+
expect(html).toContain('href="/app/notes/"');
|
|
2861
|
+
expect(html).toContain(">Manage modules<");
|
|
2862
|
+
} finally {
|
|
2863
|
+
db.close();
|
|
2864
|
+
}
|
|
2865
|
+
});
|
|
2866
|
+
|
|
2867
|
+
test("'Already installed' tile gains a 'Use it now' link too", async () => {
|
|
2868
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
2869
|
+
try {
|
|
2870
|
+
const user = await createUser(db, "owner", "pw");
|
|
2871
|
+
writeManifest(
|
|
2872
|
+
{
|
|
2873
|
+
services: [
|
|
2874
|
+
{
|
|
2875
|
+
name: "parachute-vault",
|
|
2876
|
+
version: "0.1.0",
|
|
2877
|
+
port: 1940,
|
|
2878
|
+
paths: ["/vault/default"],
|
|
2879
|
+
health: "/health",
|
|
2880
|
+
},
|
|
2881
|
+
{
|
|
2882
|
+
name: "parachute-app",
|
|
2883
|
+
version: "0.2.0",
|
|
2884
|
+
port: 1946,
|
|
2885
|
+
paths: ["/app"],
|
|
2886
|
+
health: "/app/healthz",
|
|
2887
|
+
},
|
|
2888
|
+
],
|
|
2889
|
+
},
|
|
2890
|
+
h.manifestPath,
|
|
2891
|
+
);
|
|
2892
|
+
setSetting(db, "setup_expose_mode", "localhost");
|
|
2893
|
+
const { createSession } = await import("../sessions.ts");
|
|
2894
|
+
const session = createSession(db, { userId: user.id });
|
|
2895
|
+
const res = handleSetupGet(
|
|
2896
|
+
req("/admin/setup?just_finished=1", {
|
|
2897
|
+
headers: { cookie: `${SESSION_COOKIE_NAME}=${session.id}` },
|
|
2898
|
+
}),
|
|
2899
|
+
{
|
|
2900
|
+
db,
|
|
2901
|
+
manifestPath: h.manifestPath,
|
|
2902
|
+
configDir: h.dir,
|
|
2903
|
+
issuer: "https://hub.example",
|
|
2904
|
+
registry: getDefaultOperationsRegistry(),
|
|
2905
|
+
},
|
|
2906
|
+
);
|
|
2907
|
+
const html = await res.text();
|
|
2908
|
+
expect(html).toContain("Already installed");
|
|
2909
|
+
// App's already-installed tile carries the Use it now link.
|
|
2910
|
+
expect(html).toContain('href="/app/notes/"');
|
|
2911
|
+
} finally {
|
|
2912
|
+
db.close();
|
|
2913
|
+
}
|
|
2914
|
+
});
|
|
2915
|
+
|
|
2916
|
+
test("install-log CSS includes overflow-wrap so long lines wrap in the card", async () => {
|
|
2917
|
+
// Smoke test for the CSS fold (hub#342): the .op-log block sets
|
|
2918
|
+
// overflow-x:auto and the .log-lines li set white-space:pre-wrap +
|
|
2919
|
+
// overflow-wrap:anywhere. These are the three properties Aaron's
|
|
2920
|
+
// bug report flagged — without them long install logs blow up the
|
|
2921
|
+
// wizard layout.
|
|
2922
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
2923
|
+
try {
|
|
2924
|
+
const res = handleSetupGet(req("/admin/setup"), {
|
|
2925
|
+
db,
|
|
2926
|
+
manifestPath: h.manifestPath,
|
|
2927
|
+
configDir: h.dir,
|
|
2928
|
+
issuer: "https://hub.example",
|
|
2929
|
+
registry: getDefaultOperationsRegistry(),
|
|
2930
|
+
});
|
|
2931
|
+
const html = await res.text();
|
|
2932
|
+
expect(html).toContain("overflow-x: auto");
|
|
2933
|
+
expect(html).toContain("white-space: pre-wrap");
|
|
2934
|
+
expect(html).toContain("overflow-wrap: anywhere");
|
|
2935
|
+
} finally {
|
|
2936
|
+
db.close();
|
|
2937
|
+
}
|
|
2938
|
+
});
|
|
2939
|
+
});
|
|
@@ -686,12 +686,12 @@ describe("status", () => {
|
|
|
686
686
|
try {
|
|
687
687
|
upsertService(
|
|
688
688
|
{
|
|
689
|
-
name: "
|
|
689
|
+
name: "someapp",
|
|
690
690
|
port: 1946,
|
|
691
|
-
paths: ["/
|
|
692
|
-
health: "/
|
|
691
|
+
paths: ["/someapp"],
|
|
692
|
+
health: "/someapp/health",
|
|
693
693
|
version: "0.1.4-rc.1",
|
|
694
|
-
// No installDir;
|
|
694
|
+
// No installDir; someapp isn't in FIRST_PARTY_FALLBACKS by short name,
|
|
695
695
|
// and the fallback bun-global lookup needs a known package name.
|
|
696
696
|
},
|
|
697
697
|
path,
|
|
@@ -3,7 +3,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import type { UpgradeRunner } from "../commands/upgrade.ts";
|
|
6
|
-
import { upgrade } from "../commands/upgrade.ts";
|
|
6
|
+
import { compareVersions, detectChannel, upgrade } from "../commands/upgrade.ts";
|
|
7
7
|
import { upsertService } from "../services-manifest.ts";
|
|
8
8
|
|
|
9
9
|
interface RunCall {
|
|
@@ -288,7 +288,13 @@ describe("parachute upgrade", () => {
|
|
|
288
288
|
}
|
|
289
289
|
});
|
|
290
290
|
|
|
291
|
-
|
|
291
|
+
// hub#301 Phase C/D (#330): `kind` retired and the bun-linked
|
|
292
|
+
// `kind === "frontend"` build branch retires with it. Notes-daemon's
|
|
293
|
+
// `prepublishOnly` builds dist at publish time so consumers don't need a
|
|
294
|
+
// post-install rebuild; this test pins the new behavior — even a
|
|
295
|
+
// historical frontend module (with a `build` script in package.json)
|
|
296
|
+
// does NOT trigger `bun run build` during an upgrade.
|
|
297
|
+
test("bun-linked: no bun run build invoked (kind branch retired in #330)", async () => {
|
|
292
298
|
const h = makeHarness();
|
|
293
299
|
try {
|
|
294
300
|
const installDir = join(h.installRoot, "notes");
|
|
@@ -341,7 +347,7 @@ describe("parachute upgrade", () => {
|
|
|
341
347
|
log: () => {},
|
|
342
348
|
});
|
|
343
349
|
expect(code).toBe(0);
|
|
344
|
-
expect(ranBuild.value).toBe(
|
|
350
|
+
expect(ranBuild.value).toBe(false);
|
|
345
351
|
} finally {
|
|
346
352
|
h.cleanup();
|
|
347
353
|
}
|
|
@@ -781,4 +787,357 @@ describe("parachute upgrade", () => {
|
|
|
781
787
|
h.cleanup();
|
|
782
788
|
}
|
|
783
789
|
});
|
|
790
|
+
|
|
791
|
+
// -------------------------------------------------------------------------
|
|
792
|
+
// hub#332 — channel preservation + downgrade refusal
|
|
793
|
+
// -------------------------------------------------------------------------
|
|
794
|
+
|
|
795
|
+
test("detectChannel: rc suffixes → 'rc', everything else → 'latest'", () => {
|
|
796
|
+
expect(detectChannel("0.5.13-rc.13")).toBe("rc");
|
|
797
|
+
expect(detectChannel("0.5.13-rc.1")).toBe("rc");
|
|
798
|
+
expect(detectChannel("0.5.13-rc")).toBe("rc"); // no .N suffix
|
|
799
|
+
expect(detectChannel("0.5.10")).toBe("latest");
|
|
800
|
+
expect(detectChannel("1.0.0")).toBe("latest");
|
|
801
|
+
expect(detectChannel("0.5.13-beta.1")).toBe("latest"); // not rc
|
|
802
|
+
expect(detectChannel("0.5.13-alpha")).toBe("latest");
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
test("compareVersions: stable > matching rc; later rc > earlier rc", () => {
|
|
806
|
+
// Stable beats prerelease at equal triple (semver §11.4.3)
|
|
807
|
+
expect(compareVersions("0.5.13", "0.5.13-rc.13")).toBeGreaterThan(0);
|
|
808
|
+
expect(compareVersions("0.5.13-rc.13", "0.5.13")).toBeLessThan(0);
|
|
809
|
+
// Aaron's reproducer: 0.5.10 < 0.5.13-rc.13 (lower triple beats prerelease tail)
|
|
810
|
+
expect(compareVersions("0.5.10", "0.5.13-rc.13")).toBeLessThan(0);
|
|
811
|
+
expect(compareVersions("0.5.13-rc.14", "0.5.13-rc.13")).toBeGreaterThan(0);
|
|
812
|
+
expect(compareVersions("0.5.13-rc.13", "0.5.13-rc.13")).toBe(0);
|
|
813
|
+
// Patch difference dominates prerelease tail
|
|
814
|
+
expect(compareVersions("0.5.14", "0.5.13-rc.99")).toBeGreaterThan(0);
|
|
815
|
+
// Garbage in → null
|
|
816
|
+
expect(compareVersions("not-a-version", "0.5.10")).toBeNull();
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
test("auto-detects rc channel: installed 0.5.13-rc.13 → bun add -g @openparachute/hub@rc", async () => {
|
|
820
|
+
const h = makeHarness();
|
|
821
|
+
try {
|
|
822
|
+
const hubInstallDir = join(h.installRoot, "hub");
|
|
823
|
+
writePackageJson(hubInstallDir, { name: "@openparachute/hub", version: "0.5.13-rc.13" });
|
|
824
|
+
|
|
825
|
+
const seenCmd: string[][] = [];
|
|
826
|
+
const runner: UpgradeRunner = {
|
|
827
|
+
async run(cmd) {
|
|
828
|
+
seenCmd.push([...cmd]);
|
|
829
|
+
if (cmd[0] === "bun" && cmd[1] === "add" && cmd[2] === "-g") {
|
|
830
|
+
writePackageJson(hubInstallDir, {
|
|
831
|
+
name: "@openparachute/hub",
|
|
832
|
+
version: "0.5.13-rc.14",
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
return 0;
|
|
836
|
+
},
|
|
837
|
+
async capture(cmd) {
|
|
838
|
+
if (cmd[1] === "rev-parse" && cmd[2] === "--is-inside-work-tree") {
|
|
839
|
+
return { code: 128, stdout: "" };
|
|
840
|
+
}
|
|
841
|
+
return { code: 0, stdout: "" };
|
|
842
|
+
},
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
const code = await upgrade("hub", {
|
|
846
|
+
manifestPath: h.manifestPath,
|
|
847
|
+
configDir: h.configDir,
|
|
848
|
+
runner,
|
|
849
|
+
findGlobalInstall: (pkg) =>
|
|
850
|
+
pkg === "@openparachute/hub" ? join(hubInstallDir, "package.json") : null,
|
|
851
|
+
restartFn: async () => 0,
|
|
852
|
+
// No resolveChannelVersion → npm view stub returns empty → guard skipped
|
|
853
|
+
log: () => {},
|
|
854
|
+
});
|
|
855
|
+
expect(code).toBe(0);
|
|
856
|
+
const addCall = seenCmd.find((c) => c[0] === "bun" && c[1] === "add");
|
|
857
|
+
expect(addCall).toEqual(["bun", "add", "-g", "@openparachute/hub@rc"]);
|
|
858
|
+
} finally {
|
|
859
|
+
h.cleanup();
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
test("auto-detects stable channel: installed 0.5.10 → bun add -g @openparachute/hub@latest", async () => {
|
|
864
|
+
const h = makeHarness();
|
|
865
|
+
try {
|
|
866
|
+
const hubInstallDir = join(h.installRoot, "hub");
|
|
867
|
+
writePackageJson(hubInstallDir, { name: "@openparachute/hub", version: "0.5.10" });
|
|
868
|
+
|
|
869
|
+
const seenCmd: string[][] = [];
|
|
870
|
+
const runner: UpgradeRunner = {
|
|
871
|
+
async run(cmd) {
|
|
872
|
+
seenCmd.push([...cmd]);
|
|
873
|
+
if (cmd[0] === "bun" && cmd[1] === "add" && cmd[2] === "-g") {
|
|
874
|
+
writePackageJson(hubInstallDir, { name: "@openparachute/hub", version: "0.5.11" });
|
|
875
|
+
}
|
|
876
|
+
return 0;
|
|
877
|
+
},
|
|
878
|
+
async capture(cmd) {
|
|
879
|
+
if (cmd[1] === "rev-parse" && cmd[2] === "--is-inside-work-tree") {
|
|
880
|
+
return { code: 128, stdout: "" };
|
|
881
|
+
}
|
|
882
|
+
return { code: 0, stdout: "" };
|
|
883
|
+
},
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
await upgrade("hub", {
|
|
887
|
+
manifestPath: h.manifestPath,
|
|
888
|
+
configDir: h.configDir,
|
|
889
|
+
runner,
|
|
890
|
+
findGlobalInstall: (pkg) =>
|
|
891
|
+
pkg === "@openparachute/hub" ? join(hubInstallDir, "package.json") : null,
|
|
892
|
+
restartFn: async () => 0,
|
|
893
|
+
log: () => {},
|
|
894
|
+
});
|
|
895
|
+
const addCall = seenCmd.find((c) => c[0] === "bun" && c[1] === "add");
|
|
896
|
+
expect(addCall).toEqual(["bun", "add", "-g", "@openparachute/hub@latest"]);
|
|
897
|
+
} finally {
|
|
898
|
+
h.cleanup();
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
test("--channel rc overrides stable detection", async () => {
|
|
903
|
+
const h = makeHarness();
|
|
904
|
+
try {
|
|
905
|
+
const hubInstallDir = join(h.installRoot, "hub");
|
|
906
|
+
writePackageJson(hubInstallDir, { name: "@openparachute/hub", version: "0.5.10" });
|
|
907
|
+
|
|
908
|
+
const seenCmd: string[][] = [];
|
|
909
|
+
const runner: UpgradeRunner = {
|
|
910
|
+
async run(cmd) {
|
|
911
|
+
seenCmd.push([...cmd]);
|
|
912
|
+
if (cmd[0] === "bun" && cmd[1] === "add" && cmd[2] === "-g") {
|
|
913
|
+
writePackageJson(hubInstallDir, {
|
|
914
|
+
name: "@openparachute/hub",
|
|
915
|
+
version: "0.5.13-rc.14",
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
return 0;
|
|
919
|
+
},
|
|
920
|
+
async capture(cmd) {
|
|
921
|
+
if (cmd[1] === "rev-parse" && cmd[2] === "--is-inside-work-tree") {
|
|
922
|
+
return { code: 128, stdout: "" };
|
|
923
|
+
}
|
|
924
|
+
return { code: 0, stdout: "" };
|
|
925
|
+
},
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
await upgrade("hub", {
|
|
929
|
+
manifestPath: h.manifestPath,
|
|
930
|
+
configDir: h.configDir,
|
|
931
|
+
runner,
|
|
932
|
+
findGlobalInstall: (pkg) =>
|
|
933
|
+
pkg === "@openparachute/hub" ? join(hubInstallDir, "package.json") : null,
|
|
934
|
+
restartFn: async () => 0,
|
|
935
|
+
channel: "rc",
|
|
936
|
+
log: () => {},
|
|
937
|
+
});
|
|
938
|
+
const addCall = seenCmd.find((c) => c[0] === "bun" && c[1] === "add");
|
|
939
|
+
expect(addCall).toEqual(["bun", "add", "-g", "@openparachute/hub@rc"]);
|
|
940
|
+
} finally {
|
|
941
|
+
h.cleanup();
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
test("refuses downgrade: installed 0.5.13-rc.13, @rc resolves to 0.5.10 → abort", async () => {
|
|
946
|
+
// Aaron's exact reproducer modulo the channel fix — once channel detection
|
|
947
|
+
// lands, @rc is the right tag; if @rc itself somehow resolves backward we
|
|
948
|
+
// still refuse.
|
|
949
|
+
const h = makeHarness();
|
|
950
|
+
try {
|
|
951
|
+
const hubInstallDir = join(h.installRoot, "hub");
|
|
952
|
+
writePackageJson(hubInstallDir, { name: "@openparachute/hub", version: "0.5.13-rc.13" });
|
|
953
|
+
|
|
954
|
+
const seenCmd: string[][] = [];
|
|
955
|
+
const runner: UpgradeRunner = {
|
|
956
|
+
async run(cmd) {
|
|
957
|
+
seenCmd.push([...cmd]);
|
|
958
|
+
return 0;
|
|
959
|
+
},
|
|
960
|
+
async capture(cmd) {
|
|
961
|
+
if (cmd[1] === "rev-parse" && cmd[2] === "--is-inside-work-tree") {
|
|
962
|
+
return { code: 128, stdout: "" };
|
|
963
|
+
}
|
|
964
|
+
return { code: 0, stdout: "" };
|
|
965
|
+
},
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
const logs: string[] = [];
|
|
969
|
+
let restartCalled = false;
|
|
970
|
+
const code = await upgrade("hub", {
|
|
971
|
+
manifestPath: h.manifestPath,
|
|
972
|
+
configDir: h.configDir,
|
|
973
|
+
runner,
|
|
974
|
+
findGlobalInstall: (pkg) =>
|
|
975
|
+
pkg === "@openparachute/hub" ? join(hubInstallDir, "package.json") : null,
|
|
976
|
+
restartFn: async () => {
|
|
977
|
+
restartCalled = true;
|
|
978
|
+
return 0;
|
|
979
|
+
},
|
|
980
|
+
resolveChannelVersion: async (_pkg, _channel) => "0.5.10",
|
|
981
|
+
log: (l) => logs.push(l),
|
|
982
|
+
});
|
|
983
|
+
expect(code).toBe(1);
|
|
984
|
+
expect(restartCalled).toBe(false);
|
|
985
|
+
// bun add was never run
|
|
986
|
+
const addCall = seenCmd.find((c) => c[0] === "bun" && c[1] === "add");
|
|
987
|
+
expect(addCall).toBeUndefined();
|
|
988
|
+
const joined = logs.join("\n");
|
|
989
|
+
expect(joined).toMatch(/refusing to downgrade/);
|
|
990
|
+
expect(joined).toMatch(/installed 0\.5\.13-rc\.13/);
|
|
991
|
+
expect(joined).toMatch(/0\.5\.10/);
|
|
992
|
+
expect(joined).toMatch(/--allow-downgrade/);
|
|
993
|
+
} finally {
|
|
994
|
+
h.cleanup();
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
test("--allow-downgrade bypasses the refusal", async () => {
|
|
999
|
+
const h = makeHarness();
|
|
1000
|
+
try {
|
|
1001
|
+
const hubInstallDir = join(h.installRoot, "hub");
|
|
1002
|
+
writePackageJson(hubInstallDir, { name: "@openparachute/hub", version: "0.5.13-rc.13" });
|
|
1003
|
+
|
|
1004
|
+
const seenCmd: string[][] = [];
|
|
1005
|
+
const runner: UpgradeRunner = {
|
|
1006
|
+
async run(cmd) {
|
|
1007
|
+
seenCmd.push([...cmd]);
|
|
1008
|
+
if (cmd[0] === "bun" && cmd[1] === "add" && cmd[2] === "-g") {
|
|
1009
|
+
writePackageJson(hubInstallDir, { name: "@openparachute/hub", version: "0.5.10" });
|
|
1010
|
+
}
|
|
1011
|
+
return 0;
|
|
1012
|
+
},
|
|
1013
|
+
async capture(cmd) {
|
|
1014
|
+
if (cmd[1] === "rev-parse" && cmd[2] === "--is-inside-work-tree") {
|
|
1015
|
+
return { code: 128, stdout: "" };
|
|
1016
|
+
}
|
|
1017
|
+
return { code: 0, stdout: "" };
|
|
1018
|
+
},
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
const code = await upgrade("hub", {
|
|
1022
|
+
manifestPath: h.manifestPath,
|
|
1023
|
+
configDir: h.configDir,
|
|
1024
|
+
runner,
|
|
1025
|
+
findGlobalInstall: (pkg) =>
|
|
1026
|
+
pkg === "@openparachute/hub" ? join(hubInstallDir, "package.json") : null,
|
|
1027
|
+
restartFn: async () => 0,
|
|
1028
|
+
resolveChannelVersion: async () => "0.5.10",
|
|
1029
|
+
allowDowngrade: true,
|
|
1030
|
+
log: () => {},
|
|
1031
|
+
});
|
|
1032
|
+
expect(code).toBe(0);
|
|
1033
|
+
const addCall = seenCmd.find((c) => c[0] === "bun" && c[1] === "add");
|
|
1034
|
+
// Channel was auto-detected as `rc` from the rc-suffixed installed version
|
|
1035
|
+
expect(addCall).toEqual(["bun", "add", "-g", "@openparachute/hub@rc"]);
|
|
1036
|
+
} finally {
|
|
1037
|
+
h.cleanup();
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
test("Aaron's reproducer: installed 0.5.13-rc.13 → upgrades via @rc, version goes UP not down", async () => {
|
|
1042
|
+
// This is the exact bug from hub#332. Before the fix, `parachute upgrade
|
|
1043
|
+
// hub` ran `bun add -g @openparachute/hub@latest` which (because @latest
|
|
1044
|
+
// pointed at 0.5.10) silently downgraded an rc.13 install. With auto-
|
|
1045
|
+
// channel detection: rc.13 → @rc → 0.5.13-rc.14, version increases.
|
|
1046
|
+
const h = makeHarness();
|
|
1047
|
+
try {
|
|
1048
|
+
const hubInstallDir = join(h.installRoot, "hub");
|
|
1049
|
+
writePackageJson(hubInstallDir, { name: "@openparachute/hub", version: "0.5.13-rc.13" });
|
|
1050
|
+
|
|
1051
|
+
const seenCmd: string[][] = [];
|
|
1052
|
+
const runner: UpgradeRunner = {
|
|
1053
|
+
async run(cmd) {
|
|
1054
|
+
seenCmd.push([...cmd]);
|
|
1055
|
+
if (
|
|
1056
|
+
cmd[0] === "bun" &&
|
|
1057
|
+
cmd[1] === "add" &&
|
|
1058
|
+
cmd[2] === "-g" &&
|
|
1059
|
+
cmd[3] === "@openparachute/hub@rc"
|
|
1060
|
+
) {
|
|
1061
|
+
writePackageJson(hubInstallDir, {
|
|
1062
|
+
name: "@openparachute/hub",
|
|
1063
|
+
version: "0.5.13-rc.14",
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
return 0;
|
|
1067
|
+
},
|
|
1068
|
+
async capture(cmd) {
|
|
1069
|
+
if (cmd[1] === "rev-parse" && cmd[2] === "--is-inside-work-tree") {
|
|
1070
|
+
return { code: 128, stdout: "" };
|
|
1071
|
+
}
|
|
1072
|
+
return { code: 0, stdout: "" };
|
|
1073
|
+
},
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
const logs: string[] = [];
|
|
1077
|
+
let restartedShort: string | undefined;
|
|
1078
|
+
const code = await upgrade("hub", {
|
|
1079
|
+
manifestPath: h.manifestPath,
|
|
1080
|
+
configDir: h.configDir,
|
|
1081
|
+
runner,
|
|
1082
|
+
findGlobalInstall: (pkg) =>
|
|
1083
|
+
pkg === "@openparachute/hub" ? join(hubInstallDir, "package.json") : null,
|
|
1084
|
+
restartFn: async (svc) => {
|
|
1085
|
+
restartedShort = svc;
|
|
1086
|
+
return 0;
|
|
1087
|
+
},
|
|
1088
|
+
// @rc resolves to a HIGHER version than installed — no downgrade.
|
|
1089
|
+
resolveChannelVersion: async (_pkg, channel) =>
|
|
1090
|
+
channel === "rc" ? "0.5.13-rc.14" : "0.5.10",
|
|
1091
|
+
log: (l) => logs.push(l),
|
|
1092
|
+
});
|
|
1093
|
+
expect(code).toBe(0);
|
|
1094
|
+
expect(restartedShort).toBe("hub");
|
|
1095
|
+
const addCall = seenCmd.find((c) => c[0] === "bun" && c[1] === "add");
|
|
1096
|
+
expect(addCall).toEqual(["bun", "add", "-g", "@openparachute/hub@rc"]);
|
|
1097
|
+
const joined = logs.join("\n");
|
|
1098
|
+
// Version went UP (rc.13 → rc.14), not DOWN as in the original report
|
|
1099
|
+
expect(joined).toMatch(/0\.5\.13-rc\.13 → 0\.5\.13-rc\.14/);
|
|
1100
|
+
} finally {
|
|
1101
|
+
h.cleanup();
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
test("--tag still overrides everything (back-compat for programmatic pin)", async () => {
|
|
1106
|
+
const h = makeHarness();
|
|
1107
|
+
try {
|
|
1108
|
+
const hubInstallDir = join(h.installRoot, "hub");
|
|
1109
|
+
writePackageJson(hubInstallDir, { name: "@openparachute/hub", version: "0.5.13-rc.13" });
|
|
1110
|
+
|
|
1111
|
+
const seenCmd: string[][] = [];
|
|
1112
|
+
const runner: UpgradeRunner = {
|
|
1113
|
+
async run(cmd) {
|
|
1114
|
+
seenCmd.push([...cmd]);
|
|
1115
|
+
return 0;
|
|
1116
|
+
},
|
|
1117
|
+
async capture(cmd) {
|
|
1118
|
+
if (cmd[1] === "rev-parse" && cmd[2] === "--is-inside-work-tree") {
|
|
1119
|
+
return { code: 128, stdout: "" };
|
|
1120
|
+
}
|
|
1121
|
+
return { code: 0, stdout: "" };
|
|
1122
|
+
},
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
await upgrade("hub", {
|
|
1126
|
+
manifestPath: h.manifestPath,
|
|
1127
|
+
configDir: h.configDir,
|
|
1128
|
+
runner,
|
|
1129
|
+
findGlobalInstall: (pkg) =>
|
|
1130
|
+
pkg === "@openparachute/hub" ? join(hubInstallDir, "package.json") : null,
|
|
1131
|
+
restartFn: async () => 0,
|
|
1132
|
+
// Programmatic `tag` wins over auto-detection AND --channel.
|
|
1133
|
+
tag: "next",
|
|
1134
|
+
channel: "rc",
|
|
1135
|
+
log: () => {},
|
|
1136
|
+
});
|
|
1137
|
+
const addCall = seenCmd.find((c) => c[0] === "bun" && c[1] === "add");
|
|
1138
|
+
expect(addCall).toEqual(["bun", "add", "-g", "@openparachute/hub@next"]);
|
|
1139
|
+
} finally {
|
|
1140
|
+
h.cleanup();
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
784
1143
|
});
|