@bongos/core 1.19.511 → 1.19.513

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.
Files changed (50) hide show
  1. package/.bongos-core.json +85 -50
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +2 -0
  4. package/clients/bongos-client/index.cjs +2 -0
  5. package/clients/bongos-client/index.d.ts +3 -0
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/config/branding.neutral.json +3 -2
  8. package/docs/adr/0238-the-join-door-is-a-provisioning-settings-policy-key.md +57 -0
  9. package/docs/adr/0239-a-recommendation-is-a-table-that-carries-its-own-reason.md +177 -0
  10. package/docs/adr/README.md +2 -0
  11. package/docs/api/openapi.json +40 -3
  12. package/docs/api-reference.md +3 -2
  13. package/docs/copy-inventory.md +158 -151
  14. package/docs/copy-registry.json +313 -229
  15. package/docs/module-api-changelog.md +4 -0
  16. package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
  17. package/modules/hall-ui/public/hall-kit.js +309 -0
  18. package/modules/hall-ui/public/hall-render.js +10 -38
  19. package/modules/hall-ui/public/profile-charts.js +58 -65
  20. package/modules/hall-ui/public/profile.css +9 -18
  21. package/modules/hall-ui/public/style.css +101 -15
  22. package/modules/hall-ui/public/watch.css +6 -14
  23. package/modules/hall-ui/public/watch.html +1 -1
  24. package/modules/hall-ui/public/watch.js +139 -54
  25. package/modules/provisioning/provisioning.js +27 -1
  26. package/modules/provisioning/recommendations.js +260 -0
  27. package/modules/provisioning/routes/provisioning.js +29 -0
  28. package/modules/provisioning/tests/provisioning.mjs +17 -14
  29. package/modules/public-landing/public/projects.html +125 -0
  30. package/modules/ui-design/kit/serve.js +4 -3
  31. package/package-lock.json +2 -2
  32. package/package.json +1 -1
  33. package/src/branding.js +9 -0
  34. package/src/module-api.js +1 -1
  35. package/tests/currency_label.mjs +5 -4
  36. package/tests/hall_audit.mjs +84 -0
  37. package/tests/hall_kit_charts.mjs +328 -0
  38. package/tests/hall_profile_world.mjs +27 -6
  39. package/tests/helpers.mjs +161 -0
  40. package/tests/joinability_invite_only.mjs +10 -2
  41. package/tests/project_door_ui.mjs +267 -0
  42. package/tests/projects_hub.mjs +73 -1
  43. package/tests/projects_hub_pre_uat.mjs +4 -1
  44. package/tests/provision_settings_apply.mjs +8 -7
  45. package/tests/provisioning_recommendations.mjs +448 -0
  46. package/tests/provisioning_settings.mjs +65 -6
  47. package/tests/provisioning_settings_apply.mjs +3 -3
  48. package/tests/provisioning_settings_env.mjs +10 -8
  49. package/tests/task_detail_ui.mjs +1 -153
  50. package/tests/watch_applications_queue.mjs +381 -0
@@ -187,6 +187,14 @@ test('the Watch page offers the Archon an invite by username — the way in when
187
187
  assert.match(watch, /nobody can apply, so this is how people get in/);
188
188
  const gate = (watch.match(/function renderGate\(data, target\) \{[\s\S]*?\n \}\n/) || [''])[0];
189
189
  assert.ok(gate, 'renderGate() must exist');
190
- assert.equal((gate.match(/wireInviteForm\(target\)/g) || []).length, 2, 'the form is wired in BOTH branches — an empty queue and a listed one');
191
- assert.equal((gate.match(/inviteFormHtml\(\)/g) || []).length, 2, 'and rendered in both');
190
+ // The form used to be rendered and wired once per branch, and this counted the
191
+ // pair. Task 1002332 unified the empty and listed branches onto one `head`, so
192
+ // the count is now 1 and the invariant it stood for is STRONGER: the form is
193
+ // rendered and wired UNCONDITIONALLY, outside the split, so neither branch can
194
+ // drop it. tests/watch_applications_queue.mjs EXECUTES both cases ("an empty
195
+ // view still offers the invite form and the switch out of it") — the proof a
196
+ // source read like this one can only ever approximate.
197
+ assert.equal((gate.match(/inviteFormHtml\(\)/g) || []).length, 1, 'rendered once');
198
+ assert.match(gate, /const head = inviteFormHtml\(\)/, 'into the head BOTH branches are built from');
199
+ assert.match(gate, /\n {4}wireInviteForm\(target\);\n/, 'and wired at renderGate top level, never inside a branch');
192
200
  });
@@ -0,0 +1,267 @@
1
+ // tests/project_door_ui.mjs — the owner's join-door control, EXECUTED (BV1.R05,
2
+ // task 1002324, ADR 0182 D1).
3
+ //
4
+ // tests/projects_hub.mjs pins this section's source text; this file pins its
5
+ // BEHAVIOUR. It lifts the real region out of modules/public-landing/public/
6
+ // projects.html — the option table, renderDoor, loadDoor, doDoorSave and the
7
+ // delegated click — runs it in a vm under a minimal DOM stub, PARSES the markup
8
+ // it writes into innerHTML, and drives a real click on the Save control.
9
+ //
10
+ // Why a vm harness rather than jsdom: there is no jsdom in this repo. The
11
+ // precedent is tests/task_detail_ui.mjs (after tests/goal_criterion_confirm_ui.mjs);
12
+ // this one differs in where the source comes from — the hub's script is INLINE in
13
+ // the page, so the region is sliced out rather than read from a .js file, and the
14
+ // slice boundaries are asserted so a rename can never quietly test nothing.
15
+ //
16
+ // What it proves, and why each matters:
17
+ // • the three choices render with the goal's PLAIN interview labels
18
+ // • the project's CURRENT door is the preselected one — an owner opening the
19
+ // page reads their own setting, never a default that misreports it
20
+ // • only values the SERVER offered are rendered (the options list is the source)
21
+ // • Save PATCHes the own-scoped settings channel with exactly { visibility }
22
+ // • the platform's receipt is shown verbatim, and a refusal is SAID — the radio
23
+ // never moves to a value the server did not accept
24
+ //
25
+ // Run: node --test --test-reporter=tap tests/project_door_ui.mjs
26
+
27
+ import assert from 'node:assert/strict';
28
+ import { test } from 'node:test';
29
+ import fs from 'node:fs';
30
+ import path from 'node:path';
31
+ import vm from 'node:vm';
32
+ import { fileURLToPath } from 'node:url';
33
+
34
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
35
+ const HUB = fs.readFileSync(path.join(ROOT, 'modules', 'public-landing', 'public', 'projects.html'), 'utf8');
36
+ const SCRIPT = (HUB.match(/<script>([\s\S]*?)<\/script>\s*<\/body>/) || [, ''])[1];
37
+
38
+ // The region under test, sliced from the page verbatim. Both boundaries are
39
+ // asserted: a rename that moved either one would otherwise leave this file
40
+ // executing an empty string and passing.
41
+ const START = SCRIPT.indexOf('var DOOR_OPTIONS = {');
42
+ const END = SCRIPT.indexOf('/* ── delete the project');
43
+ assert.ok(START > 0, 'the join-door region must start at DOOR_OPTIONS');
44
+ assert.ok(END > START, 'the join-door region must end before the delete section');
45
+ const REGION = SCRIPT.slice(START, END);
46
+ for (const fn of ['function renderDoor(', 'function loadDoor(', 'function doDoorSave(']) {
47
+ assert.ok(REGION.includes(fn), `the region must carry ${fn}`);
48
+ }
49
+
50
+ // ── the DOM stub ─────────────────────────────────────────────────────────────
51
+ // Narrow on purpose (the task_detail_ui.mjs rule): it supports exactly the
52
+ // selectors the region uses, so it stays honest about what it can prove.
53
+
54
+ const RADIO = /<input type="radio" name="mDoorPick" value="([^"]*)"( checked)?>/g;
55
+ const OPTION = /<label class="visOpt"><input type="radio" name="mDoorPick" value="([^"]*)"( checked)?><span><b>([^<]*)<\/b>([^<]*)<\/span><\/label>/g;
56
+
57
+ function parseOptions(html) {
58
+ const out = [];
59
+ for (const m of String(html).matchAll(OPTION)) {
60
+ out.push({ value: m[1], checked: !!m[2], label: m[3], blurb: m[4] });
61
+ }
62
+ return out;
63
+ }
64
+
65
+ // Boot the region against a scripted settings channel and hand back the handles
66
+ // a test drives it with.
67
+ function boot({ settings, options = ['public', 'private', 'stealth'], patchAnswer } = {}) {
68
+ const requests = [];
69
+ const els = new Map();
70
+ const listeners = new Map();
71
+
72
+ const makeEl = (id) => ({
73
+ _id: id, _html: '', textContent: '', className: '', disabled: false,
74
+ set innerHTML(v) { this._html = String(v); },
75
+ get innerHTML() { return this._html; },
76
+ addEventListener(type, fn) {
77
+ if (type !== 'click') return;
78
+ if (!listeners.has(id)) listeners.set(id, []);
79
+ listeners.get(id).push(fn);
80
+ },
81
+ });
82
+ const $ = (id) => {
83
+ if (!els.has(id)) els.set(id, makeEl(id));
84
+ return els.get(id);
85
+ };
86
+
87
+ // what the owner has clicked; starts as whatever the render marked checked
88
+ let picked = null;
89
+ const slotHtml = () => ($('mDoor')._html || '');
90
+ const saveBtn = { hasAttribute: (a) => a === 'data-door-save', disabled: false };
91
+
92
+ const document = {
93
+ querySelector(sel) {
94
+ if (sel === '#mDoor input[name="mDoorPick"]:checked') {
95
+ if (picked !== null) return { value: picked };
96
+ const found = [...slotHtml().matchAll(RADIO)].find((m) => m[2]);
97
+ return found ? { value: found[1] } : null;
98
+ }
99
+ if (sel === '#mDoor [data-door-save]') return slotHtml().includes('data-door-save') ? saveBtn : null;
100
+ throw new Error(`the stub does not support the selector ${sel}`);
101
+ },
102
+ };
103
+
104
+ const answer = (body, { ok = true, status = 200 } = {}) => Promise.resolve({ ok, status, _json: body });
105
+ const fetch = (url, init = {}) => {
106
+ requests.push({ url, method: init.method || 'GET', body: init.body, credentials: init.credentials, headers: init.headers });
107
+ if ((init.method || 'GET') === 'GET') {
108
+ return answer({ ok: true, settings, options: { visibility: options } });
109
+ }
110
+ return patchAnswer || answer({ ok: true, saved: { ...settings, visibility: JSON.parse(init.body).visibility }, push: { queued: true, message: 'Saved. Your project will restart to pick this up — until then it keeps its current setting.' } });
111
+ };
112
+
113
+ const sandbox = {
114
+ $, document, fetch, console, Promise, JSON, String, Object, Array, Error, RegExp,
115
+ encodeURIComponent, setTimeout, clearTimeout,
116
+ API: '/api/bongos',
117
+ manageId: '18',
118
+ manageInst: { id: 18, status: 'active' },
119
+ esc: (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'),
120
+ jsonOrEmpty: (res) => Promise.resolve(res._json),
121
+ apiError: (data, status) => ({ code: (data && data.error && data.error.code) || 'error', message: (data && data.error && data.error.message) || `HTTP ${status}` }),
122
+ signInAgain: () => '<a href="/signin">Sign in again</a>',
123
+ loadManage: () => { requests.push({ url: 'loadManage' }); },
124
+ };
125
+ sandbox.globalThis = sandbox;
126
+ vm.createContext(sandbox);
127
+ vm.runInContext(REGION, sandbox, { filename: 'projects.html#join-door' });
128
+
129
+ const settle = async () => { for (let i = 0; i < 30; i++) await Promise.resolve(); };
130
+ return {
131
+ requests, sandbox, settle,
132
+ options: () => parseOptions(slotHtml()),
133
+ slot: () => slotHtml(),
134
+ msg: () => $('mDoorMsg'),
135
+ pick: (v) => { picked = v; },
136
+ async load() { sandbox.loadDoor(); await settle(); },
137
+ async clickSave() {
138
+ const handlers = listeners.get('mDoor') || [];
139
+ assert.equal(handlers.length, 1, 'the section wires exactly one delegated click handler');
140
+ handlers[0]({ target: { closest: (sel) => (sel === 'button' ? saveBtn : null) } });
141
+ await settle();
142
+ },
143
+ };
144
+ }
145
+
146
+ // ── the tests ────────────────────────────────────────────────────────────────
147
+
148
+ test('the three choices render with the goal’s plain interview labels', async () => {
149
+ // Arrange
150
+ const ui = boot({ settings: { visibility: 'public' } });
151
+
152
+ // Act
153
+ await ui.load();
154
+
155
+ // Assert
156
+ assert.deepEqual(ui.options().map((o) => o.label), ['Public', 'Private', 'Stealth']);
157
+ assert.deepEqual(ui.options().map((o) => o.value), ['public', 'private', 'stealth']);
158
+ assert.match(ui.slot(), /role="radiogroup"/, 'the pick is a real radio group, not a list of buttons');
159
+ for (const o of ui.options()) assert.ok(o.blurb.length > 20, `${o.label} says what it changes`);
160
+ });
161
+
162
+ test('the project’s current door is the one preselected — never a default that misreports it', async () => {
163
+ // Arrange
164
+ const ui = boot({ settings: { visibility: 'stealth' } });
165
+
166
+ // Act
167
+ await ui.load();
168
+
169
+ // Assert
170
+ const checked = ui.options().filter((o) => o.checked);
171
+ assert.equal(checked.length, 1, 'exactly one option is preselected');
172
+ assert.equal(checked[0].value, 'stealth');
173
+ });
174
+
175
+ test('only the values the SERVER offered are rendered', async () => {
176
+ // Arrange — a server that offers two (an older platform, or a narrowed list)
177
+ const ui = boot({ settings: { visibility: 'public' }, options: ['public', 'private'] });
178
+
179
+ // Act
180
+ await ui.load();
181
+
182
+ // Assert
183
+ assert.deepEqual(ui.options().map((o) => o.value), ['public', 'private']);
184
+ });
185
+
186
+ test('saving PATCHes the own-scoped settings channel with exactly { visibility }', async () => {
187
+ // Arrange
188
+ const ui = boot({ settings: { visibility: 'public' } });
189
+ await ui.load();
190
+ ui.pick('private');
191
+
192
+ // Act
193
+ await ui.clickSave();
194
+
195
+ // Assert
196
+ const patch = ui.requests.find((r) => r.method === 'PATCH');
197
+ assert.ok(patch, 'a PATCH was sent');
198
+ assert.equal(patch.url, '/api/bongos/provisioning/instances/18/settings');
199
+ assert.deepEqual(JSON.parse(patch.body), { visibility: 'private' }, 'exactly the one key this section owns');
200
+ assert.equal(patch.credentials, 'same-origin', 'the cookie session is the auth — this page carries no token');
201
+ });
202
+
203
+ test('the platform’s receipt is shown verbatim — the page never writes its own promise', async () => {
204
+ // Arrange
205
+ const ui = boot({ settings: { visibility: 'public' } });
206
+ await ui.load();
207
+ ui.pick('stealth');
208
+
209
+ // Act
210
+ await ui.clickSave();
211
+
212
+ // Assert
213
+ assert.equal(ui.msg().textContent,
214
+ 'Saved. Your project will restart to pick this up — until then it keeps its current setting.');
215
+ assert.equal(ui.msg().className, 'projMsg ok');
216
+ });
217
+
218
+ test('a refusal is SAID, and the radio never moves to a value the server did not accept', async () => {
219
+ // Arrange
220
+ const ui = boot({
221
+ settings: { visibility: 'public' },
222
+ patchAnswer: Promise.resolve({ ok: false, status: 400, _json: { error: { code: 'validation_failed', message: 'visibility is not one of: public, private, stealth' } } }),
223
+ });
224
+ await ui.load();
225
+ ui.pick('unlisted');
226
+
227
+ // Act
228
+ await ui.clickSave();
229
+
230
+ // Assert
231
+ assert.equal(ui.msg().className, 'projMsg err');
232
+ assert.match(ui.msg().textContent, /not one of: public, private, stealth/, 'the server’s own sentence');
233
+ assert.equal(ui.sandbox.mDoorSettings.value, 'public', 'the stored choice is untouched by a refused save');
234
+ });
235
+
236
+ test('a 409 intent_conflict is saved-but-waiting, not a failure', async () => {
237
+ // Arrange — the settings row IS written; another run holds the restart slot
238
+ const ui = boot({
239
+ settings: { visibility: 'public' },
240
+ patchAnswer: Promise.resolve({ ok: false, status: 409, _json: { error: { code: 'intent_conflict', message: 'The change was saved, but a \'provision\' run is currently running for this project.' } } }),
241
+ });
242
+ await ui.load();
243
+ ui.pick('private');
244
+
245
+ // Act
246
+ await ui.clickSave();
247
+
248
+ // Assert
249
+ assert.equal(ui.msg().className, 'projMsg', 'not painted as an error — the change is recorded');
250
+ assert.match(ui.msg().textContent, /The change was saved/);
251
+ });
252
+
253
+ test('a signed-out save offers a way back in rather than a dead end', async () => {
254
+ // Arrange
255
+ const ui = boot({
256
+ settings: { visibility: 'public' },
257
+ patchAnswer: Promise.resolve({ ok: false, status: 401, _json: {} }),
258
+ });
259
+ await ui.load();
260
+ ui.pick('private');
261
+
262
+ // Act
263
+ await ui.clickSave();
264
+
265
+ // Assert
266
+ assert.match(ui.msg().innerHTML, /Sign in again/);
267
+ });
@@ -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 < del, 'repo visibility → who can see it → how people join → delete');
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);