@openparachute/hub 0.6.3 → 0.6.4-rc.10
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 -2
- package/src/__tests__/account-home-ui.test.ts +344 -110
- package/src/__tests__/account-mirror.test.ts +156 -0
- package/src/__tests__/account-setup.test.ts +880 -0
- package/src/__tests__/account-usage.test.ts +137 -0
- package/src/__tests__/account-vault-admin-token.test.ts +301 -0
- package/src/__tests__/account-vault-token.test.ts +53 -1
- package/src/__tests__/admin-vault-admin-token.test.ts +17 -0
- package/src/__tests__/admin-vaults.test.ts +20 -0
- package/src/__tests__/api-account.test.ts +236 -4
- package/src/__tests__/api-invites.test.ts +217 -0
- package/src/__tests__/api-mint-token.test.ts +259 -10
- package/src/__tests__/api-modules-ops.test.ts +195 -3
- package/src/__tests__/api-modules.test.ts +40 -4
- package/src/__tests__/api-settings-hub-origin.test.ts +13 -8
- package/src/__tests__/auto-wire.test.ts +101 -1
- package/src/__tests__/cli.test.ts +188 -2
- package/src/__tests__/cloudflare-state.test.ts +104 -0
- package/src/__tests__/expose-2fa-warning.test.ts +11 -8
- package/src/__tests__/expose-cloudflare.test.ts +135 -9
- package/src/__tests__/expose-interactive.test.ts +234 -7
- package/src/__tests__/expose-supervisor-version.test.ts +104 -0
- package/src/__tests__/expose.test.ts +10 -5
- package/src/__tests__/grants.test.ts +197 -8
- package/src/__tests__/hub-origin-resolution.test.ts +179 -25
- package/src/__tests__/hub-server.test.ts +761 -13
- package/src/__tests__/hub-unit.test.ts +185 -0
- package/src/__tests__/init.test.ts +579 -3
- package/src/__tests__/install.test.ts +448 -2
- package/src/__tests__/invites.test.ts +220 -0
- package/src/__tests__/launchctl-guard.test.ts +185 -0
- package/src/__tests__/migrate-cutover.test.ts +33 -0
- package/src/__tests__/module-ops-client.test.ts +68 -0
- package/src/__tests__/scope-explanations.test.ts +16 -0
- package/src/__tests__/serve-boot.test.ts +74 -1
- package/src/__tests__/serve.test.ts +121 -7
- package/src/__tests__/setup-wizard.test.ts +110 -0
- package/src/__tests__/spawn-path.test.ts +191 -0
- package/src/__tests__/status.test.ts +64 -0
- package/src/__tests__/supervisor.test.ts +374 -0
- package/src/__tests__/users.test.ts +66 -0
- package/src/__tests__/well-known.test.ts +25 -0
- package/src/__tests__/wizard.test.ts +72 -1
- package/src/account-home-ui.ts +481 -235
- package/src/account-mirror.ts +126 -0
- package/src/account-setup.ts +381 -0
- package/src/account-usage.ts +118 -0
- package/src/account-vault-admin-token.ts +242 -0
- package/src/account-vault-token.ts +36 -2
- package/src/admin-login-ui.ts +121 -0
- package/src/admin-vault-admin-token.ts +8 -2
- package/src/admin-vaults.ts +137 -29
- package/src/api-account.ts +118 -1
- package/src/api-invites.ts +345 -0
- package/src/api-mint-token.ts +81 -0
- package/src/api-modules-ops.ts +168 -53
- package/src/api-modules.ts +36 -0
- package/src/auto-wire.ts +87 -0
- package/src/cli.ts +128 -34
- package/src/cloudflare/detect.ts +1 -1
- package/src/cloudflare/state.ts +104 -8
- package/src/commands/expose-2fa-warning.ts +17 -13
- package/src/commands/expose-cloudflare.ts +103 -36
- package/src/commands/expose-interactive.ts +163 -17
- package/src/commands/expose-supervisor.ts +45 -0
- package/src/commands/init.ts +183 -4
- package/src/commands/install.ts +321 -3
- package/src/commands/migrate-cutover.ts +12 -5
- package/src/commands/serve-boot.ts +33 -3
- package/src/commands/serve.ts +158 -37
- package/src/commands/status.ts +9 -1
- package/src/commands/wizard.ts +36 -2
- package/src/grants.ts +113 -0
- package/src/help.ts +18 -5
- package/src/hub-db.ts +70 -2
- package/src/hub-server.ts +438 -41
- package/src/hub-settings.ts +3 -3
- package/src/hub-unit.ts +259 -9
- package/src/invites.ts +291 -0
- package/src/launchctl-guard.ts +131 -0
- package/src/managed-unit.ts +13 -3
- package/src/migrate-offer.ts +15 -6
- package/src/module-ops-client.ts +47 -22
- package/src/scope-attenuation.ts +19 -0
- package/src/scope-explanations.ts +9 -1
- package/src/service-spec.ts +17 -4
- package/src/setup-wizard.ts +34 -2
- package/src/spawn-path.ts +148 -0
- package/src/supervisor.ts +232 -7
- package/src/users.ts +54 -8
- package/src/vault-hub-origin-env.ts +28 -0
- package/src/vault-name.ts +13 -1
- package/src/well-known.ts +13 -0
- package/web/ui/dist/assets/{index-mz8XcVPP.css → index-BYYUeLGA.css} +1 -1
- package/web/ui/dist/assets/index-D3cDUOOj.js +61 -0
- package/web/ui/dist/index.html +2 -2
- package/web/ui/dist/assets/index-D_0TRjeo.js +0 -61
|
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { install } from "../commands/install.ts";
|
|
5
|
+
import { defaultStartLifecycleOpts, install } from "../commands/install.ts";
|
|
6
6
|
import { findService, upsertService } from "../services-manifest.ts";
|
|
7
7
|
|
|
8
8
|
function makeTempPath(): { path: string; configDir: string; cleanup: () => void } {
|
|
@@ -40,6 +40,9 @@ describe("install", () => {
|
|
|
40
40
|
const calls: string[][] = [];
|
|
41
41
|
const logs: string[] = [];
|
|
42
42
|
const code = await install("vault", {
|
|
43
|
+
// --interactive: opt back into vault's full `init` (#579). The light
|
|
44
|
+
// default skips it; this test exercises the interactive path.
|
|
45
|
+
interactive: true,
|
|
43
46
|
runner: async (cmd) => {
|
|
44
47
|
calls.push([...cmd]);
|
|
45
48
|
return 0;
|
|
@@ -67,6 +70,8 @@ describe("install", () => {
|
|
|
67
70
|
try {
|
|
68
71
|
const logs: string[] = [];
|
|
69
72
|
const code = await install("vault", {
|
|
73
|
+
// --interactive: this test asserts init wrote the authoritative entry.
|
|
74
|
+
interactive: true,
|
|
70
75
|
runner: async (cmd) => {
|
|
71
76
|
if (cmd[0] === "parachute-vault") {
|
|
72
77
|
upsertService(
|
|
@@ -133,6 +138,8 @@ describe("install", () => {
|
|
|
133
138
|
const calls: string[][] = [];
|
|
134
139
|
const logs: string[] = [];
|
|
135
140
|
const code = await install("vault", {
|
|
141
|
+
// --interactive: this test asserts init still ran after the bun quirk.
|
|
142
|
+
interactive: true,
|
|
136
143
|
runner: async (cmd) => {
|
|
137
144
|
calls.push([...cmd]);
|
|
138
145
|
// `bun add -g` exits 1; `parachute-vault init` succeeds.
|
|
@@ -334,6 +341,96 @@ describe("install", () => {
|
|
|
334
341
|
}
|
|
335
342
|
});
|
|
336
343
|
|
|
344
|
+
test("names the squatter holding the canonical port when the walk assigns a fallback (#590)", async () => {
|
|
345
|
+
// Field bug #590 item 2: a stale pre-supervisor vault zombie squats 1940;
|
|
346
|
+
// the install-time port walk silently routed to a fallback. Now it names the
|
|
347
|
+
// holder (pid + command line) + hints it may be a stale daemon. Detection
|
|
348
|
+
// only — never kills. Reuses the #581 pidOnPort / ownerOfPid seams.
|
|
349
|
+
const { path, configDir, cleanup } = makeTempPath();
|
|
350
|
+
try {
|
|
351
|
+
const logs: string[] = [];
|
|
352
|
+
const code = await install("vault", {
|
|
353
|
+
runner: async () => 0,
|
|
354
|
+
manifestPath: path,
|
|
355
|
+
configDir,
|
|
356
|
+
startService: async () => 0,
|
|
357
|
+
isLinked: () => false,
|
|
358
|
+
// Only vault's canonical 1940 is held → the walk picks a fallback in-range.
|
|
359
|
+
portProbe: async (p) => p === 1940,
|
|
360
|
+
// Inject the #581 seams: a foreign pid squats 1940.
|
|
361
|
+
pidOnPort: (p) => (p === 1940 ? 1234 : undefined),
|
|
362
|
+
ownerOfPid: (pid) => (pid === 1234 ? "bun /opt/vault/src/server.ts" : undefined),
|
|
363
|
+
log: (l) => logs.push(l),
|
|
364
|
+
});
|
|
365
|
+
expect(code).toBe(0);
|
|
366
|
+
const joined = logs.join("\n");
|
|
367
|
+
// The fallback warning still fires…
|
|
368
|
+
expect(joined).toMatch(/canonical port 1940 is in use; assigned/);
|
|
369
|
+
// …and now it NAMES the squatter + hints at a stale daemon.
|
|
370
|
+
expect(joined).toContain("pid 1234 (bun /opt/vault/src/server.ts)");
|
|
371
|
+
expect(joined).toMatch(/stale pre-supervisor daemon/);
|
|
372
|
+
expect(joined).toContain("kill 1234");
|
|
373
|
+
const entry = findService("parachute-vault", path);
|
|
374
|
+
expect(entry?.port).not.toBe(1940);
|
|
375
|
+
} finally {
|
|
376
|
+
cleanup();
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("squatter pid present but command line unreadable → names the pid alone (#590)", async () => {
|
|
381
|
+
const { path, configDir, cleanup } = makeTempPath();
|
|
382
|
+
try {
|
|
383
|
+
const logs: string[] = [];
|
|
384
|
+
const code = await install("vault", {
|
|
385
|
+
runner: async () => 0,
|
|
386
|
+
manifestPath: path,
|
|
387
|
+
configDir,
|
|
388
|
+
startService: async () => 0,
|
|
389
|
+
isLinked: () => false,
|
|
390
|
+
portProbe: async (p) => p === 1940,
|
|
391
|
+
pidOnPort: (p) => (p === 1940 ? 4321 : undefined),
|
|
392
|
+
ownerOfPid: () => undefined, // ps failed / pid gone
|
|
393
|
+
log: (l) => logs.push(l),
|
|
394
|
+
});
|
|
395
|
+
expect(code).toBe(0);
|
|
396
|
+
const joined = logs.join("\n");
|
|
397
|
+
expect(joined).toContain("held by pid 4321.");
|
|
398
|
+
expect(joined).not.toContain("(undefined)");
|
|
399
|
+
} finally {
|
|
400
|
+
cleanup();
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("no squatter naming when the canonical port is free (#590 — no false positive)", async () => {
|
|
405
|
+
const { path, configDir, cleanup } = makeTempPath();
|
|
406
|
+
try {
|
|
407
|
+
const logs: string[] = [];
|
|
408
|
+
let pidProbed = false;
|
|
409
|
+
const code = await install("vault", {
|
|
410
|
+
runner: async () => 0,
|
|
411
|
+
manifestPath: path,
|
|
412
|
+
configDir,
|
|
413
|
+
startService: async () => 0,
|
|
414
|
+
isLinked: () => false,
|
|
415
|
+
portProbe: async () => false, // canonical 1940 is free
|
|
416
|
+
pidOnPort: () => {
|
|
417
|
+
pidProbed = true;
|
|
418
|
+
return 9999;
|
|
419
|
+
},
|
|
420
|
+
ownerOfPid: () => "should-not-appear",
|
|
421
|
+
log: (l) => logs.push(l),
|
|
422
|
+
});
|
|
423
|
+
expect(code).toBe(0);
|
|
424
|
+
const joined = logs.join("\n");
|
|
425
|
+
// Canonical assigned → no fallback warning, no squatter probe at all.
|
|
426
|
+
expect(joined).not.toMatch(/is in use; assigned/);
|
|
427
|
+
expect(joined).not.toContain("should-not-appear");
|
|
428
|
+
expect(pidProbed).toBe(false);
|
|
429
|
+
} finally {
|
|
430
|
+
cleanup();
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
337
434
|
test("`install lens` aliases to notes with a rename notice", async () => {
|
|
338
435
|
// Transition alias for the brief Notes→Lens rename (Apr 19) that was
|
|
339
436
|
// reverted on launch eve (Apr 22). Accepted for one release cycle so
|
|
@@ -796,12 +893,14 @@ describe("install", () => {
|
|
|
796
893
|
}
|
|
797
894
|
});
|
|
798
895
|
|
|
799
|
-
test("linked vault still runs init and defers to init's manifest write", async () => {
|
|
896
|
+
test("linked vault still runs init and defers to init's manifest write (--interactive)", async () => {
|
|
800
897
|
const { path, cleanup } = makeTempPath();
|
|
801
898
|
try {
|
|
802
899
|
const calls: string[][] = [];
|
|
803
900
|
const logs: string[] = [];
|
|
804
901
|
const code = await install("vault", {
|
|
902
|
+
// --interactive: this test asserts vault's own init wrote the entry.
|
|
903
|
+
interactive: true,
|
|
805
904
|
runner: async (cmd) => {
|
|
806
905
|
calls.push([...cmd]);
|
|
807
906
|
if (cmd[0] === "parachute-vault") {
|
|
@@ -1721,3 +1820,350 @@ describe("install", () => {
|
|
|
1721
1820
|
}
|
|
1722
1821
|
});
|
|
1723
1822
|
});
|
|
1823
|
+
|
|
1824
|
+
describe("#579 / #580 item 1 — light manual install + guidance", () => {
|
|
1825
|
+
test("default vault install skips the interactive init (no parachute-vault init runs)", async () => {
|
|
1826
|
+
const { path, cleanup } = makeTempPath();
|
|
1827
|
+
try {
|
|
1828
|
+
const calls: string[][] = [];
|
|
1829
|
+
const logs: string[] = [];
|
|
1830
|
+
const code = await install("vault", {
|
|
1831
|
+
// No `interactive` flag → the light default path.
|
|
1832
|
+
runner: async (cmd) => {
|
|
1833
|
+
calls.push([...cmd]);
|
|
1834
|
+
return 0;
|
|
1835
|
+
},
|
|
1836
|
+
manifestPath: path,
|
|
1837
|
+
startService: async () => 0,
|
|
1838
|
+
isLinked: () => false,
|
|
1839
|
+
portProbe: async () => false,
|
|
1840
|
+
log: (l) => logs.push(l),
|
|
1841
|
+
});
|
|
1842
|
+
expect(code).toBe(0);
|
|
1843
|
+
// bun add ran; vault's interactive init did NOT.
|
|
1844
|
+
expect(calls).toEqual([["bun", "add", "-g", "@openparachute/vault"]]);
|
|
1845
|
+
expect(calls).not.toContainEqual(["parachute-vault", "init"]);
|
|
1846
|
+
// The skip is announced + points at the admin UI / --interactive.
|
|
1847
|
+
expect(logs.join("\n")).toMatch(/skipping parachute-vault init/);
|
|
1848
|
+
} finally {
|
|
1849
|
+
cleanup();
|
|
1850
|
+
}
|
|
1851
|
+
});
|
|
1852
|
+
|
|
1853
|
+
test("default vault install still starts the module under the supervisor", async () => {
|
|
1854
|
+
const { path, cleanup } = makeTempPath();
|
|
1855
|
+
try {
|
|
1856
|
+
const startCalls: string[] = [];
|
|
1857
|
+
const code = await install("vault", {
|
|
1858
|
+
runner: async () => 0,
|
|
1859
|
+
manifestPath: path,
|
|
1860
|
+
startService: async (short) => {
|
|
1861
|
+
startCalls.push(short);
|
|
1862
|
+
return 0;
|
|
1863
|
+
},
|
|
1864
|
+
isLinked: () => false,
|
|
1865
|
+
portProbe: async () => false,
|
|
1866
|
+
log: () => {},
|
|
1867
|
+
});
|
|
1868
|
+
expect(code).toBe(0);
|
|
1869
|
+
// Light ≠ no-start: the supervisor owns the lifecycle; vault is started.
|
|
1870
|
+
expect(startCalls).toEqual(["vault"]);
|
|
1871
|
+
} finally {
|
|
1872
|
+
cleanup();
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
test("guidance block prints the admin URL + extras on a supervised box (loopback)", async () => {
|
|
1877
|
+
const { path, cleanup } = makeTempPath();
|
|
1878
|
+
try {
|
|
1879
|
+
const logs: string[] = [];
|
|
1880
|
+
const code = await install("vault", {
|
|
1881
|
+
runner: async () => 0,
|
|
1882
|
+
manifestPath: path,
|
|
1883
|
+
startService: async () => 0,
|
|
1884
|
+
isLinked: () => false,
|
|
1885
|
+
portProbe: async () => false,
|
|
1886
|
+
log: (l) => logs.push(l),
|
|
1887
|
+
// Deterministic supervised-box context: hub unit installed, not exposed.
|
|
1888
|
+
guidanceCtx: { hubUnitInstalled: true, exposeState: undefined, hubPort: 1939 },
|
|
1889
|
+
});
|
|
1890
|
+
expect(code).toBe(0);
|
|
1891
|
+
const joined = logs.join("\n");
|
|
1892
|
+
expect(joined).toMatch(/Manage \+ create vaults in the admin UI/);
|
|
1893
|
+
expect(joined).toMatch(/http:\/\/127\.0\.0\.1:1939\/admin\//);
|
|
1894
|
+
expect(joined).toMatch(/parachute-vault mcp-install/);
|
|
1895
|
+
expect(joined).toMatch(/--interactive/);
|
|
1896
|
+
// It does NOT mint a token or wire MCP — just points there.
|
|
1897
|
+
expect(joined).toMatch(/Mint an API token.*admin UI/);
|
|
1898
|
+
} finally {
|
|
1899
|
+
cleanup();
|
|
1900
|
+
}
|
|
1901
|
+
});
|
|
1902
|
+
|
|
1903
|
+
test("guidance uses the exposed public FQDN when the hub is exposed", async () => {
|
|
1904
|
+
const { path, cleanup } = makeTempPath();
|
|
1905
|
+
try {
|
|
1906
|
+
const logs: string[] = [];
|
|
1907
|
+
await install("vault", {
|
|
1908
|
+
runner: async () => 0,
|
|
1909
|
+
manifestPath: path,
|
|
1910
|
+
startService: async () => 0,
|
|
1911
|
+
isLinked: () => false,
|
|
1912
|
+
portProbe: async () => false,
|
|
1913
|
+
log: (l) => logs.push(l),
|
|
1914
|
+
guidanceCtx: {
|
|
1915
|
+
hubUnitInstalled: true,
|
|
1916
|
+
exposeState: {
|
|
1917
|
+
version: 1,
|
|
1918
|
+
layer: "public",
|
|
1919
|
+
mode: "path",
|
|
1920
|
+
canonicalFqdn: "friends.parachute.computer",
|
|
1921
|
+
port: 1939,
|
|
1922
|
+
funnel: false,
|
|
1923
|
+
entries: [],
|
|
1924
|
+
},
|
|
1925
|
+
hubPort: 1939,
|
|
1926
|
+
},
|
|
1927
|
+
});
|
|
1928
|
+
const joined = logs.join("\n");
|
|
1929
|
+
expect(joined).toMatch(/https:\/\/friends\.parachute\.computer\/admin\//);
|
|
1930
|
+
expect(joined).not.toMatch(/127\.0\.0\.1/);
|
|
1931
|
+
} finally {
|
|
1932
|
+
cleanup();
|
|
1933
|
+
}
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1936
|
+
test("no guidance block on a non-supervised box (no hub unit)", async () => {
|
|
1937
|
+
const { path, cleanup } = makeTempPath();
|
|
1938
|
+
try {
|
|
1939
|
+
const logs: string[] = [];
|
|
1940
|
+
await install("vault", {
|
|
1941
|
+
runner: async () => 0,
|
|
1942
|
+
manifestPath: path,
|
|
1943
|
+
startService: async () => 0,
|
|
1944
|
+
isLinked: () => false,
|
|
1945
|
+
portProbe: async () => false,
|
|
1946
|
+
log: (l) => logs.push(l),
|
|
1947
|
+
guidanceCtx: { hubUnitInstalled: false, exposeState: undefined, hubPort: 1939 },
|
|
1948
|
+
});
|
|
1949
|
+
const joined = logs.join("\n");
|
|
1950
|
+
expect(joined).not.toMatch(/Manage \+ create vaults in the admin UI/);
|
|
1951
|
+
} finally {
|
|
1952
|
+
cleanup();
|
|
1953
|
+
}
|
|
1954
|
+
});
|
|
1955
|
+
|
|
1956
|
+
test("--interactive runs vault init and suppresses the guidance block", async () => {
|
|
1957
|
+
const { path, cleanup } = makeTempPath();
|
|
1958
|
+
try {
|
|
1959
|
+
const calls: string[][] = [];
|
|
1960
|
+
const logs: string[] = [];
|
|
1961
|
+
const code = await install("vault", {
|
|
1962
|
+
interactive: true,
|
|
1963
|
+
runner: async (cmd) => {
|
|
1964
|
+
calls.push([...cmd]);
|
|
1965
|
+
return 0;
|
|
1966
|
+
},
|
|
1967
|
+
manifestPath: path,
|
|
1968
|
+
startService: async () => 0,
|
|
1969
|
+
isLinked: () => false,
|
|
1970
|
+
portProbe: async () => false,
|
|
1971
|
+
log: (l) => logs.push(l),
|
|
1972
|
+
// Even on a supervised box, --interactive means the service's own
|
|
1973
|
+
// init owns the next-steps surface — no light guidance block.
|
|
1974
|
+
guidanceCtx: { hubUnitInstalled: true, exposeState: undefined, hubPort: 1939 },
|
|
1975
|
+
});
|
|
1976
|
+
expect(code).toBe(0);
|
|
1977
|
+
expect(calls).toContainEqual(["parachute-vault", "init"]);
|
|
1978
|
+
expect(logs.join("\n")).not.toMatch(/Manage \+ create vaults in the admin UI/);
|
|
1979
|
+
} finally {
|
|
1980
|
+
cleanup();
|
|
1981
|
+
}
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1984
|
+
test("scribe (no interactive init) is unaffected — no skip log, no vault guidance", async () => {
|
|
1985
|
+
const { path, cleanup } = makeTempPath();
|
|
1986
|
+
const configDir = join(path, "..");
|
|
1987
|
+
try {
|
|
1988
|
+
const logs: string[] = [];
|
|
1989
|
+
const code = await install("scribe", {
|
|
1990
|
+
runner: async () => 0,
|
|
1991
|
+
manifestPath: path,
|
|
1992
|
+
configDir,
|
|
1993
|
+
startService: async () => 0,
|
|
1994
|
+
isLinked: () => false,
|
|
1995
|
+
portProbe: async () => false,
|
|
1996
|
+
scribeAvailability: { kind: "not-tty" },
|
|
1997
|
+
log: (l) => logs.push(l),
|
|
1998
|
+
guidanceCtx: { hubUnitInstalled: true, exposeState: undefined, hubPort: 1939 },
|
|
1999
|
+
});
|
|
2000
|
+
expect(code).toBe(0);
|
|
2001
|
+
const joined = logs.join("\n");
|
|
2002
|
+
expect(joined).not.toMatch(/skipping/);
|
|
2003
|
+
expect(joined).not.toMatch(/Manage \+ create vaults in the admin UI/);
|
|
2004
|
+
} finally {
|
|
2005
|
+
cleanup();
|
|
2006
|
+
}
|
|
2007
|
+
});
|
|
2008
|
+
});
|
|
2009
|
+
|
|
2010
|
+
describe("#580 item 3 — install-time stale-unit sweep", () => {
|
|
2011
|
+
test("sweeps stale per-module units before starting on a supervised box", async () => {
|
|
2012
|
+
const { path, cleanup } = makeTempPath();
|
|
2013
|
+
try {
|
|
2014
|
+
const logs: string[] = [];
|
|
2015
|
+
let sweepCalls = 0;
|
|
2016
|
+
const code = await install("vault", {
|
|
2017
|
+
runner: async () => 0,
|
|
2018
|
+
manifestPath: path,
|
|
2019
|
+
startService: async () => 0,
|
|
2020
|
+
isLinked: () => false,
|
|
2021
|
+
portProbe: async () => false,
|
|
2022
|
+
log: (l) => logs.push(l),
|
|
2023
|
+
guidanceCtx: { hubUnitInstalled: true, exposeState: undefined, hubPort: 1939 },
|
|
2024
|
+
disableStaleModuleUnits: () => {
|
|
2025
|
+
sweepCalls += 1;
|
|
2026
|
+
return {
|
|
2027
|
+
actions: [
|
|
2028
|
+
{
|
|
2029
|
+
short: "vault",
|
|
2030
|
+
kind: "launchd",
|
|
2031
|
+
unit: "computer.parachute.vault",
|
|
2032
|
+
result: "disabled",
|
|
2033
|
+
messages: [" ✓ Disabled stale computer.parachute.vault"],
|
|
2034
|
+
},
|
|
2035
|
+
],
|
|
2036
|
+
};
|
|
2037
|
+
},
|
|
2038
|
+
});
|
|
2039
|
+
expect(code).toBe(0);
|
|
2040
|
+
expect(sweepCalls).toBe(1);
|
|
2041
|
+
expect(logs.join("\n")).toMatch(
|
|
2042
|
+
/Swept 1 stale per-module autostart unit\(s\).*computer\.parachute\.vault/,
|
|
2043
|
+
);
|
|
2044
|
+
} finally {
|
|
2045
|
+
cleanup();
|
|
2046
|
+
}
|
|
2047
|
+
});
|
|
2048
|
+
|
|
2049
|
+
test("does NOT sweep on a non-supervised box (no hub unit)", async () => {
|
|
2050
|
+
const { path, cleanup } = makeTempPath();
|
|
2051
|
+
try {
|
|
2052
|
+
let sweepCalls = 0;
|
|
2053
|
+
await install("vault", {
|
|
2054
|
+
runner: async () => 0,
|
|
2055
|
+
manifestPath: path,
|
|
2056
|
+
startService: async () => 0,
|
|
2057
|
+
isLinked: () => false,
|
|
2058
|
+
portProbe: async () => false,
|
|
2059
|
+
log: () => {},
|
|
2060
|
+
guidanceCtx: { hubUnitInstalled: false, exposeState: undefined, hubPort: 1939 },
|
|
2061
|
+
disableStaleModuleUnits: () => {
|
|
2062
|
+
sweepCalls += 1;
|
|
2063
|
+
return { actions: [] };
|
|
2064
|
+
},
|
|
2065
|
+
});
|
|
2066
|
+
// No supervised hub → the per-module unit is the legitimate lifecycle;
|
|
2067
|
+
// the sweep must not run.
|
|
2068
|
+
expect(sweepCalls).toBe(0);
|
|
2069
|
+
} finally {
|
|
2070
|
+
cleanup();
|
|
2071
|
+
}
|
|
2072
|
+
});
|
|
2073
|
+
|
|
2074
|
+
test("does NOT sweep under --no-start (caller owns the process model)", async () => {
|
|
2075
|
+
const { path, cleanup } = makeTempPath();
|
|
2076
|
+
try {
|
|
2077
|
+
let sweepCalls = 0;
|
|
2078
|
+
await install("vault", {
|
|
2079
|
+
runner: async () => 0,
|
|
2080
|
+
manifestPath: path,
|
|
2081
|
+
startService: async () => 0,
|
|
2082
|
+
isLinked: () => false,
|
|
2083
|
+
portProbe: async () => false,
|
|
2084
|
+
noStart: true,
|
|
2085
|
+
log: () => {},
|
|
2086
|
+
guidanceCtx: { hubUnitInstalled: true, exposeState: undefined, hubPort: 1939 },
|
|
2087
|
+
disableStaleModuleUnits: () => {
|
|
2088
|
+
sweepCalls += 1;
|
|
2089
|
+
return { actions: [] };
|
|
2090
|
+
},
|
|
2091
|
+
});
|
|
2092
|
+
expect(sweepCalls).toBe(0);
|
|
2093
|
+
} finally {
|
|
2094
|
+
cleanup();
|
|
2095
|
+
}
|
|
2096
|
+
});
|
|
2097
|
+
|
|
2098
|
+
test("does NOT sweep under --no-create (wizard defers the start; N2)", async () => {
|
|
2099
|
+
// Parallel to the --no-start guard above. `noCreate` (the wizard's
|
|
2100
|
+
// install path) also suppresses the start — and the sweep touches real
|
|
2101
|
+
// launchctl/systemctl on a live box, so it must NOT fire when we're not
|
|
2102
|
+
// about to start the module. A silent regression here would have the
|
|
2103
|
+
// wizard disabling operator units mid-init.
|
|
2104
|
+
const { path, cleanup } = makeTempPath();
|
|
2105
|
+
try {
|
|
2106
|
+
let sweepCalls = 0;
|
|
2107
|
+
await install("vault", {
|
|
2108
|
+
runner: async () => 0,
|
|
2109
|
+
manifestPath: path,
|
|
2110
|
+
startService: async () => 0,
|
|
2111
|
+
isLinked: () => false,
|
|
2112
|
+
portProbe: async () => false,
|
|
2113
|
+
noCreate: true,
|
|
2114
|
+
log: () => {},
|
|
2115
|
+
guidanceCtx: { hubUnitInstalled: true, exposeState: undefined, hubPort: 1939 },
|
|
2116
|
+
disableStaleModuleUnits: () => {
|
|
2117
|
+
sweepCalls += 1;
|
|
2118
|
+
return { actions: [] };
|
|
2119
|
+
},
|
|
2120
|
+
});
|
|
2121
|
+
expect(sweepCalls).toBe(0);
|
|
2122
|
+
} finally {
|
|
2123
|
+
cleanup();
|
|
2124
|
+
}
|
|
2125
|
+
});
|
|
2126
|
+
|
|
2127
|
+
test("a clean no-op sweep (nothing stale) logs nothing extra", async () => {
|
|
2128
|
+
const { path, cleanup } = makeTempPath();
|
|
2129
|
+
try {
|
|
2130
|
+
const logs: string[] = [];
|
|
2131
|
+
await install("vault", {
|
|
2132
|
+
runner: async () => 0,
|
|
2133
|
+
manifestPath: path,
|
|
2134
|
+
startService: async () => 0,
|
|
2135
|
+
isLinked: () => false,
|
|
2136
|
+
portProbe: async () => false,
|
|
2137
|
+
log: (l) => logs.push(l),
|
|
2138
|
+
guidanceCtx: { hubUnitInstalled: true, exposeState: undefined, hubPort: 1939 },
|
|
2139
|
+
disableStaleModuleUnits: () => ({ actions: [] }),
|
|
2140
|
+
});
|
|
2141
|
+
expect(logs.join("\n")).not.toMatch(/Swept .* stale per-module/);
|
|
2142
|
+
} finally {
|
|
2143
|
+
cleanup();
|
|
2144
|
+
}
|
|
2145
|
+
});
|
|
2146
|
+
});
|
|
2147
|
+
|
|
2148
|
+
describe("hub#573 — install auto-start converges on supervised detection", () => {
|
|
2149
|
+
test("the default start opts opt into real supervisor detection + the migrate offer", () => {
|
|
2150
|
+
const log = () => {};
|
|
2151
|
+
const opts = defaultStartLifecycleOpts({
|
|
2152
|
+
manifestPath: "/tmp/services.json",
|
|
2153
|
+
configDir: "/tmp/cfg",
|
|
2154
|
+
log,
|
|
2155
|
+
});
|
|
2156
|
+
// `supervisor: {}` (present, even if empty) → lifecycle resolves
|
|
2157
|
+
// `unitInstalled` via the real `isHubUnitInstalled` probe instead of the
|
|
2158
|
+
// omitted-supervisor default of `false`. Pre-fix this block was absent, so
|
|
2159
|
+
// the auto-start ALWAYS concluded "no unit" and printed the spurious
|
|
2160
|
+
// "No supervised hub unit is installed" + "didn't start cleanly".
|
|
2161
|
+
expect(opts.supervisor).toEqual({});
|
|
2162
|
+
// The cutover offer is armed, matching `parachute start <svc>` (cli.ts).
|
|
2163
|
+
expect(opts.migrateOffer).toEqual({ enabled: true });
|
|
2164
|
+
// Plumbing preserved.
|
|
2165
|
+
expect(opts.manifestPath).toBe("/tmp/services.json");
|
|
2166
|
+
expect(opts.configDir).toBe("/tmp/cfg");
|
|
2167
|
+
expect(opts.log).toBe(log);
|
|
2168
|
+
});
|
|
2169
|
+
});
|