@bongos/core 1.19.654 → 1.19.656
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 +54 -49
- package/.env.local.example +3 -3
- package/config/branding.neutral.json +1 -1
- package/docker-compose.yml +1 -1
- package/docs/branding-contract.md +1 -1
- package/docs/copy-inventory.md +26 -26
- package/docs/copy-registry.json +32 -32
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/self-host.md +3 -3
- package/docs/recipes/standalone-live-docs.md +1 -1
- package/docs/recipes/upgrading-the-core.md +1 -1
- package/modules/dev-box/routes/box.js +4 -2
- package/modules/hall-ui/public/watch.js +38 -14
- package/modules/lifecycle/db-rank-authz.js +30 -10
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/box.js +3 -3
- package/scripts/gds/claude-materialize.js +1 -1
- package/scripts/gds/init.js +2 -2
- package/scripts/gds/module.js +3 -2
- package/scripts/gds/oauth-secret.js +6 -3
- package/scripts/gds/provision-units.js +3 -2
- package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +1 -1
- package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +1 -1
- package/scripts/gds/seed-provisioning-tasks.js +1 -1
- package/scripts/gds/status.js +3 -2
- package/scripts/gds/upgrade.js +3 -3
- package/scripts/hall-preview/server.js +1 -1
- package/src/bongos/routes/auth.js +3 -2
- package/src/bongos/routes/backup.js +8 -4
- package/src/bongos/routes/security.js +2 -1
- package/src/bongos/serve-internal.js +2 -2
- package/src/branding.js +24 -7
- package/src/instance-config.js +74 -13
- package/src/module-api.js +1 -1
- package/src/modules.js +15 -7
- package/tests/claude_materialize.mjs +1 -1
- package/tests/cloud_bongos_pack.mjs +1 -1
- package/tests/government_claim_eligibility.mjs +82 -0
- package/tests/init.mjs +1 -1
- package/tests/instance_config.mjs +98 -2
- package/tests/upgrade.mjs +3 -3
- package/tests/watch_applications_queue.mjs +13 -1
- package/tests/watch_sealed_floor.mjs +241 -0
package/src/instance-config.js
CHANGED
|
@@ -21,7 +21,11 @@ const path = require('node:path');
|
|
|
21
21
|
|
|
22
22
|
// Last-resort default if branding can't be read at all (vanilla, never OTB).
|
|
23
23
|
const FALLBACK_DIR = 'cloudbongos';
|
|
24
|
-
|
|
24
|
+
// BONGOS is the CANONICAL env prefix (task 1003703 / criterion C3 of goal
|
|
25
|
+
// 1000073). It was CLOUDBONGOS until then; that spelling moved into
|
|
26
|
+
// LEGACY_ENV_PREFIXES below rather than being dropped, so a vanilla instance
|
|
27
|
+
// already exporting CLOUDBONGOS_* keeps resolving.
|
|
28
|
+
const FALLBACK_ENV_PREFIX = 'BONGOS';
|
|
25
29
|
|
|
26
30
|
// Historical hard-coded config dir — always also checked on READ so a session /
|
|
27
31
|
// ledger / key written before R61 (or by an instance that later changed its
|
|
@@ -30,8 +34,24 @@ const LEGACY_DIRS = ['otb'];
|
|
|
30
34
|
|
|
31
35
|
// Legacy env prefixes tried (in order) AFTER the configured one. These cover the
|
|
32
36
|
// three prefixes that coexisted pre-R61 (OTB_ operational, GDS_ API/auth, PMS_
|
|
33
|
-
// the original name)
|
|
34
|
-
|
|
37
|
+
// the original name) plus CLOUDBONGOS_, which was the canonical default until
|
|
38
|
+
// task 1003703 made it BONGOS_. Keeping them means prod's existing env is
|
|
39
|
+
// honored verbatim.
|
|
40
|
+
//
|
|
41
|
+
// WHY THESE MUST OUTLIVE THIS TASK: these names are set OUTSIDE the repo —
|
|
42
|
+
// /etc/cloudbongos/web.env on the droplet, systemd units, CI secrets, shipped
|
|
43
|
+
// Dev Box binaries, live dev boxes. Deleting a spelling here is an outage the
|
|
44
|
+
// next time one of those restarts. Task 1003706 (C3d) retires them deliberately,
|
|
45
|
+
// after evidence — and asserts their ABSENCE, which is why the fallback needs a
|
|
46
|
+
// test that it FIRES rather than one that merely tolerates it.
|
|
47
|
+
const LEGACY_ENV_PREFIXES = ['CLOUDBONGOS', 'OTB', 'GDS', 'PMS'];
|
|
48
|
+
|
|
49
|
+
// The core release in which the legacy prefixes above stop being read. Named in
|
|
50
|
+
// every deprecation warning so an operator learns the deadline from the warning
|
|
51
|
+
// itself rather than from a changelog they will not read. This lands in 1.19.x,
|
|
52
|
+
// so 1.21 leaves a full minor of overlap; task 1003706 is the task that both
|
|
53
|
+
// removes the fallback and moves this constant's meaning to "already gone".
|
|
54
|
+
const LEGACY_ENV_SUNSET_RELEASE = '1.21';
|
|
35
55
|
|
|
36
56
|
// Lazy require (ADR 0108 §1 made this a two-way edge: branding.js now requires
|
|
37
57
|
// resolveCoreRoot()/resolveInstanceRoot() from this module, so a top-level
|
|
@@ -135,21 +155,62 @@ function readConfigFileSync(name, { encoding = 'utf8', b = safeBrand() } = {}) {
|
|
|
135
155
|
}
|
|
136
156
|
|
|
137
157
|
// Resolve an env var by SUFFIX across the configured prefix + legacy prefixes.
|
|
138
|
-
// resolveEnv('API_BASE') tries <PREFIX>_API_BASE (
|
|
139
|
-
//
|
|
140
|
-
// is set to a non-empty value.
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
158
|
+
// resolveEnv('API_BASE') tries <PREFIX>_API_BASE (BONGOS_ on vanilla, OTB_ on
|
|
159
|
+
// the OTB instance), then CLOUDBONGOS_/OTB_/GDS_/PMS_API_BASE. Returns undefined
|
|
160
|
+
// if none is set to a non-empty value.
|
|
161
|
+
//
|
|
162
|
+
// THE CANONICAL SPELLING ALWAYS WINS, including over a legacy one set to a
|
|
163
|
+
// different value — an operator mid-migration who sets the new name expects it
|
|
164
|
+
// to take effect, and silently preferring the old one would make the rename
|
|
165
|
+
// untestable. Each legacy name that is READ (won or shadowed) emits ONE warning
|
|
166
|
+
// per process naming the old spelling, the new one, and the release the old
|
|
167
|
+
// stops working in; see warnLegacyEnv below. `warn` is injectable so a test can
|
|
168
|
+
// assert the warning fires rather than only that the value resolves.
|
|
169
|
+
function resolveEnv(suffix, { env = process.env, prefix = envPrefix(), legacy = LEGACY_ENV_PREFIXES, warn = warnLegacyEnv } = {}) {
|
|
170
|
+
const canonical = `${prefix}_${suffix}`;
|
|
171
|
+
const canonicalValue = prefix ? env[canonical] : undefined;
|
|
172
|
+
const canonicalSet = canonicalValue !== undefined && canonicalValue !== '';
|
|
173
|
+
// Every legacy spelling is visited even when the canonical one already won —
|
|
174
|
+
// a spelling that is merely SHADOWED is still set on some box, and it will
|
|
175
|
+
// stop being read at the sunset, so it earns its one warning too. Warning only
|
|
176
|
+
// on the winner would leave the "both set" operator with no notice at all.
|
|
177
|
+
const seen = new Set([prefix].filter(Boolean));
|
|
178
|
+
let winner = canonicalSet ? canonicalValue : undefined;
|
|
179
|
+
for (const pfx of legacy) {
|
|
145
180
|
if (!pfx || seen.has(pfx)) continue;
|
|
146
181
|
seen.add(pfx);
|
|
147
|
-
const
|
|
148
|
-
|
|
182
|
+
const name = `${pfx}_${suffix}`;
|
|
183
|
+
const v = env[name];
|
|
184
|
+
if (v === undefined || v === '') continue;
|
|
185
|
+
warn({ legacyName: name, canonicalName: canonical, shadowed: canonicalSet, differs: canonicalSet && canonicalValue !== v });
|
|
186
|
+
if (winner === undefined) winner = v;
|
|
149
187
|
}
|
|
150
|
-
return
|
|
188
|
+
return winner;
|
|
151
189
|
}
|
|
152
190
|
|
|
191
|
+
// One warning per legacy env NAME per process. A silent fallback is how an old
|
|
192
|
+
// spelling survives forever — but a warning per READ would drown a CLI that
|
|
193
|
+
// resolves the same knob in a loop, so the emitted set is the dedupe key.
|
|
194
|
+
const _legacyEnvWarned = new Set();
|
|
195
|
+
|
|
196
|
+
function warnLegacyEnv({ legacyName, canonicalName, shadowed, differs, log = console } = {}) {
|
|
197
|
+
if (_legacyEnvWarned.has(legacyName)) return false;
|
|
198
|
+
_legacyEnvWarned.add(legacyName);
|
|
199
|
+
const fate = differs
|
|
200
|
+
? `${canonicalName} is ALSO set to a different value and WINS`
|
|
201
|
+
: shadowed
|
|
202
|
+
? `${canonicalName} is also set and wins`
|
|
203
|
+
: `rename it to ${canonicalName}`;
|
|
204
|
+
log.warn(`[bongos] deprecated env ${legacyName} — ${fate}. The legacy spelling stops being read in core ${LEGACY_ENV_SUNSET_RELEASE}.`);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// NOT exported, deliberately: warnLegacyEnv, LEGACY_ENV_SUNSET_RELEASE and the
|
|
209
|
+
// ledger are internals. resolveEnv's injectable `warn` is the whole test seam a
|
|
210
|
+
// caller needs, and a test of the DEFAULT path captures console.warn — which
|
|
211
|
+
// exercises the real wiring rather than a hand-called helper. Exporting them
|
|
212
|
+
// would also add three untraceable entries to the knip dead-code ratchet.
|
|
213
|
+
|
|
153
214
|
// resolveCoreRoot() / resolveInstanceRoot() — the ADR 0108 §1 configurable-root
|
|
154
215
|
// resolvers, extending this module's established zero-risk pattern from
|
|
155
216
|
// ~/.config to the in-repo roots. Two OPPOSITE directions, both load-bearing:
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.656'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/src/modules.js
CHANGED
|
@@ -27,7 +27,7 @@ const path = require('node:path');
|
|
|
27
27
|
// resolver below spans built-in + discovered modules (ADR 0083 / BV1.R41). Empty
|
|
28
28
|
// until a module is moved into modules/ — so this is a no-op on the current tree.
|
|
29
29
|
const loader = require('./module-loader/loader');
|
|
30
|
-
const { resolveCoreRoot, resolveInstanceRoot } = require('./instance-config');
|
|
30
|
+
const { resolveCoreRoot, resolveInstanceRoot, resolveEnv, FALLBACK_ENV_PREFIX } = require('./instance-config');
|
|
31
31
|
const { responsibilitiesFor } = require('./role-responsibilities');
|
|
32
32
|
|
|
33
33
|
// ADR 0108 §1: the neutral starter ships WITH the core package; the instance
|
|
@@ -121,7 +121,7 @@ function coerceBool(v) {
|
|
|
121
121
|
// Pure resolver — all inputs injected, so tests drive it with zero I/O.
|
|
122
122
|
// Returns a frozen { <moduleKey>: boolean } covering EVERY registry key, plus a
|
|
123
123
|
// non-enumerable resolution that throws on an unknown module key (typo guard).
|
|
124
|
-
function resolveModules({ neutral = {}, instance = {}, env = {}, envPrefix =
|
|
124
|
+
function resolveModules({ neutral = {}, instance = {}, env = {}, envPrefix = FALLBACK_ENV_PREFIX, registry = MODULE_REGISTRY } = {}) {
|
|
125
125
|
// The known-key set + defaults come from the passed registry: callers inject
|
|
126
126
|
// the built-in MODULE_REGISTRY (the default, what the pure unit tests use) or
|
|
127
127
|
// the effective built-in+discovered registry (what loadModules passes). This
|
|
@@ -157,8 +157,12 @@ function resolveModules({ neutral = {}, instance = {}, env = {}, envPrefix = 'CL
|
|
|
157
157
|
// Env overrides: <PREFIX>_MODULE_<KEY> where KEY is upper-snake of the module
|
|
158
158
|
// name (dev-box -> DEV_BOX). Highest precedence.
|
|
159
159
|
for (const key of known) {
|
|
160
|
-
const
|
|
161
|
-
const
|
|
160
|
+
const suffix = `MODULE_${key.toUpperCase().replace(/-/g, '_')}`;
|
|
161
|
+
const envName = `${envPrefix}_${suffix}`;
|
|
162
|
+
// resolveEnv, not env[envName]: a provisioned instance sets these per box
|
|
163
|
+
// (provision-units.js writes them into the systemd unit), so the pre-rename
|
|
164
|
+
// spelling must keep switching the same module off. Warns once per legacy name.
|
|
165
|
+
const raw = resolveEnv(suffix, { env, prefix: envPrefix });
|
|
162
166
|
if (raw !== undefined && raw !== '') {
|
|
163
167
|
const b = coerceBool(raw);
|
|
164
168
|
if (b === undefined) throw new Error(`modules: ${envName}="${raw}" is not a boolean (use 1/0, on/off, true/false)`);
|
|
@@ -203,14 +207,18 @@ function loadModules({ neutralPath = NEUTRAL_PATH, instancePath, env = process.e
|
|
|
203
207
|
const neutral = readJson(neutralPath) || {};
|
|
204
208
|
let ip = instancePath;
|
|
205
209
|
if (ip === undefined) {
|
|
206
|
-
|
|
210
|
+
// Canonical spelling pinned to FALLBACK_ENV_PREFIX for the same reason as
|
|
211
|
+
// branding's selector — read before the pack that names the prefix. The
|
|
212
|
+
// legacy prefixes still resolve (one warning each), so an already-deployed
|
|
213
|
+
// GDS_MODULES_FILE / GDS_BRANDING_FILE keeps selecting the same pack.
|
|
214
|
+
const override = resolveEnv('MODULES_FILE', { env, prefix: FALLBACK_ENV_PREFIX });
|
|
207
215
|
if (override) ip = path.resolve(resolveInstanceRoot(), override);
|
|
208
|
-
else if (env
|
|
216
|
+
else if (resolveEnv('BRANDING_FILE', { env, prefix: FALLBACK_ENV_PREFIX })) ip = NEUTRAL_PATH;
|
|
209
217
|
else ip = INSTANCE_PATH;
|
|
210
218
|
}
|
|
211
219
|
const instance = readJson(ip) || {};
|
|
212
220
|
// Reuse the branding env prefix so an instance has ONE prefix, not two.
|
|
213
|
-
let envPrefix =
|
|
221
|
+
let envPrefix = FALLBACK_ENV_PREFIX;
|
|
214
222
|
try { envPrefix = require('./branding').branding().envPrefix || envPrefix; } catch { /* fail-to-default */ }
|
|
215
223
|
// Resolve against the EFFECTIVE registry (built-in + discovered) so a config
|
|
216
224
|
// flag for a discovered module is honored. No-op until a module exists.
|
|
@@ -466,7 +466,7 @@ t('materializeClaude: copied hooks carry NO founder origin/config-dir; a disable
|
|
|
466
466
|
const dir = mkdtempSync(join(tmpdir(), 'rb-e2e-'));
|
|
467
467
|
// Target instance: demo-branded, art-pipeline OFF (so its otb-* skills must be dropped).
|
|
468
468
|
mkdirSync(join(dir, 'config'), { recursive: true });
|
|
469
|
-
writeFileSync(join(dir, 'config', 'branding.json'), JSON.stringify({ domains: { publicOrigin: 'https://demo.cloudbongos.com', buildersOrigin: 'https://demo.cloudbongos.com', statusOrigin: 'https://demo.cloudbongos.com', oauthOrigin: 'https://demo.cloudbongos.com' }, configDir: 'cloudbongos', envPrefix: '
|
|
469
|
+
writeFileSync(join(dir, 'config', 'branding.json'), JSON.stringify({ domains: { publicOrigin: 'https://demo.cloudbongos.com', buildersOrigin: 'https://demo.cloudbongos.com', statusOrigin: 'https://demo.cloudbongos.com', oauthOrigin: 'https://demo.cloudbongos.com' }, configDir: 'cloudbongos', envPrefix: 'BONGOS' }));
|
|
470
470
|
writeFileSync(join(dir, 'config', 'modules.json'), JSON.stringify({ modules: { 'art-pipeline': false } }));
|
|
471
471
|
const res = m.materializeClaude({ coreRoot: ROOT, instanceDir: dir, dryRun: false });
|
|
472
472
|
// Founder core → demo: a branding delta exists and rules are built. The NEUTRAL
|
|
@@ -28,7 +28,7 @@ test('R74: a vanilla boot resolves to the Cloud Bongos identity', () => {
|
|
|
28
28
|
assert.equal(b.identity.productName, 'Cloud Bongos');
|
|
29
29
|
assert.equal(b.identity.worldName, 'Cloud Bongos');
|
|
30
30
|
assert.equal(b.currency.label, 'credits'); // not "drachmae"
|
|
31
|
-
assert.equal(b.envPrefix, '
|
|
31
|
+
assert.equal(b.envPrefix, 'BONGOS'); // canonical since task 1003703; CLOUDBONGOS_ still resolves as a legacy prefix
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
test('R74: the vanilla pack carries the Cloud Bongos LOOK (chrome palette, Manrope, bongo favicon)', () => {
|
|
@@ -37,6 +37,13 @@ const LIFECYCLE = path.join(ROOT, 'modules', 'lifecycle');
|
|
|
37
37
|
const eligibility = require(path.join(LIFECYCLE, 'claim-eligibility.js'));
|
|
38
38
|
const db = require(path.join(LIFECYCLE, 'db.js'));
|
|
39
39
|
const catalog = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
|
|
40
|
+
// The doorway, so section D can stand a stub `government` port up in front of the
|
|
41
|
+
// REAL claimPermissionsFor instead of re-implementing its branches in the test.
|
|
42
|
+
const moduleApi = require(path.join(ROOT, 'src', 'module-api.js'));
|
|
43
|
+
// claimPermissionsFor is NOT on the db.js facade — db-claims.js imports it from
|
|
44
|
+
// the authz module directly, so the test reads it from the same place the real
|
|
45
|
+
// caller does rather than from a re-export that does not exist.
|
|
46
|
+
const rankAuthz = require(path.join(LIFECYCLE, 'db-rank-authz.js'));
|
|
40
47
|
import { lifecycleDbSource } from './helpers.mjs';
|
|
41
48
|
|
|
42
49
|
const { CLAIM_ANY, CLAIM_NEWCOMER, permissionAllowsClaim, claimEligibilityAllows } = eligibility;
|
|
@@ -262,3 +269,78 @@ test('the abuse case stays blocked: a xenos cannot reach the general queue by an
|
|
|
262
269
|
assert.equal(permissionAllowsClaim(held, false), false);
|
|
263
270
|
assert.equal(permissionAllowsClaim(held, true), true);
|
|
264
271
|
});
|
|
272
|
+
|
|
273
|
+
// --- D. claimPermissionsFor's three-way answer (task 1003816) ---------------
|
|
274
|
+
//
|
|
275
|
+
// The function this section covers had NO test of its own: every case above
|
|
276
|
+
// drives the pure consumer (claimEligibilityAllows) with a hand-made
|
|
277
|
+
// heldPermissions value, so nothing proved which value the REAL reader produces.
|
|
278
|
+
// That is exactly where the bug lived — an empty permission set was folded in
|
|
279
|
+
// with "could not establish authority" and silently became a rank-rule fallback.
|
|
280
|
+
//
|
|
281
|
+
// The distinction is the whole point, so it is asserted as a distinction: null
|
|
282
|
+
// (fall back) for the two unestablished cases, a Set (a real verdict, denied when
|
|
283
|
+
// empty) whenever the resolver actually answered.
|
|
284
|
+
const realResolveOptional = moduleApi.resolveOptional;
|
|
285
|
+
async function withGovernance(stub, fn) {
|
|
286
|
+
moduleApi.resolveOptional = (name) => (name === 'government' ? stub : realResolveOptional.call(moduleApi, name));
|
|
287
|
+
try { return await fn(); } finally { moduleApi.resolveOptional = realResolveOptional; }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
test('no governance port — authority cannot be established, so the rank rule decides', async () => {
|
|
291
|
+
await withGovernance(undefined, async () => {
|
|
292
|
+
assert.equal(await rankAuthz.claimPermissionsFor('42'), null);
|
|
293
|
+
});
|
|
294
|
+
// A port object that does not implement the resolver is the same situation.
|
|
295
|
+
await withGovernance({}, async () => {
|
|
296
|
+
assert.equal(await rankAuthz.claimPermissionsFor('42'), null);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('the resolver throwing is UNKNOWN, not empty — still the rank rule', async () => {
|
|
301
|
+
await withGovernance({ resolveBuilderPermissions: async () => { throw new Error('synthetic resolver outage'); } }, async () => {
|
|
302
|
+
assert.equal(await rankAuthz.claimPermissionsFor('42'), null);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test('a broken port contract (non-array) is unknown, never a deny', async () => {
|
|
307
|
+
for (const bad of [null, undefined, 'task.claim.any', 42, {}]) {
|
|
308
|
+
await withGovernance({ resolveBuilderPermissions: async () => bad }, async () => {
|
|
309
|
+
assert.equal(await rankAuthz.claimPermissionsFor('42'), null, `a ${typeof bad} return must not read as "denied"`);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('ZERO permissions is a real verdict now, not a fallback (the 1003816 fix)', async () => {
|
|
315
|
+
await withGovernance({ resolveBuilderPermissions: async () => [] }, async () => {
|
|
316
|
+
const held = await rankAuthz.claimPermissionsFor('42');
|
|
317
|
+
assert.ok(held instanceof Set, 'an answered resolve returns a Set, so the rank rule is NOT consulted');
|
|
318
|
+
assert.equal(held.size, 0);
|
|
319
|
+
// And the consumer turns that into a deny for every queue, however permissive
|
|
320
|
+
// the incumbent rank rule would have been.
|
|
321
|
+
for (const nf of NF_VALUES) {
|
|
322
|
+
assert.equal(
|
|
323
|
+
claimEligibilityAllows({ heldPermissions: held, newcomerFriendly: nf, rankRuleAllows: true }),
|
|
324
|
+
false,
|
|
325
|
+
'a builder with no role row is denied even where the rank rule would allow',
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('a real permission set is passed through as the verdict', async () => {
|
|
332
|
+
await withGovernance({ resolveBuilderPermissions: async () => [CLAIM_ANY] }, async () => {
|
|
333
|
+
const held = await rankAuthz.claimPermissionsFor('42');
|
|
334
|
+
assert.ok(held instanceof Set);
|
|
335
|
+
assert.equal(held.has(CLAIM_ANY), true);
|
|
336
|
+
assert.equal(claimEligibilityAllows({ heldPermissions: held, newcomerFriendly: false, rankRuleAllows: false }), true);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('the doc comment no longer argues for the fallback it describes retiring', () => {
|
|
341
|
+
const src = fs.readFileSync(path.join(LIFECYCLE, 'db-rank-authz.js'), 'utf8');
|
|
342
|
+
assert.ok(
|
|
343
|
+
!/nothing yet re-assigns a rank-role/.test(src),
|
|
344
|
+
'the stale "nothing yet re-assigns a rank-role" claim is false since BV1.R95b + task 1003193 — it must not survive the fix',
|
|
345
|
+
);
|
|
346
|
+
});
|
package/tests/init.mjs
CHANGED
|
@@ -51,7 +51,7 @@ test('buildBrandingConfig: lean overrides only (no theme block)', () => {
|
|
|
51
51
|
assert.equal(b.repo.owner, 'lars589');
|
|
52
52
|
assert.equal(b.firstAdmin, 'lars589');
|
|
53
53
|
assert.equal(b.currency.label, 'credits');
|
|
54
|
-
assert.equal(b.envPrefix, '
|
|
54
|
+
assert.equal(b.envPrefix, 'BONGOS'); // canonical since task 1003703
|
|
55
55
|
assert.equal(b.copy.tagline, 'Building software is laying down a beat.');
|
|
56
56
|
assert.equal(b.theme, undefined); // lean — neutral fills the look
|
|
57
57
|
assert.equal(b.domains.buildersOrigin, 'http://localhost:3000');
|
|
@@ -8,7 +8,7 @@ import ic from '../src/instance-config.js';
|
|
|
8
8
|
|
|
9
9
|
const {
|
|
10
10
|
configDirName, envPrefix, configHome, configReadDirs, configReadPaths,
|
|
11
|
-
resolveEnv, FALLBACK_DIR, FALLBACK_ENV_PREFIX,
|
|
11
|
+
resolveEnv, FALLBACK_DIR, FALLBACK_ENV_PREFIX, LEGACY_ENV_PREFIXES,
|
|
12
12
|
resolveInstanceRootExplicit, resolveDocsRoot, resolveCoreRoot,
|
|
13
13
|
} = ic;
|
|
14
14
|
|
|
@@ -22,7 +22,10 @@ test('configDirName: from branding, else the vanilla fallback (never OTB)', () =
|
|
|
22
22
|
test('envPrefix: from branding, else the vanilla fallback', () => {
|
|
23
23
|
assert.equal(envPrefix({ envPrefix: 'OTB' }), 'OTB');
|
|
24
24
|
assert.equal(envPrefix({}), FALLBACK_ENV_PREFIX);
|
|
25
|
-
|
|
25
|
+
// BONGOS since task 1003703 (criterion C3a). CLOUDBONGOS did not disappear —
|
|
26
|
+
// it moved into LEGACY_ENV_PREFIXES, which the fallback tests below pin.
|
|
27
|
+
assert.equal(FALLBACK_ENV_PREFIX, 'BONGOS');
|
|
28
|
+
assert.ok(LEGACY_ENV_PREFIXES.includes('CLOUDBONGOS'), 'the previous canonical prefix must stay readable');
|
|
26
29
|
});
|
|
27
30
|
|
|
28
31
|
test('configHome: ~/.config/<dir>', () => {
|
|
@@ -61,6 +64,99 @@ test('resolveEnv: legacy PMS prefix still honored (original-name back-compat)',
|
|
|
61
64
|
assert.equal(resolveEnv('GITHUB_CLIENT_ID', { prefix: 'CLOUDBONGOS', env: { PMS_GITHUB_CLIENT_ID: 'old' } }), 'old');
|
|
62
65
|
});
|
|
63
66
|
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// The legacy-prefix DEPRECATION WARNING (task 1003703 / criterion C3a).
|
|
69
|
+
//
|
|
70
|
+
// These assert the fallback FIRES, not merely that the new spelling works. A
|
|
71
|
+
// happy-path-only test would still pass with the whole legacy path deleted,
|
|
72
|
+
// which is exactly the regression task 1003706 (C3d) exists to catch when it
|
|
73
|
+
// removes the fallback on purpose — at which point these tests must be inverted,
|
|
74
|
+
// not quietly dropped.
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
// Collects warn() payloads instead of printing, so a case can assert on shape.
|
|
78
|
+
function capture() {
|
|
79
|
+
const calls = [];
|
|
80
|
+
return { calls, warn: (payload) => { calls.push(payload); return true; } };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Runs fn with console.warn captured, so a case can assert on the REAL default
|
|
84
|
+
// warn path rather than a hand-called helper. resolveEnv's `warn` option is the
|
|
85
|
+
// only seam the module exports; the renderer and its warn-once ledger are
|
|
86
|
+
// internals, reached here exactly the way production reaches them.
|
|
87
|
+
function captureConsole(fn) {
|
|
88
|
+
const lines = [];
|
|
89
|
+
const real = console.warn;
|
|
90
|
+
console.warn = (m) => lines.push(String(m));
|
|
91
|
+
try { fn(); } finally { console.warn = real; }
|
|
92
|
+
return lines;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
test('resolveEnv: ONLY the old spelling set — resolves, and warns naming old, new and the sunset', () => {
|
|
96
|
+
const { calls, warn } = capture();
|
|
97
|
+
const v = resolveEnv('API_BASE', { prefix: 'BONGOS', env: { GDS_API_BASE: 'legacy' }, warn });
|
|
98
|
+
assert.equal(v, 'legacy', 'the legacy spelling must still resolve — boxes still export it');
|
|
99
|
+
assert.equal(calls.length, 1, 'exactly one warning for the one legacy name that was read');
|
|
100
|
+
assert.deepEqual(calls[0], { legacyName: 'GDS_API_BASE', canonicalName: 'BONGOS_API_BASE', shadowed: false, differs: false });
|
|
101
|
+
|
|
102
|
+
// …and the DEFAULT renderer puts all three facts an operator needs on the line.
|
|
103
|
+
// A suffix used by no other case, so the process-global warn-once ledger cannot
|
|
104
|
+
// have already spent this name.
|
|
105
|
+
const lines = captureConsole(() => {
|
|
106
|
+
resolveEnv('C3A_RENDER_PROBE', { prefix: 'BONGOS', env: { CLOUDBONGOS_C3A_RENDER_PROBE: 'legacy' } });
|
|
107
|
+
});
|
|
108
|
+
assert.equal(lines.length, 1);
|
|
109
|
+
assert.match(lines[0], /CLOUDBONGOS_C3A_RENDER_PROBE/, 'names the old spelling');
|
|
110
|
+
assert.match(lines[0], /BONGOS_C3A_RENDER_PROBE/, 'names the new spelling');
|
|
111
|
+
assert.match(lines[0], /stops being read in core 1\.21/, 'names the release it stops working in');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('resolveEnv: ONLY the new spelling set — resolves, and is SILENT', () => {
|
|
115
|
+
const { calls, warn } = capture();
|
|
116
|
+
assert.equal(resolveEnv('API_BASE', { prefix: 'BONGOS', env: { BONGOS_API_BASE: 'new' }, warn }), 'new');
|
|
117
|
+
assert.deepEqual(calls, [], 'the canonical spelling must never warn');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('resolveEnv: BOTH set and disagreeing — the new one wins, and the warning says so', () => {
|
|
121
|
+
const { calls, warn } = capture();
|
|
122
|
+
const v = resolveEnv('API_BASE', { prefix: 'BONGOS', env: { BONGOS_API_BASE: 'new', GDS_API_BASE: 'legacy' }, warn });
|
|
123
|
+
assert.equal(v, 'new', 'an operator who sets the new name expects it to take effect');
|
|
124
|
+
assert.equal(calls.length, 1, 'a SHADOWED legacy name still earns its one warning — it is set on some box');
|
|
125
|
+
assert.equal(calls[0].shadowed, true);
|
|
126
|
+
assert.equal(calls[0].differs, true);
|
|
127
|
+
const lines = captureConsole(() => {
|
|
128
|
+
resolveEnv('C3A_DISAGREE_PROBE', { prefix: 'BONGOS', env: { BONGOS_C3A_DISAGREE_PROBE: 'new', CLOUDBONGOS_C3A_DISAGREE_PROBE: 'legacy' } });
|
|
129
|
+
});
|
|
130
|
+
assert.match(lines[0], /ALSO set to a different value and WINS/, 'the disagreement is spelled out, not implied');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('resolveEnv: both set to the SAME value — warns, but does not claim a disagreement', () => {
|
|
134
|
+
const { calls, warn } = capture();
|
|
135
|
+
resolveEnv('API_BASE', { prefix: 'BONGOS', env: { BONGOS_API_BASE: 'same', GDS_API_BASE: 'same' }, warn });
|
|
136
|
+
assert.equal(calls[0].shadowed, true);
|
|
137
|
+
assert.equal(calls[0].differs, false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('resolveEnv: every legacy spelling set gets its OWN warning, and the first one wins', () => {
|
|
141
|
+
const { calls, warn } = capture();
|
|
142
|
+
const v = resolveEnv('API_BASE', { prefix: 'BONGOS', env: { CLOUDBONGOS_API_BASE: 'a', OTB_API_BASE: 'b', PMS_API_BASE: 'c' }, warn });
|
|
143
|
+
assert.equal(v, 'a', 'LEGACY_ENV_PREFIXES order decides: CLOUDBONGOS before OTB before PMS');
|
|
144
|
+
assert.deepEqual(calls.map((c) => c.legacyName), ['CLOUDBONGOS_API_BASE', 'OTB_API_BASE', 'PMS_API_BASE']);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('the deprecation warning is once per NAME per process, not once per read', () => {
|
|
148
|
+
const env = { CLOUDBONGOS_C3A_ONCE_PROBE: 'x', CLOUDBONGOS_C3A_ONCE_OTHER: 'y' };
|
|
149
|
+
const lines = captureConsole(() => {
|
|
150
|
+
resolveEnv('C3A_ONCE_PROBE', { prefix: 'BONGOS', env });
|
|
151
|
+
resolveEnv('C3A_ONCE_PROBE', { prefix: 'BONGOS', env }); // same name again
|
|
152
|
+
resolveEnv('C3A_ONCE_PROBE', { prefix: 'BONGOS', env });
|
|
153
|
+
resolveEnv('C3A_ONCE_OTHER', { prefix: 'BONGOS', env }); // a DIFFERENT name
|
|
154
|
+
});
|
|
155
|
+
assert.equal(lines.length, 2, 'three reads of one name warn once; a second name warns again');
|
|
156
|
+
assert.match(lines[0], /CLOUDBONGOS_C3A_ONCE_PROBE/);
|
|
157
|
+
assert.match(lines[1], /CLOUDBONGOS_C3A_ONCE_OTHER/);
|
|
158
|
+
});
|
|
159
|
+
|
|
64
160
|
// task 2051: docs+client are per-instance artifacts written to the instance root when
|
|
65
161
|
// it is EXPLICITLY pointed at (a consumer running `bongos upgrade`), else the coreRoot.
|
|
66
162
|
test('resolveInstanceRootExplicit: env pointer (branded or fallback prefix), else null', () => {
|
package/tests/upgrade.mjs
CHANGED
|
@@ -211,7 +211,7 @@ t('regenerateApiArtifacts: spawns docs then client from the new core, targeting
|
|
|
211
211
|
assert.equal(c.cmd, process.execPath, 'runs node');
|
|
212
212
|
assert.equal(c.opts.cwd, '/inst', 'cwd is the instance');
|
|
213
213
|
// the env points the generators OUTPUT at the instance repo, not node_modules
|
|
214
|
-
assert.equal(c.opts.env.
|
|
214
|
+
assert.equal(c.opts.env.BONGOS_INSTANCE_ROOT, '/inst');
|
|
215
215
|
}
|
|
216
216
|
});
|
|
217
217
|
|
|
@@ -805,7 +805,7 @@ t('regenerateNavDocs: default set is the whole-file nav docs (session-index only
|
|
|
805
805
|
assert.deepEqual(u.NAV_WHOLE_FILE_GENERATORS, ['gen-session-index.js']);
|
|
806
806
|
});
|
|
807
807
|
|
|
808
|
-
t('regenerateNavDocs: spawns each generator from the CORE against the INSTANCE root (cwd +
|
|
808
|
+
t('regenerateNavDocs: spawns each generator from the CORE against the INSTANCE root (cwd + BONGOS_INSTANCE_ROOT), ok when all succeed', () => {
|
|
809
809
|
const calls = [];
|
|
810
810
|
const run = (cmd, args, opts) => { calls.push({ cmd, arg0: args[0], cwd: opts && opts.cwd, env: opts && opts.env }); return { status: 0 }; };
|
|
811
811
|
const fsImpl = { existsSync: () => true };
|
|
@@ -815,7 +815,7 @@ t('regenerateNavDocs: spawns each generator from the CORE against the INSTANCE r
|
|
|
815
815
|
assert.deepEqual(res.failed, []);
|
|
816
816
|
assert.equal(calls.length, 1);
|
|
817
817
|
assert.equal(calls[0].cwd, '/inst', 'runs with cwd = the instance dir');
|
|
818
|
-
assert.equal(calls[0].env.
|
|
818
|
+
assert.equal(calls[0].env.BONGOS_INSTANCE_ROOT, '/inst', 'sets the instance-root env so the generator writes into the instance');
|
|
819
819
|
assert.equal(calls[0].arg0, join('/core', 'scripts', 'gds', 'gen-session-index.js'), 'spawns the generator from the CORE package');
|
|
820
820
|
});
|
|
821
821
|
|
|
@@ -368,8 +368,20 @@ await test('an empty view still offers the invite form and the switch out of it'
|
|
|
368
368
|
});
|
|
369
369
|
|
|
370
370
|
await test('a 403 seals the section rather than blanking the page', async () => {
|
|
371
|
+
// This used to assert the literal "Archons only". Task 1003449 removed that
|
|
372
|
+
// sentence from the page: the floor is read off the denial's `details.floors`
|
|
373
|
+
// now, because ADR 0157 moved most of this surface to metic and a page must
|
|
374
|
+
// not name a floor its gate does not enforce. THIS harness answers 403 with a
|
|
375
|
+
// null body, so there is no floor to name and the honest render is the subject
|
|
376
|
+
// clause alone. What the test was really pinning — the section seals, the page
|
|
377
|
+
// does not blank — is unchanged and is what it asserts.
|
|
378
|
+
//
|
|
379
|
+
// The floor-derivation itself is executed in tests/watch_sealed_floor.mjs,
|
|
380
|
+
// which scripts real envelopes at both floors.
|
|
371
381
|
const h = await boot({ queueStatus: 403 });
|
|
372
|
-
assert.match(h.gate().textContent, /
|
|
382
|
+
assert.match(h.gate().textContent, /not visible at your rank/);
|
|
383
|
+
assert.doesNotMatch(h.gate().textContent, /Archon|Metic/,
|
|
384
|
+
'a denial with no floor in its body must name no rank');
|
|
373
385
|
assert.equal(h.rows().length, 0);
|
|
374
386
|
});
|
|
375
387
|
|