@bongos/core 1.17.0 → 1.17.1
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/.bongos-core.json +154 -83
- package/CLAUDE.md +11 -238
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +4 -4
- package/clients/bongos-client/bongos-client.global.js +14 -1
- package/clients/bongos-client/index.cjs +14 -1
- package/clients/bongos-client/index.d.ts +19 -2
- package/clients/bongos-client/index.mjs +11 -1
- package/clients/bongos-client/package.json +1 -1
- package/docs/adr/0133-github-app-manifest-onboarding-signin.md +2 -0
- package/docs/adr/0134-private-first-npm-distribution.md +20 -6
- package/docs/adr/0135-module-upstream-submission-interim-queue.md +34 -0
- package/docs/adr/README.md +5 -4
- package/docs/api/openapi.json +315 -6
- package/docs/api-reference.md +10 -5
- package/docs/architecture.md +1 -1
- package/docs/file-map.md +2 -2
- package/docs/modules-contract.md +2 -0
- package/docs/recipes/instance-deploy.md +4 -4
- package/docs/recipes/instance-first-run.md +51 -0
- package/docs/recipes/packaging-the-core.md +5 -5
- package/docs/recipes/private-npm-distribution.md +22 -12
- package/docs/recipes/self-host.md +1 -1
- package/docs/recipes/upgrading-the-core.md +8 -8
- package/migrations/core_191_module_submissions.sql +45 -0
- package/modules/autonomy/CLAUDE.md +11 -45
- package/modules/builder-settings/CLAUDE.md +11 -54
- package/modules/dev-box/CLAUDE.md +11 -34
- package/modules/discord/CLAUDE.md +11 -46
- package/modules/economy/CLAUDE.md +11 -44
- package/modules/grading/CLAUDE.md +11 -38
- package/modules/hall-ui/CLAUDE.md +11 -57
- package/modules/hall-ui/public/goals-page.js +227 -22
- package/modules/hall-ui/public/goals.html +16 -0
- package/modules/hall-ui/public/new-project-wizard.js +5 -4
- package/modules/ideas/CLAUDE.md +11 -33
- package/modules/lifecycle/CLAUDE.md +11 -59
- package/modules/lifecycle/routes/goals.js +80 -0
- package/modules/memory/CLAUDE.md +11 -36
- package/modules/onboarding/CLAUDE.md +11 -51
- package/modules/security/CLAUDE.md +11 -40
- package/modules/sessions/CLAUDE.md +11 -35
- package/modules/status-ui/CLAUDE.md +11 -43
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +58 -12
- package/public-landing/index.html +40 -21
- package/scripts/gds/CLAUDE.md +11 -140
- package/scripts/gds/claude-materialize.js +151 -18
- package/scripts/gds/claude-md-template.js +84 -0
- package/scripts/gds/cli-lib.js +112 -2
- package/scripts/gds/gen-api-client.js +10 -10
- package/scripts/gds/init.js +53 -13
- package/scripts/gds/migration-namespace.js +1 -1
- package/scripts/gds/module.js +118 -4
- package/scripts/gds/npmrc.js +93 -0
- package/scripts/gds/onboard.js +13 -24
- package/scripts/gds/package-core.js +56 -14
- package/scripts/gds/provision.js +102 -17
- package/scripts/gds/seed-adopt-tasks.js +1 -1
- package/scripts/gds/status.js +6 -1
- package/scripts/gds/task.js +166 -0
- package/scripts/gds/upgrade.js +58 -18
- package/src/bongos/CLAUDE.md +11 -60
- package/src/bongos/auth.js +11 -0
- package/src/bongos/module-submissions.js +56 -0
- package/src/bongos/route-rank-check.js +6 -0
- package/src/bongos/routes/CLAUDE.md +11 -39
- package/src/bongos/routes/auth.js +6 -1
- package/src/bongos/routes/modules.js +123 -21
- package/src/instance-config.js +1 -1
- package/src/module-api.js +13 -1
- package/tests/claude_materialize.mjs +105 -0
- package/tests/device_flow_start.mjs +73 -0
- package/tests/first_run_engine_check.mjs +79 -0
- package/tests/goal_membership_requests.mjs +33 -3
- package/tests/hook_user_agents.mjs +62 -0
- package/tests/init.mjs +59 -6
- package/tests/instance_mismatch_guard.mjs +101 -0
- package/tests/module_cli.mjs +130 -2
- package/tests/module_submissions.mjs +135 -0
- package/tests/npmrc.mjs +105 -0
- package/tests/onboard.mjs +49 -0
- package/tests/package_core.mjs +68 -22
- package/tests/platform_boot.mjs +5 -2
- package/tests/provision.mjs +107 -8
- package/tests/provisioning_github_app_routes.mjs +29 -2
- package/tests/served_paths_shipped.mjs +2 -2
- package/tests/task_cli.mjs +99 -0
- package/tests/upgrade.mjs +89 -25
package/tests/provision.mjs
CHANGED
|
@@ -46,6 +46,33 @@ t('dbCreateCmd / migrateCmd use the slug db + GDS-only migrate', () => {
|
|
|
46
46
|
assert.match(P.migrateCmd(coTenant), /PGDATABASE=mercury MEDUSA_GDS_ONLY=1 \.\/scripts\/migrate\.sh/);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
t('seedFirstVersionCmd: idempotent (only-if-empty) V1 seed against the slug db, track explicit, one criterion — the fix for the empty-versions bug (task 2119)', () => {
|
|
50
|
+
const cmd = P.seedFirstVersionCmd(coTenant);
|
|
51
|
+
// targets the instance's OWN db, matching createdb/migrate
|
|
52
|
+
assert.match(cmd, /psql .*-d mercury /, 'seeds the instance db');
|
|
53
|
+
assert.match(cmd, /ON_ERROR_STOP=1/, 'aborts on any error (atomic, no half-seed)');
|
|
54
|
+
// the version the greenfield init spec defines — ONE source of truth
|
|
55
|
+
const v = P.instanceInitSpec(coTenant).version;
|
|
56
|
+
assert.match(cmd, /INSERT INTO versions \(id, name, track, status, done_when\)/);
|
|
57
|
+
assert.match(cmd, new RegExp(`\\$v\\$${reEsc(v.id)}\\$v\\$`), 'seeds the spec version id (V1)');
|
|
58
|
+
assert.match(cmd, /\$v\$product\$v\$/, 'track is set EXPLICITLY (versions.track is NOT NULL, no default since mig 112)');
|
|
59
|
+
assert.match(cmd, /\$v\$building\$v\$/, 'status building, mirroring seedViaApi');
|
|
60
|
+
assert.ok(cmd.includes(v.done_when), 'carries the spec done_when verbatim (incl. the → arrows)');
|
|
61
|
+
// the IDEMPOTENT guard — only seeds when there is NO version yet, so it never disturbs an existing project
|
|
62
|
+
assert.match(cmd, /WHERE NOT EXISTS \(SELECT 1 FROM versions\)/, 'only-if-empty guard');
|
|
63
|
+
// one done-when criterion so /status has a finish line (matches seedViaApi's 'first-goal')
|
|
64
|
+
assert.match(cmd, /INSERT INTO done_when_criteria/);
|
|
65
|
+
assert.match(cmd, /\$v\$first-goal\$v\$/);
|
|
66
|
+
// dollar-quoted literals ⇒ the whole -c arg is single-quoted (safe inline / SSH / cloud-init)
|
|
67
|
+
assert.match(cmd, /-c '.*'/, 'the SQL is a single-quoted -c argument');
|
|
68
|
+
assert.doesNotMatch(cmd, /-c ".*"/, 'never double-quoted (would break cloud-init bash -lc "…")');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
t('seedFirstVersionCmd: privileged runs psql as the postgres role; default is bare (peer auth) — mirrors createdb/migrate', () => {
|
|
72
|
+
assert.match(P.seedFirstVersionCmd(coTenant, { privileged: false }), /^psql /);
|
|
73
|
+
assert.match(P.seedFirstVersionCmd(coTenant, { privileged: true }), /^sudo -u postgres psql /);
|
|
74
|
+
});
|
|
75
|
+
|
|
49
76
|
t('serviceUnit binds the instance port + DB + per-instance env file, runs platform-server', () => {
|
|
50
77
|
const u = P.serviceUnit(coTenant, { etcBase: '/etc' });
|
|
51
78
|
assert.match(u, /Environment=PORT=3003/);
|
|
@@ -60,7 +87,7 @@ t('serviceUnit(standalone) runs the CORE PACKAGE server from the instance\'s OWN
|
|
|
60
87
|
const u = P.serviceUnit(inst, { etcBase: '/etc' });
|
|
61
88
|
const root = P.standaloneRoot(inst);
|
|
62
89
|
assert.match(u, new RegExp('WorkingDirectory=' + reEsc(root)), 'runs from the instance\'s own repo dir');
|
|
63
|
-
assert.match(u, /ExecStart=\/usr\/bin\/node node_modules\/@
|
|
90
|
+
assert.match(u, /ExecStart=\/usr\/bin\/node node_modules\/@bongos\/core\/src\/platform-server\.js/, 'runs the pinned core package server');
|
|
64
91
|
assert.match(u, /Environment=PORT=3003/);
|
|
65
92
|
assert.match(u, /EnvironmentFile=\/etc\/mercury\/web\.env/);
|
|
66
93
|
});
|
|
@@ -70,7 +97,7 @@ t('standalone step generators: root under standaloneBase + pull / npm-ci / insta
|
|
|
70
97
|
assert.match(P.standaloneRoot(inst), /\/mercury$/);
|
|
71
98
|
assert.equal(P.standalonePullCmd(), 'git pull --ff-only origin main');
|
|
72
99
|
assert.equal(P.standaloneInstallCmd(), 'npm ci --omit=dev');
|
|
73
|
-
assert.match(P.standaloneMigrateCmd(inst), /PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules\/@
|
|
100
|
+
assert.match(P.standaloneMigrateCmd(inst), /PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules\/@bongos\/core\/scripts\/migrate\.sh/);
|
|
74
101
|
});
|
|
75
102
|
|
|
76
103
|
t('webEnvBody: OAuth creds use the LOADED pack\'s envPrefix (not the slug) + brand/port/db', () => {
|
|
@@ -98,6 +125,18 @@ t('webEnvBody: a domain writes the single-host origin overrides so the hall load
|
|
|
98
125
|
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_OAUTH_ORIGIN=${O}`));
|
|
99
126
|
});
|
|
100
127
|
|
|
128
|
+
t('webEnvBody: the repo target_ref seats the founding owner via <prefix>_FIRST_ADMIN (goal 35 / criterion 165)', () => {
|
|
129
|
+
// Regression (caught live on demo.cloudbongos.com, 2026-07-07): a provisioned instance
|
|
130
|
+
// MUST convey its founding owner, or src/bongos/auth.js maybeSeatFirstAdmin can never
|
|
131
|
+
// seat an Archon on first sign-in and the owner can't claim their own instance. Derived
|
|
132
|
+
// from the repo owner in target_ref — the SAME source instanceInitSpec uses for the
|
|
133
|
+
// scaffold's branding.firstAdmin, so the env override and config/branding.json agree.
|
|
134
|
+
const b = P.webEnvBody({ ...coTenant, hosting_shape: 'standalone', domain: 'mercury.example.com', target_ref: 'octocat/mercury' });
|
|
135
|
+
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_FIRST_ADMIN=octocat`));
|
|
136
|
+
// No target_ref ⇒ the line is omitted (org-owned or unknown owner sets it by hand).
|
|
137
|
+
assert.doesNotMatch(P.webEnvBody(coTenant), /_FIRST_ADMIN=/);
|
|
138
|
+
});
|
|
139
|
+
|
|
101
140
|
t('caddyBlock: a domain → on-demand-TLS reverse_proxy to the local port; no domain → a comment', () => {
|
|
102
141
|
const withDomain = P.caddyBlock({ ...coTenant, domain: 'mercury.example.com' });
|
|
103
142
|
assert.match(withDomain, /mercury\.example\.com \{/);
|
|
@@ -142,11 +181,22 @@ t('healthzCmd hits the instance port /healthz', () => {
|
|
|
142
181
|
assert.match(P.healthzCmd(coTenant), /127\.0\.0\.1:3003\/healthz/);
|
|
143
182
|
});
|
|
144
183
|
|
|
145
|
-
t('dedicatedUserData bakes createdb + migrate into cloud-init', () => {
|
|
184
|
+
t('dedicatedUserData bakes createdb + migrate + first-version seed into cloud-init (task 2119: dedicated must not diverge)', () => {
|
|
146
185
|
const ud = P.dedicatedUserData(coTenant);
|
|
147
186
|
assert.match(ud, /^#cloud-config/);
|
|
148
187
|
assert.match(ud, /createdb mercury/);
|
|
149
188
|
assert.match(ud, /MEDUSA_GDS_ONLY=1/);
|
|
189
|
+
// the seed rides cloud-init too, so a dedicated droplet also comes up able to take tasks
|
|
190
|
+
assert.match(ud, /INSERT INTO versions/, 'seed baked into cloud-init');
|
|
191
|
+
// it must be its OWN runcmd line AFTER migrate (the schema must exist first)
|
|
192
|
+
const lines = ud.split('\n');
|
|
193
|
+
const iMig = lines.findIndex((l) => /migrate\.sh/.test(l));
|
|
194
|
+
const iSeed = lines.findIndex((l) => /INSERT INTO versions/.test(l));
|
|
195
|
+
assert.ok(iMig >= 0 && iSeed > iMig, 'seed line comes after the migrate line');
|
|
196
|
+
// the seed line is a valid bash -lc "…" item: single-quoted -c arg, NO double quotes to break the YAML scalar
|
|
197
|
+
const seedLine = lines[iSeed];
|
|
198
|
+
assert.match(seedLine, /- \[ bash, -lc, "psql .*" \]/, 'well-formed cloud-init runcmd item');
|
|
199
|
+
assert.doesNotMatch(seedLine.replace(/^.*-lc, "/, '').replace(/" \]$/, ''), /"/, 'the seed command carries no inner double quotes');
|
|
150
200
|
});
|
|
151
201
|
|
|
152
202
|
console.log('\nport allocation:');
|
|
@@ -215,12 +265,15 @@ await ta('provisionInstance(co-tenant) composes db → migrate → service → h
|
|
|
215
265
|
const joined = cmds.join('\n');
|
|
216
266
|
assert.match(joined, /createdb mercury/);
|
|
217
267
|
assert.match(joined, /MEDUSA_GDS_ONLY=1/);
|
|
268
|
+
assert.match(joined, /INSERT INTO versions/, 'seeds the first version (task 2119)');
|
|
218
269
|
assert.match(joined, /systemctl daemon-reload/);
|
|
219
270
|
assert.match(joined, /systemctl enable --now mercury/);
|
|
220
271
|
assert.match(joined, /healthz/);
|
|
221
|
-
// ordering: createdb before migrate before enable before healthz
|
|
272
|
+
// ordering: createdb before migrate before SEED before enable before healthz
|
|
222
273
|
const idx = (re) => cmds.findIndex((c) => re.test(c));
|
|
223
274
|
assert.ok(idx(/createdb/) < idx(/migrate\.sh/), 'createdb before migrate');
|
|
275
|
+
assert.ok(idx(/migrate\.sh/) < idx(/INSERT INTO versions/), 'migrate before the version seed (schema must exist)');
|
|
276
|
+
assert.ok(idx(/INSERT INTO versions/) < idx(/enable --now/), 'seed before service enable');
|
|
224
277
|
assert.ok(idx(/migrate\.sh/) < idx(/enable --now/), 'migrate before service enable');
|
|
225
278
|
assert.ok(idx(/enable --now/) < idx(/healthz/), 'service before healthz');
|
|
226
279
|
// writes: the per-instance env file + the systemd unit
|
|
@@ -243,8 +296,9 @@ await ta('provisionInstance(standalone) SCAFFOLDS its repo, then syncs (pull + n
|
|
|
243
296
|
assert.match(joined, /git pull --ff-only origin main/, 'pulls the instance repo');
|
|
244
297
|
assert.match(joined, /npm ci --omit=dev/, 'installs the pinned core');
|
|
245
298
|
assert.match(joined, /createdb mercury/);
|
|
246
|
-
assert.match(joined, /node_modules\/@
|
|
299
|
+
assert.match(joined, /node_modules\/@bongos\/core\/scripts\/migrate\.sh/, 'migrates from the INSTANCE root\'s core');
|
|
247
300
|
assert.ok(!cmds.some((c) => /\.\/scripts\/migrate\.sh/.test(c)), 'standalone does NOT use the control-plane checkout migrate');
|
|
301
|
+
assert.match(joined, /INSERT INTO versions/, 'standalone seeds its first version too (task 2119)');
|
|
248
302
|
// ordering: clone → package-core → init → push → pull → npm ci → migrate → service
|
|
249
303
|
const idx = (re) => cmds.findIndex((c) => re.test(c));
|
|
250
304
|
assert.ok(idx(/git clone/) < idx(/package-core/), 'clone before tarball build');
|
|
@@ -253,6 +307,8 @@ await ta('provisionInstance(standalone) SCAFFOLDS its repo, then syncs (pull + n
|
|
|
253
307
|
assert.ok(idx(/push -u origin main/) < idx(/git pull/), 'scaffold push before the deploy pull');
|
|
254
308
|
assert.ok(idx(/git pull/) < idx(/npm ci/), 'pull before npm ci');
|
|
255
309
|
assert.ok(idx(/npm ci/) < idx(/migrate\.sh/), 'npm ci before migrate');
|
|
310
|
+
assert.ok(idx(/migrate\.sh/) < idx(/INSERT INTO versions/), 'migrate before the version seed');
|
|
311
|
+
assert.ok(idx(/INSERT INTO versions/) < idx(/enable --now/), 'seed before service enable');
|
|
256
312
|
assert.ok(idx(/migrate\.sh/) < idx(/enable --now/), 'migrate before service enable');
|
|
257
313
|
assert.ok(writes.some((w) => /\/mercury\.service$/.test(w)), 'wrote the (standalone) systemd unit');
|
|
258
314
|
});
|
|
@@ -316,6 +372,47 @@ await ta('scaffoldStandaloneRepo: idempotent — a repo that already carries pac
|
|
|
316
372
|
}
|
|
317
373
|
});
|
|
318
374
|
|
|
375
|
+
// ── ADR 0134 (task 2090): registry core-install path + fail-soft to vendored ──
|
|
376
|
+
// Runs the scaffold against a FRESH standaloneBase (no package.json → it proceeds) with
|
|
377
|
+
// an injected env, and asserts which core-resolution shape it chose from the commands.
|
|
378
|
+
async function scaffoldWithEnv(env) {
|
|
379
|
+
const tmp = mkdtempSync(join(tmpdir(), 'scaffold-'));
|
|
380
|
+
const origBase = P.CONFIG.standaloneBase;
|
|
381
|
+
P.CONFIG.standaloneBase = tmp;
|
|
382
|
+
try {
|
|
383
|
+
const { deps, cmds } = recorder({ apply: true });
|
|
384
|
+
await P.scaffoldStandaloneRepo(
|
|
385
|
+
{ slug: 'demo', hosting_shape: 'standalone', target_ref: 'lars589/demo' },
|
|
386
|
+
{ ...deps, env },
|
|
387
|
+
);
|
|
388
|
+
return cmds.join('\n');
|
|
389
|
+
} finally {
|
|
390
|
+
P.CONFIG.standaloneBase = origBase;
|
|
391
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
await ta('scaffoldStandaloneRepo: PROVISION_CORE_FROM_REGISTRY=1 + NPM_TOKEN → registry path (init --registry-core, no tarball build)', async () => {
|
|
396
|
+
const joined = await scaffoldWithEnv({ PROVISION_CORE_FROM_REGISTRY: '1', NPM_TOKEN: 'npm_readonly' });
|
|
397
|
+
assert.match(joined, /init\.js --from .* --dir .* --no-seed --force --registry-core/, 'materializes via --registry-core');
|
|
398
|
+
assert.ok(!/package-core\.js/.test(joined), 'does NOT build a core tarball on the registry path');
|
|
399
|
+
assert.ok(!/--vendor-core/.test(joined), 'does NOT vendor a tarball on the registry path');
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
await ta('scaffoldStandaloneRepo: registry requested but NO NPM_TOKEN → FAILS SOFT to the vendored tarball', async () => {
|
|
403
|
+
const joined = await scaffoldWithEnv({ PROVISION_CORE_FROM_REGISTRY: '1' });
|
|
404
|
+
assert.match(joined, /node scripts\/gds\/package-core\.js/, 'builds the core tarball (vendored fallback)');
|
|
405
|
+
assert.match(joined, /init\.js --from .* --dir .* --no-seed --force --vendor-core /, 'file:-pins the vendored tarball');
|
|
406
|
+
assert.ok(!/--registry-core/.test(joined), 'does NOT take the registry path without a token');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
await ta('scaffoldStandaloneRepo: default (no registry env) stays on the vendored tarball path', async () => {
|
|
410
|
+
const joined = await scaffoldWithEnv({});
|
|
411
|
+
assert.match(joined, /node scripts\/gds\/package-core\.js/, 'vendored is the default');
|
|
412
|
+
assert.match(joined, /--vendor-core /);
|
|
413
|
+
assert.ok(!/--registry-core/.test(joined));
|
|
414
|
+
});
|
|
415
|
+
|
|
319
416
|
await ta('provisionInstance is idempotent: an already-active instance is a no-op', async () => {
|
|
320
417
|
const { deps, cmds } = recorder();
|
|
321
418
|
const r = await P.provisionInstance({ ...coTenant, status: 'active' }, deps);
|
|
@@ -323,10 +420,11 @@ await ta('provisionInstance is idempotent: an already-active instance is a no-op
|
|
|
323
420
|
assert.equal(cmds.length, 0, 'no commands run for an active instance');
|
|
324
421
|
});
|
|
325
422
|
|
|
326
|
-
await ta('provisionInstance(dedicated) does NOT run local createdb/migrate (they ride cloud-init)', async () => {
|
|
423
|
+
await ta('provisionInstance(dedicated) does NOT run local createdb/migrate/seed (they ride cloud-init)', async () => {
|
|
327
424
|
const { deps, cmds } = recorder();
|
|
328
425
|
await P.provisionInstance({ ...coTenant, hosting_shape: 'dedicated', tier: 's-2vcpu-4gb' }, deps);
|
|
329
426
|
assert.ok(!cmds.some((c) => /createdb/.test(c)), 'dedicated does not run createdb locally');
|
|
427
|
+
assert.ok(!cmds.some((c) => /INSERT INTO versions/.test(c)), 'dedicated does not run the version seed locally (it rides cloud-init)');
|
|
330
428
|
});
|
|
331
429
|
|
|
332
430
|
console.log('\nDNS UPSERT composition (P4, CONFIG override):');
|
|
@@ -552,8 +650,8 @@ t('migrateCmd: privileged runs migrate as postgres via env+bash (survives sudo e
|
|
|
552
650
|
|
|
553
651
|
t('standaloneMigrateCmd: privileged runs the instance-root migrate as postgres; default unchanged', () => {
|
|
554
652
|
const inst = { ...coTenant, hosting_shape: 'standalone' };
|
|
555
|
-
assert.equal(P.standaloneMigrateCmd(inst), 'PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules/@
|
|
556
|
-
assert.equal(P.standaloneMigrateCmd(inst, { privileged: true }), 'sudo -u postgres env PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules/@
|
|
653
|
+
assert.equal(P.standaloneMigrateCmd(inst), 'PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules/@bongos/core/scripts/migrate.sh');
|
|
654
|
+
assert.equal(P.standaloneMigrateCmd(inst, { privileged: true }), 'sudo -u postgres env PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules/@bongos/core/scripts/migrate.sh');
|
|
557
655
|
});
|
|
558
656
|
|
|
559
657
|
t('makeWriteFile(privileged): a { sudo:true } write stages a temp file + `sudo install -D -m <mode>` (never writes /etc directly)', () => {
|
|
@@ -615,6 +713,7 @@ await ta('provisionInstance(co-tenant, privileged): createdb/migrate run as post
|
|
|
615
713
|
const joined = cmds.join('\n');
|
|
616
714
|
assert.match(joined, /sudo -u postgres createdb mercury/, 'createdb as postgres');
|
|
617
715
|
assert.match(joined, /sudo -u postgres env PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash \.\/scripts\/migrate\.sh/, 'migrate as postgres');
|
|
716
|
+
assert.match(joined, /sudo -u postgres psql .*INSERT INTO versions/, 'first-version seed runs as postgres too (task 2119)');
|
|
618
717
|
assert.match(joined, /sudo systemctl daemon-reload/, 'daemon-reload sudo-prefixed');
|
|
619
718
|
assert.match(joined, /sudo systemctl enable --now mercury/, 'enable sudo-prefixed');
|
|
620
719
|
assert.match(joined, /sudo systemctl reload caddy/, 'reload caddy sudo-prefixed');
|
|
@@ -30,7 +30,7 @@ provisioning.attachManifestCode = async (_db, id, code) => ({ id, status: 'code_
|
|
|
30
30
|
// A branding with a known publicOrigin so mothershipOrigin() resolves deterministically.
|
|
31
31
|
const branding = require(`${ROOT}/src/branding.js`);
|
|
32
32
|
const realBranding = branding.branding;
|
|
33
|
-
branding.branding = () => ({ ...realBranding(), domains: { publicOrigin: 'https://cloudbongos.com' } });
|
|
33
|
+
branding.branding = () => ({ ...realBranding(), identity: { ...(realBranding().identity || {}), productName: 'Cloud Bongos' }, domains: { publicOrigin: 'https://cloudbongos.com' } });
|
|
34
34
|
|
|
35
35
|
// Inject the signed-in owner (the real requireBuilder needs a session).
|
|
36
36
|
const api = require(`${ROOT}/src/module-api.js`);
|
|
@@ -66,6 +66,7 @@ console.log('POST /provisioning/instances/:id/github-app (mint the one-click lin
|
|
|
66
66
|
t('post_url targets GitHub personal apps/new carrying the state', j.post_url === `https://github.com/settings/apps/new?state=${j.state}`, j.post_url);
|
|
67
67
|
t('manifest.redirect_url is the MOTHERSHIP callback', j.manifest && j.manifest.redirect_url === 'https://cloudbongos.com/api/bongos/provisioning/github-app/callback', j.manifest && j.manifest.redirect_url);
|
|
68
68
|
t('manifest.callback_urls are the INSTANCE sign-in (both prefixes)', j.manifest && j.manifest.callback_urls[0] === 'https://mercury.example.com/api/gds/auth/web/callback' && j.manifest.callback_urls[1] === 'https://mercury.example.com/api/bongos/auth/web/callback');
|
|
69
|
+
t('manifest.name is the instance domain (unique + matches the consent-screen redirect) within 34 chars', j.manifest && j.manifest.name === 'mercury.example.com' && j.manifest.name.length <= 34, j.manifest && j.manifest.name);
|
|
69
70
|
t('org_url derived from the target_ref owner (acme)', j.org_url === `https://github.com/organizations/acme/settings/apps/new?state=${j.state}`, j.org_url);
|
|
70
71
|
t('the attempt row was persisted with the minted state', !!(stored && stored.instanceId === 42 && stored.state === j.state && stored.requestedBy === 7));
|
|
71
72
|
}
|
|
@@ -74,7 +75,7 @@ console.log('\nGET /provisioning/github-app/callback (the public GitHub redirect
|
|
|
74
75
|
{
|
|
75
76
|
const res = await fetch(`${base}/provisioning/github-app/callback?code=GHCODE123&state=GOODSTATE`);
|
|
76
77
|
const html = await res.text();
|
|
77
|
-
t('good state → 200 text/html
|
|
78
|
+
t('good state → 200 text/html progress page wired to auto-open', res.status === 200 && /text\/html/.test(res.headers.get('content-type') || '') && /Setting up your instance/.test(html) && /Placing credentials/.test(html) && /__CB_SETUP__/.test(html) && html.includes('GOODSTATE'), `status=${res.status}`);
|
|
78
79
|
t('the page names the instance host', /mercury\.example\.com/.test(html));
|
|
79
80
|
}
|
|
80
81
|
{
|
|
@@ -87,6 +88,32 @@ console.log('\nGET /provisioning/github-app/callback (the public GitHub redirect
|
|
|
87
88
|
t('missing code/state → 400', res.status === 400, `status=${res.status}`);
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
console.log('\nGET /provisioning/github-app/status (the page poll → auto-open signal):');
|
|
92
|
+
{
|
|
93
|
+
const res = await fetch(`${base}/provisioning/github-app/status?state=GOODSTATE`);
|
|
94
|
+
const j = await res.json();
|
|
95
|
+
t('pending → 200 ok, not ready, names the domain', res.status === 200 && j.ok === true && j.status === 'pending' && j.ready === false && j.domain === 'mercury.example.com', `status=${res.status} ${JSON.stringify(j)}`);
|
|
96
|
+
}
|
|
97
|
+
{
|
|
98
|
+
// Swap the stub to a placed-credentials ('done') attempt to prove ready flips true.
|
|
99
|
+
const prev = provisioning.getOAuthManifestByState;
|
|
100
|
+
provisioning.getOAuthManifestByState = async (_db, state) => (state === 'GOODSTATE'
|
|
101
|
+
? { id: 99, instance_id: 42, requested_by: 7, status: 'done', expires_at: new Date(Date.now() + 600000).toISOString() }
|
|
102
|
+
: null);
|
|
103
|
+
const res = await fetch(`${base}/provisioning/github-app/status?state=GOODSTATE`);
|
|
104
|
+
const j = await res.json();
|
|
105
|
+
t("status 'done' → ready:true + terminal:true", res.status === 200 && j.ready === true && j.terminal === true, JSON.stringify(j));
|
|
106
|
+
provisioning.getOAuthManifestByState = prev;
|
|
107
|
+
}
|
|
108
|
+
{
|
|
109
|
+
const res = await fetch(`${base}/provisioning/github-app/status?state=NOPE`);
|
|
110
|
+
t('unknown state → 404 (no oracle)', res.status === 404, `status=${res.status}`);
|
|
111
|
+
}
|
|
112
|
+
{
|
|
113
|
+
const res = await fetch(`${base}/provisioning/github-app/status`);
|
|
114
|
+
t('missing state → 400', res.status === 400, `status=${res.status}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
90
117
|
console.log('\nparseId contract (the (req,res) signature — a bad id is a clean 400, not a 500):');
|
|
91
118
|
{
|
|
92
119
|
const res = await fetch(`${base}/provisioning/instances/not-a-number/github-app`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' });
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// GUARD (task 2078): every static asset serve-internal.js reads at runtime from the
|
|
4
4
|
// core tree MUST be included in the packaged core — i.e. isPublishable() per
|
|
5
5
|
// scripts/gds/publish-manifest.js. A standalone instance runs the PACKAGED core
|
|
6
|
-
// (@
|
|
6
|
+
// (@bongos/core, ADR 0108), so a served path the publish boundary EXCLUDES is
|
|
7
7
|
// simply absent on the box and 404s.
|
|
8
8
|
//
|
|
9
9
|
// This is exactly how demo.cloudbongos.com rendered a blank page: the browser client
|
|
@@ -101,7 +101,7 @@ test('every ROOT-served asset ships in the core package (isPublishable)', () =>
|
|
|
101
101
|
offenders,
|
|
102
102
|
[],
|
|
103
103
|
`serve-internal.js reads these from ROOT but the publish boundary EXCLUDES them, so the ` +
|
|
104
|
-
`packaged core (@
|
|
104
|
+
`packaged core (@bongos/core) omits them and a standalone 404s (task 2078): ${offenders.join(', ')}. ` +
|
|
105
105
|
`Fix: add each to PUBLISH_ALLOWLIST in scripts/gds/publish-manifest.js (or, if truly host-only, ` +
|
|
106
106
|
`document it in INTENTIONALLY_UNSHIPPED in this test).`
|
|
107
107
|
);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// tests/task_cli.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 2123 (demo-audit fix C1) — locks the PURE argv→body mapping of the new
|
|
4
|
+
// `bongos task new` command (scripts/gds/task.js). Hermetic: no session/DB/network
|
|
5
|
+
// (buildTaskBody never touches the API), so the pr-unit-tests gate auto-includes it.
|
|
6
|
+
//
|
|
7
|
+
// Run: node tests/task_cli.mjs
|
|
8
|
+
|
|
9
|
+
import { strict as assert } from 'node:assert';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const { buildTaskBody, VALID_KINDS, VALID_DISCIPLINES, csv, hallLink } = require('../scripts/gds/task.js');
|
|
14
|
+
|
|
15
|
+
let passed = 0, failed = 0;
|
|
16
|
+
function t(name, fn) {
|
|
17
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
18
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
t('requires --title and --description', () => {
|
|
22
|
+
const { errors } = buildTaskBody([]);
|
|
23
|
+
assert.ok(errors.some((e) => /--title/.test(e)));
|
|
24
|
+
assert.ok(errors.some((e) => /--description/.test(e)));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
t('minimal valid: defaults kind=feature, discipline=engineer, status=backlog', () => {
|
|
28
|
+
const { body, errors } = buildTaskBody(['--title', 'T', '--description', 'D']);
|
|
29
|
+
assert.equal(errors.length, 0);
|
|
30
|
+
assert.equal(body.title, 'T');
|
|
31
|
+
assert.equal(body.description, 'D');
|
|
32
|
+
assert.equal(body.kind, 'feature');
|
|
33
|
+
assert.equal(body.discipline, 'engineer');
|
|
34
|
+
assert.equal(body.status, 'backlog');
|
|
35
|
+
assert.equal(body.source, 'bongos-task-cli');
|
|
36
|
+
// optional fields absent unless provided
|
|
37
|
+
assert.equal('priority' in body, false);
|
|
38
|
+
assert.equal('version_id' in body, false);
|
|
39
|
+
assert.equal('goal_id' in body, false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
t('--ready flips status to ready', () => {
|
|
43
|
+
const { body } = buildTaskBody(['--title', 'T', '--description', 'D', '--ready']);
|
|
44
|
+
assert.equal(body.status, 'ready');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
t('maps all optional flags → body', () => {
|
|
48
|
+
const { body, criteria, errors } = buildTaskBody([
|
|
49
|
+
'--title', 'Add X', '--description', 'Because Y',
|
|
50
|
+
'--kind', 'bug', '--discipline', 'ui', '--priority', '2',
|
|
51
|
+
'--version', 'BONGOS-V1', '--goal', '26', '--criteria', 'C1,gcarry-prod-vanilla',
|
|
52
|
+
'--touches', 'src/a.js,tests/', '--est-minutes', '45', '--manual-degree', '3', '--credits', '15',
|
|
53
|
+
]);
|
|
54
|
+
assert.equal(errors.length, 0);
|
|
55
|
+
assert.equal(body.kind, 'bug');
|
|
56
|
+
assert.equal(body.discipline, 'ui');
|
|
57
|
+
assert.equal(body.priority, 2);
|
|
58
|
+
assert.equal(body.version_id, 'BONGOS-V1');
|
|
59
|
+
assert.equal(body.goal_id, 26);
|
|
60
|
+
assert.deepEqual(body.touches, ['src/a.js', 'tests/']);
|
|
61
|
+
assert.deepEqual(body.criterion_ids, ['C1', 'gcarry-prod-vanilla']);
|
|
62
|
+
assert.deepEqual(criteria, ['C1', 'gcarry-prod-vanilla']);
|
|
63
|
+
assert.equal(body.est_minutes, 45);
|
|
64
|
+
assert.equal(body.manual_degree, 3);
|
|
65
|
+
assert.equal(body.credits_reward, 15);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
t('rejects an invalid kind', () => {
|
|
69
|
+
const { errors } = buildTaskBody(['--title', 'T', '--description', 'D', '--kind', 'nonsense']);
|
|
70
|
+
assert.ok(errors.some((e) => /--kind/.test(e)));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
t('rejects a priority out of 1–5', () => {
|
|
74
|
+
const { errors } = buildTaskBody(['--title', 'T', '--description', 'D', '--priority', '9']);
|
|
75
|
+
assert.ok(errors.some((e) => /--priority/.test(e)));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
t('rejects a non-positive --goal', () => {
|
|
79
|
+
const { errors } = buildTaskBody(['--title', 'T', '--description', 'D', '--goal', '0']);
|
|
80
|
+
assert.ok(errors.some((e) => /--goal/.test(e)));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
t('csv trims + drops blanks', () => {
|
|
84
|
+
assert.deepEqual(csv(' a , b ,,c '), ['a', 'b', 'c']);
|
|
85
|
+
assert.deepEqual(csv(''), []);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
t('hallLink builds a hall URL from the API base origin, falls back cleanly', () => {
|
|
89
|
+
assert.equal(hallLink('https://demo.cloudbongos.com/api/bongos', 7), 'https://demo.cloudbongos.com/builders#/task/7');
|
|
90
|
+
assert.equal(hallLink('not-a-url', 7), 'task #7');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
t('enum vocab is the expected shape', () => {
|
|
94
|
+
assert.ok(VALID_KINDS.includes('feature') && VALID_KINDS.includes('bug'));
|
|
95
|
+
assert.deepEqual(VALID_DISCIPLINES, ['engineer', 'artist', 'ideator', 'ui']);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
99
|
+
process.exit(failed ? 1 : 0);
|
package/tests/upgrade.mjs
CHANGED
|
@@ -34,13 +34,13 @@ function scratchConsumer({ pinned = '1.14.1', installed = '1.14.1' } = {}) {
|
|
|
34
34
|
const dir = mkdtempSync(join(tmpdir(), 'otb-upgrade-'));
|
|
35
35
|
writeFileSync(join(dir, 'package.json'), JSON.stringify({
|
|
36
36
|
name: 'test-consumer', version: '0.1.0',
|
|
37
|
-
dependencies: { '@
|
|
37
|
+
dependencies: { '@bongos/core': `file:vendor/bongos-core-${pinned}.tgz` },
|
|
38
38
|
scripts: { migrate: 'true', start: 'true' },
|
|
39
39
|
}, null, 2) + '\n');
|
|
40
40
|
if (installed) {
|
|
41
|
-
const cd = join(dir, 'node_modules', '@
|
|
41
|
+
const cd = join(dir, 'node_modules', '@bongos', 'core');
|
|
42
42
|
mkdirSync(cd, { recursive: true });
|
|
43
|
-
writeFileSync(join(cd, 'package.json'), JSON.stringify({ name: '@
|
|
43
|
+
writeFileSync(join(cd, 'package.json'), JSON.stringify({ name: '@bongos/core', version: installed }) + '\n');
|
|
44
44
|
}
|
|
45
45
|
return dir;
|
|
46
46
|
}
|
|
@@ -52,8 +52,8 @@ function fakeRun(calls, { dirty = '', installTo = null, dir = null } = {}) {
|
|
|
52
52
|
calls.push(`${cmd} ${(args || []).join(' ')}`.trim());
|
|
53
53
|
if (cmd === 'git' && args[0] === 'status') return { status: 0, stdout: dirty };
|
|
54
54
|
if (cmd === 'npm' && args[0] === 'install' && installTo && dir) {
|
|
55
|
-
writeFileSync(join(dir, 'node_modules', '@
|
|
56
|
-
JSON.stringify({ name: '@
|
|
55
|
+
writeFileSync(join(dir, 'node_modules', '@bongos', 'core', 'package.json'),
|
|
56
|
+
JSON.stringify({ name: '@bongos/core', version: installTo }) + '\n');
|
|
57
57
|
}
|
|
58
58
|
return { status: 0 };
|
|
59
59
|
};
|
|
@@ -61,20 +61,20 @@ function fakeRun(calls, { dirty = '', installTo = null, dir = null } = {}) {
|
|
|
61
61
|
|
|
62
62
|
// ---- 1. pure pin helpers ----------------------------------------------------
|
|
63
63
|
t('versionFromTgzPath: extracts the version from a vendored tarball path', () => {
|
|
64
|
-
assert.equal(u.versionFromTgzPath('file:vendor/
|
|
65
|
-
assert.equal(u.versionFromTgzPath('vendor/
|
|
64
|
+
assert.equal(u.versionFromTgzPath('file:vendor/bongos-core-1.14.1.tgz'), '1.14.1');
|
|
65
|
+
assert.equal(u.versionFromTgzPath('vendor/bongos-core-1.15.0.tgz'), '1.15.0');
|
|
66
66
|
assert.equal(u.versionFromTgzPath('^1.2.3'), null);
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
t('readPinnedCoreVersion / writePin: round-trip the file: dependency', () => {
|
|
70
70
|
const dir = scratchConsumer({ pinned: '1.14.1' });
|
|
71
71
|
assert.equal(u.readPinnedCoreVersion(dir), '1.14.1');
|
|
72
|
-
const prev = u.writePin(dir, 'vendor/
|
|
73
|
-
assert.equal(prev, 'file:vendor/
|
|
72
|
+
const prev = u.writePin(dir, 'vendor/bongos-core-1.15.0.tgz');
|
|
73
|
+
assert.equal(prev, 'file:vendor/bongos-core-1.14.1.tgz');
|
|
74
74
|
assert.equal(u.readPinnedCoreVersion(dir), '1.15.0');
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
t('readInstalledCoreVersion: reads node_modules/@
|
|
77
|
+
t('readInstalledCoreVersion: reads node_modules/@bongos/core', () => {
|
|
78
78
|
const dir = scratchConsumer({ installed: '1.14.1' });
|
|
79
79
|
assert.equal(u.readInstalledCoreVersion(dir), '1.14.1');
|
|
80
80
|
assert.equal(u.readInstalledCoreVersion(mkdtempSync(join(tmpdir(), 'empty-'))), null);
|
|
@@ -83,13 +83,13 @@ t('readInstalledCoreVersion: reads node_modules/@cloudbongos/core', () => {
|
|
|
83
83
|
// ---- 2. vendorTarball -------------------------------------------------------
|
|
84
84
|
t('vendorTarball: copies the .tgz + side-car manifest under the canonical name', () => {
|
|
85
85
|
const src = mkdtempSync(join(tmpdir(), 'src-'));
|
|
86
|
-
writeFileSync(join(src, '
|
|
87
|
-
writeFileSync(join(src, '
|
|
86
|
+
writeFileSync(join(src, 'bongos-core-1.15.0.tgz'), 'TGZBYTES');
|
|
87
|
+
writeFileSync(join(src, 'bongos-core-1.15.0.manifest.json'), '{"v":1}');
|
|
88
88
|
const dir = scratchConsumer();
|
|
89
|
-
const rel = u.vendorTarball({ instanceDir: dir, fromTgz: join(src, '
|
|
90
|
-
assert.equal(rel, join('vendor', '
|
|
89
|
+
const rel = u.vendorTarball({ instanceDir: dir, fromTgz: join(src, 'bongos-core-1.15.0.tgz'), targetVersion: '1.15.0' });
|
|
90
|
+
assert.equal(rel, join('vendor', 'bongos-core-1.15.0.tgz'));
|
|
91
91
|
assert.equal(readFileSync(join(dir, rel), 'utf8'), 'TGZBYTES');
|
|
92
|
-
assert.ok(existsSync(join(dir, 'vendor', '
|
|
92
|
+
assert.ok(existsSync(join(dir, 'vendor', 'bongos-core-1.15.0.manifest.json')), 'manifest copied too');
|
|
93
93
|
});
|
|
94
94
|
|
|
95
95
|
// ---- 3. step shims issue the right argv ------------------------------------
|
|
@@ -137,11 +137,11 @@ t('guard: already on the target refuses (unless --force)', async () => {
|
|
|
137
137
|
// ---- 5. dry-run mutates nothing --------------------------------------------
|
|
138
138
|
t('dry-run: plans without rewriting package.json or spawning npm', async () => {
|
|
139
139
|
const src = mkdtempSync(join(tmpdir(), 'src-'));
|
|
140
|
-
writeFileSync(join(src, '
|
|
140
|
+
writeFileSync(join(src, 'bongos-core-1.15.0.tgz'), 'X');
|
|
141
141
|
const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
|
|
142
142
|
const calls = [];
|
|
143
143
|
const res = await u.runUpgrade(
|
|
144
|
-
{ to: '1.15.0', from: join(src, '
|
|
144
|
+
{ to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, dryRun: true },
|
|
145
145
|
{ log: () => {}, err: () => {}, run: fakeRun(calls, { dirty: '' }) }
|
|
146
146
|
);
|
|
147
147
|
assert.equal(res.ok, true);
|
|
@@ -153,13 +153,13 @@ t('dry-run: plans without rewriting package.json or spawning npm', async () => {
|
|
|
153
153
|
// ---- 6. full happy-path (every side effect injected; ledger skipped, DB-free) ----
|
|
154
154
|
t('full: pre-flight → pin → install → migrate → materialize → restart → verify', async () => {
|
|
155
155
|
const src = mkdtempSync(join(tmpdir(), 'src-'));
|
|
156
|
-
writeFileSync(join(src, '
|
|
156
|
+
writeFileSync(join(src, 'bongos-core-1.15.0.tgz'), 'TGZ');
|
|
157
157
|
const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
|
|
158
158
|
const calls = [];
|
|
159
159
|
let materialized = null;
|
|
160
160
|
let regenArgs = null;
|
|
161
161
|
const res = await u.runUpgrade(
|
|
162
|
-
{ to: '1.15.0', from: join(src, '
|
|
162
|
+
{ to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'emersonian.service' },
|
|
163
163
|
{
|
|
164
164
|
log: () => {}, err: () => {},
|
|
165
165
|
run: fakeRun(calls, { dirty: '', installTo: '1.15.0', dir }),
|
|
@@ -174,26 +174,26 @@ t('full: pre-flight → pin → install → migrate → materialize → restart
|
|
|
174
174
|
assert.equal(res.versionOk, true, 'installed version must equal the target after install');
|
|
175
175
|
// pin rewritten, tarball vendored
|
|
176
176
|
assert.equal(u.readPinnedCoreVersion(dir), '1.15.0');
|
|
177
|
-
assert.equal(readFileSync(join(dir, 'vendor', '
|
|
177
|
+
assert.equal(readFileSync(join(dir, 'vendor', 'bongos-core-1.15.0.tgz'), 'utf8'), 'TGZ');
|
|
178
178
|
// steps ran, in the expected order
|
|
179
179
|
assert.deepEqual(
|
|
180
180
|
calls.filter((c) => c.startsWith('npm') || c.startsWith('git') || c.startsWith('systemctl')),
|
|
181
181
|
['git status --porcelain', 'npm install --no-audit --no-fund', 'npm run migrate', 'systemctl restart emersonian.service']
|
|
182
182
|
);
|
|
183
183
|
// materialize pointed at the freshly-installed core
|
|
184
|
-
assert.ok(materialized && materialized.coreRoot.endsWith(join('node_modules', '@
|
|
184
|
+
assert.ok(materialized && materialized.coreRoot.endsWith(join('node_modules', '@bongos', 'core')));
|
|
185
185
|
assert.equal(materialized.instanceDir, dir);
|
|
186
186
|
// task 2051: the API docs+client regen ran too, pointed at the same new core + instance
|
|
187
187
|
assert.ok(regenArgs, 'regenerateApiArtifacts step invoked');
|
|
188
188
|
assert.equal(regenArgs.instanceDir, dir);
|
|
189
|
-
assert.ok(regenArgs.coreRoot.endsWith(join('node_modules', '@
|
|
189
|
+
assert.ok(regenArgs.coreRoot.endsWith(join('node_modules', '@bongos', 'core')));
|
|
190
190
|
});
|
|
191
191
|
|
|
192
192
|
// ---- 6b. regenerate the API artifacts on upgrade (task 2051) ----------------
|
|
193
193
|
t('regenerateApiArtifacts: spawns docs then client from the new core, targeting the instance', () => {
|
|
194
194
|
const calls = [];
|
|
195
195
|
const run = (cmd, args, opts) => { calls.push({ cmd, args, opts }); return { status: 0 }; };
|
|
196
|
-
const res = u.regenerateApiArtifacts({ instanceDir: '/inst', coreRoot: '/inst/node_modules/@
|
|
196
|
+
const res = u.regenerateApiArtifacts({ instanceDir: '/inst', coreRoot: '/inst/node_modules/@bongos/core' }, run, { existsSync: () => true });
|
|
197
197
|
assert.equal(res.ok, true);
|
|
198
198
|
assert.deepEqual(res.ran, ['gen-api-docs.js', 'gen-api-client.js']);
|
|
199
199
|
assert.equal(calls.length, 2);
|
|
@@ -225,11 +225,11 @@ t('regenerateApiArtifacts: a generator non-zero exit stops + reports (docs ran,
|
|
|
225
225
|
|
|
226
226
|
t('runUpgrade: --skip-regen-docs skips the regen step', async () => {
|
|
227
227
|
const src = mkdtempSync(join(tmpdir(), 'src-'));
|
|
228
|
-
writeFileSync(join(src, '
|
|
228
|
+
writeFileSync(join(src, 'bongos-core-1.15.0.tgz'), 'TGZ');
|
|
229
229
|
const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
|
|
230
230
|
let regenCalled = false;
|
|
231
231
|
const res = await u.runUpgrade(
|
|
232
|
-
{ to: '1.15.0', from: join(src, '
|
|
232
|
+
{ to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, skipRegenDocs: true },
|
|
233
233
|
{
|
|
234
234
|
log: () => {}, err: () => {},
|
|
235
235
|
run: fakeRun([], { dirty: '', installTo: '1.15.0', dir }),
|
|
@@ -352,6 +352,70 @@ t('pollHealth: a persistent non-2xx status is reported (not treated as ready)',
|
|
|
352
352
|
assert.equal(res.timedOut, true);
|
|
353
353
|
});
|
|
354
354
|
|
|
355
|
+
// ---- 7. registry update channel (ADR 0134, task 2090) -----------------------
|
|
356
|
+
t('versionFromDep: parses bare/caret/tilde semver; file: spec → null (tgz path is versionFromTgzPath\'s job)', () => {
|
|
357
|
+
assert.equal(u.versionFromDep('1.17.0'), '1.17.0');
|
|
358
|
+
assert.equal(u.versionFromDep('^1.17.0'), '1.17.0');
|
|
359
|
+
assert.equal(u.versionFromDep('~1.17.0'), '1.17.0');
|
|
360
|
+
assert.equal(u.versionFromDep('file:vendor/bongos-core-1.17.0.tgz'), null);
|
|
361
|
+
assert.equal(u.versionFromDep(''), null);
|
|
362
|
+
assert.equal(u.versionFromDep(null), null);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
t('writeRegistryPin / readPinnedCoreVersion: round-trip an exact registry version', () => {
|
|
366
|
+
const dir = scratchConsumer({ pinned: '1.16.0', installed: '1.16.0' });
|
|
367
|
+
const prev = u.writeRegistryPin(dir, '1.17.0');
|
|
368
|
+
assert.equal(prev, 'file:vendor/bongos-core-1.16.0.tgz', 'returns the previous pin');
|
|
369
|
+
assert.equal(JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).dependencies['@bongos/core'], '1.17.0', 'pins the exact version (not file:)');
|
|
370
|
+
assert.equal(u.readPinnedCoreVersion(dir), '1.17.0', 'reads the registry pin back');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
t('runUpgrade --registry (dry-run): plans a registry pin, no npm, pin unchanged', async () => {
|
|
374
|
+
const dir = scratchConsumer({ pinned: '1.16.0', installed: '1.16.0' });
|
|
375
|
+
const calls = [];
|
|
376
|
+
const res = await u.runUpgrade(
|
|
377
|
+
{ to: '1.17.0', registry: true, instance: dir, dryRun: true },
|
|
378
|
+
{ log: () => {}, err: () => {}, run: fakeRun(calls, { dirty: '' }) }
|
|
379
|
+
);
|
|
380
|
+
assert.equal(res.ok, true);
|
|
381
|
+
assert.equal(res.dryRun, true);
|
|
382
|
+
assert.equal(u.readPinnedCoreVersion(dir), '1.16.0', 'pin unchanged in dry-run');
|
|
383
|
+
assert.ok(!existsSync(join(dir, '.npmrc')), 'dry-run writes no .npmrc');
|
|
384
|
+
assert.ok(!calls.some((c) => c.startsWith('npm')), 'no npm spawned in dry-run');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
t('runUpgrade --registry + --from: refuses (mutually exclusive core sources)', async () => {
|
|
388
|
+
const dir = scratchConsumer({ pinned: '1.16.0', installed: '1.16.0' });
|
|
389
|
+
const res = await u.runUpgrade(
|
|
390
|
+
{ to: '1.17.0', registry: true, from: '/tmp/x.tgz', instance: dir },
|
|
391
|
+
{ log: () => {}, err: () => {}, run: fakeRun([], { dirty: '' }) }
|
|
392
|
+
);
|
|
393
|
+
assert.equal(res.ok, false);
|
|
394
|
+
assert.match(res.error, /mutually exclusive/);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
t('runUpgrade --registry (full): pins the exact version + writes the env-fed .npmrc, then install→migrate→verify', async () => {
|
|
398
|
+
const dir = scratchConsumer({ pinned: '1.16.0', installed: '1.16.0' });
|
|
399
|
+
const calls = [];
|
|
400
|
+
const res = await u.runUpgrade(
|
|
401
|
+
{ to: '1.17.0', registry: true, instance: dir, service: 'demo.service' },
|
|
402
|
+
{
|
|
403
|
+
log: () => {}, err: () => {},
|
|
404
|
+
run: fakeRun(calls, { dirty: '', installTo: '1.17.0', dir }),
|
|
405
|
+
materialize: () => ({ skills: 1, settings: true, hooks: 1 }),
|
|
406
|
+
regenerateApiArtifacts: () => ({ ok: true, ran: [] }),
|
|
407
|
+
databaseUrl: '', pgDatabase: '',
|
|
408
|
+
}
|
|
409
|
+
);
|
|
410
|
+
assert.equal(res.ok, true, 'registry upgrade succeeds');
|
|
411
|
+
assert.equal(res.toVersion, '1.17.0');
|
|
412
|
+
assert.equal(JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).dependencies['@bongos/core'], '1.17.0', 'exact registry pin (no file:)');
|
|
413
|
+
const npmrc = readFileSync(join(dir, '.npmrc'), 'utf8');
|
|
414
|
+
assert.match(npmrc, /_authToken=\$\{NPM_TOKEN\}/, 'wrote the env-fed .npmrc');
|
|
415
|
+
assert.ok(calls.some((c) => /^npm install/.test(c)), 'ran npm install (resolves from the registry)');
|
|
416
|
+
assert.ok(!calls.some((c) => /vendor/.test(c)), 'did not vendor a tarball');
|
|
417
|
+
});
|
|
418
|
+
|
|
355
419
|
await Promise.all(pending);
|
|
356
420
|
console.log(`\nupgrade: ${passed} passed, ${failed} failed`);
|
|
357
421
|
process.exit(failed ? 1 : 0);
|