@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.
- package/.bongos-core.json +85 -50
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- 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/0239-a-recommendation-is-a-table-that-carries-its-own-reason.md +177 -0
- package/docs/adr/README.md +2 -0
- package/docs/api/openapi.json +40 -3
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +158 -151
- package/docs/copy-registry.json +313 -229
- package/docs/module-api-changelog.md +4 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/hall-ui/public/hall-kit.js +309 -0
- package/modules/hall-ui/public/hall-render.js +10 -38
- package/modules/hall-ui/public/profile-charts.js +58 -65
- package/modules/hall-ui/public/profile.css +9 -18
- package/modules/hall-ui/public/style.css +101 -15
- package/modules/hall-ui/public/watch.css +6 -14
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/watch.js +139 -54
- package/modules/provisioning/provisioning.js +27 -1
- package/modules/provisioning/recommendations.js +260 -0
- package/modules/provisioning/routes/provisioning.js +29 -0
- 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/hall_audit.mjs +84 -0
- package/tests/hall_kit_charts.mjs +328 -0
- package/tests/hall_profile_world.mjs +27 -6
- package/tests/helpers.mjs +161 -0
- package/tests/joinability_invite_only.mjs +10 -2
- 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_recommendations.mjs +448 -0
- 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/task_detail_ui.mjs +1 -153
- 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
|
-
|
|
191
|
-
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'),
|
|
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
|
+
});
|
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);
|