@bongos/core 1.24.56 → 1.24.58
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 +71 -51
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +0 -2
- package/clients/bongos-client/index.cjs +0 -2
- package/clients/bongos-client/index.d.ts +1 -3
- package/clients/bongos-client/index.mjs +0 -2
- package/docs/adr/0398-every-hosted-project-takes-its-own-update-and-nobody-asks-for-one.md +45 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +1 -37
- package/docs/api-reference.md +2 -3
- package/docs/copy-inventory.md +38 -44
- package/docs/copy-registry.json +44 -108
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +4 -4
- package/modules/hall-ui/public/deploy-handoff.js +11 -3
- package/modules/hall-ui/public/deploy.js +10 -3
- package/modules/hall-ui/public/settings-software-update.js +4 -71
- package/modules/hall-ui/public/update-pill.js +6 -31
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/bongos/css-literal-scan.js +144 -0
- package/scripts/bongos/design-generators-fresh.js +32 -0
- package/scripts/bongos/fitness-ratchets.js +70 -3
- package/scripts/bongos/fitness.js +3 -2
- package/scripts/bongos/provision-core-upgrade.js +30 -1
- package/scripts/bongos/provision-units.js +18 -0
- package/scripts/gds/css-literal-scan.js +7 -0
- package/scripts/gds/design-generators-fresh.js +6 -0
- package/scripts/hall-preview/server.js +0 -3
- package/src/bongos/module-scope-map.js +1 -1
- package/src/bongos/routes/core-update.js +36 -76
- package/src/bongos/routes/feedback.js +1 -2
- package/src/module-api.js +1 -1
- package/tests/deploy_handoff.mjs +23 -2
- package/tests/deploy_page_projects.mjs +35 -3
- package/tests/design_counts_ratchet.mjs +134 -0
- package/tests/provision_settings_apply.mjs +52 -0
- package/tests/software_update.mjs +35 -5
- package/tests/update_pill.mjs +9 -5
- package/src/bongos/upgrade-request.js +0 -158
- package/tests/upgrade_request.mjs +0 -348
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/css-literal-scan.js — @bongos-forwarder (task 1004848). Generated by
|
|
3
|
+
// scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
|
|
4
|
+
// The real script is scripts/bongos/css-literal-scan.js. This forwarder keeps callers that still name
|
|
5
|
+
// the old path working, and warns once so they get repointed. Task 1003706 removes it.
|
|
6
|
+
'use strict';
|
|
7
|
+
require('../bongos/forwarder.js').forward(module, 'css-literal-scan.js');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// scripts/gds/design-generators-fresh.js — @bongos-forwarder (task 1004848). Generated by
|
|
2
|
+
// scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
|
|
3
|
+
// The real script is scripts/bongos/design-generators-fresh.js. This forwarder keeps callers that still name
|
|
4
|
+
// the old path working, and warns once so they get repointed. Task 1003706 removes it.
|
|
5
|
+
'use strict';
|
|
6
|
+
require('../bongos/forwarder.js').forward(module, 'design-generators-fresh.js');
|
|
@@ -208,9 +208,6 @@ const CANNED_WRITES = [
|
|
|
208
208
|
// a vote cast from the street's matter card (task 1004528), so the card's
|
|
209
209
|
// answer (the toast, the building lighting) renders here too.
|
|
210
210
|
['POST', /^government\/board\/items\/\d+\/votes$/, 'government__board__items__votes.post'],
|
|
211
|
-
// Settings → Software update's "Ask for this upgrade" (task 1004595), so the
|
|
212
|
-
// pending line the panel draws after the press renders here too.
|
|
213
|
-
['POST', /^software-update\/request$/, 'software-update__request.post'],
|
|
214
211
|
// the studio's AI pages (task 1004921): a file uploads, a request is sent
|
|
215
212
|
// (it answers as 1002, the running one), and money is added.
|
|
216
213
|
['POST', /^ai-studio\/uploads$/, 'ai-studio__uploads.post'],
|
|
@@ -473,7 +473,7 @@ const MODULE_GLOBS = {
|
|
|
473
473
|
'scripts/bongos/adr-namespace.js', 'scripts/bongos/baseline-staleness.js',
|
|
474
474
|
'scripts/bongos/category-advisory-guard.js', 'scripts/bongos/client-baseurl-guard.js',
|
|
475
475
|
'scripts/bongos/doc-cli-guard.js', 'scripts/bongos/doc-control-claims.js', 'scripts/bongos/doc-comment-coverage.js',
|
|
476
|
-
'scripts/bongos/exec-path-guard.js', 'scripts/bongos/fitness-lib.js',
|
|
476
|
+
'scripts/bongos/exec-path-guard.js', 'scripts/bongos/fitness-lib.js', 'scripts/bongos/design-generators-fresh.js',
|
|
477
477
|
'scripts/bongos/fitness-checks-error-envelope.js', 'scripts/bongos/fitness-checks-identity.js',
|
|
478
478
|
'scripts/bongos/fitness-checks-packaging.js', 'scripts/bongos/fitness-checks-route-pins.js',
|
|
479
479
|
'scripts/bongos/fitness-checks-vocabulary.js', 'scripts/bongos/fitness-checks-write-validation.js',
|
|
@@ -34,7 +34,6 @@ const { readSoftwareUpdate } = require('../software-update');
|
|
|
34
34
|
const { followForShape } = require('../../../scripts/bongos/update-channel.js');
|
|
35
35
|
const seams = require('../../module-seams');
|
|
36
36
|
const { loadIdpConfig } = require('../auth-config');
|
|
37
|
-
const upgradeRequest = require('../upgrade-request');
|
|
38
37
|
const { readUpgradeHold } = require('../upgrade-hold');
|
|
39
38
|
const { moveNotice, moveHistory, readInstalledReleaseNotes } = require('../core-moves');
|
|
40
39
|
|
|
@@ -82,10 +81,35 @@ function hubManageLinks(config) {
|
|
|
82
81
|
// address as <PREFIX>_HUB_UPGRADE_URL (task 1004608). Without it, those owners were told to
|
|
83
82
|
// run `bongos upgrade` on a server the platform runs. Only the hub's /deploy path is taken.
|
|
84
83
|
const HUB_UPGRADE_URL_RE = /^https?:\/\/[^\s"'<>/]+\/deploy\?project=[A-Za-z0-9._%-]+$/;
|
|
85
|
-
|
|
84
|
+
// THE LAST FALLBACK (task 1005128). A hosted project whose web.env predates the line above
|
|
85
|
+
// (toast, 2026-11) named neither, fell to `cli`, and its owner was offered "Ask for this
|
|
86
|
+
// upgrade" instead of the Update button — the owner: "if the update is generally available
|
|
87
|
+
// then they should be able to download it". So a project that answers on a name under its
|
|
88
|
+
// hub's own domain (the hub's rule for "a project we host") is sent to that hub's door by its
|
|
89
|
+
// address instead of its slug: `<hub>/deploy?host=<host>`. The hub resolves the address among
|
|
90
|
+
// the visitor's OWN projects, exactly as it resolves a slug, and the hand-off still only
|
|
91
|
+
// accepts the project's registered origin. A self-hosted instance on its own domain stays
|
|
92
|
+
// `cli`: nobody at the hub can reach its server.
|
|
93
|
+
function projectHost(raw) {
|
|
94
|
+
const host = String(raw || '').trim().toLowerCase();
|
|
95
|
+
if (!/^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(host)) return null;
|
|
96
|
+
if (/^\d+(\.\d+)+$/.test(host)) return null;
|
|
97
|
+
if (/(^|\.)(localhost|local|internal|test|invalid)$/.test(host)) return null;
|
|
98
|
+
return host;
|
|
99
|
+
}
|
|
100
|
+
function hubHostedDoor({ hub, host }) {
|
|
101
|
+
const h = projectHost(host);
|
|
102
|
+
let origin = null;
|
|
103
|
+
let hubHost = null;
|
|
104
|
+
try { const u = new URL(String(hub)); if (/^https?:$/.test(u.protocol)) { origin = u.origin; hubHost = u.host.toLowerCase(); } } catch { /* no hub */ }
|
|
105
|
+
if (!h || !origin || h === hubHost || !h.endsWith(`.${hubHost}`)) return null;
|
|
106
|
+
return `${origin}/deploy?host=${encodeURIComponent(h)}`;
|
|
107
|
+
}
|
|
108
|
+
function updateDoor({ provisioning, idp, hubUpgradeUrl, hub = null, host = null }) {
|
|
86
109
|
if (provisioning) return { kind: 'local', url: '/deploy' };
|
|
87
110
|
const url = hubDeployUrl(idp)
|
|
88
|
-
|| (typeof hubUpgradeUrl === 'string' && HUB_UPGRADE_URL_RE.test(hubUpgradeUrl.trim()) ? hubUpgradeUrl.trim() : null)
|
|
111
|
+
|| (typeof hubUpgradeUrl === 'string' && HUB_UPGRADE_URL_RE.test(hubUpgradeUrl.trim()) ? hubUpgradeUrl.trim() : null)
|
|
112
|
+
|| hubHostedDoor({ hub, host });
|
|
89
113
|
return url ? { kind: 'hub', url } : { kind: 'cli', url: null };
|
|
90
114
|
}
|
|
91
115
|
|
|
@@ -187,25 +211,15 @@ async function resolveChannel(selfChannel) {
|
|
|
187
211
|
return { channel, source: channel ? 'deploy' : 'default' };
|
|
188
212
|
}
|
|
189
213
|
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const ps = require('../project-settings');
|
|
195
|
-
const fb = require('./feedback');
|
|
196
|
-
return {
|
|
197
|
-
hub: fb.feedbackHub(),
|
|
198
|
-
host: configuredHost(),
|
|
199
|
-
getSetting: ps.getSetting,
|
|
200
|
-
setSetting: ps.setSetting,
|
|
201
|
-
issueReceipt: (r) => fb.defaultReceiptStore().issue(r),
|
|
202
|
-
};
|
|
214
|
+
// The hub this project belongs to: the same answer the direct feedback door uses (null on the
|
|
215
|
+
// hub itself, so the platform never points at itself).
|
|
216
|
+
function safeHub() {
|
|
217
|
+
try { return require('./feedback').feedbackHub(); } catch { return null; }
|
|
203
218
|
}
|
|
204
219
|
|
|
205
220
|
// The host this project answers on, from its OWN branding pack (domains is server-only):
|
|
206
|
-
//
|
|
207
|
-
// Host or X-Forwarded-Host header is the caller's to write
|
|
208
|
-
// hub's call-back at any address they like. '' when the pack names none.
|
|
221
|
+
// what decides whether the hub hosts it (hubHostedDoor). Never read from the request: a
|
|
222
|
+
// Host or X-Forwarded-Host header is the caller's to write. '' when the pack names none.
|
|
209
223
|
function configuredHost() {
|
|
210
224
|
try {
|
|
211
225
|
const d = require('../../branding').branding().domains || {};
|
|
@@ -213,18 +227,6 @@ function configuredHost() {
|
|
|
213
227
|
} catch { return ''; }
|
|
214
228
|
}
|
|
215
229
|
|
|
216
|
-
// The page's ask state. Fail-soft: a stored row that cannot be read is "not asked yet",
|
|
217
|
-
// never a 500 on the whole Software update panel.
|
|
218
|
-
async function readAsk({ update, door, deps }) {
|
|
219
|
-
const offered = update && update.reading === 'ok' ? update.offered : null;
|
|
220
|
-
const base = { offered, host: upgradeRequest.projectHost(deps.host), doorKind: door.kind, hub: deps.hub };
|
|
221
|
-
let stored = null;
|
|
222
|
-
if (offered && upgradeRequest.isOperatorRun(base)) {
|
|
223
|
-
try { stored = upgradeRequest.parseStored(await deps.getSetting(upgradeRequest.SETTING_KEY)); } catch { stored = null; }
|
|
224
|
-
}
|
|
225
|
-
return upgradeRequest.askState({ ...base, stored });
|
|
226
|
-
}
|
|
227
|
-
|
|
228
230
|
// A core move this project's own ledger says was rolled back (task 1004730,
|
|
229
231
|
// src/bongos/upgrade-hold.js): which version was tried, why in plain words, and that this
|
|
230
232
|
// project is still on its old version and working. Both reads carry it, the Software update
|
|
@@ -270,16 +272,13 @@ function buildCoreUpdateRouter({
|
|
|
270
272
|
upgrades = readCoreUpgrades,
|
|
271
273
|
softwareUpdate = readSoftwareUpdate,
|
|
272
274
|
canUpdate = holdsCorePin,
|
|
273
|
-
doorInputs = () => ({ provisioning: provisioningEnabled(), idp: safeIdpConfig(), hubUpgradeUrl: safeHubUpgradeUrl() }),
|
|
274
|
-
askDeps = defaultAskDeps,
|
|
275
|
+
doorInputs = () => ({ provisioning: provisioningEnabled(), idp: safeIdpConfig(), hubUpgradeUrl: safeHubUpgradeUrl(), hub: safeHub(), host: configuredHost() }),
|
|
275
276
|
upgradeHold = readUpgradeHold,
|
|
276
277
|
releaseNotes = readInstalledReleaseNotes,
|
|
277
278
|
seenMove = defaultSeenMove,
|
|
278
279
|
} = {}) {
|
|
279
280
|
contributeCoreUpdateToDocket();
|
|
280
281
|
const router = express.Router();
|
|
281
|
-
// One send at a time: a double press waits for the first and is answered from its row.
|
|
282
|
-
let asking = null;
|
|
283
282
|
|
|
284
283
|
router.get(
|
|
285
284
|
'/core-update',
|
|
@@ -341,53 +340,13 @@ function buildCoreUpdateRouter({
|
|
|
341
340
|
resolveChannel(selfChannel).then(async ({ channel, source }) => ({ ...(await softwareUpdate({ running, channel, follow: followFor(door) })), channel_source: source })),
|
|
342
341
|
canUpdate(req),
|
|
343
342
|
]);
|
|
344
|
-
const ask = await readAsk({ update, door, deps: askDeps() });
|
|
345
343
|
const held = await safeHold(upgradeHold, { running, offered: update && update.reading === 'ok' ? update.offered : null });
|
|
346
344
|
// The history only for a viewer who holds core.pin.move (see safeMoved's gate note).
|
|
347
345
|
const history = can === true ? await safeHistory(upgrades) : null;
|
|
348
|
-
res.json({ ok: true, update, door, can_update: can === true,
|
|
346
|
+
res.json({ ok: true, update, door, can_update: can === true, held, history });
|
|
349
347
|
}, { errorCode: 'software_update_failed', message: false }),
|
|
350
348
|
);
|
|
351
349
|
|
|
352
|
-
// POST /software-update/request — "Ask for this upgrade" (task 1004595). On a project
|
|
353
|
-
// the hub's operator runs, sends one request for the version this project is offered
|
|
354
|
-
// to the operator, through the direct feedback door, and remembers it so the page can
|
|
355
|
-
// say it is pending. The version is the server's own reading, never the caller's.
|
|
356
|
-
// Idempotent per version; a different version waits out a 24h cooldown (429).
|
|
357
|
-
// rank: core.pin.move — the same atom as the Update button the panel draws.
|
|
358
|
-
router.post(
|
|
359
|
-
'/software-update/request',
|
|
360
|
-
auth.requireBuilder,
|
|
361
|
-
auth.requirePermission('core.pin.move'),
|
|
362
|
-
asyncHandler('POST /software-update/request', async (req, res) => {
|
|
363
|
-
while (asking) { try { await asking; } catch { /* the next one tries for itself */ } }
|
|
364
|
-
const run = (async () => {
|
|
365
|
-
let running = null;
|
|
366
|
-
try { running = require('../../module-api').CORE_VERSION; } catch { /* version unknown */ }
|
|
367
|
-
const { channel } = await resolveChannel(selfChannel);
|
|
368
|
-
const door = updateDoor(doorInputs());
|
|
369
|
-
const update = await softwareUpdate({ running, channel, follow: followFor(door) });
|
|
370
|
-
const deps = askDeps();
|
|
371
|
-
return upgradeRequest.requestUpgrade({
|
|
372
|
-
host: deps.host,
|
|
373
|
-
running,
|
|
374
|
-
wanted: update && update.reading === 'ok' ? update.offered : null,
|
|
375
|
-
builder: req.builder,
|
|
376
|
-
doorKind: door.kind,
|
|
377
|
-
}, deps);
|
|
378
|
-
})();
|
|
379
|
-
asking = run;
|
|
380
|
-
let out;
|
|
381
|
-
try { out = await run; } finally { if (asking === run) asking = null; }
|
|
382
|
-
if (out.status >= 400) {
|
|
383
|
-
const { error, message, ...details } = out.body;
|
|
384
|
-
if (details.retry_after_seconds) res.set('Retry-After', String(details.retry_after_seconds));
|
|
385
|
-
return res.fail(error, { status: out.status, message, details });
|
|
386
|
-
}
|
|
387
|
-
res.status(out.status).json(out.body);
|
|
388
|
-
}, { errorCode: 'upgrade_request_failed', message: false }),
|
|
389
|
-
);
|
|
390
|
-
|
|
391
350
|
// GET /project/manage-link — the hub's page for THIS project, where its owner takes it
|
|
392
351
|
// offline or disconnects it (task 1004570). On a hosted project the owner is its Archon,
|
|
393
352
|
// so the read is Archon-only: a builder below that is refused and the hall draws no
|
|
@@ -410,4 +369,5 @@ module.exports = buildCoreUpdateRouter;
|
|
|
410
369
|
module.exports.hubManageLinks = hubManageLinks;
|
|
411
370
|
module.exports.hubDeployUrl = hubDeployUrl;
|
|
412
371
|
module.exports.updateDoor = updateDoor;
|
|
372
|
+
module.exports.hubHostedDoor = hubHostedDoor;
|
|
413
373
|
module.exports.coreUpdateDocketMatters = coreUpdateDocketMatters;
|
|
@@ -78,8 +78,7 @@ function makeReceiptStore({ now = Date.now, ttlMs = RECEIPT_TTL_MS, max = RECEIP
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// The process's one receipt store. Shared, because a receipt is redeemed at the public
|
|
81
|
-
// route below whoever issued it
|
|
82
|
-
// "Ask for this upgrade" (src/bongos/upgrade-request.js, task 1004595).
|
|
81
|
+
// route below whoever issued it (the CLI's POST /feedback/receipt).
|
|
83
82
|
let sharedStore = null;
|
|
84
83
|
function defaultReceiptStore() {
|
|
85
84
|
if (!sharedStore) sharedStore = makeReceiptStore();
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.24.
|
|
84
|
+
const CORE_VERSION = '1.24.58'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
package/tests/deploy_handoff.mjs
CHANGED
|
@@ -85,7 +85,7 @@ afterEach(() => { for (const id of armed) clearTimeout(id); armed.clear(); });
|
|
|
85
85
|
const settle = (ms = 0) => new Promise((r) => setTimeout(r, ms));
|
|
86
86
|
const plain = (x) => JSON.parse(JSON.stringify(x));
|
|
87
87
|
|
|
88
|
-
function boot({ opener = { postMessage(msg, origin) { told.push({ msg, origin }); } }, preview = PREVIEW, post = { ok: true, status: 200, data: { ok: true } }, rows = [{ id: '14', slug: 'toast', domain: 'toast.cloudbongos.com' }] } = {}) {
|
|
88
|
+
function boot({ opener = { postMessage(msg, origin) { told.push({ msg, origin }); } }, preview = PREVIEW, post = { ok: true, status: 200, data: { ok: true } }, rows = [{ id: '14', slug: 'toast', domain: 'toast.cloudbongos.com' }], search = '?project=toast&handoff=1' } = {}) {
|
|
89
89
|
const told = [];
|
|
90
90
|
const requests = [];
|
|
91
91
|
const listeners = {};
|
|
@@ -105,7 +105,7 @@ function boot({ opener = { postMessage(msg, origin) { told.push({ msg, origin })
|
|
|
105
105
|
if (opener) opener.postMessage = (msg, origin) => told.push({ msg, origin });
|
|
106
106
|
const windowObj = {
|
|
107
107
|
OTB: realOTB(), document: documentObj, opener, __handoffHearMs: 30,
|
|
108
|
-
location: { protocol: 'https:', pathname: '/deploy', search
|
|
108
|
+
location: { protocol: 'https:', pathname: '/deploy', search, assign() {} },
|
|
109
109
|
addEventListener: (ev, fn) => { (listeners[ev] ||= []).push(fn); },
|
|
110
110
|
setTimeout: (fn, ms) => { const id = setTimeout(fn, ms); armed.add(id); return id; },
|
|
111
111
|
close() { closed = true; },
|
|
@@ -150,6 +150,27 @@ test('the go from the project\'s own hall moves it to that version, says so, and
|
|
|
150
150
|
assert.match(b.body.innerHTML, /The update has started/);
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
+
// task 1005128: a hosted project whose server never learned its slug sends ?host=<address>.
|
|
154
|
+
test('?host= finds the project by its registered address, among the visitor\'s own, and moves it the same way', async () => {
|
|
155
|
+
const rows = [{ id: '9', slug: 'other', domain: 'other.cloudbongos.com' }, { id: '14', slug: 'toast', domain: 'Toast.CloudBongos.com' }];
|
|
156
|
+
const b = boot({ rows, search: '?host=toast.cloudbongos.com&handoff=1' });
|
|
157
|
+
await settle(5);
|
|
158
|
+
assert.deepEqual(plain(b.told), [{ msg: { tag: 'bongos-update', step: 'ready' }, origin: 'https://toast.cloudbongos.com' }]);
|
|
159
|
+
b.message(GO);
|
|
160
|
+
await settle(5);
|
|
161
|
+
assert.equal(b.posts().length, 1);
|
|
162
|
+
assert.equal(b.posts()[0].path, '/api/bongos/provisioning/instances/14/core-upgrade');
|
|
163
|
+
|
|
164
|
+
const none = boot({ rows, search: '?host=nobody.cloudbongos.com&handoff=1' });
|
|
165
|
+
await settle(5);
|
|
166
|
+
assert.equal(none.posts().length, 0);
|
|
167
|
+
assert.match(none.body.innerHTML, /No project called nobody\.cloudbongos\.com/);
|
|
168
|
+
|
|
169
|
+
const slugIsNotHost = boot({ rows, search: '?host=toast&handoff=1' });
|
|
170
|
+
await settle(5);
|
|
171
|
+
assert.equal(slugIsNotHost.told.length, 0, 'a bare slug in ?host= matches no address');
|
|
172
|
+
});
|
|
173
|
+
|
|
153
174
|
test('a go from another site, or another window, moves nothing', async () => {
|
|
154
175
|
const b = boot();
|
|
155
176
|
await settle(5);
|
|
@@ -34,14 +34,14 @@ const preview = (id, slug, over = {}) => ({
|
|
|
34
34
|
|
|
35
35
|
// The fleet as the control plane really reports it: mostly torn down, a couple live.
|
|
36
36
|
const FLEET = [
|
|
37
|
-
{ id: '31', slug: 'mercury', status: 'active', hosting_shape: 'cloud-host' },
|
|
37
|
+
{ id: '31', slug: 'mercury', status: 'active', hosting_shape: 'cloud-host', domain: 'mercury.cloudbongos.com' },
|
|
38
38
|
{ id: '32', slug: 'hoodify-finance', status: 'active', hosting_shape: 'co-tenant' },
|
|
39
39
|
{ id: '33', slug: 'walk-lantern', status: 'torn_down', hosting_shape: 'cloud-host' },
|
|
40
40
|
{ id: '34', slug: 'testccc', status: 'torn_down', hosting_shape: 'cloud-host' },
|
|
41
41
|
{ id: '35', slug: 'unreadable-one', status: 'active', hosting_shape: 'cloud-host' },
|
|
42
42
|
];
|
|
43
43
|
|
|
44
|
-
function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus = 200, fleet = FLEET, fleetStatus = 200, instanceOver = null, moveTries = 4, postStatus = 200, scope = null, href = null } = {}) {
|
|
44
|
+
function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus = 200, fleet = FLEET, fleetStatus = 200, instanceOver = null, moveTries = 4, postStatus = 200, scope = null, search = null, href = null } = {}) {
|
|
45
45
|
const els = new Map();
|
|
46
46
|
const posts = [];
|
|
47
47
|
const gets = [];
|
|
@@ -99,7 +99,7 @@ function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus =
|
|
|
99
99
|
sleep: (ms) => new Promise((r) => setTimeout(r, ms)) }, // dom-utils' OTB.sleep (task 1004893)
|
|
100
100
|
// `search` is what decides which project the page is about. A stub without it is the
|
|
101
101
|
// shape the page has to survive, and the default here keeps that path exercised.
|
|
102
|
-
location: { assign() {}, search: scope ? `?project=${encodeURIComponent(scope)}` : '', ...(href ? { href } : {}) },
|
|
102
|
+
location: { assign() {}, search: search != null ? search : (scope ? `?project=${encodeURIComponent(scope)}` : ''), ...(href ? { href } : {}) },
|
|
103
103
|
};
|
|
104
104
|
// The page's poll interval, driven rather than slept through. Injecting the REAL
|
|
105
105
|
// setTimeout made every case whose preview never converges sit out the page's full
|
|
@@ -155,6 +155,38 @@ test('?project= renders THAT project, by slug, and still only one row', async ()
|
|
|
155
155
|
assert.ok(!b.gets.includes('/provisioning/control-plane'), 'and the self row is not even read');
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
+
// task 1005128: a hosted project whose server never learned its slug sends its owner by address.
|
|
159
|
+
test('?host= renders the project registered at THAT address, named by its slug, on its own id', async () => {
|
|
160
|
+
const b = boot({ search: '?host=Mercury.CloudBongos.com' });
|
|
161
|
+
await b.settle();
|
|
162
|
+
const html = b.panel();
|
|
163
|
+
assert.match(html, /mercury/);
|
|
164
|
+
assert.doesNotMatch(html, /hoodify-finance/, 'no other project');
|
|
165
|
+
assert.ok(!b.gets.includes('/provisioning/control-plane'), 'the self row is not read');
|
|
166
|
+
b.els.get('dp-preview-31')._on.click();
|
|
167
|
+
b.els.get('dp-confirm-31')._on.click();
|
|
168
|
+
await b.settle();
|
|
169
|
+
const moves = b.posts.filter((x) => !(x.body && x.body.dry_run));
|
|
170
|
+
assert.equal(moves.length, 1);
|
|
171
|
+
assert.equal(moves[0].path, '/provisioning/instances/31/core-upgrade');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('?host= matches only a registered address: a slug, or an address nobody registered, finds nothing', async () => {
|
|
175
|
+
for (const search of ['?host=mercury', '?host=nobody.cloudbongos.com', '?host=hoodify-finance']) {
|
|
176
|
+
const b = boot({ search });
|
|
177
|
+
await b.settle();
|
|
178
|
+
assert.match(b.panel(), /no project by that name is hosted here/, search);
|
|
179
|
+
assert.equal(b.posts.length, 0, search);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('?project= wins over ?host= when both are given', async () => {
|
|
184
|
+
const b = boot({ search: '?project=hoodify-finance&host=mercury.cloudbongos.com' });
|
|
185
|
+
await b.settle();
|
|
186
|
+
assert.match(b.panel(), /hoodify-finance/);
|
|
187
|
+
assert.doesNotMatch(b.panel(), /mercury/);
|
|
188
|
+
});
|
|
189
|
+
|
|
158
190
|
test('a scoped project is offered its own move, on its own id', async () => {
|
|
159
191
|
const b = boot({ scope: 'mercury' });
|
|
160
192
|
await b.settle();
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// tests/design_counts_ratchet.mjs — design counts that can only go down, and the
|
|
2
|
+
// design generators wired into the fitness freshness gate (task 1005093, goal 1000138).
|
|
3
|
+
//
|
|
4
|
+
// Done when: adding a new hard-coded font-size to a hall sheet fails fitness, and
|
|
5
|
+
// editing the shared token sheet without regenerating fails the freshness gate.
|
|
6
|
+
// Both are proved here on the real tree: the first by scanning the real sheets with
|
|
7
|
+
// one extra declaration, the second by running the gate on a copied tree.
|
|
8
|
+
//
|
|
9
|
+
// Run: node --test tests/design_counts_ratchet.mjs
|
|
10
|
+
|
|
11
|
+
import { strict as assert } from 'node:assert';
|
|
12
|
+
import { test } from 'node:test';
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
const scan = require(path.join(ROOT, 'scripts', 'bongos', 'css-literal-scan.js'));
|
|
22
|
+
const ratchets = require(path.join(ROOT, 'scripts', 'bongos', 'fitness-ratchets.js'));
|
|
23
|
+
const fitness = require(path.join(ROOT, 'scripts', 'bongos', 'fitness.js'));
|
|
24
|
+
const { checkDesignGeneratorsFresh } = require(path.join(ROOT, 'scripts', 'bongos', 'design-generators-fresh.js'));
|
|
25
|
+
|
|
26
|
+
const HALL_SHEET = 'modules/hall-ui/public/style.css';
|
|
27
|
+
const readReal = (f) => fs.readFileSync(path.join(ROOT, f), 'utf8');
|
|
28
|
+
|
|
29
|
+
test('the detector counts typed-in values and leaves tokens alone', () => {
|
|
30
|
+
const c = scan.countSheet(`
|
|
31
|
+
:root { --ds-x: 14px; --tone: #123456; } /* token definitions: not counted */
|
|
32
|
+
.a { font-size: 13px; border-radius: 6px; color: #abc; }
|
|
33
|
+
.b { font-size: var(--ds-text-sm); border-radius: var(--ds-radius-tile); color: var(--ink); }
|
|
34
|
+
.c { box-shadow: 0 2px 4px rgba(0, 0, 0, .2); } /* a shadow; the black scrim is allowed */
|
|
35
|
+
.d { box-shadow: none; background: url("data:image/svg+xml;fill='#fff'"); }
|
|
36
|
+
.e { background: color-mix(in srgb, var(--ink) 40%, rgb(12 34 56)); padding: 0; }
|
|
37
|
+
@media (max-width: 720px) { .f { border-radius: 0; } }
|
|
38
|
+
@media (prefers-reduced-motion: reduce) { .g { transition: none; } }
|
|
39
|
+
`);
|
|
40
|
+
assert.deepEqual(c, { colours: 2, font_sizes: 1, radii: 1, shadows: 1, breakpoints: 1 });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('a selector with a colon is not read as a declaration', () => {
|
|
44
|
+
assert.equal(scan.countSheet('a:hover, b:focus-visible { color: var(--ink); }').colours, 0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('the real tree matches its committed design-count baselines', () => {
|
|
48
|
+
const { sheets, scanned } = scan.scanCssLiterals();
|
|
49
|
+
assert.ok(scanned > 40, `the scan should see the live stylesheets (saw ${scanned})`);
|
|
50
|
+
const r = ratchets.checkQualityRatchets({ metrics: {}, designCounts: sheets });
|
|
51
|
+
assert.deepEqual(r.violations, [], 'design counts regressed against config/fitness-baselines.json');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('a NEW hard-coded font-size in a hall sheet fails fitness', () => {
|
|
55
|
+
const { sheets } = scan.scanCssLiterals({
|
|
56
|
+
read: (f) => readReal(f) + (f === HALL_SHEET ? '\n.planted { font-size: 13px; }\n' : ''),
|
|
57
|
+
});
|
|
58
|
+
const r = ratchets.checkQualityRatchets({ metrics: {}, designCounts: sheets });
|
|
59
|
+
assert.equal(r.hardFail, true);
|
|
60
|
+
assert.ok(r.violations.some((v) => v.includes(`font_sizes in ${HALL_SHEET} regressed`)), r.violations.join('\n'));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('a new sheet starts at zero: any typed-in colour in it fails', () => {
|
|
64
|
+
const d = ratchets.compareDesignCounts({ 'modules/x/public/new.css': { colours: 1 } }, { _comment: 'note' });
|
|
65
|
+
assert.equal(d.violations.length, 1);
|
|
66
|
+
assert.match(d.violations[0], /colours in modules\/x\/public\/new\.css regressed: 1 > baseline 0/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('a lower count nudges a ratchet-down and does not fail', () => {
|
|
70
|
+
const d = ratchets.compareDesignCounts({ 'a.css': { radii: 2 } }, { 'a.css': { radii: 3 } });
|
|
71
|
+
assert.deepEqual(d.violations, []);
|
|
72
|
+
assert.match(d.warnings[0], /ratchet-down available: design count radii in a\.css improved to 2/);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('a baseline file with no design_counts fails closed', () => {
|
|
76
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-counts-'));
|
|
77
|
+
const file = path.join(dir, 'baselines.json');
|
|
78
|
+
fs.writeFileSync(file, JSON.stringify({ metrics: {} }));
|
|
79
|
+
try {
|
|
80
|
+
const r = ratchets.checkQualityRatchets({ baselinePath: file, metrics: {}, designCounts: {} });
|
|
81
|
+
assert.equal(r.hardFail, true);
|
|
82
|
+
assert.match(r.violations[0], /no "design_counts" object/);
|
|
83
|
+
} finally {
|
|
84
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// The generators resolve their sources from their own location, so a copy of the
|
|
89
|
+
// files they read is a complete, isolated tree the gate can run against.
|
|
90
|
+
const GENERATOR_SOURCES = [
|
|
91
|
+
'config/branding.neutral.json',
|
|
92
|
+
'config/design-tokens.neutral.json',
|
|
93
|
+
'modules/ui-design/public/tokens.css',
|
|
94
|
+
'modules/ui-design/public/brand.html',
|
|
95
|
+
'modules/ui-design/public/brand',
|
|
96
|
+
'modules/ui-design/scripts/gen-design-tokens.js',
|
|
97
|
+
'modules/ui-design/scripts/gen-design-reference.js',
|
|
98
|
+
'modules/hall-ui/public/style.css',
|
|
99
|
+
'modules/hall-ui/public/controls.css',
|
|
100
|
+
'modules/hall-ui/public/dom-utils.js',
|
|
101
|
+
'scripts/bongos/gen-brand.js',
|
|
102
|
+
'DESIGN.md',
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
function copiedTree() {
|
|
106
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-fresh-'));
|
|
107
|
+
for (const rel of GENERATOR_SOURCES) {
|
|
108
|
+
fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true });
|
|
109
|
+
fs.cpSync(path.join(ROOT, rel), path.join(dir, rel), { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
return dir;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
test('the freshness gate passes on a fresh tree and fails when the token sheet changes without a regeneration', () => {
|
|
115
|
+
const dir = copiedTree();
|
|
116
|
+
try {
|
|
117
|
+
assert.deepEqual(checkDesignGeneratorsFresh(dir), [], 'the copied tree should start fresh');
|
|
118
|
+
const sheet = path.join(dir, 'modules/ui-design/public/tokens.css');
|
|
119
|
+
const before = fs.readFileSync(sheet, 'utf8');
|
|
120
|
+
const after = before.replace(/--ds-space-xs:\s*[^;]+;/, '--ds-space-xs: 11px;');
|
|
121
|
+
assert.notEqual(after, before, 'the token sheet should declare --ds-space-xs');
|
|
122
|
+
fs.writeFileSync(sheet, after);
|
|
123
|
+
const v = checkDesignGeneratorsFresh(dir);
|
|
124
|
+
assert.ok(v.some((x) => x.startsWith('config/design-tokens.neutral.json is STALE')), v.join('\n'));
|
|
125
|
+
assert.ok(v.some((x) => x.startsWith('modules/ui-design/public/brand.html')), v.join('\n'));
|
|
126
|
+
// …and the registered fitness check carries that verdict, so the build goes red.
|
|
127
|
+
assert.ok(fitness.CHECKS.includes(fitness.checkGeneratedArtifactsFresh));
|
|
128
|
+
const gate = fitness.checkGeneratedArtifactsFresh({ designRoot: dir });
|
|
129
|
+
assert.equal(gate.hardFail, true);
|
|
130
|
+
assert.ok(gate.violations.some((x) => x.startsWith('config/design-tokens.neutral.json is STALE')), gate.violations.join('\n'));
|
|
131
|
+
} finally {
|
|
132
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
@@ -658,5 +658,57 @@ t('both tenant shapes get the hub door for THEIR slug; no other shape gets the k
|
|
|
658
658
|
}
|
|
659
659
|
});
|
|
660
660
|
|
|
661
|
+
console.log('\nthe backfill — every core move writes the Update button\'s address (task 1005128):');
|
|
662
|
+
|
|
663
|
+
t('a web.env with no line gets it; one already right is left alone; an empty file or a shape with no door is never patched', () => {
|
|
664
|
+
const saved = process.env.PROVISION_HUB_ORIGIN;
|
|
665
|
+
process.env.PROVISION_HUB_ORIGIN = 'https://hub.example.com';
|
|
666
|
+
try {
|
|
667
|
+
const U = require('../scripts/bongos/provision-units.js');
|
|
668
|
+
const key = `${NEUTRAL_PREFIX}_HUB_UPGRADE_URL`;
|
|
669
|
+
const want = 'https://hub.example.com/deploy?project=toast';
|
|
670
|
+
const toast = { slug: 'toast', hosting_shape: 'cloud-host' };
|
|
671
|
+
const before = 'PORT=3011\nPGDATABASE=toast\n';
|
|
672
|
+
const after = U.hubUpgradeEnvPatch(before, toast);
|
|
673
|
+
assert.match(after, new RegExp(`^${key}=https://hub\\.example\\.com/deploy\\?project=toast$`, 'm'), 'the line is added');
|
|
674
|
+
assert.match(after, /^PORT=3011$/m, 'every other line kept');
|
|
675
|
+
assert.match(after, /^PGDATABASE=toast$/m);
|
|
676
|
+
assert.equal(U.hubUpgradeEnvPatch(after, toast), null, 'already right: nothing to write');
|
|
677
|
+
assert.match(U.hubUpgradeEnvPatch(`${before}${key}=\n`, toast), new RegExp(`^${key}=${want.replace(/[.?]/g, '\\$&')}$`, 'm'), 'an empty line is filled');
|
|
678
|
+
assert.equal(U.hubUpgradeEnvPatch('', toast), null, 'an empty file is never patched into a one-line body');
|
|
679
|
+
assert.equal(U.hubUpgradeEnvPatch(null, toast), null);
|
|
680
|
+
assert.equal(U.hubUpgradeEnvPatch(before, { slug: 'x', hosting_shape: 'byo-host' }), null, 'no door on this hub, nothing written');
|
|
681
|
+
} finally {
|
|
682
|
+
if (saved === undefined) delete process.env.PROVISION_HUB_ORIGIN; else process.env.PROVISION_HUB_ORIGIN = saved;
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
t('the move writes it before it runs, and a failed read or write never stops the move', () => {
|
|
687
|
+
const saved = process.env.PROVISION_HUB_ORIGIN;
|
|
688
|
+
process.env.PROVISION_HUB_ORIGIN = 'https://hub.example.com';
|
|
689
|
+
try {
|
|
690
|
+
const { backfillHubUpgradeUrl } = require('../scripts/bongos/provision-core-upgrade.js');
|
|
691
|
+
const toast = { slug: 'toast', hosting_shape: 'cloud-host' };
|
|
692
|
+
const writes = [];
|
|
693
|
+
const logs = [];
|
|
694
|
+
const deps = { privileged: true, log: (l) => logs.push(l), controlWriteFile: (p, body, o) => writes.push({ p, body, o }) };
|
|
695
|
+
const exec = (cmd) => (/cat .*web\.env$/.test(cmd) ? { stdout: 'PORT=3011\n' } : { stdout: '' });
|
|
696
|
+
assert.equal(backfillHubUpgradeUrl(toast, deps, exec), true);
|
|
697
|
+
assert.equal(writes.length, 1);
|
|
698
|
+
assert.match(writes[0].p, /toast\/web\.env$/);
|
|
699
|
+
assert.match(writes[0].body, /_HUB_UPGRADE_URL=https:\/\/hub\.example\.com\/deploy\?project=toast$/m);
|
|
700
|
+
assert.equal(writes[0].o.mode, 0o600);
|
|
701
|
+
assert.equal(backfillHubUpgradeUrl(toast, deps, () => ({ softFailed: true })), false, 'unreadable: nothing written');
|
|
702
|
+
const ran = [];
|
|
703
|
+
assert.equal(backfillHubUpgradeUrl({ slug: 'x; rm -rf /', hosting_shape: 'cloud-host' }, deps, (c) => { ran.push(c); return { stdout: 'PORT=1\n' }; }), false, 'a slug that is not a slug');
|
|
704
|
+
assert.deepEqual(ran, [], 'and no command is built from it');
|
|
705
|
+
assert.equal(backfillHubUpgradeUrl(toast, { ...deps, controlWriteFile: () => { throw new Error('EACCES'); } }, exec), false, 'a refused write is logged, not thrown');
|
|
706
|
+
assert.ok(logs.some((l) => /the move goes on/.test(l)));
|
|
707
|
+
assert.equal(writes.length, 1);
|
|
708
|
+
} finally {
|
|
709
|
+
if (saved === undefined) delete process.env.PROVISION_HUB_ORIGIN; else process.env.PROVISION_HUB_ORIGIN = saved;
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
|
|
661
713
|
console.log(`\nprovision_settings_apply: ${passed} passed, ${failed} failed`);
|
|
662
714
|
process.exit(failed ? 1 : 0);
|
|
@@ -219,8 +219,7 @@ async function drive(deps) {
|
|
|
219
219
|
const build = require('../src/bongos/routes/core-update.js');
|
|
220
220
|
const express = require('express');
|
|
221
221
|
const app = express();
|
|
222
|
-
|
|
223
|
-
app.use(build({ askDeps: () => ({ hub: 'https://cloudbongos.com', getSetting: async () => null }), ...deps }));
|
|
222
|
+
app.use(build({ ...deps }));
|
|
224
223
|
auth.requireBuilder = saved;
|
|
225
224
|
const server = app.listen(0);
|
|
226
225
|
await new Promise((r) => server.once('listening', r));
|
|
@@ -261,6 +260,33 @@ test('HUB_UPGRADE_URL is taken only as a hub /deploy address; anything else stay
|
|
|
261
260
|
}
|
|
262
261
|
});
|
|
263
262
|
|
|
263
|
+
// Task 1005128: toast predates HUB_UPGRADE_URL and federates no sign-in, so it fell to `cli`
|
|
264
|
+
// and its owner was offered "Ask for this upgrade". A project on a name under its hub's own
|
|
265
|
+
// domain is the hub's, so it goes to the hub's door by its address.
|
|
266
|
+
test('a hosted project with neither address still gets the hub\'s door, by its own address (task 1005128)', async () => {
|
|
267
|
+
const r = await drive({ softwareUpdate: async () => STUB_UPDATE, canUpdate: async () => true,
|
|
268
|
+
doorInputs: () => ({ provisioning: false, idp: null, hubUpgradeUrl: null, hub: 'https://cloudbongos.com', host: 'Toast.CloudBongos.com' }) });
|
|
269
|
+
assert.deepEqual(r.body.door, { kind: 'hub', url: 'https://cloudbongos.com/deploy?host=toast.cloudbongos.com' });
|
|
270
|
+
assert.equal('ask' in r.body, false, 'the panel is never told to offer an ask');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('only a name under the hub\'s domain is the hub\'s: a self-hosted name, the hub itself, or no hub stays cli (task 1005128)', async () => {
|
|
274
|
+
const cases = [
|
|
275
|
+
{ hub: 'https://cloudbongos.com', host: 'shop.example.org' },
|
|
276
|
+
{ hub: 'https://cloudbongos.com', host: 'cloudbongos.com' },
|
|
277
|
+
{ hub: 'https://cloudbongos.com', host: 'evilcloudbongos.com' },
|
|
278
|
+
{ hub: null, host: 'toast.cloudbongos.com' },
|
|
279
|
+
{ hub: 'https://cloudbongos.com', host: 'localhost' },
|
|
280
|
+
{ hub: 'https://cloudbongos.com', host: '' },
|
|
281
|
+
{ hub: 'javascript:alert(1)', host: 'toast.cloudbongos.com' },
|
|
282
|
+
];
|
|
283
|
+
for (const c of cases) {
|
|
284
|
+
const r = await drive({ softwareUpdate: async () => STUB_UPDATE, canUpdate: async () => true,
|
|
285
|
+
doorInputs: () => ({ provisioning: false, idp: null, ...c }) });
|
|
286
|
+
assert.deepEqual(r.body.door, { kind: 'cli', url: null }, JSON.stringify(c));
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
264
290
|
test('a self-hosted instance has no door to link, and says how it is upgraded instead', async () => {
|
|
265
291
|
const r = await drive({ softwareUpdate: async () => STUB_UPDATE, canUpdate: async () => true, doorInputs: () => ({ provisioning: false, idp: null }) });
|
|
266
292
|
assert.deepEqual(r.body.door, { kind: 'cli', url: null });
|
|
@@ -600,9 +626,13 @@ test('the route hands the reader the follow of the hall it serves: candidates on
|
|
|
600
626
|
}
|
|
601
627
|
});
|
|
602
628
|
|
|
603
|
-
test('[source]
|
|
604
|
-
|
|
605
|
-
assert.
|
|
629
|
+
test('[source] there is no "Ask for this upgrade" route or panel any more (task 1005128)', () => {
|
|
630
|
+
assert.equal(ROUTE_SRC.includes('/software-update/request'), false, 'the route is gone');
|
|
631
|
+
assert.equal(fs.existsSync(path.join(ROOT, 'src/bongos/upgrade-request.js')), false, 'and its module');
|
|
632
|
+
for (const f of ['modules/hall-ui/public/settings-software-update.js', 'modules/hall-ui/public/update-pill.js']) {
|
|
633
|
+
const src = fs.readFileSync(path.join(ROOT, f), 'utf8');
|
|
634
|
+
assert.equal(/software-update\/request|Ask for this/.test(src), false, f);
|
|
635
|
+
}
|
|
606
636
|
});
|
|
607
637
|
|
|
608
638
|
test('[source] the port the route reads is the port the provisioning module registers and declares (task 1004535)', () => {
|