@bongos/core 1.19.511 → 1.19.512
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 +38 -28
- package/config/branding.neutral.json +3 -2
- package/docs/adr/0238-the-join-door-is-a-provisioning-settings-policy-key.md +57 -0
- package/docs/adr/README.md +1 -0
- package/docs/copy-inventory.md +124 -118
- package/docs/copy-registry.json +223 -158
- package/docs/module-api-changelog.md +2 -0
- package/modules/provisioning/provisioning.js +27 -1
- package/modules/provisioning/tests/provisioning.mjs +17 -14
- package/modules/public-landing/public/projects.html +125 -0
- package/modules/ui-design/kit/serve.js +4 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/branding.js +9 -0
- package/src/module-api.js +1 -1
- package/tests/currency_label.mjs +5 -4
- package/tests/project_door_ui.mjs +267 -0
- package/tests/projects_hub.mjs +73 -1
- package/tests/projects_hub_pre_uat.mjs +4 -1
- package/tests/provision_settings_apply.mjs +8 -7
- package/tests/provisioning_settings.mjs +65 -6
- package/tests/provisioning_settings_apply.mjs +3 -3
- package/tests/provisioning_settings_env.mjs +10 -8
package/tests/projects_hub.mjs
CHANGED
|
@@ -1135,7 +1135,7 @@ test('the manage view ships as real markup with the four settings homes (C7)', (
|
|
|
1135
1135
|
assert.match(HUB, /id="view-manage"/);
|
|
1136
1136
|
for (const id of ['mGate', 'mSignin', 'mMissing', 'mAlert', 'mLoading', 'mMain',
|
|
1137
1137
|
'mName', 'mChip', 'mMeta', 'mAddr', 'mCreated',
|
|
1138
|
-
'mVisibility', 'mJoinability', 'mRepoVisibility', 'mDelete']) {
|
|
1138
|
+
'mVisibility', 'mJoinability', 'mDoorState', 'mRepoVisibility', 'mDelete']) {
|
|
1139
1139
|
assert.match(HUB, new RegExp(`id="${id}"`), `${id} must be real markup`);
|
|
1140
1140
|
}
|
|
1141
1141
|
// every settings home is live now (tasks 1003042 / 1003043 / 1003044) — no not-yet copy remains
|
|
@@ -1475,6 +1475,78 @@ test('the joinability section is live markup with its body slot and a sibling fo
|
|
|
1475
1475
|
assert.match(fold, /restarts the project/, 'the restart is said, not hidden (ADR 0190)');
|
|
1476
1476
|
});
|
|
1477
1477
|
|
|
1478
|
+
/* the join door (task 1002324, ADR 0182 D1): the third settings axis — how the
|
|
1479
|
+
project stands on the platform's MAP, beside its own sign-in door and its own
|
|
1480
|
+
membership door. Its labels are the goal's interview decision: PLAIN words,
|
|
1481
|
+
because the space metaphor belongs to the drawn orb and never to the copy. */
|
|
1482
|
+
|
|
1483
|
+
test('the join-door section is live markup with its body slot and a sibling fold (C7)', () => {
|
|
1484
|
+
assert.match(HUB, /id="mDoor"/, 'the volatile slot is real markup, not JS-built');
|
|
1485
|
+
assert.match(HUB, /id="mDoorWhat"/, 'the what-changes fold ships with the page');
|
|
1486
|
+
const card = (HUB.match(/<div class="card mSec" id="mDoorState">[\s\S]*?\n <\/div>/) || [''])[0];
|
|
1487
|
+
assert.ok(card, 'the join-door card must be extractable');
|
|
1488
|
+
const slotAt = card.indexOf('id="mDoor"');
|
|
1489
|
+
const foldAt = card.indexOf('id="mDoorWhat"');
|
|
1490
|
+
assert.ok(slotAt > 0 && foldAt > slotAt, 'the fold sits outside the slot it must survive');
|
|
1491
|
+
const fold = (HUB.match(/<details class="finePrint" id="mDoorWhat">[\s\S]*?<\/details>/) || [''])[0];
|
|
1492
|
+
for (const label of ['Public', 'Private', 'Stealth']) {
|
|
1493
|
+
assert.match(fold, new RegExp(`<b>${label}</b>`), `${label} is explained in the fold`);
|
|
1494
|
+
}
|
|
1495
|
+
// ADR 0182 D4: the bar is identity, and the gate must NAME what is missing
|
|
1496
|
+
assert.match(fold, /only once it has a name and a description/, 'the publish bar is stated, so a Public pick that shows nowhere is never a mystery');
|
|
1497
|
+
assert.match(fold, /separate from “Who can see it”/, 'the two doors are named as separate settings');
|
|
1498
|
+
assert.match(fold, /restarts the project/, 'the restart is said, not hidden (ADR 0190)');
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
test('the join-door labels are PLAIN — the space metaphor is the orb’s, never the copy’s (the goal’s interview decision)', () => {
|
|
1502
|
+
assert.match(SCRIPT, /var DOOR_OPTIONS = \{\s*public: \{ label: 'Public'/);
|
|
1503
|
+
assert.match(SCRIPT, /private: \{ label: 'Private'/);
|
|
1504
|
+
assert.match(SCRIPT, /stealth: \{ label: 'Stealth'/);
|
|
1505
|
+
// the card is the whole owner-facing surface of this setting: its markup plus
|
|
1506
|
+
// the option table the render reads its words from
|
|
1507
|
+
const card = (HUB.match(/<div class="card mSec" id="mDoorState">[\s\S]*?\n <\/div>/) || [''])[0];
|
|
1508
|
+
const options = (SCRIPT.match(/var DOOR_OPTIONS = \{[\s\S]*?\n \};/) || [''])[0];
|
|
1509
|
+
assert.ok(card && options, 'both the card and the option table must be extractable');
|
|
1510
|
+
// whole words only — "restarts" legitimately contains "star"
|
|
1511
|
+
for (const word of ['planet', 'star', 'black hole', 'dark matter', 'dark-matter']) {
|
|
1512
|
+
const re = new RegExp(`\\b${word}\\b`);
|
|
1513
|
+
assert.doesNotMatch(card.toLowerCase(), re, `"${word}" must not reach the owner’s copy`);
|
|
1514
|
+
assert.doesNotMatch(options.toLowerCase(), re, `"${word}" must not reach a choice’s label or blurb`);
|
|
1515
|
+
}
|
|
1516
|
+
// and the derived fourth state is never offered as a pick (ADR 0182 D2)
|
|
1517
|
+
assert.doesNotMatch(options, /unlisted/i, 'dark matter is DERIVED — there is deliberately no way to choose it');
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
test('the join-door pick is a real radio group built from the server’s own options list', () => {
|
|
1521
|
+
const render = fnHub('renderDoor');
|
|
1522
|
+
assert.match(render, /\(s\.options \|\| \[\]\)\.filter\(function \(v\) \{ return !!DOOR_OPTIONS\[v\]; \}\)/,
|
|
1523
|
+
'only values the page can describe are offered — the server’s options list is the source');
|
|
1524
|
+
assert.match(render, /role="radiogroup"/);
|
|
1525
|
+
assert.match(render, /type="radio" name="mDoorPick"/);
|
|
1526
|
+
assert.match(render, /v === s\.value \? ' checked' : ''/, 'the project’s current door is the preselected one');
|
|
1527
|
+
assert.match(render, /data-door-save/, 'one Save control');
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
test('the join-door save sends the one key and shows the platform’s receipt verbatim', () => {
|
|
1531
|
+
const save = fnHub('doDoorSave');
|
|
1532
|
+
assert.match(save, /method: 'PATCH'/);
|
|
1533
|
+
assert.match(save, /\/settings'/, 'PATCH /provisioning/instances/:id/settings — the own-scoped channel its two siblings use');
|
|
1534
|
+
assert.match(save, /JSON\.stringify\(\{ visibility: value \}\)/, 'the body is the one key this section owns');
|
|
1535
|
+
assert.match(save, /\(data\.push && data\.push\.message\)/, 'push.message is rendered as the server wrote it');
|
|
1536
|
+
assert.match(save, /e\.code === 'intent_conflict' \? 'projMsg' : 'projMsg err'/,
|
|
1537
|
+
'a 409 intent_conflict is saved-but-waiting — shown as the server’s sentence, not as a failure');
|
|
1538
|
+
assert.match(save, /loadManage\(true, true\)/, 'the strip + in-flight line re-read open_intent after a save');
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
test('the join door is read on entry + tab return, like every other settings section', () => {
|
|
1542
|
+
assert.match(SCRIPT, /if \(!keepRepo\) loadDoor\(\);/, 'never from the in-flight tick');
|
|
1543
|
+
// deliberately NOT a manifest read-back: an already-deployed project reports no
|
|
1544
|
+
// door yet, and a line that could only ever say "your project doesn’t report
|
|
1545
|
+
// this" is worse than no line. The read-back lands with the enforcement.
|
|
1546
|
+
const load = fnHub('loadDoor');
|
|
1547
|
+
assert.doesNotMatch(load, /api\/gds\/instance/, 'this section asks the project’s manifest nothing');
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1478
1550
|
test('the joinability choices are the platform’s vocabulary, each in plain words, and the pick is a real radio group', () => {
|
|
1479
1551
|
assert.match(SCRIPT, /var JOIN_OPTIONS = \{\s*open: \{ label: 'Anyone can join'/);
|
|
1480
1552
|
assert.match(SCRIPT, /apply: \{ label: 'Apply to join'/);
|
|
@@ -267,8 +267,11 @@ test('the manage sections keep their order, and no placeholder copy remains (tas
|
|
|
267
267
|
const repo = HUB.indexOf('id="mRepoVisibility"');
|
|
268
268
|
const vis = HUB.indexOf('id="mVisibility"');
|
|
269
269
|
const join = HUB.indexOf('id="mJoinability"');
|
|
270
|
+
// the join door (task 1002324) sits with the other two doors, before delete
|
|
271
|
+
const door = HUB.indexOf('id="mDoorState"');
|
|
270
272
|
const del = HUB.indexOf('id="mDelete"');
|
|
271
|
-
assert.ok(repo > 0 && repo < vis && vis < join && join <
|
|
273
|
+
assert.ok(repo > 0 && repo < vis && vis < join && join < door && door < del,
|
|
274
|
+
'repo visibility → who can see it → how people join → on the map → delete');
|
|
272
275
|
// "who can see it" is live since task 1003043 — its not-yet line is gone
|
|
273
276
|
const visCard = HUB.slice(vis, join);
|
|
274
277
|
assert.doesNotMatch(visCard, /isn’t wired up|For now, every project’s pages are open to everyone/);
|
|
@@ -59,6 +59,7 @@ PGDATABASE=spike
|
|
|
59
59
|
# 1003139). Written at standup from the platform's provisioning_instances.settings;
|
|
60
60
|
${NEUTRAL_PREFIX}_PLATFORM_VISIBILITY=public
|
|
61
61
|
${NEUTRAL_PREFIX}_JOINABILITY=apply
|
|
62
|
+
${NEUTRAL_PREFIX}_VISIBILITY=public
|
|
62
63
|
`;
|
|
63
64
|
|
|
64
65
|
t('replaces an existing KEY= line where it stands; every other line survives byte-for-byte', () => {
|
|
@@ -111,7 +112,7 @@ t('is PURE: same input, same output; empty body yields just the lines', () => {
|
|
|
111
112
|
t('settingsEnvVarsFor: the runner pushes the SAME pair webEnvBody writes at standup, under the pack prefix', () => {
|
|
112
113
|
const inst = { slug: 'spike', port: 3010, db_name: 'spike', hosting_shape: 'standalone', target_ref: 'jaxri/spike', settings: { platform_visibility: 'gated' } };
|
|
113
114
|
const vars = P.settingsEnvVarsFor(inst);
|
|
114
|
-
assert.deepEqual(vars, { [`${NEUTRAL_PREFIX}_PLATFORM_VISIBILITY`]: 'gated', [`${NEUTRAL_PREFIX}_JOINABILITY`]: 'apply' });
|
|
115
|
+
assert.deepEqual(vars, { [`${NEUTRAL_PREFIX}_PLATFORM_VISIBILITY`]: 'gated', [`${NEUTRAL_PREFIX}_JOINABILITY`]: 'apply', [`${NEUTRAL_PREFIX}_VISIBILITY`]: 'public' });
|
|
115
116
|
const body = P.webEnvBody(inst, {});
|
|
116
117
|
for (const [k, v] of Object.entries(vars)) assert.match(body, new RegExp(`^${k}=${v}$`, 'm'), `${k} at standup`);
|
|
117
118
|
});
|
|
@@ -122,10 +123,10 @@ const GATED = { settings: { platform_visibility: 'gated', joinability: 'invite_o
|
|
|
122
123
|
const manifestWith = (project) => JSON.stringify({ schema: 'instance-manifest/v1', brand: project === undefined ? {} : { project } });
|
|
123
124
|
|
|
124
125
|
t('true when every pushed key matches brand.project (camelCase, the ENV_OVERRIDES targets)', () => {
|
|
125
|
-
assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'gated', joinability: 'invite_only' }), GATED), true);
|
|
126
|
+
assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public' }), GATED), true);
|
|
126
127
|
});
|
|
127
128
|
t('false when the instance still reports a different value', () => {
|
|
128
|
-
assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'public', joinability: 'invite_only' }), GATED), false);
|
|
129
|
+
assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'public', joinability: 'invite_only', visibility: 'public' }), GATED), false);
|
|
129
130
|
assert.equal(P.settingsConsumed(manifestWith({ joinability: 'invite_only' }), GATED), false, 'a missing key is a mismatch, not a pass');
|
|
130
131
|
});
|
|
131
132
|
t('null (unknown) when the manifest carries no project block — a core that predates task 1003139 — or does not parse', () => {
|
|
@@ -143,8 +144,8 @@ t('instanceManifestCmd asks the instance on loopback, like healthzCmd', () => {
|
|
|
143
144
|
function recorder({ apply = false, envBody = LIVE_ENV, readFails = false, healthzFails = false, privileged = false,
|
|
144
145
|
manifest = 'match', restartThrows = false, writeThrows = false } = {}) {
|
|
145
146
|
const cmds = [], writes = [], events = [], probes = [], cleared = [], logs = [];
|
|
146
|
-
const manifestOut = manifest === 'match' ? manifestWith({ platformVisibility: 'gated', joinability: 'invite_only' })
|
|
147
|
-
: manifest === 'mismatch' ? manifestWith({ platformVisibility: 'public', joinability: 'apply' })
|
|
147
|
+
const manifestOut = manifest === 'match' ? manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public' })
|
|
148
|
+
: manifest === 'mismatch' ? manifestWith({ platformVisibility: 'public', joinability: 'apply', visibility: 'public' })
|
|
148
149
|
: manifest === 'none' ? manifestWith(undefined) : 'nope';
|
|
149
150
|
const exec = (cmd, opts = {}) => {
|
|
150
151
|
cmds.push(cmd);
|
|
@@ -212,7 +213,7 @@ await ta('dry-run PLANS the patch: reads nothing real, writes NOTHING (no synthe
|
|
|
212
213
|
assert.equal(r.ok, true);
|
|
213
214
|
assert.equal(r.healthy, null, 'nothing ran, nothing observed');
|
|
214
215
|
assert.equal(writes.length, 0, 'a dry-run must not plan a two-line overwrite of web.env');
|
|
215
|
-
assert.ok(logs.some((l) => /would patch .*_PLATFORM_VISIBILITY, .*_JOINABILITY in \/etc\/spike\/web\.env in place/.test(l)), logs.join('\n'));
|
|
216
|
+
assert.ok(logs.some((l) => /would patch .*_PLATFORM_VISIBILITY, .*_JOINABILITY, .*_VISIBILITY in \/etc\/spike\/web\.env in place/.test(l)), logs.join('\n'));
|
|
216
217
|
assert.ok(cmds.some((c) => /restart spike/.test(c)), 'the restart is still in the plan');
|
|
217
218
|
assert.ok(logs.some((l) => /dry-run — nothing ran/.test(l)), 'the closing line does not claim a restart happened');
|
|
218
219
|
});
|
|
@@ -397,7 +398,7 @@ await ta("cmdRunIntents (--apply) routes a 'settings-apply' intent to the leg, w
|
|
|
397
398
|
claimNextOAuthManifest: async () => null,
|
|
398
399
|
resolveOAuthManifest: async () => {},
|
|
399
400
|
},
|
|
400
|
-
exec: (cmd) => { cmds.push(cmd); return /\bcat /.test(cmd) ? { ok: true, stdout: LIVE_ENV } : /instance/.test(cmd) ? { ok: true, stdout: manifestWith({ platformVisibility: 'gated', joinability: 'open' }) } : { ok: true, stdout: '{"ok":true}' }; },
|
|
401
|
+
exec: (cmd) => { cmds.push(cmd); return /\bcat /.test(cmd) ? { ok: true, stdout: LIVE_ENV } : /instance/.test(cmd) ? { ok: true, stdout: manifestWith({ platformVisibility: 'gated', joinability: 'open', visibility: 'public' }) } : { ok: true, stdout: '{"ok":true}' }; },
|
|
401
402
|
writeFile: (p, body) => { writes.push(body); return { ok: true }; }, doApi: null,
|
|
402
403
|
};
|
|
403
404
|
const r = await P.cmdRunIntents(deps);
|
|
@@ -164,11 +164,14 @@ await ta('owner reads DEFAULTS (public / apply) on a row with empty settings —
|
|
|
164
164
|
reset();
|
|
165
165
|
const r = await get('/provisioning/instances/18/settings');
|
|
166
166
|
assert.equal(r.status, 200, r.raw);
|
|
167
|
-
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply' });
|
|
167
|
+
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply', visibility: 'public' });
|
|
168
168
|
// the planet rides beside the settings (task 1003347): no default — null = the platform's own pick
|
|
169
169
|
assert.deepEqual(r.body.planet, { planet_template: null, planet_accent: null, card_backdrop: null });
|
|
170
170
|
assert.deepEqual(r.body.options, {
|
|
171
171
|
platform_visibility: ['public', 'gated'], joinability: ['open', 'apply', 'invite_only'],
|
|
172
|
+
// the join door (task 1002324, ADR 0182 D1) — the third policy axis, offered
|
|
173
|
+
// to the form the same way as its two neighbours
|
|
174
|
+
visibility: ['public', 'private', 'stealth'],
|
|
172
175
|
planet_template: [...provisioning.PLANET_TEMPLATES], planet_accent: ['sea', 'terracotta', 'gold-soft', 'sun-lit', 'accent'],
|
|
173
176
|
// the card's backdrop (task 1003352) is the same kind of key: platform-drawn,
|
|
174
177
|
// closed list, no default, no push — so it rides the same vocab
|
|
@@ -181,14 +184,14 @@ await ta('a stored planet reads back; a name that left the vocabulary reads null
|
|
|
181
184
|
const r = await get('/provisioning/instances/18/settings');
|
|
182
185
|
assert.equal(r.status, 200, r.raw);
|
|
183
186
|
assert.deepEqual(r.body.planet, { planet_template: 'bead-obsidian-tint', planet_accent: null, card_backdrop: null });
|
|
184
|
-
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply' }, 'the planet is not a setting: the policy read is untouched');
|
|
187
|
+
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply', visibility: 'public' }, 'the planet is not a setting: the policy read is untouched');
|
|
185
188
|
});
|
|
186
189
|
|
|
187
190
|
await ta('stored keys overlay the defaults', async () => {
|
|
188
191
|
reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', settings: { joinability: 'invite_only' } } });
|
|
189
192
|
const r = await get('/provisioning/instances/18/settings');
|
|
190
193
|
assert.equal(r.status, 200, r.raw);
|
|
191
|
-
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'invite_only' });
|
|
194
|
+
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'invite_only', visibility: 'public' });
|
|
192
195
|
});
|
|
193
196
|
|
|
194
197
|
await ta('ANOTHER builder\'s GET → 404 (no existence leak)', async () => {
|
|
@@ -216,7 +219,7 @@ await ta('owner changes one key — merged, returned effective, audited api:self
|
|
|
216
219
|
reset();
|
|
217
220
|
const r = await patch('/provisioning/instances/18/settings', { joinability: 'open' });
|
|
218
221
|
assert.equal(r.status, 200, r.raw);
|
|
219
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open' }, 'the SAVED value — the response never claims the instance runs it (task 1003140)');
|
|
222
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open', visibility: 'public' }, 'the SAVED value — the response never claims the instance runs it (task 1003140)');
|
|
220
223
|
assert.equal(r.body.push.queued, true, 'an active co-tenant gets the live push (task 1003140)');
|
|
221
224
|
assert.deepEqual(updateCalls, [{ id: 18, patch: { joinability: 'open' } }], 'exactly the sent key, nothing else');
|
|
222
225
|
assert.equal(events.length, 1);
|
|
@@ -229,7 +232,7 @@ await ta('owner changes both keys in one call', async () => {
|
|
|
229
232
|
reset();
|
|
230
233
|
const r = await patch('/provisioning/instances/18/settings', { platform_visibility: 'gated', joinability: 'invite_only' });
|
|
231
234
|
assert.equal(r.status, 200, r.raw);
|
|
232
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'invite_only' });
|
|
235
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'invite_only', visibility: 'public' });
|
|
233
236
|
});
|
|
234
237
|
|
|
235
238
|
await ta('the OWN-SCOPE WALL: another builder\'s PATCH → 404 and the write path is never reached', async () => {
|
|
@@ -257,6 +260,62 @@ await ta('an off-vocabulary value → 400 validation_failed (nothing written)',
|
|
|
257
260
|
assert.equal(updateCalls.length, 0);
|
|
258
261
|
});
|
|
259
262
|
|
|
263
|
+
// ── the join door (BV1.R05, task 1002324) ────────────────────────────────────
|
|
264
|
+
// ADR 0182 D1's STORED axis — Public / Private / Stealth — reaching the owner on
|
|
265
|
+
// the same own-scoped channel as its two neighbours. What these pin: the server,
|
|
266
|
+
// not the form, decides a value is legal; a fourth state cannot be picked because
|
|
267
|
+
// dark matter is DERIVED (D2); and the door is a POLICY, so it rides the push.
|
|
268
|
+
console.log('\nPATCH /provisioning/instances/:id/settings — the join door (task 1002324):');
|
|
269
|
+
|
|
270
|
+
await ta('the owner sets the door to stealth — stored, echoed effective, audited, and pushed like the policy it is', async () => {
|
|
271
|
+
reset();
|
|
272
|
+
const r = await patch('/provisioning/instances/18/settings', { visibility: 'stealth' });
|
|
273
|
+
assert.equal(r.status, 200, r.raw);
|
|
274
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply', visibility: 'stealth' });
|
|
275
|
+
assert.deepEqual(updateCalls, [{ id: 18, patch: { visibility: 'stealth' } }], 'exactly the sent key, nothing else');
|
|
276
|
+
assert.equal(events[0].actor, 'api:self');
|
|
277
|
+
assert.match(events[0].detail, /visibility=stealth/);
|
|
278
|
+
// ADR 0182 D6 makes the door the CEILING joinability may only narrow, and D1/D5
|
|
279
|
+
// put that authority on the instance — so it must ride web.env + the restart,
|
|
280
|
+
// not the planet's draw-only channel.
|
|
281
|
+
assert.equal(r.body.push.queued, true, 'the door is a policy the instance obeys — it pushes');
|
|
282
|
+
assert.equal(r.body.push.restart, true, 'and a restart is what applies it');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
await ta('each of the three states is accepted', async () => {
|
|
286
|
+
for (const value of ['public', 'private', 'stealth']) {
|
|
287
|
+
reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', hosting_shape: 'co-tenant', settings: { visibility: value === 'public' ? 'stealth' : 'public' } } });
|
|
288
|
+
const r = await patch('/provisioning/instances/18/settings', { visibility: value });
|
|
289
|
+
assert.equal(r.status, 200, `${value}: ${r.raw}`);
|
|
290
|
+
assert.equal(r.body.saved.visibility, value);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
await ta('a fourth state is refused server-side — dark matter is DERIVED, never picked (ADR 0182 D2)', async () => {
|
|
295
|
+
for (const bogus of ['unlisted', 'dark-matter', 'gated', 'hidden', '']) {
|
|
296
|
+
reset();
|
|
297
|
+
const r = await patch('/provisioning/instances/18/settings', { visibility: bogus });
|
|
298
|
+
assert.equal(r.status, 400, `${JSON.stringify(bogus)} must be refused: ${r.raw}`);
|
|
299
|
+
assert.equal(r.body.error.code, 'validation_failed');
|
|
300
|
+
assert.equal(updateCalls.length, 0, 'nothing written on a refused value');
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await ta('the OWN-SCOPE WALL holds for the door: another builder\'s PATCH → 404, nothing written', async () => {
|
|
305
|
+
reset({ as: { id: 8, github_login: 'stranger', rank: 'thetes' } });
|
|
306
|
+
const r = await patch('/provisioning/instances/18/settings', { visibility: 'private' });
|
|
307
|
+
assert.equal(r.status, 404, r.raw);
|
|
308
|
+
assert.equal(r.body.error.code, 'instance_not_found', 'no existence leak');
|
|
309
|
+
assert.equal(updateCalls.length, 0);
|
|
310
|
+
assert.equal(events.length, 0);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
await ta('an unknown project → 404 for the door too, archon included', async () => {
|
|
314
|
+
reset({ inst: null, as: { id: 999, github_login: 'archon', rank: 'archon' } });
|
|
315
|
+
const r = await patch('/provisioning/instances/999/settings', { visibility: 'private' });
|
|
316
|
+
assert.equal(r.status, 404, r.raw);
|
|
317
|
+
});
|
|
318
|
+
|
|
260
319
|
await ta('an unknown key → 400 (strict: a typo fails loudly, never silently dropped)', async () => {
|
|
261
320
|
reset();
|
|
262
321
|
const r = await patch('/provisioning/instances/18/settings', { joinabilty: 'open' });
|
|
@@ -272,7 +331,7 @@ await ta('the owner picks a planet: saved, echoed as `planet`, audited — and N
|
|
|
272
331
|
const r = await patch('/provisioning/instances/18/settings', { planet_template: 'bead-obsidian-tint', planet_accent: 'sea' });
|
|
273
332
|
assert.equal(r.status, 200, r.raw);
|
|
274
333
|
assert.deepEqual(r.body.planet, { planet_template: 'bead-obsidian-tint', planet_accent: 'sea', card_backdrop: null });
|
|
275
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply' }, 'the policy half is untouched');
|
|
334
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply', visibility: 'public' }, 'the policy half is untouched');
|
|
276
335
|
assert.deepEqual(updateCalls, [{ id: 18, patch: { planet_template: 'bead-obsidian-tint', planet_accent: 'sea' } }]);
|
|
277
336
|
assert.equal(r.body.push.queued, false);
|
|
278
337
|
assert.equal(r.body.push.restart, false, 'an active co-tenant would get a push for a POLICY change — never for the planet');
|
|
@@ -212,7 +212,7 @@ await ta('ACTIVE standalone: the row is written FIRST, then a settings-apply int
|
|
|
212
212
|
reset();
|
|
213
213
|
const r = await patch('/provisioning/instances/18/settings', { platform_visibility: 'gated' });
|
|
214
214
|
assert.equal(r.status, 200, r.raw);
|
|
215
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'apply' }, 'the SAVED (requested) value — never `settings`, which a form would render as live');
|
|
215
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'apply', visibility: 'public' }, 'the SAVED (requested) value — never `settings`, which a form would render as live');
|
|
216
216
|
assert.equal(r.body.settings, undefined);
|
|
217
217
|
assert.deepEqual(updateCalls.map((c) => c.patch), [{ platform_visibility: 'gated' }]);
|
|
218
218
|
assert.equal(updateCalls[0].enqueuedSoFar, 0, 'the runner reads the row — the write must precede the intent');
|
|
@@ -261,7 +261,7 @@ await ta('an enqueue that THROWS still answers 200 "saved" — never a 500 that
|
|
|
261
261
|
enqueueThrows = new Error('db down');
|
|
262
262
|
const r = await patch('/provisioning/instances/18/settings', { joinability: 'open' });
|
|
263
263
|
assert.equal(r.status, 200, r.raw);
|
|
264
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open' });
|
|
264
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open', visibility: 'public' });
|
|
265
265
|
assert.equal(updateCalls.length, 1, 'the write landed');
|
|
266
266
|
receiptShape(r.body.push, { queued: false, restart: true, reachable: true });
|
|
267
267
|
assert.match(r.body.push.message, /Saved, but the restart could not be queued/);
|
|
@@ -274,7 +274,7 @@ await ta('same values as already stored → no write, no event, no intent, "No c
|
|
|
274
274
|
reset({ inst: { ...ACTIVE, settings: { platform_visibility: 'gated', joinability: 'open' } } });
|
|
275
275
|
const r = await patch('/provisioning/instances/18/settings', { platform_visibility: 'gated', joinability: 'open' });
|
|
276
276
|
assert.equal(r.status, 200, r.raw);
|
|
277
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'open' });
|
|
277
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'open', visibility: 'public' });
|
|
278
278
|
assert.equal(updateCalls.length, 0, 'nothing written (no updated_at reshuffle)');
|
|
279
279
|
assert.equal(events.length, 0, 'no "settings changed" event for a change that is not one');
|
|
280
280
|
assert.equal(enqueueCalls.length, 0, 'a whole-form save must not bounce a live project');
|
|
@@ -55,7 +55,9 @@ t('a row with NO stored settings emits today\'s behavior EXPLICITLY, never nothi
|
|
|
55
55
|
// an instance must never have to infer a policy from an absent variable
|
|
56
56
|
assert.equal(v.CLOUDBONGOS_PLATFORM_VISIBILITY, 'public');
|
|
57
57
|
assert.equal(v.CLOUDBONGOS_JOINABILITY, 'apply');
|
|
58
|
-
|
|
58
|
+
// the join door rides the same transport (task 1002324, ADR 0182 D1)
|
|
59
|
+
assert.equal(v.CLOUDBONGOS_VISIBILITY, 'public');
|
|
60
|
+
assert.equal(linesOf(provisioning.settingsEnvLines({}, 'X')).length, 3, 'every policy key always present');
|
|
59
61
|
});
|
|
60
62
|
|
|
61
63
|
t('an off-vocabulary stored value falls back to the default, never reaches the instance', () => {
|
|
@@ -94,11 +96,11 @@ t('it names the live push by the intent the runner actually dispatches (task 100
|
|
|
94
96
|
});
|
|
95
97
|
|
|
96
98
|
t('settingsEnvVars is the single source both legs derive from — same keys, same values as the lines', () => {
|
|
97
|
-
const row = { settings: { platform_visibility: 'gated', joinability: 'open' } };
|
|
99
|
+
const row = { settings: { platform_visibility: 'gated', joinability: 'open', visibility: 'stealth' } };
|
|
98
100
|
const vars = provisioning.settingsEnvVars(row, 'CLOUDBONGOS');
|
|
99
|
-
assert.deepEqual(vars, { CLOUDBONGOS_PLATFORM_VISIBILITY: 'gated', CLOUDBONGOS_JOINABILITY: 'open' });
|
|
101
|
+
assert.deepEqual(vars, { CLOUDBONGOS_PLATFORM_VISIBILITY: 'gated', CLOUDBONGOS_JOINABILITY: 'open', CLOUDBONGOS_VISIBILITY: 'stealth' });
|
|
100
102
|
assert.deepEqual(varsOf(provisioning.settingsEnvLines(row, 'CLOUDBONGOS')), vars, 'the standup lines ARE the vars');
|
|
101
|
-
assert.deepEqual(provisioning.settingsEnvVars({}, 'X'), { X_PLATFORM_VISIBILITY: 'public', X_JOINABILITY: 'apply' }, 'defaults, explicitly');
|
|
103
|
+
assert.deepEqual(provisioning.settingsEnvVars({}, 'X'), { X_PLATFORM_VISIBILITY: 'public', X_JOINABILITY: 'apply', X_VISIBILITY: 'public' }, 'defaults, explicitly');
|
|
102
104
|
});
|
|
103
105
|
|
|
104
106
|
console.log('\nwebEnvBody — the settings ride the real per-instance env file:');
|
|
@@ -131,12 +133,12 @@ t('clientBranding\'s project fallbacks are DEFAULT_SETTINGS, key for key — the
|
|
|
131
133
|
|
|
132
134
|
t('the env override resolves onto project.* in a real process', () => {
|
|
133
135
|
const read = (env) => JSON.parse(execFileSync(process.execPath, ['-e',
|
|
134
|
-
"const b=require('./src/branding.js');const r=(b.branding||b)();console.log(JSON.stringify({v:r.project.platformVisibility,j:r.project.joinability}))"],
|
|
136
|
+
"const b=require('./src/branding.js');const r=(b.branding||b)();console.log(JSON.stringify({v:r.project.platformVisibility,j:r.project.joinability,d:r.project.visibility}))"],
|
|
135
137
|
{ cwd: ROOT, encoding: 'utf8', env: { ...process.env, ...env } }));
|
|
136
|
-
assert.deepEqual(read({}), { v: 'public', j: 'apply' }, 'no env → the pack default');
|
|
138
|
+
assert.deepEqual(read({}), { v: 'public', j: 'apply', d: 'public' }, 'no env → the pack default');
|
|
137
139
|
assert.deepEqual(
|
|
138
|
-
read({ CLOUDBONGOS_PLATFORM_VISIBILITY: 'gated', CLOUDBONGOS_JOINABILITY: 'invite_only' }),
|
|
139
|
-
{ v: 'gated', j: 'invite_only' },
|
|
140
|
+
read({ CLOUDBONGOS_PLATFORM_VISIBILITY: 'gated', CLOUDBONGOS_JOINABILITY: 'invite_only', CLOUDBONGOS_VISIBILITY: 'stealth' }),
|
|
141
|
+
{ v: 'gated', j: 'invite_only', d: 'stealth' },
|
|
140
142
|
'the web.env the runner writes is what the instance reads');
|
|
141
143
|
});
|
|
142
144
|
|