@bongos/core 1.19.1052 → 1.19.1054
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 +33 -33
- package/docs/copy-inventory.md +100 -114
- package/docs/copy-registry.json +163 -401
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/goal-map.html +18 -24
- package/modules/hall-ui/public/goal-map.states.json +2 -2
- package/modules/hall-ui/public/sky-data.js +22 -18
- package/modules/hall-ui/public/sky-interaction.js +23 -22
- package/modules/hall-ui/public/sky-layout.js +5 -5
- package/modules/hall-ui/public/sky-panel.js +3 -2
- package/modules/hall-ui/public/sky-tour.js +37 -26
- package/modules/hall-ui/public/thinking.css +2 -0
- package/modules/hall-ui/public/thinking.html +16 -25
- package/modules/hall-ui/public/thinking.states.json +1 -1
- package/modules/hall-ui/records/sky.md +11 -0
- package/modules/provisioning/control-plane-guard.js +14 -1
- package/modules/provisioning/provisioning.js +5 -4
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/provision-units.js +38 -2
- package/scripts/gds/provision.js +6 -6
- package/src/module-api.js +1 -1
- package/tests/core_upgrade_door.mjs +16 -2
- package/tests/goal_map_page.mjs +49 -25
- package/tests/hall_sky.mjs +49 -12
- package/tests/helpers.mjs +2 -1
- package/tests/provision_settings_apply.mjs +84 -0
- package/tests/provisioning_settings_apply.mjs +50 -5
|
@@ -315,6 +315,90 @@ await ta('the shape refusal: dedicated → ok:false with guidance naming the val
|
|
|
315
315
|
assert.equal(writes.length, 0);
|
|
316
316
|
});
|
|
317
317
|
|
|
318
|
+
console.log('\nthe control plane\'s own row — the platform pushes a setting to itself (task 1004334):');
|
|
319
|
+
|
|
320
|
+
const U = require('../scripts/gds/provision-units.js');
|
|
321
|
+
const { FALLBACK_ENV_PREFIX } = require('../src/instance-config.js');
|
|
322
|
+
|
|
323
|
+
t('selfEnvPrefix: the web.env BRANDING_FILE line wins, then the instance pack, then the neutral pack, then the fallback', () => {
|
|
324
|
+
const packs = (map) => (p) => {
|
|
325
|
+
const hit = Object.keys(map).find((k) => p.replace(/\\/g, '/').endsWith(k));
|
|
326
|
+
if (!hit) { const e = new Error('ENOENT'); e.code = 'ENOENT'; throw e; }
|
|
327
|
+
return JSON.stringify(map[hit]);
|
|
328
|
+
};
|
|
329
|
+
const neutral = { 'config/branding.neutral.json': { envPrefix: 'NEUTRALX' } };
|
|
330
|
+
const withOwn = { ...neutral, 'config/branding.json': { envPrefix: 'OWNPACK' }, 'packs/site.json': { envPrefix: 'OVERRIDE' } };
|
|
331
|
+
const root = '/srv/instance';
|
|
332
|
+
assert.equal(U.selfEnvPrefix({ envBody: 'PORT=1\nBONGOS_BRANDING_FILE=packs/site.json\n', instanceRoot: root, readFile: packs(withOwn) }), 'OVERRIDE');
|
|
333
|
+
assert.equal(U.selfEnvPrefix({ envBody: 'GDS_BRANDING_FILE=packs/site.json\r\n', instanceRoot: root, readFile: packs(withOwn) }), 'OVERRIDE', 'a legacy spelling still names the pack, as loadBranding reads it');
|
|
334
|
+
assert.equal(U.selfEnvPrefix({ envBody: 'PORT=1\n', instanceRoot: root, readFile: packs(withOwn) }), 'OWNPACK', 'no override → the instance root\'s config/branding.json');
|
|
335
|
+
assert.equal(U.selfEnvPrefix({ envBody: 'FOO_BRANDING_FILE=packs/site.json\n', instanceRoot: root, readFile: packs(withOwn) }), 'OWNPACK', 'an unknown prefix is not the branding knob');
|
|
336
|
+
assert.equal(U.selfEnvPrefix({ envBody: '# BONGOS_BRANDING_FILE=packs/site.json\n', instanceRoot: root, readFile: packs(withOwn) }), 'OWNPACK', 'a commented line is not set');
|
|
337
|
+
// loadBranding's own rule: an override that names a missing file loads NO instance pack,
|
|
338
|
+
// so the neutral one answers — config/branding.json is not consulted as a second chance.
|
|
339
|
+
assert.equal(U.selfEnvPrefix({ envBody: 'BONGOS_BRANDING_FILE=packs/gone.json\n', instanceRoot: root, readFile: packs(withOwn) }), 'NEUTRALX');
|
|
340
|
+
assert.equal(U.selfEnvPrefix({ envBody: '', instanceRoot: root, readFile: packs({ ...neutral, 'config/branding.json': { name: 'no prefix here' } }) }), 'NEUTRALX', 'a pack that names no envPrefix inherits the neutral one');
|
|
341
|
+
assert.equal(U.selfEnvPrefix({ envBody: '', instanceRoot: root, readFile: packs({}) }), FALLBACK_ENV_PREFIX, 'nothing readable → the fallback');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
t('settingsEnvVarsFor: only the control-plane row reads the self prefix — every other shape is byte-identical to before', () => {
|
|
345
|
+
const body = 'BONGOS_BRANDING_FILE=/nowhere/at/all.json\n';
|
|
346
|
+
for (const shape of ['cloud-host', 'co-tenant']) {
|
|
347
|
+
assert.deepEqual(P.settingsEnvVarsFor({ ...activeStandalone, hosting_shape: shape }, body), P.settingsEnvVarsFor({ ...activeStandalone, hosting_shape: shape }), shape);
|
|
348
|
+
assert.ok(Object.keys(P.settingsEnvVarsFor({ ...activeStandalone, hosting_shape: shape }, body)).every((k) => k.startsWith(`${NEUTRAL_PREFIX}_`)), shape);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const selfRow = {
|
|
353
|
+
id: 41, slug: 'cloudbongos', hosting_shape: 'control-plane', status: 'active', owner_builder_id: 3,
|
|
354
|
+
port: 3002, domain: 'cloudbongos.com', settings: { artist_gate: 'off' },
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
await ta('--apply, the control-plane row: its OWN /etc/<slug>/web.env on the control-plane pair, under ITS pack\'s prefix, then restart <slug>; nothing else moves', async () => {
|
|
358
|
+
const fs = await import('node:fs');
|
|
359
|
+
const os = await import('node:os');
|
|
360
|
+
const path = await import('node:path');
|
|
361
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cp-pack-'));
|
|
362
|
+
const pack = path.join(dir, 'site.json');
|
|
363
|
+
fs.writeFileSync(pack, JSON.stringify({ envPrefix: 'CPSELF' }));
|
|
364
|
+
const selfEnv = `CPSELF_GITHUB_CLIENT_SECRET=keepme0123456789\nBONGOS_BRANDING_FILE=${pack}\nPORT=3002\nCPSELF_ARTIST_GATE=strict\nGITHUB_PUSH_TOKEN=keepme-too\n`;
|
|
365
|
+
const seen = { box: [], control: [] };
|
|
366
|
+
const mk = (bucket) => (cmd) => {
|
|
367
|
+
seen[bucket].push(cmd);
|
|
368
|
+
if (/\bcat /.test(cmd)) return { ok: true, stdout: selfEnv };
|
|
369
|
+
if (/api\/bongos\/instance/.test(cmd)) return { ok: true, stdout: manifestWith({ platformVisibility: 'public', joinability: 'apply', visibility: 'public', joinGrant: 'full', artistGate: 'off', spendPayer: 'none' }) };
|
|
370
|
+
return { ok: true, stdout: '{"ok":true}' };
|
|
371
|
+
};
|
|
372
|
+
try {
|
|
373
|
+
const { deps, writes, events } = recorder({ apply: true });
|
|
374
|
+
deps.exec = mk('box'); deps.controlExec = mk('control');
|
|
375
|
+
const r = await P.settingsApplyInstance(selfRow, deps);
|
|
376
|
+
assert.equal(r.ok, true, JSON.stringify(r));
|
|
377
|
+
assert.equal(r.consumed, true, 'the read-back saw the live site report the new value');
|
|
378
|
+
assert.equal(seen.box.length, 0, 'nothing runs on the co-hosting box executor — the self row IS the control plane');
|
|
379
|
+
assert.ok(seen.control.some((c) => /^cat \/etc\/cloudbongos\/web\.env$/.test(c)), seen.control.join(' | '));
|
|
380
|
+
assert.ok(seen.control.some((c) => /^systemctl restart cloudbongos$/.test(c)), 'the service the core-upgrade door already restarts');
|
|
381
|
+
assert.deepEqual(writes.map((w) => [w.via, w.path]), [['control', '/etc/cloudbongos/web.env']]);
|
|
382
|
+
const out = writes[0].body;
|
|
383
|
+
assert.match(out, /^CPSELF_ARTIST_GATE=off$/m, 'replaced IN PLACE under the prefix the site resolves');
|
|
384
|
+
assert.doesNotMatch(out, /^BONGOS_ARTIST_GATE=/m, 'not written under the provisioned floor, which this site would never read');
|
|
385
|
+
assert.match(out, /^CPSELF_GITHUB_CLIENT_SECRET=keepme0123456789$/m, 'creds untouched');
|
|
386
|
+
assert.match(out, /^GITHUB_PUSH_TOKEN=keepme-too$/m);
|
|
387
|
+
assert.match(out, /^PORT=3002$/m);
|
|
388
|
+
assert.ok(![...seen.control].some((c) => /systemctl (stop|disable)|caddy|box-dns|rm -|upgrade\.js|npm/.test(c)), 'settings only — no teardown, DNS, Caddy or pin legs');
|
|
389
|
+
assert.match(events[0].detail, /reads the new values/);
|
|
390
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
await ta('the control-plane row keeps every refusal of the leg: an unreadable web.env writes nothing and restarts nothing', async () => {
|
|
394
|
+
const { deps, cmds, writes } = recorder({ apply: true, readFails: true });
|
|
395
|
+
const r = await P.settingsApplyInstance(selfRow, deps);
|
|
396
|
+
assert.equal(r.ok, false);
|
|
397
|
+
assert.match(r.error, /cannot read \/etc\/cloudbongos\/web\.env/);
|
|
398
|
+
assert.equal(writes.length, 0);
|
|
399
|
+
assert.ok(!cmds.some((c) => /restart/.test(c)), 'the platform is never restarted over a file it could not read');
|
|
400
|
+
});
|
|
401
|
+
|
|
318
402
|
console.log('\nthe verdict — recorded only when earned:');
|
|
319
403
|
|
|
320
404
|
await ta('--apply success, manifest matches: consumed=true, event says it reads the new values, F19 latch released, domain-bearing PASS records no liveness verdict', async () => {
|
|
@@ -72,19 +72,30 @@ t('SETTINGS_APPLY_FROM is exactly {active} — the provision intent cannot stand
|
|
|
72
72
|
assert.ok(!provisioning.PROVISION_ENQUEUE_FROM.has('active'), 'the premise the gate rests on');
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
t('SETTINGS_APPLY_SHAPES is the reachable
|
|
76
|
-
|
|
75
|
+
t('SETTINGS_APPLY_SHAPES is the reachable three, and the set is a subset of HOSTING_SHAPES', () => {
|
|
76
|
+
// 'control-plane' joined in task 1004334: the runner's OWN box, admitted by name in
|
|
77
|
+
// control-plane-guard.js, so a setting saved on the platform's row reaches the live site.
|
|
78
|
+
assert.deepEqual([...provisioning.SETTINGS_APPLY_SHAPES].sort(), ['cloud-host', 'co-tenant', 'control-plane']);
|
|
77
79
|
for (const s of provisioning.SETTINGS_APPLY_SHAPES) assert.ok(provisioning.HOSTING_SHAPES.includes(s), s);
|
|
78
80
|
// The exclusion this used to assert was 'dedicated', which task 1004182 removed from
|
|
79
81
|
// HOSTING_SHAPES entirely — so that assertion became vacuously true and stopped testing
|
|
80
|
-
// anything. Name the
|
|
81
|
-
// of ours runs a byo-host
|
|
82
|
-
for (const unreachable of ['byo-host'
|
|
82
|
+
// anything. Name the shape that DOES exist and is still deliberately unreachable:
|
|
83
|
+
// nothing of ours runs a byo-host.
|
|
84
|
+
for (const unreachable of ['byo-host']) {
|
|
83
85
|
assert.ok(provisioning.HOSTING_SHAPES.includes(unreachable), `${unreachable} must be a real shape or this assertion is vacuous`);
|
|
84
86
|
assert.ok(!provisioning.SETTINGS_APPLY_SHAPES.has(unreachable), unreachable);
|
|
85
87
|
}
|
|
86
88
|
});
|
|
87
89
|
|
|
90
|
+
t('the two gates agree: every settings-apply shape is one the self-row guard lets a settings-apply through', () => {
|
|
91
|
+
// A shape on SETTINGS_APPLY_SHAPES that the guard refuses would make the route say
|
|
92
|
+
// "a restart is coming" and then answer the owner with control_plane_protected.
|
|
93
|
+
const G = require('../modules/provisioning/control-plane-guard.js');
|
|
94
|
+
for (const s of provisioning.SETTINGS_APPLY_SHAPES) assert.equal(G.intentAllowedForShape('settings-apply', s), true, s);
|
|
95
|
+
// and the restart door was NOT opened on the self row as a side effect
|
|
96
|
+
assert.ok(!provisioning.RESTART_SHAPES.has('control-plane'), 'task 1004334 widened the settings push only');
|
|
97
|
+
});
|
|
98
|
+
|
|
88
99
|
await ta('enqueueSettingsApply: a fresh intent → created:true; the INSERT carries the settings-apply action', async () => {
|
|
89
100
|
const calls = [];
|
|
90
101
|
const db = { query: async (sql, params) => { calls.push({ sql, params }); return { rows: [{ id: 1, action: params[1], state: 'pending' }] }; } };
|
|
@@ -104,6 +115,26 @@ await ta('enqueueSettingsApply: a fresh intent → created:true; the INSERT carr
|
|
|
104
115
|
assert.deepEqual(insert.params, [18, 'settings-apply', 'api:self', null]);
|
|
105
116
|
});
|
|
106
117
|
|
|
118
|
+
await ta('enqueueSettingsApply on the CONTROL-PLANE row passes the self-row guard and INSERTs; a teardown on the same row is still refused (task 1004334)', async () => {
|
|
119
|
+
const selfRow = (calls) => ({ query: async (sql, params) => {
|
|
120
|
+
calls.push({ sql, params });
|
|
121
|
+
if (/SELECT hosting_shape FROM provisioning_instances/.test(sql)) return { rows: [{ hosting_shape: 'control-plane' }] };
|
|
122
|
+
return { rows: [{ id: 3, action: params[1], state: 'pending' }] };
|
|
123
|
+
} });
|
|
124
|
+
const calls = [];
|
|
125
|
+
const r = await provisioning.enqueueSettingsApply(selfRow(calls), 41, 'api:self');
|
|
126
|
+
assert.equal(r.created, true, 'the real guard (not a stub) admits the push onto the self row');
|
|
127
|
+
const insert = calls.find((c) => /INSERT INTO provisioning_intents/.test(c.sql));
|
|
128
|
+
assert.deepEqual(insert.params, [41, 'settings-apply', 'api:self', null]);
|
|
129
|
+
// Everything the guard existed to refuse is still refused through the same door.
|
|
130
|
+
for (const destructive of ['teardown', 'disconnect', 'provision', 'domain-attach', 'repo-private']) {
|
|
131
|
+
const seen = [];
|
|
132
|
+
await assert.rejects(provisioning.enqueueIntent(selfRow(seen), 41, destructive, 'api:self'),
|
|
133
|
+
(e) => e.code === 'control_plane_protected' && e.status === 409, destructive);
|
|
134
|
+
assert.ok(!seen.some((c) => /INSERT/.test(c.sql)), `${destructive}: nothing written`);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
107
138
|
await ta('enqueueSettingsApply: an identical PENDING push already open → created:false, no conflict (it carries this change)', async () => {
|
|
108
139
|
const db = { query: async (sql) => {
|
|
109
140
|
if (/INSERT/.test(sql)) { const e = new Error('dup'); e.code = '23505'; throw e; }
|
|
@@ -260,6 +291,20 @@ await ta('ACTIVE co-tenant queues too; the Archon override names WHICH archon on
|
|
|
260
291
|
assert.equal(events[0].actor, 'api:archon:999');
|
|
261
292
|
});
|
|
262
293
|
|
|
294
|
+
await ta('ACTIVE control-plane row (the platform itself): artist_gate off QUEUES a push and says a restart is coming (task 1004334)', async () => {
|
|
295
|
+
// The case TW01 hit: this save used to answer "runs on a machine the platform doesn't
|
|
296
|
+
// manage", stored 'off', and left the live site reading 'strict' with no way to apply it.
|
|
297
|
+
reset({ inst: { owner_builder_id: 7, slug: 'cloudbongos', status: 'active', hosting_shape: 'control-plane', domain: 'cloudbongos.com', port: 3002, settings: {} } });
|
|
298
|
+
const r = await patch('/provisioning/instances/41/settings', { artist_gate: 'off' });
|
|
299
|
+
assert.equal(r.status, 200, r.raw);
|
|
300
|
+
assert.equal(r.body.saved.artist_gate, 'off');
|
|
301
|
+
assert.deepEqual(updateCalls.map((c) => c.patch), [{ artist_gate: 'off' }]);
|
|
302
|
+
assert.equal(updateCalls[0].enqueuedSoFar, 0, 'the row is written before the intent the runner reads it through');
|
|
303
|
+
assert.deepEqual(enqueueCalls, [{ id: 41, requestedBy: 'api:self' }]);
|
|
304
|
+
receiptShape(r.body.push, { queued: true, restart: true, reachable: true });
|
|
305
|
+
assert.doesNotMatch(r.body.push.message, /doesn.t manage/, 'no longer the unreachable-machine sentence');
|
|
306
|
+
});
|
|
307
|
+
|
|
263
308
|
await ta('an identical PENDING push already open → 200, queued:false, "already queued … will carry this change"', async () => {
|
|
264
309
|
reset();
|
|
265
310
|
enqueueResult = { intent: { id: 5, action: 'settings-apply', state: 'pending' }, created: false };
|