@openparachute/hub 0.7.7-rc.9 → 0.7.7

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.
Files changed (78) hide show
  1. package/README.md +7 -7
  2. package/package.json +4 -12
  3. package/src/__tests__/account-api.test.ts +29 -2
  4. package/src/__tests__/account-session.test.ts +9 -1
  5. package/src/__tests__/account-token.test.ts +26 -2
  6. package/src/__tests__/admin-connections-credentials.test.ts +41 -0
  7. package/src/__tests__/admin-lock.test.ts +3 -14
  8. package/src/__tests__/admin-module-token.test.ts +10 -30
  9. package/src/__tests__/admin-surfaces.test.ts +21 -0
  10. package/src/__tests__/api-hub-upgrade.test.ts +11 -0
  11. package/src/__tests__/api-mint-token.test.ts +25 -0
  12. package/src/__tests__/api-modules-ops.test.ts +34 -29
  13. package/src/__tests__/api-modules.test.ts +50 -58
  14. package/src/__tests__/api-revoke-token.test.ts +23 -0
  15. package/src/__tests__/api-settings-hub-origin.test.ts +12 -0
  16. package/src/__tests__/api-settings-root-redirect.test.ts +159 -4
  17. package/src/__tests__/api-tokens.test.ts +44 -0
  18. package/src/__tests__/audience-gate.test.ts +24 -0
  19. package/src/__tests__/bearer-scheme-casing.test.ts +110 -0
  20. package/src/__tests__/chrome-strip.test.ts +18 -1
  21. package/src/__tests__/doctor.test.ts +10 -17
  22. package/src/__tests__/hub-command.test.ts +70 -2
  23. package/src/__tests__/hub-server.test.ts +322 -1
  24. package/src/__tests__/hub-settings.test.ts +110 -6
  25. package/src/__tests__/hub.test.ts +29 -0
  26. package/src/__tests__/install.test.ts +210 -38
  27. package/src/__tests__/migrate.test.ts +3 -1
  28. package/src/__tests__/oauth-handlers.test.ts +19 -8
  29. package/src/__tests__/operator-token.test.ts +1 -2
  30. package/src/__tests__/port-assign.test.ts +17 -26
  31. package/src/__tests__/root-serve.test.ts +139 -0
  32. package/src/__tests__/scope-explanations.test.ts +0 -2
  33. package/src/__tests__/serve-boot.test.ts +25 -36
  34. package/src/__tests__/service-spec-discovery.test.ts +30 -35
  35. package/src/__tests__/services-manifest.test.ts +22 -92
  36. package/src/__tests__/setup-wizard.test.ts +301 -22
  37. package/src/__tests__/setup.test.ts +8 -13
  38. package/src/__tests__/status.test.ts +0 -5
  39. package/src/__tests__/surface-notes-alias.test.ts +296 -0
  40. package/src/__tests__/wizard-transcription.test.ts +35 -0
  41. package/src/__tests__/wizard.test.ts +79 -0
  42. package/src/account-api.ts +6 -0
  43. package/src/admin-connections.ts +4 -2
  44. package/src/admin-lock.ts +1 -2
  45. package/src/admin-module-token.ts +13 -8
  46. package/src/admin-surfaces.ts +2 -1
  47. package/src/api-hub-upgrade.ts +2 -1
  48. package/src/api-mint-token.ts +2 -1
  49. package/src/api-modules-ops.ts +2 -1
  50. package/src/api-modules.ts +9 -7
  51. package/src/api-revoke-token.ts +2 -1
  52. package/src/api-settings-hub-origin.ts +2 -1
  53. package/src/api-settings-root-redirect.ts +97 -20
  54. package/src/api-tokens.ts +2 -1
  55. package/src/audience-gate.ts +2 -1
  56. package/src/chrome-strip.ts +16 -4
  57. package/src/commands/hub.ts +104 -0
  58. package/src/commands/install.ts +72 -27
  59. package/src/commands/migrate.ts +5 -1
  60. package/src/commands/setup.ts +0 -2
  61. package/src/commands/wizard-transcription.ts +24 -0
  62. package/src/commands/wizard.ts +34 -1
  63. package/src/help.ts +0 -1
  64. package/src/hub-server.ts +111 -54
  65. package/src/hub-settings.ts +147 -0
  66. package/src/hub.ts +64 -31
  67. package/src/module-ops-client.ts +2 -1
  68. package/src/oauth-handlers.ts +1 -11
  69. package/src/operator-token.ts +0 -1
  70. package/src/origin-check.ts +2 -2
  71. package/src/root-serve.ts +156 -0
  72. package/src/scope-explanations.ts +2 -5
  73. package/src/service-spec.ts +18 -47
  74. package/src/services-manifest.ts +9 -42
  75. package/src/setup-wizard.ts +144 -31
  76. package/src/surface-notes-alias.ts +126 -0
  77. package/src/__tests__/admin-agent-token.test.ts +0 -173
  78. package/src/admin-agent-token.ts +0 -147
@@ -219,6 +219,80 @@ describe("deriveWizardState", () => {
219
219
  }
220
220
  });
221
221
 
222
+ test("real-version vault row with paths:[] is NOT a real vault (e2e boot-spawned-child fix)", async () => {
223
+ // The Tier-1 e2e wizard-skip root cause. When the hub boot-spawns the vault
224
+ // MODULE child, vault's selfRegister writes its services.json row at the
225
+ // REAL package version (e.g. 0.7.4-rc.1, NOT SEED_VERSION) but with
226
+ // `paths: []` because no vault instance exists yet. The SEED_VERSION
227
+ // sentinel alone did NOT catch this — a bare `version !== SEED_VERSION`
228
+ // check read the empty-paths row as a real vault, so the wizard skipped its
229
+ // vault step and `--vault-name` was ignored. hasRealVault must be
230
+ // INSTANCE-aware: no mount path → not a real vault (the #478 empty-paths
231
+ // discrimination findExistingVault already uses).
232
+ const db = openHubDb(hubDbPath(h.dir));
233
+ try {
234
+ await createUser(db, "owner", "pw");
235
+ writeManifest(
236
+ {
237
+ services: [
238
+ {
239
+ name: "parachute-vault",
240
+ version: "0.7.4-rc.1",
241
+ port: 1940,
242
+ paths: [],
243
+ health: "/health",
244
+ },
245
+ ],
246
+ },
247
+ h.manifestPath,
248
+ );
249
+ const s = deriveWizardState({
250
+ db,
251
+ manifestPath: h.manifestPath,
252
+ readExposeStateFn: h.readExposeStateFn,
253
+ });
254
+ expect(s.hasRealVault).toBe(false);
255
+ expect(s.hasVault).toBe(false);
256
+ expect(s.step).toBe("vault");
257
+ } finally {
258
+ db.close();
259
+ }
260
+ });
261
+
262
+ test("real-version vault row WITH a mount path IS a real vault (positive control)", async () => {
263
+ // The counterpart to the empty-paths case: a genuinely-created vault carries
264
+ // a `/vault/<name>` path at a real version, so hasRealVault must be true and
265
+ // the wizard advances past the vault step.
266
+ const db = openHubDb(hubDbPath(h.dir));
267
+ try {
268
+ await createUser(db, "owner", "pw");
269
+ writeManifest(
270
+ {
271
+ services: [
272
+ {
273
+ name: "parachute-vault",
274
+ version: "0.7.4-rc.1",
275
+ port: 1940,
276
+ paths: ["/vault/e2e"],
277
+ health: "/health",
278
+ },
279
+ ],
280
+ },
281
+ h.manifestPath,
282
+ );
283
+ const s = deriveWizardState({
284
+ db,
285
+ manifestPath: h.manifestPath,
286
+ readExposeStateFn: h.readExposeStateFn,
287
+ });
288
+ expect(s.hasRealVault).toBe(true);
289
+ expect(s.hasVault).toBe(true);
290
+ expect(s.step).toBe("expose");
291
+ } finally {
292
+ db.close();
293
+ }
294
+ });
295
+
222
296
  test("expose step when admin + vault exist but expose mode not set yet (hub#268 Item 2)", async () => {
223
297
  const db = openHubDb(hubDbPath(h.dir));
224
298
  try {
@@ -1712,13 +1786,15 @@ describe("handleSetupVaultPost", () => {
1712
1786
  }
1713
1787
  });
1714
1788
 
1715
- test("idempotent — second POST while supervisor is running doesn't fire a second `bun add` (N2)", async () => {
1789
+ test("second POST while supervisor is running but vault UNREGISTERED synthesize success, no 2nd bun add, no double provision (N2 race)", async () => {
1716
1790
  // Reviewer-flagged race: two concurrent POSTs before either seeds
1717
- // services.json both pass `state.hasVault === false` and each fire
1718
- // `runInstall` each fires `bun add -g`. The wizard mirrors the
1719
- // `handleInstall` guard pattern: if the supervisor already has a
1720
- // live (starting/running/restarting) state for vault, mark the new
1721
- // op succeeded synchronously and skip the second install.
1791
+ // services.json both pass `state.hasVault === false`. The supervisor is
1792
+ // already running vault (a prior POST's runInstall spawned it) but the
1793
+ // services.json row hasn't been written yet so `vaultAlreadyRegistered`
1794
+ // is false. The wizard must NOT fire a second `bun add -g` AND must NOT
1795
+ // fire `provisionVault` (which would `parachute install vault` a second
1796
+ // time in this pre-seed window — reviewer N2). Instead it synthesizes a
1797
+ // succeeded op and lets the in-flight install create the vault.
1722
1798
  const db = openHubDb(hubDbPath(h.dir));
1723
1799
  try {
1724
1800
  const user = await createUser(db, "owner", "pw");
@@ -1734,8 +1810,9 @@ describe("handleSetupVaultPost", () => {
1734
1810
  });
1735
1811
  const csrf = setCookie(get, CSRF_COOKIE_NAME) ?? "";
1736
1812
  // Real supervisor with the never-exits spawn stub from makeSupervisor.
1737
- // Pre-spawn vault so `supervisor.get("vault").status === "starting"`
1738
- // by the time the wizard's POST runs.
1813
+ // Pre-spawn vault so `supervisor.get("vault").status === "starting"`.
1814
+ // The harness manifest is EMPTY (vault not registered) — the pre-seed
1815
+ // window.
1739
1816
  const supervisor = makeSupervisor();
1740
1817
  await supervisor.start({ short: "vault", cmd: ["bun", "noop"] });
1741
1818
  const runCalls: string[][] = [];
@@ -1743,6 +1820,17 @@ describe("handleSetupVaultPost", () => {
1743
1820
  runCalls.push([...cmd]);
1744
1821
  return 0;
1745
1822
  };
1823
+ // provisionVault must NOT be called in the unregistered race window.
1824
+ const provisionCalls: string[] = [];
1825
+ const provisionVaultImpl = async (name: string) => {
1826
+ provisionCalls.push(name);
1827
+ return {
1828
+ ok: true as const,
1829
+ created: true,
1830
+ entry: { name, url: `https://hub.example/vault/${name}`, version: "1.0.0" },
1831
+ createJson: null,
1832
+ };
1833
+ };
1746
1834
  const post = await handleSetupVaultPost(
1747
1835
  req("/admin/setup/vault", {
1748
1836
  method: "POST",
@@ -1763,17 +1851,17 @@ describe("handleSetupVaultPost", () => {
1763
1851
  supervisor,
1764
1852
  registry: getDefaultOperationsRegistry(),
1765
1853
  run: stubbedRun,
1854
+ provisionVaultImpl,
1766
1855
  },
1767
1856
  );
1768
1857
  expect(post.status).toBe(303);
1769
1858
  const location = post.headers.get("location") ?? "";
1770
1859
  expect(location).toMatch(/^\/admin\/setup\?op=/);
1771
- // Yield enough for any background runInstall promise to fire if
1772
- // the guard failed. Then assert: no `bun add` was invoked, and
1773
- // the op went straight to `succeeded` with the canonical
1774
- // "already supervised" log line.
1860
+ // Yield for any background promise. Then assert: no `bun add`, NO
1861
+ // provisionVault call (no double-install), op synthesized succeeded.
1775
1862
  await new Promise((r) => setTimeout(r, 50));
1776
1863
  expect(runCalls.length).toBe(0);
1864
+ expect(provisionCalls).toEqual([]);
1777
1865
  const opId = new URL(location, "http://x").searchParams.get("op") ?? "";
1778
1866
  const op = getDefaultOperationsRegistry().get(opId);
1779
1867
  expect(op?.status).toBe("succeeded");
@@ -1783,6 +1871,200 @@ describe("handleSetupVaultPost", () => {
1783
1871
  }
1784
1872
  });
1785
1873
 
1874
+ test("boot-spawned vault child (real version, paths:[]) — wizard create actually provisions the named vault (e2e wizard-skip fix)", async () => {
1875
+ // The exact Tier-1 e2e bug. The hub boot-spawns the vault MODULE child so it
1876
+ // can selfRegister; that child writes a services.json row at the REAL
1877
+ // package version (NOT SEED_VERSION) with `paths: []` because no vault
1878
+ // instance exists yet, AND the supervisor already reports vault "running".
1879
+ // Pre-fix: hasRealVault read the real-version row as a real vault, the
1880
+ // wizard skipped its vault step, and even if it POSTed, the already-
1881
+ // supervised short-circuit synthesized a succeeded op that created NOTHING
1882
+ // — `--vault-name e2e` was silently dropped. Post-fix: the empty-paths row
1883
+ // reads as no-real-vault (hasRealVault=false), the POST is honored, and the
1884
+ // short-circuit drives provisionVault to actually create the named vault.
1885
+ const db = openHubDb(hubDbPath(h.dir));
1886
+ try {
1887
+ const user = await createUser(db, "owner", "pw");
1888
+ const { createSession, SESSION_COOKIE_NAME: SC } = await import("../sessions.ts");
1889
+ const session = createSession(db, { userId: user.id });
1890
+ // Boot-spawned child's self-registered row: real version, paths: [].
1891
+ writeManifest(
1892
+ {
1893
+ services: [
1894
+ {
1895
+ name: "parachute-vault",
1896
+ version: "0.7.4-rc.1",
1897
+ port: 1940,
1898
+ paths: [],
1899
+ health: "/health",
1900
+ },
1901
+ ],
1902
+ },
1903
+ h.manifestPath,
1904
+ );
1905
+ // deriveWizardState must NOT treat the empty-paths real-version row as a
1906
+ // real vault (this is what un-skipped the wizard's vault step).
1907
+ const derived = deriveWizardState({
1908
+ db,
1909
+ manifestPath: h.manifestPath,
1910
+ readExposeStateFn: h.readExposeStateFn,
1911
+ });
1912
+ expect(derived.hasRealVault).toBe(false);
1913
+ expect(derived.step).toBe("vault");
1914
+
1915
+ const get = handleSetupGet(req("/admin/setup"), {
1916
+ db,
1917
+ manifestPath: h.manifestPath,
1918
+ configDir: h.dir,
1919
+ readExposeStateFn: h.readExposeStateFn,
1920
+ issuer: "https://hub.example",
1921
+ registry: getDefaultOperationsRegistry(),
1922
+ });
1923
+ const csrf = setCookie(get, CSRF_COOKIE_NAME) ?? "";
1924
+ // Supervisor already running the boot-spawned vault module child.
1925
+ const supervisor = makeSupervisor();
1926
+ await supervisor.start({ short: "vault", cmd: ["bun", "noop"] });
1927
+ const runCalls: string[][] = [];
1928
+ const provisionCalls: string[] = [];
1929
+ const provisionVaultImpl = async (name: string) => {
1930
+ provisionCalls.push(name);
1931
+ return {
1932
+ ok: true as const,
1933
+ created: true,
1934
+ entry: { name, url: `https://hub.example/vault/${name}`, version: "0.7.4-rc.1" },
1935
+ createJson: null,
1936
+ };
1937
+ };
1938
+ const post = await handleSetupVaultPost(
1939
+ req("/admin/setup/vault", {
1940
+ method: "POST",
1941
+ body: new URLSearchParams({
1942
+ [CSRF_FIELD_NAME]: csrf,
1943
+ mode: "create",
1944
+ vault_name: "e2e",
1945
+ }).toString(),
1946
+ headers: {
1947
+ "content-type": "application/x-www-form-urlencoded",
1948
+ cookie: `${CSRF_COOKIE_NAME}=${csrf}; ${SC}=${session.id}`,
1949
+ },
1950
+ }),
1951
+ {
1952
+ db,
1953
+ manifestPath: h.manifestPath,
1954
+ configDir: h.dir,
1955
+ readExposeStateFn: h.readExposeStateFn,
1956
+ issuer: "https://hub.example",
1957
+ supervisor,
1958
+ registry: getDefaultOperationsRegistry(),
1959
+ run: async (cmd) => {
1960
+ runCalls.push([...cmd]);
1961
+ return 0;
1962
+ },
1963
+ provisionVaultImpl,
1964
+ },
1965
+ );
1966
+ expect(post.status).toBe(303);
1967
+ const location = post.headers.get("location") ?? "";
1968
+ expect(location).toMatch(/^\/admin\/setup\?op=/);
1969
+ await new Promise((r) => setTimeout(r, 50));
1970
+ // The named vault WAS provisioned (not silently dropped) …
1971
+ expect(provisionCalls).toEqual(["e2e"]);
1972
+ // … and no `bun add -g` fired (module already supervised).
1973
+ expect(runCalls.some((c) => c.join(" ").includes("bun add -g"))).toBe(false);
1974
+ const opId = new URL(location, "http://x").searchParams.get("op") ?? "";
1975
+ const op = getDefaultOperationsRegistry().get(opId);
1976
+ expect(op?.status).toBe("succeeded");
1977
+ expect(op?.log.join("\n")).toContain('vault "e2e" created');
1978
+ } finally {
1979
+ db.close();
1980
+ }
1981
+ });
1982
+
1983
+ test("import mode on an already-supervised box is REJECTED (no empty vault + fake success) — review must-fix", async () => {
1984
+ // On a boot-spawned box (vault module already supervised), the short-circuit
1985
+ // create path provisions an EMPTY vault. An IMPORT request must NOT be run
1986
+ // through it: that would leave an empty vault named after the import target
1987
+ // + a false "vault created" banner (a data-integrity footgun). The wizard
1988
+ // rejects import with an actionable message + a FAILED op instead.
1989
+ const db = openHubDb(hubDbPath(h.dir));
1990
+ try {
1991
+ const user = await createUser(db, "owner", "pw");
1992
+ const { createSession, SESSION_COOKIE_NAME: SC } = await import("../sessions.ts");
1993
+ const session = createSession(db, { userId: user.id });
1994
+ writeManifest(
1995
+ {
1996
+ services: [
1997
+ {
1998
+ name: "parachute-vault",
1999
+ version: "0.7.4-rc.1",
2000
+ port: 1940,
2001
+ paths: [],
2002
+ health: "/health",
2003
+ },
2004
+ ],
2005
+ },
2006
+ h.manifestPath,
2007
+ );
2008
+ const get = handleSetupGet(req("/admin/setup"), {
2009
+ db,
2010
+ manifestPath: h.manifestPath,
2011
+ configDir: h.dir,
2012
+ readExposeStateFn: h.readExposeStateFn,
2013
+ issuer: "https://hub.example",
2014
+ registry: getDefaultOperationsRegistry(),
2015
+ });
2016
+ const csrf = setCookie(get, CSRF_COOKIE_NAME) ?? "";
2017
+ const supervisor = makeSupervisor();
2018
+ await supervisor.start({ short: "vault", cmd: ["bun", "noop"] });
2019
+ const provisionCalls: string[] = [];
2020
+ const post = await handleSetupVaultPost(
2021
+ req("/admin/setup/vault", {
2022
+ method: "POST",
2023
+ headers: {
2024
+ accept: "application/json",
2025
+ "content-type": "application/json",
2026
+ cookie: `${CSRF_COOKIE_NAME}=${csrf}; ${SC}=${session.id}`,
2027
+ },
2028
+ body: JSON.stringify({
2029
+ [CSRF_FIELD_NAME]: csrf,
2030
+ mode: "import",
2031
+ vault_name: "fromgit",
2032
+ remote_url: "https://example.com/repo.git",
2033
+ import_mode: "merge",
2034
+ }),
2035
+ }),
2036
+ {
2037
+ db,
2038
+ manifestPath: h.manifestPath,
2039
+ configDir: h.dir,
2040
+ readExposeStateFn: h.readExposeStateFn,
2041
+ issuer: "https://hub.example",
2042
+ supervisor,
2043
+ registry: getDefaultOperationsRegistry(),
2044
+ provisionVaultImpl: async (name: string) => {
2045
+ provisionCalls.push(name);
2046
+ return {
2047
+ ok: true as const,
2048
+ created: true,
2049
+ entry: { name, url: `https://hub.example/vault/${name}`, version: "0.7.4-rc.1" },
2050
+ createJson: null,
2051
+ };
2052
+ },
2053
+ },
2054
+ );
2055
+ expect(post.status).toBe(200);
2056
+ const body = (await post.json()) as { op_id?: string; message?: string };
2057
+ expect(body.message ?? "").toContain("isn't supported");
2058
+ // Import was NOT provisioned (no empty vault created).
2059
+ expect(provisionCalls).toEqual([]);
2060
+ // The op is FAILED, not a fake success.
2061
+ const op = body.op_id ? getDefaultOperationsRegistry().get(body.op_id) : undefined;
2062
+ expect(op?.status).toBe("failed");
2063
+ } finally {
2064
+ db.close();
2065
+ }
2066
+ });
2067
+
1786
2068
  test("create on a SEED-placeholder box: does NOT short-circuit + drives the supervisor to start vault (hub#607 + hub#608)", async () => {
1787
2069
  // hub#607 + hub#608 coupled fresh-operator flow. On an init'd box,
1788
2070
  // services.json already carries a `parachute-vault` placeholder at
@@ -2066,19 +2348,16 @@ describe("handleSetupVaultPost", () => {
2066
2348
  test("scribe transcribe=local resolves to the host platform's backend (the Linux trap fix)", async () => {
2067
2349
  // The bug: `local` mapped UNCONDITIONALLY to parakeet-mlx (macOS-only),
2068
2350
  // silently broken on every Linux box. After the fix it resolves to the
2069
- // platform backend. We assert against the actual host so it's correct on
2070
- // both Mac (parakeet-mlx) and Linux CI (onnx-asr). The RAM gate only kicks
2071
- // in below 2 GB — CI/dev boxes clear it, so the choice stays `local`.
2072
- const { platformLocalProvider } = await import("../scribe-config.ts");
2073
- const expected = platformLocalProvider(process.platform);
2351
+ // platform backend. The expectation uses the same host RAM decision so
2352
+ // constrained CI boxes correctly assert the cloud fallback instead.
2353
+ const { decideLocalProvider, readAvailableRamMib } = await import("../scribe-config.ts");
2354
+ const decision = decideLocalProvider(process.platform, readAvailableRamMib());
2074
2355
  const { response } = await postVaultWithFields(h, { scribe_provider: "local" });
2075
2356
  expect(response.status).toBe(303);
2076
2357
  const cfg = readScribeConfig(h.dir);
2077
- if (expected !== null) {
2078
- // On a supported platform with enough RAM, `local` resolves to the
2079
- // platform backend — NOT a hardcoded parakeet-mlx on Linux.
2080
- expect(cfg?.transcribe).toEqual({ provider: expected });
2081
- }
2358
+ expect(cfg?.transcribe).toEqual({
2359
+ provider: decision.ok ? decision.provider : decision.steerTo,
2360
+ });
2082
2361
  });
2083
2362
  });
2084
2363
 
@@ -185,18 +185,14 @@ describe("setup", () => {
185
185
  }
186
186
  });
187
187
 
188
- test("fresh box: the offered 'Available to install' list excludes deprecated notes + removed runner", async () => {
188
+ test("fresh box: the offered list excludes deprecated notes and removed Agent/runner", async () => {
189
189
  const h = makeHarness();
190
190
  try {
191
- // 'all' picks every OFFERED service. With a clean services.json the survey
192
- // sees every known short; the offered filter must drop notes (deprecated,
193
- // 2026-06-25) while runner never even reaches the survey (registry
194
- // removal, 2026-07-01) — keeping vault/scribe/surface/agent. Only vault +
195
- // scribe have pre-install follow-up prompts (vault name, scribe provider);
196
- // surface + agent have none — so the scripted answers below are complete.
191
+ // 'all' picks every offered service. The clean-box offer excludes notes
192
+ // (deprecated) and retired Agent/runner, keeping vault/scribe/surface/app.
197
193
  const availability = scriptedAvailability([
198
- "all", // pick everything offered
199
- "default", // vault name (vault is in the offered set)
194
+ "all",
195
+ "default", // vault name
200
196
  "1", // scribe provider
201
197
  ]);
202
198
  const code = await setup({
@@ -210,20 +206,19 @@ describe("setup", () => {
210
206
  },
211
207
  });
212
208
  expect(code).toBe(0);
213
- // The "Available to install" banner must NOT list notes / runner.
214
209
  const joined = h.logs.join("\n");
215
210
  const availableBlock = joined.slice(joined.indexOf("Available to install:"));
216
211
  expect(availableBlock).not.toMatch(/\bnotes\b/);
217
212
  expect(availableBlock).not.toMatch(/\brunner\b/);
218
- // …and `install()` is never invoked for the deprecated shorts.
213
+ expect(availableBlock).not.toMatch(/\bagent\b/);
219
214
  const installedShorts = h.calls.map((c) => c.short);
220
215
  expect(installedShorts).not.toContain("notes");
221
216
  expect(installedShorts).not.toContain("runner");
222
- // The non-deprecated set is still offered + installed.
217
+ expect(installedShorts).not.toContain("agent");
223
218
  expect(installedShorts).toContain("vault");
224
219
  expect(installedShorts).toContain("scribe");
225
220
  expect(installedShorts).toContain("surface");
226
- expect(installedShorts).toContain("agent");
221
+ expect(installedShorts).toContain("app");
227
222
  } finally {
228
223
  h.cleanup();
229
224
  }
@@ -225,11 +225,6 @@ describe("status — per-module URL deep-links (manifestRowBase / urlForEntry)",
225
225
  expect(out).toMatch(/:5173\/notes/);
226
226
  });
227
227
 
228
- test("channel row prints port + /channel mount", async () => {
229
- const out = await urlFor("parachute-channel", 1943, ["/channel"]);
230
- expect(out).toMatch(/:1943\/channel/);
231
- });
232
-
233
228
  test("unknown third-party service falls back to bare host:port + paths[0]", async () => {
234
229
  const { path, configDir, cleanup } = makeTempPath();
235
230
  try {