@openparachute/hub 0.5.14-rc.2 → 0.5.14-rc.4
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-config.test.ts +16 -10
- package/src/__tests__/api-modules.test.ts +92 -75
- package/src/__tests__/setup-wizard.test.ts +76 -41
- package/src/__tests__/vault-auth-status.test.ts +271 -11
- package/src/api-modules.ts +29 -12
- package/src/setup-wizard.ts +32 -85
- package/src/vault/auth-status.ts +145 -22
package/package.json
CHANGED
|
@@ -23,6 +23,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
23
23
|
import { tmpdir } from "node:os";
|
|
24
24
|
import { join } from "node:path";
|
|
25
25
|
import { decodeJwt } from "jose";
|
|
26
|
+
import type { CuratedModuleShort } from "../api-modules.ts";
|
|
26
27
|
import {
|
|
27
28
|
API_MODULES_CONFIG_REQUIRED_SCOPE,
|
|
28
29
|
MODULE_CONFIG_PROXY_CLIENT_ID,
|
|
@@ -152,14 +153,19 @@ describe("parseModulesConfigPath", () => {
|
|
|
152
153
|
});
|
|
153
154
|
});
|
|
154
155
|
|
|
155
|
-
test("matches vault and
|
|
156
|
+
test("matches vault and scribe (curated modules)", () => {
|
|
156
157
|
expect(parseModulesConfigPath("/api/modules/vault/config")?.short).toBe("vault");
|
|
157
|
-
expect(parseModulesConfigPath("/api/modules/
|
|
158
|
+
expect(parseModulesConfigPath("/api/modules/scribe/config/schema")?.short).toBe("scribe");
|
|
158
159
|
});
|
|
159
160
|
|
|
160
161
|
test("rejects unknown short (non-curated)", () => {
|
|
161
162
|
expect(parseModulesConfigPath("/api/modules/unknown/config")).toBeUndefined();
|
|
162
163
|
expect(parseModulesConfigPath("/api/modules/channel/config")).toBeUndefined();
|
|
164
|
+
// Curated list trimmed 2026-05-27: notes / runner / surface are no
|
|
165
|
+
// longer curated and reject at the parse boundary.
|
|
166
|
+
expect(parseModulesConfigPath("/api/modules/notes/config")).toBeUndefined();
|
|
167
|
+
expect(parseModulesConfigPath("/api/modules/runner/config")).toBeUndefined();
|
|
168
|
+
expect(parseModulesConfigPath("/api/modules/surface/config")).toBeUndefined();
|
|
163
169
|
});
|
|
164
170
|
|
|
165
171
|
test("rejects non-config suffix shapes", () => {
|
|
@@ -302,7 +308,7 @@ describe("handleApiModulesConfig — FALLBACK retirement (hub#310)", () => {
|
|
|
302
308
|
makeReq("/api/modules/runner/config/schema", {
|
|
303
309
|
headers: { authorization: `Bearer ${bearer}` },
|
|
304
310
|
}),
|
|
305
|
-
{ short: "runner", suffix: "schema" },
|
|
311
|
+
{ short: "runner" as CuratedModuleShort, suffix: "schema" },
|
|
306
312
|
{ db: h.db, issuer: ISSUER, manifestPath: h.manifestPath },
|
|
307
313
|
);
|
|
308
314
|
expect(res.status).toBe(404);
|
|
@@ -365,7 +371,7 @@ describe("handleApiModulesConfig — FALLBACK retirement (hub#310)", () => {
|
|
|
365
371
|
makeReq("/api/modules/runner/config/schema", {
|
|
366
372
|
headers: { authorization: `Bearer ${bearer}` },
|
|
367
373
|
}),
|
|
368
|
-
{ short: "runner", suffix: "schema" },
|
|
374
|
+
{ short: "runner" as CuratedModuleShort, suffix: "schema" },
|
|
369
375
|
{
|
|
370
376
|
db: h.db,
|
|
371
377
|
issuer: ISSUER,
|
|
@@ -614,7 +620,7 @@ describe("handleApiModulesConfig — stripPrefix=false (notes-shape)", () => {
|
|
|
614
620
|
makeReq("/api/modules/notes/config/schema", {
|
|
615
621
|
headers: { authorization: `Bearer ${bearer}` },
|
|
616
622
|
}),
|
|
617
|
-
{ short: "notes", suffix: "schema" },
|
|
623
|
+
{ short: "notes" as CuratedModuleShort, suffix: "schema" },
|
|
618
624
|
{
|
|
619
625
|
db: h.db,
|
|
620
626
|
issuer: ISSUER,
|
|
@@ -668,7 +674,7 @@ describe("handleApiModulesConfig — hostsBareParachute (hub#307)", () => {
|
|
|
668
674
|
makeReq("/api/modules/runner/config/schema", {
|
|
669
675
|
headers: { authorization: `Bearer ${bearer}` },
|
|
670
676
|
}),
|
|
671
|
-
{ short: "runner", suffix: "schema" },
|
|
677
|
+
{ short: "runner" as CuratedModuleShort, suffix: "schema" },
|
|
672
678
|
{
|
|
673
679
|
db: h.db,
|
|
674
680
|
issuer: ISSUER,
|
|
@@ -700,7 +706,7 @@ describe("handleApiModulesConfig — hostsBareParachute (hub#307)", () => {
|
|
|
700
706
|
makeReq("/api/modules/runner/config", {
|
|
701
707
|
headers: { authorization: `Bearer ${bearer}` },
|
|
702
708
|
}),
|
|
703
|
-
{ short: "runner", suffix: "" },
|
|
709
|
+
{ short: "runner" as CuratedModuleShort, suffix: "" },
|
|
704
710
|
{
|
|
705
711
|
db: h.db,
|
|
706
712
|
issuer: ISSUER,
|
|
@@ -733,7 +739,7 @@ describe("handleApiModulesConfig — hostsBareParachute (hub#307)", () => {
|
|
|
733
739
|
},
|
|
734
740
|
body: JSON.stringify({ intervalSeconds: 120 }),
|
|
735
741
|
}),
|
|
736
|
-
{ short: "runner", suffix: "" },
|
|
742
|
+
{ short: "runner" as CuratedModuleShort, suffix: "" },
|
|
737
743
|
{
|
|
738
744
|
db: h.db,
|
|
739
745
|
issuer: ISSUER,
|
|
@@ -760,7 +766,7 @@ describe("handleApiModulesConfig — hostsBareParachute (hub#307)", () => {
|
|
|
760
766
|
makeReq("/api/modules/runner/config", {
|
|
761
767
|
headers: { authorization: `Bearer ${bearer}` },
|
|
762
768
|
}),
|
|
763
|
-
{ short: "runner", suffix: "" },
|
|
769
|
+
{ short: "runner" as CuratedModuleShort, suffix: "" },
|
|
764
770
|
{
|
|
765
771
|
db: h.db,
|
|
766
772
|
issuer: ISSUER,
|
|
@@ -863,7 +869,7 @@ describe("handleApiModulesConfig — hostsBareParachute (hub#307)", () => {
|
|
|
863
869
|
makeReq("/api/modules/runner/config/schema", {
|
|
864
870
|
headers: { authorization: `Bearer ${bearer}` },
|
|
865
871
|
}),
|
|
866
|
-
{ short: "runner", suffix: "schema" },
|
|
872
|
+
{ short: "runner" as CuratedModuleShort, suffix: "schema" },
|
|
867
873
|
{
|
|
868
874
|
db: h.db,
|
|
869
875
|
issuer: ISSUER,
|
|
@@ -149,10 +149,12 @@ describe("GET /api/modules", () => {
|
|
|
149
149
|
|
|
150
150
|
test("200 + curated list on fresh container (empty services.json)", async () => {
|
|
151
151
|
// The v0.6 hot path: brand-new Render container, no services.json
|
|
152
|
-
// yet. UI must render "install vault /
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
152
|
+
// yet. UI must render "install vault / scribe" cards even though
|
|
153
|
+
// nothing's installed. Trimmed 2026-05-27 (Aaron-directed launch
|
|
154
|
+
// focus): notes (notes-daemon), surface (host module), and runner
|
|
155
|
+
// (experimental) are no longer curated — notes.parachute.computer
|
|
156
|
+
// is the hosted PWA, surface-client is the library for custom UI
|
|
157
|
+
// builders, and runner isn't in the launch focus set.
|
|
156
158
|
const bearer = await mintBearer(h, [API_MODULES_REQUIRED_SCOPE]);
|
|
157
159
|
const res = await handleApiModules(getReq({ authorization: `Bearer ${bearer}` }), {
|
|
158
160
|
db: h.db,
|
|
@@ -170,8 +172,10 @@ describe("GET /api/modules", () => {
|
|
|
170
172
|
}>;
|
|
171
173
|
supervisor_available: boolean;
|
|
172
174
|
};
|
|
173
|
-
// Curated order is preserved: vault →
|
|
174
|
-
|
|
175
|
+
// Curated order is preserved: vault → scribe (vault first per the
|
|
176
|
+
// recommended install order — the wizard's vault step already runs
|
|
177
|
+
// before this catalog surfaces).
|
|
178
|
+
expect(body.modules.map((m) => m.short)).toEqual(["vault", "scribe"]);
|
|
175
179
|
expect(body.modules.every((m) => m.available)).toBe(true);
|
|
176
180
|
expect(body.modules.every((m) => !m.installed)).toBe(true);
|
|
177
181
|
expect(body.modules.every((m) => m.latest_version === "0.9.9")).toBe(true);
|
|
@@ -179,18 +183,25 @@ describe("GET /api/modules", () => {
|
|
|
179
183
|
expect(body.supervisor_available).toBe(false);
|
|
180
184
|
});
|
|
181
185
|
|
|
182
|
-
test("
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
186
|
+
test("scribe row carries package + display props from KNOWN_MODULES", async () => {
|
|
187
|
+
// Spot-check the wire shape resolves scribe-specific fields
|
|
188
|
+
// (package, displayName, tagline) from KNOWN_MODULES rather than a
|
|
189
|
+
// stale default. Vault is exercised via the install-state test below;
|
|
190
|
+
// this pins the other curated row's KNOWN_MODULES round-trip.
|
|
191
|
+
//
|
|
192
|
+
// Pre-2026-05-27 this test pinned the `surface` row (added by
|
|
193
|
+
// hub#323), and a sibling pinned the `runner` FIRST_PARTY_FALLBACKS
|
|
194
|
+
// row (hub#305). Both modules retired from CURATED_MODULES — the
|
|
195
|
+
// FIRST_PARTY_FALLBACKS / KNOWN_MODULES entries persist for the
|
|
196
|
+
// install-bootstrap path but `/api/modules` doesn't return them.
|
|
197
|
+
// The "uncurated modules don't surface here" test below pins that
|
|
198
|
+
// boundary.
|
|
188
199
|
const bearer = await mintBearer(h, [API_MODULES_REQUIRED_SCOPE]);
|
|
189
200
|
const res = await handleApiModules(getReq({ authorization: `Bearer ${bearer}` }), {
|
|
190
201
|
db: h.db,
|
|
191
202
|
issuer: ISSUER,
|
|
192
203
|
manifestPath: h.manifestPath,
|
|
193
|
-
fetchLatestVersion: async () => "0.
|
|
204
|
+
fetchLatestVersion: async () => "0.4.4",
|
|
194
205
|
});
|
|
195
206
|
expect(res.status).toBe(200);
|
|
196
207
|
const body = (await res.json()) as {
|
|
@@ -202,42 +213,41 @@ describe("GET /api/modules", () => {
|
|
|
202
213
|
available: boolean;
|
|
203
214
|
}>;
|
|
204
215
|
};
|
|
205
|
-
const
|
|
206
|
-
expect(
|
|
207
|
-
expect(
|
|
208
|
-
expect(
|
|
209
|
-
expect(
|
|
210
|
-
expect(
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test("runner
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
216
|
+
const scribe = body.modules.find((m) => m.short === "scribe");
|
|
217
|
+
expect(scribe).toBeDefined();
|
|
218
|
+
expect(scribe?.package).toBe("@openparachute/scribe");
|
|
219
|
+
expect(scribe?.display_name).toBe("Scribe");
|
|
220
|
+
expect(scribe?.tagline).toContain("transcription");
|
|
221
|
+
expect(scribe?.available).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("uncurated modules (notes / runner / surface) are NOT returned by GET /api/modules", async () => {
|
|
225
|
+
// CURATED_MODULES was trimmed 2026-05-27 to [vault, scribe]. The
|
|
226
|
+
// KNOWN_MODULES + FIRST_PARTY_FALLBACKS registries still carry
|
|
227
|
+
// entries for notes / runner (install-bootstrap path), but
|
|
228
|
+
// /api/modules only returns CURATED rows. Pins the boundary so a
|
|
229
|
+
// future re-curation has to be intentional, not a stale registry
|
|
230
|
+
// leak.
|
|
218
231
|
const bearer = await mintBearer(h, [API_MODULES_REQUIRED_SCOPE]);
|
|
219
232
|
const res = await handleApiModules(getReq({ authorization: `Bearer ${bearer}` }), {
|
|
220
233
|
db: h.db,
|
|
221
234
|
issuer: ISSUER,
|
|
222
235
|
manifestPath: h.manifestPath,
|
|
223
|
-
fetchLatestVersion: async () =>
|
|
236
|
+
fetchLatestVersion: async () => null,
|
|
224
237
|
});
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
expect(
|
|
237
|
-
expect(
|
|
238
|
-
expect(runner?.display_name).toBe("Runner");
|
|
239
|
-
expect(runner?.tagline).toContain("Vault-as-job-substrate");
|
|
240
|
-
expect(runner?.available).toBe(true);
|
|
238
|
+
const body = (await res.json()) as { modules: Array<{ short: string }> };
|
|
239
|
+
const shorts = body.modules.map((m) => m.short);
|
|
240
|
+
// Positive shape assertion — stronger than `not.toContain` because
|
|
241
|
+
// it also catches "we accidentally added a new uncurated entry"
|
|
242
|
+
// and "we accidentally removed an existing curated entry." Update
|
|
243
|
+
// this assertion intentionally when CURATED_MODULES changes.
|
|
244
|
+
expect(shorts).toEqual(["vault", "scribe"]);
|
|
245
|
+
// Belt + suspenders: explicit negatives for the modules dropped
|
|
246
|
+
// 2026-05-27, so a developer regressing the curated list sees both
|
|
247
|
+
// the shape failure AND the named-module failure messages.
|
|
248
|
+
expect(shorts).not.toContain("notes");
|
|
249
|
+
expect(shorts).not.toContain("runner");
|
|
250
|
+
expect(shorts).not.toContain("surface");
|
|
241
251
|
});
|
|
242
252
|
|
|
243
253
|
test("surfaces installed_version from services.json", async () => {
|
|
@@ -272,11 +282,11 @@ describe("GET /api/modules", () => {
|
|
|
272
282
|
expect(vault?.installed_version).toBe("0.4.5");
|
|
273
283
|
expect(vault?.latest_version).toBe("0.5.0");
|
|
274
284
|
expect(vault?.install_dir).toBe("/parachute/modules/node_modules/@openparachute/vault");
|
|
275
|
-
// The other curated
|
|
276
|
-
// only vault, so
|
|
277
|
-
const
|
|
278
|
-
expect(
|
|
279
|
-
expect(
|
|
285
|
+
// The other curated row stays installed:false — the test installed
|
|
286
|
+
// only vault, so scribe still renders as available-but-not-installed.
|
|
287
|
+
const scribe = body.modules.find((m) => m.short === "scribe");
|
|
288
|
+
expect(scribe?.installed).toBe(false);
|
|
289
|
+
expect(scribe?.installed_version).toBeNull();
|
|
280
290
|
});
|
|
281
291
|
|
|
282
292
|
test("includes supervisor status + pid when a supervisor is injected", async () => {
|
|
@@ -309,9 +319,9 @@ describe("GET /api/modules", () => {
|
|
|
309
319
|
expect(vault?.pid).toBe(12345);
|
|
310
320
|
// Modules without a supervisor entry get null status — the UI
|
|
311
321
|
// disables Restart/Stop for those since there's no live process.
|
|
312
|
-
const
|
|
313
|
-
expect(
|
|
314
|
-
expect(
|
|
322
|
+
const scribe = body.modules.find((m) => m.short === "scribe");
|
|
323
|
+
expect(scribe?.supervisor_status).toBeNull();
|
|
324
|
+
expect(scribe?.pid).toBeNull();
|
|
315
325
|
expect(body.supervisor_available).toBe(true);
|
|
316
326
|
});
|
|
317
327
|
|
|
@@ -366,22 +376,28 @@ describe("GET /api/modules", () => {
|
|
|
366
376
|
});
|
|
367
377
|
|
|
368
378
|
test("management_url does not double-prepend mount when managementUrl is already mount-prefixed (hub#380)", async () => {
|
|
369
|
-
// Audit caught 2026-05-25:
|
|
379
|
+
// Audit caught 2026-05-25: surface declared `managementUrl: "/surface/admin/"`
|
|
370
380
|
// (full hub-origin path) and `paths: ["/surface", "/.parachute"]`. The
|
|
371
|
-
// SPA's Services dropdown was navigating to `/
|
|
372
|
-
// because api-modules unconditionally prepended the mount onto
|
|
373
|
-
// candidate. Fix: detect already-mount-prefixed paths and pass
|
|
381
|
+
// SPA's Services dropdown was navigating to `/surface/surface/admin/`
|
|
382
|
+
// (404) because api-modules unconditionally prepended the mount onto
|
|
383
|
+
// the candidate. Fix: detect already-mount-prefixed paths and pass
|
|
384
|
+
// through.
|
|
374
385
|
//
|
|
375
386
|
// Single-instance modules conventionally declare the full path; only
|
|
376
387
|
// multi-instance modules (vault) use the per-instance relative form.
|
|
388
|
+
// Post 2026-05-27 CURATED trim the canonical single-instance example
|
|
389
|
+
// is scribe (when scribe ships a managementUrl — scribe#53). For now
|
|
390
|
+
// we exercise the same code path with vault declaring an
|
|
391
|
+
// already-mount-prefixed managementUrl: any module whose declared
|
|
392
|
+
// URL starts with its mount must pass through unchanged.
|
|
377
393
|
writeManifest(h.manifestPath, [
|
|
378
394
|
{
|
|
379
|
-
name: "parachute-
|
|
380
|
-
port:
|
|
381
|
-
paths: ["/
|
|
382
|
-
health: "/
|
|
383
|
-
version: "0.
|
|
384
|
-
installDir: "/install/dir/
|
|
395
|
+
name: "parachute-vault",
|
|
396
|
+
port: 1940,
|
|
397
|
+
paths: ["/vault/default"],
|
|
398
|
+
health: "/vault/default/health",
|
|
399
|
+
version: "0.4.5",
|
|
400
|
+
installDir: "/install/dir/vault",
|
|
385
401
|
},
|
|
386
402
|
]);
|
|
387
403
|
const bearer = await mintBearer(h, [API_MODULES_REQUIRED_SCOPE]);
|
|
@@ -391,17 +407,18 @@ describe("GET /api/modules", () => {
|
|
|
391
407
|
manifestPath: h.manifestPath,
|
|
392
408
|
fetchLatestVersion: async () => null,
|
|
393
409
|
readModuleManifest: async (installDir) => {
|
|
394
|
-
if (installDir === "/install/dir/
|
|
410
|
+
if (installDir === "/install/dir/vault") {
|
|
395
411
|
return {
|
|
396
|
-
name: "
|
|
397
|
-
manifestName: "parachute-
|
|
398
|
-
displayName: "
|
|
412
|
+
name: "parachute-vault",
|
|
413
|
+
manifestName: "parachute-vault",
|
|
414
|
+
displayName: "Vault",
|
|
399
415
|
tagline: "",
|
|
400
|
-
port:
|
|
401
|
-
paths: ["/
|
|
402
|
-
health: "/
|
|
403
|
-
|
|
404
|
-
|
|
416
|
+
port: 1940,
|
|
417
|
+
paths: ["/vault/default"],
|
|
418
|
+
health: "/vault/default/health",
|
|
419
|
+
// Already-mount-prefixed managementUrl — must NOT have the
|
|
420
|
+
// mount prepended again.
|
|
421
|
+
managementUrl: "/vault/default/admin/",
|
|
405
422
|
} as unknown as Awaited<
|
|
406
423
|
ReturnType<typeof import("../module-manifest.ts").readModuleManifest>
|
|
407
424
|
>;
|
|
@@ -413,9 +430,9 @@ describe("GET /api/modules", () => {
|
|
|
413
430
|
const body = (await res.json()) as {
|
|
414
431
|
modules: Array<{ short: string; management_url: string | null }>;
|
|
415
432
|
};
|
|
416
|
-
const
|
|
417
|
-
// Single `/
|
|
418
|
-
expect(
|
|
433
|
+
const vault = body.modules.find((m) => m.short === "vault");
|
|
434
|
+
// Single `/vault/default/`, not `/vault/default/vault/default/`.
|
|
435
|
+
expect(vault?.management_url).toBe("/vault/default/admin/");
|
|
419
436
|
});
|
|
420
437
|
|
|
421
438
|
test("management_url prefix-ish names don't collide (hub#380 — /app vs /app-foo)", async () => {
|
|
@@ -786,8 +803,8 @@ describe("GET /api/modules", () => {
|
|
|
786
803
|
},
|
|
787
804
|
]);
|
|
788
805
|
// Other curated rows stay empty — uis is per-row, not global.
|
|
789
|
-
const
|
|
790
|
-
expect(
|
|
806
|
+
const scribe = body.modules.find((m) => m.short === "scribe");
|
|
807
|
+
expect(scribe?.uis).toEqual([]);
|
|
791
808
|
});
|
|
792
809
|
|
|
793
810
|
test("optional fields ride through verbatim, missing fields become null on the wire", async () => {
|
|
@@ -2162,6 +2162,9 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2162
2162
|
const db = openHubDb(hubDbPath(h.dir));
|
|
2163
2163
|
try {
|
|
2164
2164
|
const user = await createUser(db, "owner", "pw");
|
|
2165
|
+
// Seed services.json with `parachute-scribe` so the wizard's scribe
|
|
2166
|
+
// install tile renders the already-installed shape. Post-2026-05-27
|
|
2167
|
+
// CURATED trim scribe is the only non-vault install tile.
|
|
2165
2168
|
writeManifest(
|
|
2166
2169
|
{
|
|
2167
2170
|
services: [
|
|
@@ -2172,15 +2175,12 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2172
2175
|
paths: ["/vault/default"],
|
|
2173
2176
|
health: "/health",
|
|
2174
2177
|
},
|
|
2175
|
-
// hub#323: app replaces notes as the wizard's first install tile.
|
|
2176
|
-
// Seeding services.json with `parachute-app` exercises the
|
|
2177
|
-
// already-installed render path on the wizard's first tile.
|
|
2178
2178
|
{
|
|
2179
|
-
name: "parachute-
|
|
2180
|
-
version: "0.
|
|
2181
|
-
port:
|
|
2182
|
-
paths: ["/
|
|
2183
|
-
health: "/
|
|
2179
|
+
name: "parachute-scribe",
|
|
2180
|
+
version: "0.4.4",
|
|
2181
|
+
port: 1943,
|
|
2182
|
+
paths: ["/scribe"],
|
|
2183
|
+
health: "/scribe/health",
|
|
2184
2184
|
},
|
|
2185
2185
|
],
|
|
2186
2186
|
},
|
|
@@ -2203,13 +2203,16 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2203
2203
|
);
|
|
2204
2204
|
const html = await res.text();
|
|
2205
2205
|
expect(html).toContain("Already installed");
|
|
2206
|
-
|
|
2206
|
+
// The scribe tile rendered the installed shape, not the install form.
|
|
2207
|
+
expect(html).not.toContain('action="/admin/setup/install/scribe"');
|
|
2208
|
+
// "Manage in admin" is the secondary link on the already-installed tile.
|
|
2209
|
+
expect(html).toContain("Manage in admin");
|
|
2207
2210
|
} finally {
|
|
2208
2211
|
db.close();
|
|
2209
2212
|
}
|
|
2210
2213
|
});
|
|
2211
2214
|
|
|
2212
|
-
test("done screen renders op-poll panel when ?
|
|
2215
|
+
test("done screen renders op-poll panel when ?op_scribe=<id> matches a registry op", async () => {
|
|
2213
2216
|
const db = openHubDb(hubDbPath(h.dir));
|
|
2214
2217
|
try {
|
|
2215
2218
|
const user = await createUser(db, "owner", "pw");
|
|
@@ -2229,15 +2232,16 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2229
2232
|
);
|
|
2230
2233
|
setSetting(db, "setup_expose_mode", "localhost");
|
|
2231
2234
|
const reg = getDefaultOperationsRegistry();
|
|
2232
|
-
//
|
|
2233
|
-
//
|
|
2234
|
-
//
|
|
2235
|
-
|
|
2236
|
-
reg.
|
|
2235
|
+
// Post-2026-05-27 CURATED trim, scribe is the only non-vault wizard
|
|
2236
|
+
// install tile, so it carries the op-poll panel. Same shape as the
|
|
2237
|
+
// prior `op_app=<id>` / `op_notes=<id>` flows — the rendering code
|
|
2238
|
+
// is per-`?op_<short>=<id>` query and tile-row agnostic.
|
|
2239
|
+
const op = reg.create("install", "scribe");
|
|
2240
|
+
reg.update(op.id, { status: "running" }, "running bun add -g @openparachute/scribe@latest");
|
|
2237
2241
|
const { createSession } = await import("../sessions.ts");
|
|
2238
2242
|
const session = createSession(db, { userId: user.id });
|
|
2239
2243
|
const res = handleSetupGet(
|
|
2240
|
-
req(`/admin/setup?just_finished=1&
|
|
2244
|
+
req(`/admin/setup?just_finished=1&op_scribe=${op.id}`, {
|
|
2241
2245
|
headers: { cookie: `${SESSION_COOKIE_NAME}=${session.id}` },
|
|
2242
2246
|
}),
|
|
2243
2247
|
{
|
|
@@ -2293,7 +2297,7 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2293
2297
|
return 0;
|
|
2294
2298
|
};
|
|
2295
2299
|
const post = await handleSetupInstallPost(
|
|
2296
|
-
req("/admin/setup/install/
|
|
2300
|
+
req("/admin/setup/install/scribe", {
|
|
2297
2301
|
method: "POST",
|
|
2298
2302
|
body: new URLSearchParams({ [CSRF_FIELD_NAME]: csrf }).toString(),
|
|
2299
2303
|
headers: {
|
|
@@ -2301,7 +2305,7 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2301
2305
|
cookie: `${CSRF_COOKIE_NAME}=${csrf}; ${SESSION_COOKIE_NAME}=${session.id}`,
|
|
2302
2306
|
},
|
|
2303
2307
|
}),
|
|
2304
|
-
"
|
|
2308
|
+
"scribe",
|
|
2305
2309
|
{
|
|
2306
2310
|
db,
|
|
2307
2311
|
manifestPath: h.manifestPath,
|
|
@@ -2315,10 +2319,10 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2315
2319
|
);
|
|
2316
2320
|
expect(post.status).toBe(303);
|
|
2317
2321
|
const location = post.headers.get("location") ?? "";
|
|
2318
|
-
expect(location).toMatch(/^\/admin\/setup\?just_finished=1&
|
|
2322
|
+
expect(location).toMatch(/^\/admin\/setup\?just_finished=1&op_scribe=/);
|
|
2319
2323
|
await new Promise((r) => setTimeout(r, 50));
|
|
2320
2324
|
expect(runCalls.length).toBeGreaterThan(0);
|
|
2321
|
-
expect(runCalls[0]?.join(" ")).toContain("bun add -g @openparachute/
|
|
2325
|
+
expect(runCalls[0]?.join(" ")).toContain("bun add -g @openparachute/scribe@latest");
|
|
2322
2326
|
} finally {
|
|
2323
2327
|
db.close();
|
|
2324
2328
|
}
|
|
@@ -2405,7 +2409,7 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2405
2409
|
});
|
|
2406
2410
|
const csrf = setCookie(get, CSRF_COOKIE_NAME) ?? "";
|
|
2407
2411
|
const post = await handleSetupInstallPost(
|
|
2408
|
-
req("/admin/setup/install/
|
|
2412
|
+
req("/admin/setup/install/scribe", {
|
|
2409
2413
|
method: "POST",
|
|
2410
2414
|
body: new URLSearchParams({ [CSRF_FIELD_NAME]: csrf }).toString(),
|
|
2411
2415
|
headers: {
|
|
@@ -2413,7 +2417,7 @@ describe("done screen install tiles (hub#272 Item B)", () => {
|
|
|
2413
2417
|
cookie: `${CSRF_COOKIE_NAME}=${csrf}`,
|
|
2414
2418
|
},
|
|
2415
2419
|
}),
|
|
2416
|
-
"
|
|
2420
|
+
"scribe",
|
|
2417
2421
|
{
|
|
2418
2422
|
db,
|
|
2419
2423
|
manifestPath: h.manifestPath,
|
|
@@ -3296,7 +3300,14 @@ describe("done screen — 'Start using your vault' tile (hub#342)", () => {
|
|
|
3296
3300
|
}
|
|
3297
3301
|
});
|
|
3298
3302
|
|
|
3299
|
-
test("
|
|
3303
|
+
test("lead tile always points at notes.parachute.computer (canonical hosted PWA) regardless of local module installs", async () => {
|
|
3304
|
+
// Pre-2026-05-27 the lead tile flipped to `/surface/notes/` when the
|
|
3305
|
+
// Surface module was installed locally. Aaron's launch-focus
|
|
3306
|
+
// directive: notes.parachute.computer is the canonical user-facing
|
|
3307
|
+
// UI, and the wizard should always point operators at it (rather
|
|
3308
|
+
// than maybe-or-maybe-not-installed local Surface). This test pins
|
|
3309
|
+
// that the lead tile is invariant under the install state of
|
|
3310
|
+
// uncurated modules.
|
|
3300
3311
|
const db = openHubDb(hubDbPath(h.dir));
|
|
3301
3312
|
try {
|
|
3302
3313
|
const user = await createUser(db, "owner", "pw");
|
|
@@ -3310,6 +3321,9 @@ describe("done screen — 'Start using your vault' tile (hub#342)", () => {
|
|
|
3310
3321
|
paths: ["/vault/default"],
|
|
3311
3322
|
health: "/health",
|
|
3312
3323
|
},
|
|
3324
|
+
// Even with parachute-surface installed locally (an uncurated
|
|
3325
|
+
// module post-trim), the lead tile must NOT flip to a local
|
|
3326
|
+
// path.
|
|
3313
3327
|
{
|
|
3314
3328
|
name: "parachute-surface",
|
|
3315
3329
|
version: "0.2.0",
|
|
@@ -3338,15 +3352,27 @@ describe("done screen — 'Start using your vault' tile (hub#342)", () => {
|
|
|
3338
3352
|
);
|
|
3339
3353
|
const html = await res.text();
|
|
3340
3354
|
expect(html).toContain("Start using your vault");
|
|
3341
|
-
//
|
|
3342
|
-
expect(html).toContain(
|
|
3355
|
+
// Lead CTA always targets the hosted PWA.
|
|
3356
|
+
expect(html).toContain("https://notes.parachute.computer/add?url=");
|
|
3343
3357
|
expect(html).toContain("Open Notes");
|
|
3358
|
+
// The pre-trim local-surface fallback is gone — the lead tile does
|
|
3359
|
+
// NOT link to /surface/notes/ anymore.
|
|
3360
|
+
expect(html).not.toContain('href="/surface/notes/"');
|
|
3344
3361
|
} finally {
|
|
3345
3362
|
db.close();
|
|
3346
3363
|
}
|
|
3347
3364
|
});
|
|
3348
3365
|
|
|
3349
|
-
test("succeeded install op renders
|
|
3366
|
+
test("succeeded install op renders 'Manage modules' link (no 'Use it now' for modules without a hosted surface)", async () => {
|
|
3367
|
+
// Pre-2026-05-27 the surface module had a USE_IT_NOW_URLS entry
|
|
3368
|
+
// pointing at `/surface/notes/`, so a succeeded surface install tile
|
|
3369
|
+
// rendered a primary "Use it now" link. Post-trim only scribe + vault
|
|
3370
|
+
// are curated; vault has its own lead tile (above the install row);
|
|
3371
|
+
// scribe doesn't ship a user-facing landing surface today
|
|
3372
|
+
// (scribe#53 tracks the eventual admin SPA), so USE_IT_NOW_URLS is
|
|
3373
|
+
// empty and a succeeded scribe install renders only the "Manage
|
|
3374
|
+
// modules" secondary affordance. Future per-module surfaces can
|
|
3375
|
+
// re-add an entry to that map.
|
|
3350
3376
|
const db = openHubDb(hubDbPath(h.dir));
|
|
3351
3377
|
try {
|
|
3352
3378
|
const user = await createUser(db, "owner", "pw");
|
|
@@ -3366,12 +3392,12 @@ describe("done screen — 'Start using your vault' tile (hub#342)", () => {
|
|
|
3366
3392
|
);
|
|
3367
3393
|
setSetting(db, "setup_expose_mode", "localhost");
|
|
3368
3394
|
const reg = getDefaultOperationsRegistry();
|
|
3369
|
-
const op = reg.create("install", "
|
|
3370
|
-
reg.update(op.id, { status: "succeeded" }, "installed @openparachute/
|
|
3395
|
+
const op = reg.create("install", "scribe");
|
|
3396
|
+
reg.update(op.id, { status: "succeeded" }, "installed @openparachute/scribe");
|
|
3371
3397
|
const { createSession } = await import("../sessions.ts");
|
|
3372
3398
|
const session = createSession(db, { userId: user.id });
|
|
3373
3399
|
const res = handleSetupGet(
|
|
3374
|
-
req(`/admin/setup?just_finished=1&
|
|
3400
|
+
req(`/admin/setup?just_finished=1&op_scribe=${op.id}`, {
|
|
3375
3401
|
headers: { cookie: `${SESSION_COOKIE_NAME}=${session.id}` },
|
|
3376
3402
|
}),
|
|
3377
3403
|
{
|
|
@@ -3384,17 +3410,24 @@ describe("done screen — 'Start using your vault' tile (hub#342)", () => {
|
|
|
3384
3410
|
);
|
|
3385
3411
|
const html = await res.text();
|
|
3386
3412
|
expect(html).toContain("status: succeeded");
|
|
3387
|
-
//
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3413
|
+
// No "Use it now" — scribe has no entry in USE_IT_NOW_URLS today.
|
|
3414
|
+
expect(html).not.toContain(">Use it now<");
|
|
3415
|
+
// "Manage modules" secondary link is always present on a terminal-
|
|
3416
|
+
// succeeded install tile.
|
|
3391
3417
|
expect(html).toContain(">Manage modules<");
|
|
3392
3418
|
} finally {
|
|
3393
3419
|
db.close();
|
|
3394
3420
|
}
|
|
3395
3421
|
});
|
|
3396
3422
|
|
|
3397
|
-
test("'Already installed' tile
|
|
3423
|
+
test("'Already installed' tile renders without a 'Use it now' link when the module has no hosted surface", async () => {
|
|
3424
|
+
// Post-2026-05-27 CURATED trim, USE_IT_NOW_URLS is empty (scribe has
|
|
3425
|
+
// no first-class user-facing landing surface yet; vault gets its
|
|
3426
|
+
// own lead tile, not an install tile). The already-installed tile
|
|
3427
|
+
// therefore renders only the "Manage in admin" secondary link. Pre-
|
|
3428
|
+
// trim the surface module had a USE_IT_NOW_URLS entry that drove
|
|
3429
|
+
// this surface, so the test now pins the absence rather than the
|
|
3430
|
+
// presence.
|
|
3398
3431
|
const db = openHubDb(hubDbPath(h.dir));
|
|
3399
3432
|
try {
|
|
3400
3433
|
const user = await createUser(db, "owner", "pw");
|
|
@@ -3409,11 +3442,11 @@ describe("done screen — 'Start using your vault' tile (hub#342)", () => {
|
|
|
3409
3442
|
health: "/health",
|
|
3410
3443
|
},
|
|
3411
3444
|
{
|
|
3412
|
-
name: "parachute-
|
|
3413
|
-
version: "0.
|
|
3414
|
-
port:
|
|
3415
|
-
paths: ["/
|
|
3416
|
-
health: "/
|
|
3445
|
+
name: "parachute-scribe",
|
|
3446
|
+
version: "0.4.4",
|
|
3447
|
+
port: 1943,
|
|
3448
|
+
paths: ["/scribe"],
|
|
3449
|
+
health: "/scribe/health",
|
|
3417
3450
|
},
|
|
3418
3451
|
],
|
|
3419
3452
|
},
|
|
@@ -3436,8 +3469,10 @@ describe("done screen — 'Start using your vault' tile (hub#342)", () => {
|
|
|
3436
3469
|
);
|
|
3437
3470
|
const html = await res.text();
|
|
3438
3471
|
expect(html).toContain("Already installed");
|
|
3439
|
-
//
|
|
3440
|
-
expect(html).toContain(
|
|
3472
|
+
// No "Use it now" on the scribe already-installed tile.
|
|
3473
|
+
expect(html).not.toContain(">Use it now<");
|
|
3474
|
+
// Secondary affordance still present.
|
|
3475
|
+
expect(html).toContain("Manage in admin");
|
|
3441
3476
|
} finally {
|
|
3442
3477
|
db.close();
|
|
3443
3478
|
}
|