@bongos/core 1.19.510 → 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 +48 -38
- package/config/branding.neutral.json +3 -2
- package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +16 -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 +4 -0
- package/modules/copy-desk/proposals.js +13 -0
- package/modules/copy-desk/routes/copy-desk.js +9 -2
- package/modules/copy-desk/tests/copy_proposals.mjs +41 -0
- package/modules/lifecycle/conflict-resolve.js +59 -3
- package/modules/provisioning/provisioning.js +27 -1
- package/modules/provisioning/tests/provisioning.mjs +17 -14
- package/modules/public-landing/public/assets/cosmos.css +3 -1
- 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/scripts/gds/copy-apply.js +54 -4
- package/scripts/gds/git-merge-regen.js +18 -0
- package/src/branding.js +9 -0
- package/src/module-api.js +1 -1
- package/tests/conflict_resolve.mjs +118 -4
- package/tests/currency_label.mjs +5 -4
- package/tests/git_merge_regen.mjs +128 -2
- package/tests/project_door_ui.mjs +267 -0
- package/tests/projects_hub.mjs +86 -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
|
@@ -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'/);
|
|
@@ -1995,6 +2067,19 @@ test('the wizard takes the world by scope: no card ground, the six-step strip as
|
|
|
1995
2067
|
assert.doesNotMatch(block, /#[0-9a-f]{3,8}\b/i, 'every colour is a token');
|
|
1996
2068
|
});
|
|
1997
2069
|
|
|
2070
|
+
/* ── one fold for the whole wizard (task 1003538, a task 1003346 follow-up). The rail folded
|
|
2071
|
+
at the sheet's 900 while the wizard's fields, choice bands and review ledger folded at the
|
|
2072
|
+
page's 840 (cosmos's createSplit collapse), so between the two the rail was already three-up
|
|
2073
|
+
beside full-width bands. Both sheets now name the same width. ── */
|
|
2074
|
+
test('the wizard folds at one width: the rail\'s media rule sits at 840px with the bands, and no 900px rule is left in the #view-new block (task 1003538)', () => {
|
|
2075
|
+
const at = COSMOS.indexOf('THE CREATE WIZARD, IN THE WORLD');
|
|
2076
|
+
assert.ok(at > -1, 'the #view-new block exists in cosmos.css');
|
|
2077
|
+
const block = COSMOS.slice(at);
|
|
2078
|
+
assert.match(block, /@media \(max-width:840px\)\{\s*#view-new \.steps\{grid-template-columns:repeat\(3,1fr\);/, 'the rail folds to three columns at 840, the width the bands fold at');
|
|
2079
|
+
assert.doesNotMatch(block, /@media \(max-width:900px\)/, 'no 900px rule is left in the wizard block');
|
|
2080
|
+
assert.match(STYLE, /@media \(max-width:840px\)\{[\s\S]*?\.review\{grid-template-columns:1fr;/, 'the page\'s own wizard block still folds its bands and ledger at that same 840');
|
|
2081
|
+
});
|
|
2082
|
+
|
|
1998
2083
|
test('the wizard\'s own structures wear the world: hairline fields, cells of one band, the review as a ledger, the human steps as a numbered ledger, the standup strip as the rail (task 1003346)', () => {
|
|
1999
2084
|
const at = STYLE.indexOf('the create wizard (task 1002763');
|
|
2000
2085
|
assert.ok(at > -1, 'the wizard block is still the page\'s own');
|
|
@@ -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');
|