@bongos/core 1.19.653 → 1.19.655
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 +58 -53
- 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/ideas/routes/inbox.js +22 -3
- 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/main-audit.js +22 -8
- 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/spark.js +26 -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/cli_exit_no_abort.mjs +109 -2
- package/tests/cloud_bongos_pack.mjs +1 -1
- package/tests/idea_develop_spark.mjs +43 -0
- package/tests/idea_resubmit.mjs +7 -3
- 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
|
@@ -51,7 +51,7 @@ const { stealthGate } = require('./stealth-gate');
|
|
|
51
51
|
const { platformVisibilityGate } = require('./platform-visibility-gate');
|
|
52
52
|
const { clientModules, isModuleEnabled } = require('../modules');
|
|
53
53
|
const { hallWidgetScripts, moduleWebSurfaces } = require('../module-loader/loader');
|
|
54
|
-
const { resolveCoreRoot, resolveDocsRoot, resolveInstanceRoot } = require('../instance-config');
|
|
54
|
+
const { resolveCoreRoot, resolveDocsRoot, resolveInstanceRoot, resolveEnv } = require('../instance-config');
|
|
55
55
|
const { API_PREFIX, LEGACY_API_PREFIXES, API_VERSION, VERSIONED_API_PREFIX, ALL_API_PREFIXES } = require('./api-prefix');
|
|
56
56
|
|
|
57
57
|
// ADR 0108 §1: every use of ROOT below (infra/ installers, modules/, docs/) is
|
|
@@ -640,7 +640,7 @@ function mountInternalSurfaces(app) {
|
|
|
640
640
|
// Pinned public origin for the served installer scripts (never a request header —
|
|
641
641
|
// a spoofed Host can't redirect a `curl | sh`). The fallback comes from the
|
|
642
642
|
// branding pack, not a hardcoded host brand (ADR 0062 §3).
|
|
643
|
-
const BONGOS_PUBLIC_ORIGIN =
|
|
643
|
+
const BONGOS_PUBLIC_ORIGIN = resolveEnv('PUBLIC_ORIGIN') || branding().domains.publicOrigin;
|
|
644
644
|
const readBongosInstallScript = (name) => {
|
|
645
645
|
try {
|
|
646
646
|
return fs.readFileSync(path.join(ROOT, 'infra', name), 'utf8').split('__GDS_API_BASE__').join(BONGOS_PUBLIC_ORIGIN);
|
package/src/branding.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
const fs = require('node:fs');
|
|
20
20
|
const path = require('node:path');
|
|
21
|
-
const { resolveCoreRoot, resolveInstanceRoot } = require('./instance-config');
|
|
21
|
+
const { resolveCoreRoot, resolveInstanceRoot, resolveEnv, FALLBACK_ENV_PREFIX } = require('./instance-config');
|
|
22
22
|
|
|
23
23
|
// ADR 0108 §1: the neutral starter ships WITH the core package; the instance
|
|
24
24
|
// pack is host content. Both resolvers return the repo root today, so this is
|
|
@@ -106,9 +106,15 @@ function resolveBranding({ neutral = {}, instance = {}, env = {} } = {}) {
|
|
|
106
106
|
// neutral/instance inputs — otherwise the deepFreeze below would also freeze
|
|
107
107
|
// the caller's input objects (deepMerge shallow-copies only the top level).
|
|
108
108
|
const resolved = structuredClone(deepMerge(neutral, instance));
|
|
109
|
-
const prefix = resolved.envPrefix ||
|
|
109
|
+
const prefix = resolved.envPrefix || FALLBACK_ENV_PREFIX;
|
|
110
110
|
for (const [suffix, p, coerce] of ENV_OVERRIDES) {
|
|
111
|
-
|
|
111
|
+
// resolveEnv, not a bare env[] read: these overrides are set OUTSIDE the repo
|
|
112
|
+
// (/etc/cloudbongos/web.env, systemd units, the provisioning runner), so when
|
|
113
|
+
// task 1003703 moved the canonical prefix from CLOUDBONGOS_ to BONGOS_ a
|
|
114
|
+
// direct read would have silently stopped honoring every already-deployed
|
|
115
|
+
// CLOUDBONGOS_PUBLIC_ORIGIN — an origin regression with no error, found only
|
|
116
|
+
// by a broken OAuth callback. The legacy spellings warn once each instead.
|
|
117
|
+
const v = resolveEnv(suffix, { env, prefix });
|
|
112
118
|
if (v === undefined || v === '') continue;
|
|
113
119
|
// Most overrides are string-valued (origins, cookie domain) and set verbatim. A
|
|
114
120
|
// 'bool'-tagged override (auth.idp) coerces the env string to a real boolean —
|
|
@@ -139,15 +145,26 @@ function readJson(p) {
|
|
|
139
145
|
// OTB on the apex (server.js, the committed config/branding.json) and a vanilla
|
|
140
146
|
// Cloud Bongos process pointed at its own pack (config/branding.cloudbongos.json)
|
|
141
147
|
// — differentiated only by env, with no divergent clone to maintain. The loader
|
|
142
|
-
// runs before the resolved envPrefix is known, so this is a FIXED env name
|
|
143
|
-
//
|
|
144
|
-
//
|
|
148
|
+
// runs before the resolved envPrefix is known, so this is a FIXED env name, not
|
|
149
|
+
// a <PREFIX>_ one — which is exactly why it cannot go through resolveEnv() and
|
|
150
|
+
// needs the two spellings listed by hand here. A relative path resolves against
|
|
151
|
+
// the repo root.
|
|
152
|
+
//
|
|
153
|
+
// BONGOS_BRANDING_FILE is canonical since task 1003703; GDS_BRANDING_FILE is
|
|
154
|
+
// still read (one deprecation warning per process) because it is set outside the
|
|
155
|
+
// repo — the hall-preview server, CI jobs and side-by-side dev processes all
|
|
156
|
+
// export it. Task 1003706 retires the old spelling.
|
|
145
157
|
function loadBranding({ neutralPath = NEUTRAL_PATH, instancePath, env = process.env } = {}) {
|
|
146
158
|
const neutral = readJson(neutralPath);
|
|
147
159
|
if (!neutral) throw new Error(`branding: neutral starter not found at ${neutralPath}`);
|
|
148
160
|
let ip = instancePath;
|
|
149
161
|
if (ip === undefined) {
|
|
150
|
-
|
|
162
|
+
// prefix pinned to FALLBACK_ENV_PREFIX, not envPrefix(): this read happens
|
|
163
|
+
// BEFORE the pack that would name the prefix can be loaded, so the canonical
|
|
164
|
+
// spelling has to be a fixed one. Same pin, same reason, as
|
|
165
|
+
// resolveInstanceRootExplicit(). The legacy prefixes still resolve, which is
|
|
166
|
+
// what keeps every already-deployed GDS_BRANDING_FILE working.
|
|
167
|
+
const override = resolveEnv('BRANDING_FILE', { env, prefix: FALLBACK_ENV_PREFIX });
|
|
151
168
|
ip = override ? path.resolve(resolveInstanceRoot(), override) : INSTANCE_PATH;
|
|
152
169
|
}
|
|
153
170
|
const instance = readJson(ip) || {};
|
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.655'; // 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
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
// tests/cli_exit_no_abort.mjs — the GDS CLIs must not abort natively on exit
|
|
2
2
|
// (task 1002734; the pre-push twin of this is tests/pre_push_hook.mjs, task 1002725).
|
|
3
3
|
//
|
|
4
|
+
// Guards api.js AND main-audit.js (task 1003772). main-audit --verify-head IS the
|
|
5
|
+
// deploy gate (infra/deploy.sh calls it so a poisoned main never reaches
|
|
6
|
+
// production), so there the destroyed exit code is not cosmetic: the abort fires
|
|
7
|
+
// AFTER the verdict prints, and a caller reading the code cannot tell a clean
|
|
8
|
+
// audit from a real blocker. It reproduced 1/1 on Windows + Node 24 before the fix
|
|
9
|
+
// and went 5/5 clean after it.
|
|
10
|
+
//
|
|
4
11
|
// THE BUG. On Windows + Node 24, calling process.exit() after the CLI has made a
|
|
5
12
|
// request aborts the process before it can exit cleanly:
|
|
6
13
|
//
|
|
@@ -26,13 +33,24 @@ import { test } from 'node:test';
|
|
|
26
33
|
import { spawnSync, spawn } from 'node:child_process';
|
|
27
34
|
import fs from 'node:fs';
|
|
28
35
|
import path from 'node:path';
|
|
36
|
+
import { createRequire } from 'node:module';
|
|
29
37
|
import { fileURLToPath } from 'node:url';
|
|
30
38
|
|
|
31
39
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
32
40
|
const API = path.join(ROOT, 'scripts', 'gds', 'api.js');
|
|
41
|
+
const MAIN_AUDIT = path.join(ROOT, 'scripts', 'gds', 'main-audit.js');
|
|
33
42
|
const CLI_LIB = path.join(ROOT, 'scripts', 'gds', 'cli-lib.js');
|
|
34
43
|
const DEAD_BASE = 'http://127.0.0.1:9'; // discard port — connection refused, instantly
|
|
35
44
|
|
|
45
|
+
// Point a CLI at a base URL. DERIVED, not hardcoded: instance-config resolves
|
|
46
|
+
// <PREFIX>_API_BASE first and only then the legacy GDS_/OTB_/PMS_ spellings, so a
|
|
47
|
+
// literal here would (a) add to the legacy-GDS ratchet the rename is retiring and
|
|
48
|
+
// (b) silently stop steering the CLI on any instance whose brand pack sets a
|
|
49
|
+
// different prefix — the test would then hit the REAL API instead of the stub.
|
|
50
|
+
const { envPrefix } = createRequire(import.meta.url)(path.join(ROOT, 'src', 'instance-config.js'));
|
|
51
|
+
const API_BASE_ENV = `${envPrefix()}_API_BASE`;
|
|
52
|
+
const atBase = (url) => ({ [API_BASE_ENV]: url });
|
|
53
|
+
|
|
36
54
|
// The native abort prints this and nothing else can be relied on; it is NOT a
|
|
37
55
|
// JS-level error, so it never appears as a caught exception.
|
|
38
56
|
const ABORT_RE = /Assertion failed|UV_HANDLE_CLOSING/i;
|
|
@@ -41,7 +59,7 @@ function runApi(args, env = {}) {
|
|
|
41
59
|
return spawnSync(process.execPath, [API, ...args], {
|
|
42
60
|
cwd: ROOT,
|
|
43
61
|
encoding: 'utf8',
|
|
44
|
-
env: { ...process.env,
|
|
62
|
+
env: { ...process.env, ...atBase(DEAD_BASE), ...env },
|
|
45
63
|
});
|
|
46
64
|
}
|
|
47
65
|
|
|
@@ -104,7 +122,7 @@ test('api.js exits 0 after a SUCCESSFUL request — the path that aborted in the
|
|
|
104
122
|
const { proc, port } = await startStub();
|
|
105
123
|
try {
|
|
106
124
|
for (let i = 0; i < 3; i++) {
|
|
107
|
-
const r = runApi(['GET', '/api/gds/me', '--quiet'],
|
|
125
|
+
const r = runApi(['GET', '/api/gds/me', '--quiet'], atBase(`http://127.0.0.1:${port}`));
|
|
108
126
|
assert.doesNotMatch(r.stderr || '', ABORT_RE, `run ${i + 1} aborted natively after a 200:\n${r.stderr}`);
|
|
109
127
|
assert.equal(r.status, 0, `a successful request must exit 0, got ${r.status} (127 = the abort); stderr: ${r.stderr}`);
|
|
110
128
|
assert.match(r.stdout, /"ok"/, 'the JSON body must still reach stdout');
|
|
@@ -133,3 +151,92 @@ test('api.js routes its post-request exits through cliExit, not process.exit', (
|
|
|
133
151
|
);
|
|
134
152
|
assert.match(afterRequest, /cliExit\(/, 'the post-request paths must set the exit code via cliExit');
|
|
135
153
|
});
|
|
154
|
+
|
|
155
|
+
// ---- main-audit.js — the deploy gate (task 1003772) ------------------------
|
|
156
|
+
// Ranges are always HEAD..HEAD: it resolves on every checkout including a shallow
|
|
157
|
+
// CI clone, audits zero commits, and still runs the WHOLE pipeline — fetchRankMap
|
|
158
|
+
// is called before the verdict, so the exit is a post-request exit either way.
|
|
159
|
+
//
|
|
160
|
+
// THE STATIC CHECK IS THE REAL NET, and this was measured, not assumed: with the
|
|
161
|
+
// hard exit deliberately put back, the static test failed while all four
|
|
162
|
+
// behavioural tests below still PASSED. The same run against a real range
|
|
163
|
+
// (`--range origin/main~15..origin/main`, 31 commits, a live roster call) aborted
|
|
164
|
+
// at exit 127. So the abort needs the full stack to line up — enough commits and a
|
|
165
|
+
// real request — which a network-free test cannot stage, exactly as the api.js
|
|
166
|
+
// note above says. The behavioural tests are still worth their runtime: they pin
|
|
167
|
+
// the exit CODES (0 / 2) that callers branch on, and their timeouts catch the
|
|
168
|
+
// documented conversion trap where a leaked timer turns the crash into a hang.
|
|
169
|
+
|
|
170
|
+
function runAudit(args, env = {}) {
|
|
171
|
+
return spawnSync(process.execPath, [MAIN_AUDIT, ...args], {
|
|
172
|
+
cwd: ROOT,
|
|
173
|
+
encoding: 'utf8',
|
|
174
|
+
timeout: 60_000, // dropping a hard exit trades a crash for a HANG if a timer leaks
|
|
175
|
+
env: { ...process.env, ...atBase(DEAD_BASE), ...env },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
test('main-audit.js routes every exit through cliExit, not process.exit', () => {
|
|
180
|
+
const src = fs.readFileSync(MAIN_AUDIT, 'utf8');
|
|
181
|
+
const stripped = src.replace(/\/\/[^\n]*/g, '');
|
|
182
|
+
// Unlike api.js, main-audit has no pre-request exits worth keeping separate —
|
|
183
|
+
// all seven sites were converted, so the guard is simply "none left".
|
|
184
|
+
assert.ok(
|
|
185
|
+
!/process\.exit\s*\(/.test(stripped),
|
|
186
|
+
'no process.exit() may remain in main-audit.js — that is what aborts node on Windows, '
|
|
187
|
+
+ 'and this script IS the deploy gate whose exit code the abort destroys'
|
|
188
|
+
);
|
|
189
|
+
assert.match(src, /cliExit\(/, 'the exit paths must set the exit code via cliExit');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('main-audit.js exits cleanly after a FAILED roster request', () => {
|
|
193
|
+
const r = runAudit(['--range', 'HEAD..HEAD']);
|
|
194
|
+
assert.notEqual(r.signal, 'SIGTERM', 'timed out — a leaked timer turned the crash into a hang');
|
|
195
|
+
assert.doesNotMatch(r.stderr || '', ABORT_RE, `native abort on the audit path:\n${r.stderr}`);
|
|
196
|
+
assert.equal(r.status, 0, `a clean audit must exit 0, not ${r.status} (127 = the abort)`);
|
|
197
|
+
assert.match(r.stdout, /✓ clean/, 'the verdict must still reach stdout');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('main-audit.js --verify-head exits 0 on a clean audit — the deploy gate reads this code', () => {
|
|
201
|
+
// The gate path specifically: infra/deploy.sh treats non-zero as "do not deploy",
|
|
202
|
+
// so an abort here blocks a deploy that nothing is actually wrong with.
|
|
203
|
+
for (let i = 0; i < 3; i++) {
|
|
204
|
+
const r = runAudit(['--verify-head', '--range', 'HEAD..HEAD']);
|
|
205
|
+
assert.notEqual(r.signal, 'SIGTERM', `run ${i + 1} timed out — leaked handle`);
|
|
206
|
+
assert.doesNotMatch(r.stderr || '', ABORT_RE, `run ${i + 1} aborted natively:\n${r.stderr}`);
|
|
207
|
+
assert.equal(r.status, 0, `run ${i + 1} exited ${r.status}; stderr: ${r.stderr}`);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('main-audit.js --require-roster still exits 2 when the roster is unreachable', () => {
|
|
212
|
+
// The post-request DID-NOT-RUN path. Converting it to cliExit must not turn a
|
|
213
|
+
// refusal into a vacuous green — exit 2 is what stops CI calling this a pass.
|
|
214
|
+
const r = runAudit(['--require-roster', '--range', 'HEAD..HEAD']);
|
|
215
|
+
assert.notEqual(r.signal, 'SIGTERM', 'timed out — leaked handle');
|
|
216
|
+
assert.doesNotMatch(r.stderr || '', ABORT_RE);
|
|
217
|
+
assert.equal(r.status, 2, `an unavailable roster must exit 2, got ${r.status}`);
|
|
218
|
+
assert.match(r.stderr, /DID NOT RUN/);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('main-audit.js reports an unresolvable range as exit 2 (pre-request path)', () => {
|
|
222
|
+
const r = runAudit(['--range', 'bogusref..HEAD']);
|
|
223
|
+
assert.doesNotMatch(r.stderr || '', ABORT_RE);
|
|
224
|
+
assert.equal(r.status, 2, 'an unresolvable range is exit 2, and must not be mistaken for clean');
|
|
225
|
+
assert.match(r.stderr, /DID NOT RUN/);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('main-audit.js exits 0 after a SUCCESSFUL roster request — the teardown that aborted in the wild', async () => {
|
|
229
|
+
// A refused connection tears down far less machinery than a 200. The wild repro
|
|
230
|
+
// was a successful roster call, so this is the case that actually matters.
|
|
231
|
+
const { proc, port } = await startStub();
|
|
232
|
+
try {
|
|
233
|
+
for (let i = 0; i < 3; i++) {
|
|
234
|
+
const r = runAudit(['--range', 'HEAD..HEAD'], atBase(`http://127.0.0.1:${port}`));
|
|
235
|
+
assert.notEqual(r.signal, 'SIGTERM', `run ${i + 1} timed out — leaked handle`);
|
|
236
|
+
assert.doesNotMatch(r.stderr || '', ABORT_RE, `run ${i + 1} aborted natively after a 200:\n${r.stderr}`);
|
|
237
|
+
assert.equal(r.status, 0, `run ${i + 1} exited ${r.status} (127 = the abort); stderr: ${r.stderr}`);
|
|
238
|
+
}
|
|
239
|
+
} finally {
|
|
240
|
+
proc.kill();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
@@ -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)', () => {
|
|
@@ -382,6 +382,49 @@ await test('the CLI names both parties, and never prints "undefined"', () => {
|
|
|
382
382
|
assert.doesNotMatch(out, /undefined|null/);
|
|
383
383
|
});
|
|
384
384
|
|
|
385
|
+
// task 1003654 — the R157 proof walk read "A board ratification window is open
|
|
386
|
+
// until null" five times on cloudbongos.com. A NULL closes_at is the no-clock
|
|
387
|
+
// constitution, NOT a broken window, so the copy has to name how the sitting
|
|
388
|
+
// closes. One case per pass rule, plus the unknown-rule fallback, because the
|
|
389
|
+
// whole failure was one branch that assumed a deadline always exists.
|
|
390
|
+
for (const [rule, expected] of [
|
|
391
|
+
['first_ratifier', /no clock; it closes on the first vote/],
|
|
392
|
+
['consent', /no clock; it stands unless someone objects/],
|
|
393
|
+
['majority', /no clock; it waits for a majority/],
|
|
394
|
+
['unanimous', /no clock; it waits for a unanimous yes/],
|
|
395
|
+
[null, /no clock; it stays open until the board decides/],
|
|
396
|
+
]) {
|
|
397
|
+
await test(`a clockless ${rule || 'unknown-rule'} window says how it closes, never "until null"`, () => {
|
|
398
|
+
const out = cli.renderDeveloped({
|
|
399
|
+
idea: { id: '77', completeness_score: 88 },
|
|
400
|
+
author_id: '9', developer_id: '42', self_developed: false,
|
|
401
|
+
board_window: { item_id: '3', closes_at: null, pass_rule: rule },
|
|
402
|
+
});
|
|
403
|
+
assert.match(out, /ratification window is open/);
|
|
404
|
+
assert.match(out, expected);
|
|
405
|
+
assert.doesNotMatch(out, /until null|undefined/);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Held equal to government's own rule list, the way board.js's FULL_IDEA_PASS_BAR
|
|
410
|
+
// is held equal to economy's — the CLI cannot require() a sibling module, so the
|
|
411
|
+
// test is where the two lists meet. Without this, adding a fifth pass rule
|
|
412
|
+
// silently drops its authors onto the unreadable-rule fallback.
|
|
413
|
+
await test('EVERY pass rule government declares gets its own sentence, not the fallback', () => {
|
|
414
|
+
const { PASS_RULES } = require('../modules/government/config.js');
|
|
415
|
+
const fallback = cli.renderDeveloped({
|
|
416
|
+
idea: { id: '77', completeness_score: 88 }, author_id: '9', developer_id: '42', self_developed: false,
|
|
417
|
+
board_window: { item_id: '3', closes_at: null, pass_rule: '__no_such_rule__' },
|
|
418
|
+
});
|
|
419
|
+
for (const rule of PASS_RULES) {
|
|
420
|
+
const out = cli.renderDeveloped({
|
|
421
|
+
idea: { id: '77', completeness_score: 88 }, author_id: '9', developer_id: '42', self_developed: false,
|
|
422
|
+
board_window: { item_id: '3', closes_at: null, pass_rule: rule },
|
|
423
|
+
});
|
|
424
|
+
assert.notEqual(out, fallback, `pass rule '${rule}' falls through to the unreadable-rule fallback`);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
385
428
|
await test('self-development reads as one person, not a handoff to yourself', () => {
|
|
386
429
|
const out = cli.renderDeveloped({ idea: { id: '1', completeness_score: 70 }, author_id: '9', developer_id: '9', self_developed: true });
|
|
387
430
|
assert.match(out, /You had the thought and you developed it/);
|
package/tests/idea_resubmit.mjs
CHANGED
|
@@ -25,7 +25,7 @@ const api = require('../src/module-api.js');
|
|
|
25
25
|
|
|
26
26
|
// ---- the government port stub (registered BEFORE the routes load) ----------
|
|
27
27
|
let rejectionAnswer = null; // what latestRejectionForIdea returns
|
|
28
|
-
let windowAnswer = { opened: true, item: { id: '31', closes_at: null } };
|
|
28
|
+
let windowAnswer = { opened: true, item: { id: '31', closes_at: null, constitution: { pass_rule: 'first_ratifier' } } };
|
|
29
29
|
const portCalls = [];
|
|
30
30
|
if (!api.hasProvider('government')) {
|
|
31
31
|
api.registerProvider('government', {
|
|
@@ -84,7 +84,7 @@ const fullIdea = (over = {}) => ({
|
|
|
84
84
|
let passed = 0, failed = 0;
|
|
85
85
|
async function test(name, fn) {
|
|
86
86
|
queries = []; portCalls.length = 0;
|
|
87
|
-
rejectionAnswer = null; windowAnswer = { opened: true, item: { id: '31', closes_at: null } };
|
|
87
|
+
rejectionAnswer = null; windowAnswer = { opened: true, item: { id: '31', closes_at: null, constitution: { pass_rule: 'first_ratifier' } } };
|
|
88
88
|
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
89
89
|
catch (err) { failed++; console.error(` FAIL ${name}\n ${err.message}`); }
|
|
90
90
|
}
|
|
@@ -113,7 +113,11 @@ await test('a board objection outranks the mechanical flag, and an ANSWERED sect
|
|
|
113
113
|
assert.equal(r.payload.flagged_section, 'purpose', 'the human objection wins over the weakest-section flag');
|
|
114
114
|
assert.equal(r.payload.flagged_section_changed, true);
|
|
115
115
|
assert.equal(r.payload.rejection_source, 'board');
|
|
116
|
-
|
|
116
|
+
// The pass rule rides along with the deadline (task 1003654): a clockless
|
|
117
|
+
// sitting reports closes_at null, and without the rule a caller cannot tell
|
|
118
|
+
// that apart from a broken window — which is how "open until null" reached
|
|
119
|
+
// authors in the first place.
|
|
120
|
+
assert.deepEqual(r.payload.board_window, { item_id: '31', closes_at: null, pass_rule: 'first_ratifier' }, 'a new window opened, and it says how it closes');
|
|
117
121
|
});
|
|
118
122
|
|
|
119
123
|
await test('reworking everything EXCEPT the flagged section reads changed:false', async () => {
|
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');
|