@bongos/core 1.24.26 → 1.24.27
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 +142 -92
- package/docs/adr/0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md +12 -0
- package/docs/copy-inventory.md +67 -48
- package/docs/copy-registry.json +279 -78
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +10 -4
- package/docs/page-readings.json +4335 -4314
- package/docs/recipes/ui-look-before-you-ship.md +1 -0
- package/modules/autonomy/fence.js +16 -1
- package/modules/government/protected-surfaces.json +2 -0
- package/modules/hall-ui/public/ai-studio.html +1 -1
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/city.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/core-move-notice.js +9 -6
- package/modules/hall-ui/public/deploy-handoff.js +203 -0
- package/modules/hall-ui/public/deploy.html +4 -2
- package/modules/hall-ui/public/deploy.js +7 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/market-watch-item.html +1 -1
- package/modules/hall-ui/public/market-watch-rules.html +1 -1
- package/modules/hall-ui/public/market-watch-settings.html +1 -1
- package/modules/hall-ui/public/market-watch.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/my-agents.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings-software-update.js +22 -8
- package/modules/hall-ui/public/settings.html +7 -5
- package/modules/hall-ui/public/shell.js +25 -73
- package/modules/hall-ui/public/street.html +1 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +119 -39
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/update-pill.js +623 -0
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/update-pill.md +12 -0
- package/modules/lifecycle/github-push.js +1 -0
- package/modules/lifecycle/routes/tasks.js +2 -2
- package/modules/lifecycle/runner-self-guard.js +122 -0
- package/modules/lifecycle/runner-self.js +154 -0
- package/modules/public-landing/public/account.html +3 -3
- package/modules/public-landing/public/assets/cosmos.css +45 -0
- package/modules/public-landing/public/community.html +4 -4
- package/modules/public-landing/public/connections.html +1 -1
- package/modules/public-landing/public/guild.html +1 -1
- package/modules/public-landing/public/invitations.html +4 -4
- package/modules/public-landing/public/projects.html +26 -26
- package/modules/status-ui/public/index.html +2 -2
- package/modules/status-ui/public/index.states.json +12 -0
- package/modules/status-ui/public/status.js +10 -2
- package/modules/status-ui/public/style.css +57 -0
- package/modules/ui-design/kit/README.md +1 -1
- package/modules/ui-design/kit/serve.js +8 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +14 -0
- package/scripts/bongos/autobongos-loop.js +4 -0
- package/scripts/bongos/autobongos-run.js +21 -4
- package/scripts/bongos/autobongos-tweak-lane.js +5 -3
- package/scripts/bongos/ship-flow.js +20 -0
- package/src/module-api.js +1 -1
- package/tests/autobongos_host_access.mjs +5 -3
- package/tests/autobongos_self_modification.mjs +420 -0
- package/tests/core_update_banner.mjs +6 -92
- package/tests/core_upgrade_owner_door.mjs +4 -56
- package/tests/deploy_handoff.mjs +206 -0
- package/tests/government_protected_surfaces.mjs +15 -0
- package/tests/hub_connections_page.mjs +1 -1
- package/tests/hub_invitations_page.mjs +1 -1
- package/tests/loading_mark.mjs +203 -0
- package/tests/nav_permission_atoms.mjs +3 -2
- package/tests/software_update.mjs +6 -2
- package/tests/update_pill.mjs +431 -0
|
@@ -430,26 +430,12 @@ function boot({ provisioning, deployUrl, pathname = '/settings', hash = '', held
|
|
|
430
430
|
vm.runInContext(SHELL, sandbox, { filename: 'shell.js' });
|
|
431
431
|
windowObj.OTBShell.applyAccess({ authed: true, rank: 'archon', permissions: { 'core.pin.move': true } });
|
|
432
432
|
return {
|
|
433
|
-
banner: () => inserted.find((e) => String(e._html).includes('is available')),
|
|
434
|
-
notice: () => inserted.find((e) => e.id === 'core-update-notice'),
|
|
435
433
|
deployItem: () => els.get('nav-deploy'),
|
|
436
434
|
};
|
|
437
435
|
}
|
|
438
436
|
const settle = () => new Promise((r) => setTimeout(r, 0));
|
|
439
437
|
const HUB = 'https://cloudbongos.com/deploy?project=hoodify-finance';
|
|
440
438
|
|
|
441
|
-
// Task 1004296 moved the hand-off: a hall without a door of its own sends its owner to
|
|
442
|
-
// Settings → Software update, whose Update button carries the hub address (GET
|
|
443
|
-
// /software-update's `door`). The banner and the rail no longer link off-site.
|
|
444
|
-
await t('on a hosted project, the banner\'s Upgrade goes to Settings → Software update, not off-site', async () => {
|
|
445
|
-
const b = boot({ provisioning: false, deployUrl: HUB });
|
|
446
|
-
await settle();
|
|
447
|
-
assert.ok(b.banner(), 'the banner renders');
|
|
448
|
-
assert.match(b.banner().innerHTML, /href="\/settings#software-update"/, 'Upgrade opens the Software update panel');
|
|
449
|
-
assert.ok(!b.banner().innerHTML.includes(HUB), 'never the hub address directly, even when an older server sends one');
|
|
450
|
-
assert.doesNotMatch(b.banner().innerHTML, /href="\/deploy"/, 'never the local /deploy, which this hall no longer has');
|
|
451
|
-
});
|
|
452
|
-
|
|
453
439
|
await t('on a hosted project, the rail\'s Deploy item stays hidden — it is the platform hall\'s page', async () => {
|
|
454
440
|
const b = boot({ provisioning: false, deployUrl: HUB });
|
|
455
441
|
await settle();
|
|
@@ -458,54 +444,16 @@ await t('on a hosted project, the rail\'s Deploy item stays hidden — it is the
|
|
|
458
444
|
assert.equal(item.dataset.module, 'provisioning', 'its module gate stays, so a hall without the door hides it');
|
|
459
445
|
});
|
|
460
446
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
assert.match(b.banner().innerHTML, /href="\/settings#software-update"/, `Software update for ${JSON.stringify(url)}`);
|
|
466
|
-
assert.doesNotMatch(b.banner().innerHTML, /javascript:/);
|
|
467
|
-
}
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
await t('on the Software update panel itself, the banner points rather than linking to the page it is on', async () => {
|
|
471
|
-
const b = boot({ provisioning: false, pathname: '/settings', hash: '#software-update' });
|
|
472
|
-
await settle();
|
|
473
|
-
assert.doesNotMatch(b.banner().innerHTML, /core-update-notice__action/, 'no link to where you already are');
|
|
474
|
-
assert.match(b.banner().innerHTML, /core-update-notice__hint/);
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
await t('on the platform, the local door wins: the banner links /deploy and the rail keeps it', async () => {
|
|
447
|
+
// The banner that used to link here is the Update pill now (task 1005041): where its Update now
|
|
448
|
+
// goes on each kind of hall, and the held version it refuses to offer, are pinned against the
|
|
449
|
+
// real pill in tests/update_pill.mjs. What stays here is the rail.
|
|
450
|
+
await t('on the platform, the rail keeps its local /deploy', async () => {
|
|
478
451
|
const b = boot({ provisioning: true, deployUrl: HUB });
|
|
479
452
|
await settle();
|
|
480
|
-
assert.match(b.banner().innerHTML, /href="\/deploy"/);
|
|
481
|
-
assert.ok(!b.banner().innerHTML.includes(HUB));
|
|
482
|
-
assert.ok(!b.banner().innerHTML.includes('software-update'), 'the platform takes its update at its own door');
|
|
483
453
|
const item = b.deployItem();
|
|
484
454
|
assert.notEqual(item.getAttribute('href'), HUB, 'the rail keeps the local /deploy');
|
|
485
455
|
assert.equal(item.dataset.module, 'provisioning', 'and its module gate, which the platform satisfies');
|
|
486
456
|
});
|
|
487
457
|
|
|
488
|
-
// Task 1004730: a version this project already tried and rolled back is not announced as
|
|
489
|
-
// "available" with an Upgrade button. The banner says what happened instead, in the owner's
|
|
490
|
-
// words, and points at Software update for the rest.
|
|
491
|
-
await t('a held version: the banner says it was tried and rolled back, and the project is still fine', async () => {
|
|
492
|
-
const held = { version: '1.19.1033', still_on: '1.19.871', message: 'long form' };
|
|
493
|
-
const b = boot({ provisioning: false, held });
|
|
494
|
-
await settle();
|
|
495
|
-
const n = b.notice();
|
|
496
|
-
assert.ok(n, 'the banner renders');
|
|
497
|
-
assert.doesNotMatch(n.innerHTML, /is available/);
|
|
498
|
-
assert.match(n.innerHTML, /1\.19\.1033<\/strong> was tried on this project and rolled back/);
|
|
499
|
-
assert.match(n.innerHTML, /still on 1\.19\.871 and working/);
|
|
500
|
-
assert.match(n.innerHTML, /href="\/settings#software-update">Details</);
|
|
501
|
-
assert.doesNotMatch(n.innerHTML, />Upgrade</, 'no button to take the version that just failed');
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
await t('a hold on an OLDER version than the one now out leaves the ordinary banner', async () => {
|
|
505
|
-
const b = boot({ provisioning: false, held: { version: '1.19.900', still_on: '1.19.871', message: 'x' } });
|
|
506
|
-
await settle();
|
|
507
|
-
assert.ok(b.banner(), 'the newer release is announced as usual');
|
|
508
|
-
});
|
|
509
|
-
|
|
510
458
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
511
459
|
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// tests/deploy_handoff.mjs — the hub's half of the in-place update (task 1005041).
|
|
2
|
+
//
|
|
3
|
+
// A hosted project's hall opens the hub's deploy door as a small window and tells it which
|
|
4
|
+
// version to take (update-pill.js). The safety of that rests on three refusals, each pinned
|
|
5
|
+
// here by EXECUTING the real deploy-handoff.js: a message from any window but the opener, or
|
|
6
|
+
// from any origin but the project's registered address, is not heard; a version the hub's own
|
|
7
|
+
// preview would not move to is shown to the owner, never sent; and with no word from the hall
|
|
8
|
+
// the window waits for a press. It never moves anything on load by itself.
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { test, afterEach } from 'node:test';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import vm from 'node:vm';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { realOTB } from './helpers.mjs';
|
|
17
|
+
|
|
18
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
const require_ = createRequire(import.meta.url);
|
|
20
|
+
const FILE = path.join(ROOT, 'modules/hall-ui/public/deploy-handoff.js');
|
|
21
|
+
const SRC = fs.readFileSync(FILE, 'utf8');
|
|
22
|
+
const H = require_(FILE);
|
|
23
|
+
|
|
24
|
+
const PREVIEW = { instance_id: '14', slug: 'toast', reading: 'ok', behind: true, recommended_version: '1.21.30', viewer_may_move: true, open_intent: null };
|
|
25
|
+
const OPENER = {};
|
|
26
|
+
const GO = { tag: 'bongos-update', step: 'go', to: '1.21.30' };
|
|
27
|
+
const base = { data: GO, source: OPENER, opener: OPENER, origin: 'https://toast.cloudbongos.com', allowed: 'https://toast.cloudbongos.com', preview: PREVIEW };
|
|
28
|
+
|
|
29
|
+
// ── the rule ──────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
test('the one message that may start the move', () => {
|
|
32
|
+
assert.equal(H.judgeGo(base), null);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('a message from any window but the one that opened this is not heard', () => {
|
|
36
|
+
assert.equal(H.judgeGo({ ...base, source: {} }), 'not_opener');
|
|
37
|
+
assert.equal(H.judgeGo({ ...base, opener: null }), 'not_opener');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('a message from any origin but the project\'s own address is not heard', () => {
|
|
41
|
+
assert.equal(H.judgeGo({ ...base, origin: 'https://evil.example' }), 'wrong_origin');
|
|
42
|
+
assert.equal(H.judgeGo({ ...base, origin: 'https://toast.cloudbongos.com.evil.example' }), 'wrong_origin');
|
|
43
|
+
assert.equal(H.judgeGo({ ...base, origin: 'http://toast.cloudbongos.com' }), 'wrong_origin', 'the scheme counts');
|
|
44
|
+
assert.equal(H.judgeGo({ ...base, allowed: null }), 'wrong_origin', 'a project with no address can be asked by nobody');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('only the version the hub itself would move to is sent', () => {
|
|
48
|
+
assert.equal(H.judgeGo({ ...base, data: { ...GO, to: '1.21.31' } }), 'wrong_version');
|
|
49
|
+
assert.equal(H.judgeGo({ ...base, data: { ...GO, to: undefined } }), 'wrong_version');
|
|
50
|
+
assert.equal(H.judgeGo({ ...base, data: { ...GO, to: '1.21.30 ' } }), 'wrong_version');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('nothing to move, a viewer who may not, or a run already open: refused', () => {
|
|
54
|
+
assert.equal(H.judgeGo({ ...base, preview: { ...PREVIEW, behind: false } }), 'nothing_to_move');
|
|
55
|
+
assert.equal(H.judgeGo({ ...base, preview: { ...PREVIEW, reading: 'no_answer' } }), 'nothing_to_move');
|
|
56
|
+
assert.equal(H.judgeGo({ ...base, preview: { ...PREVIEW, viewer_may_move: false } }), 'not_allowed');
|
|
57
|
+
assert.equal(H.judgeGo({ ...base, preview: { ...PREVIEW, open_intent: { action: 'restart' } } }), 'busy');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('another page\'s message is not this protocol', () => {
|
|
61
|
+
assert.equal(H.judgeGo({ ...base, data: { ...GO, tag: 'other' } }), 'not_go');
|
|
62
|
+
assert.equal(H.judgeGo({ ...base, data: 'go' }), 'not_go');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('the allowed origin is the project\'s registered address, and nothing that is not a host', () => {
|
|
66
|
+
assert.equal(H.projectOrigin({ domain: 'Toast.CloudBongos.com' }, 'https:'), 'https://toast.cloudbongos.com');
|
|
67
|
+
assert.equal(H.projectOrigin({ domain: 'toast.cloudbongos.com' }, 'http:'), 'http://toast.cloudbongos.com', 'http only on a harness served over http');
|
|
68
|
+
for (const domain of [null, '', 'localhost', 'a b.com', 'evil.com/path', 'evil.com?x', 'https://evil.com', '-x.com']) {
|
|
69
|
+
assert.equal(H.projectOrigin({ domain }, 'https:'), null, JSON.stringify(domain));
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('the window is the handoff only when asked for it', () => {
|
|
74
|
+
assert.equal(H.wanted({ search: '?project=toast&handoff=1' }), true);
|
|
75
|
+
assert.equal(H.wanted({ search: '?handoff=1' }), true);
|
|
76
|
+
assert.equal(H.wanted({ search: '?project=toast' }), false);
|
|
77
|
+
assert.equal(H.wanted({ search: '?handoff=10' }), false);
|
|
78
|
+
assert.equal(H.wanted(null), false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ── the page, run for real ────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
const armed = new Set();
|
|
84
|
+
afterEach(() => { for (const id of armed) clearTimeout(id); armed.clear(); });
|
|
85
|
+
const settle = (ms = 0) => new Promise((r) => setTimeout(r, ms));
|
|
86
|
+
const plain = (x) => JSON.parse(JSON.stringify(x));
|
|
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' }] } = {}) {
|
|
89
|
+
const told = [];
|
|
90
|
+
const requests = [];
|
|
91
|
+
const listeners = {};
|
|
92
|
+
let closed = false;
|
|
93
|
+
const main = { _html: '', set innerHTML(v) { this._html = String(v); }, get innerHTML() { return this._html; } };
|
|
94
|
+
const bodyEl = { _html: '', set innerHTML(v) { this._html = String(v); }, get innerHTML() { return this._html; } };
|
|
95
|
+
const buttons = {};
|
|
96
|
+
const documentObj = {
|
|
97
|
+
documentElement: { setAttribute() {} },
|
|
98
|
+
getElementById(id) {
|
|
99
|
+
if (id === 'hall-main') return main;
|
|
100
|
+
if (id === 'ho-body') return bodyEl;
|
|
101
|
+
if (!bodyEl._html.includes(`id="${id}"`)) return null;
|
|
102
|
+
return (buttons[id] ||= { _on: {}, addEventListener(ev, fn) { this._on[ev] = fn; } });
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
if (opener) opener.postMessage = (msg, origin) => told.push({ msg, origin });
|
|
106
|
+
const windowObj = {
|
|
107
|
+
OTB: realOTB(), document: documentObj, opener, __handoffHearMs: 30,
|
|
108
|
+
location: { protocol: 'https:', pathname: '/deploy', search: '?project=toast&handoff=1', assign() {} },
|
|
109
|
+
addEventListener: (ev, fn) => { (listeners[ev] ||= []).push(fn); },
|
|
110
|
+
setTimeout: (fn, ms) => { const id = setTimeout(fn, ms); armed.add(id); return id; },
|
|
111
|
+
close() { closed = true; },
|
|
112
|
+
};
|
|
113
|
+
const api = {
|
|
114
|
+
request: (method, p, opts) => {
|
|
115
|
+
requests.push({ method, path: p, body: opts && opts.body });
|
|
116
|
+
if (p === '/api/bongos/provisioning/fleet') return Promise.resolve({ ok: true, status: 200, data: { instances: rows } });
|
|
117
|
+
if (method === 'GET' && /core-upgrade$/.test(p)) return Promise.resolve({ ok: true, status: 200, data: { ok: true, preview } });
|
|
118
|
+
if (method === 'POST') return Promise.resolve(post);
|
|
119
|
+
return Promise.resolve({ ok: false, status: 404, data: null });
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
const sandbox = { window: windowObj, console, Promise, JSON };
|
|
123
|
+
sandbox.globalThis = sandbox;
|
|
124
|
+
vm.createContext(sandbox);
|
|
125
|
+
vm.runInContext(SRC, sandbox, { filename: 'deploy-handoff.js' });
|
|
126
|
+
windowObj.OTBDeployHandoff.run({ api });
|
|
127
|
+
return {
|
|
128
|
+
told, requests, body: bodyEl, buttons, closed: () => closed,
|
|
129
|
+
message: (data, { source = opener, origin = 'https://toast.cloudbongos.com' } = {}) => { for (const fn of listeners.message || []) fn({ data, source, origin }); },
|
|
130
|
+
posts: () => requests.filter((r) => r.method === 'POST'),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
test('it says "ready" only to the project\'s own address, and moves nothing until it hears', async () => {
|
|
135
|
+
const b = boot();
|
|
136
|
+
await settle(5);
|
|
137
|
+
assert.deepEqual(plain(b.told), [{ msg: { tag: 'bongos-update', step: 'ready' }, origin: 'https://toast.cloudbongos.com' }]);
|
|
138
|
+
assert.equal(b.posts().length, 0, 'nothing moves on load');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('the go from the project\'s own hall moves it to that version, says so, and closes', async () => {
|
|
142
|
+
const b = boot();
|
|
143
|
+
await settle(5);
|
|
144
|
+
b.message(GO);
|
|
145
|
+
await settle(5);
|
|
146
|
+
assert.equal(b.posts().length, 1);
|
|
147
|
+
assert.equal(b.posts()[0].path, '/api/bongos/provisioning/instances/14/core-upgrade');
|
|
148
|
+
assert.deepEqual(plain(b.posts()[0].body), { to: '1.21.30' });
|
|
149
|
+
assert.deepEqual(plain(b.told[1]), { msg: { tag: 'bongos-update', step: 'started', to: '1.21.30' }, origin: 'https://toast.cloudbongos.com' });
|
|
150
|
+
assert.match(b.body.innerHTML, /The update has started/);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('a go from another site, or another window, moves nothing', async () => {
|
|
154
|
+
const b = boot();
|
|
155
|
+
await settle(5);
|
|
156
|
+
b.message(GO, { origin: 'https://evil.example' });
|
|
157
|
+
b.message(GO, { source: {} });
|
|
158
|
+
await settle(5);
|
|
159
|
+
assert.equal(b.posts().length, 0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('a go naming another version is shown to the owner, never sent', async () => {
|
|
163
|
+
const b = boot();
|
|
164
|
+
await settle(5);
|
|
165
|
+
b.message({ ...GO, to: '1.21.31' });
|
|
166
|
+
await settle(5);
|
|
167
|
+
assert.equal(b.posts().length, 0);
|
|
168
|
+
assert.match(b.body.innerHTML, /A different version is ready/);
|
|
169
|
+
assert.match(b.body.innerHTML, /Update to 1\.21\.30/, 'the press names the hub\'s own version');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('no word from the hall: one press, naming the version, and only the press moves it', async () => {
|
|
173
|
+
const b = boot();
|
|
174
|
+
await settle(60);
|
|
175
|
+
assert.equal(b.posts().length, 0, 'waiting is not consent');
|
|
176
|
+
assert.match(b.body.innerHTML, /id="ho-go"/);
|
|
177
|
+
b.buttons['ho-go']._on.click();
|
|
178
|
+
await settle(5);
|
|
179
|
+
assert.deepEqual(plain(b.posts()[0].body), { to: '1.21.30' });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('opened by hand, with no hall behind it: the press, never an automatic move', async () => {
|
|
183
|
+
const b = boot({ opener: null });
|
|
184
|
+
await settle(5);
|
|
185
|
+
assert.equal(b.posts().length, 0);
|
|
186
|
+
assert.match(b.body.innerHTML, /Update to 1\.21\.30/);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('a refusal is said in the server\'s words, here and to the hall', async () => {
|
|
190
|
+
const b = boot({ post: { ok: false, status: 409, data: { error: { code: 'intent_conflict', message: 'A restart is running for this project.' } } } });
|
|
191
|
+
await settle(5);
|
|
192
|
+
b.message(GO);
|
|
193
|
+
await settle(5);
|
|
194
|
+
assert.match(b.body.innerHTML, /A restart is running for this project\./);
|
|
195
|
+
assert.deepEqual(plain(b.told[1]), { msg: { tag: 'bongos-update', step: 'refused', message: 'A restart is running for this project.' }, origin: 'https://toast.cloudbongos.com' });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('a project already current, or one with no address, says so and moves nothing', async () => {
|
|
199
|
+
const cur = boot({ preview: { ...PREVIEW, behind: false } });
|
|
200
|
+
await settle(5);
|
|
201
|
+
assert.match(cur.body.innerHTML, /already runs the newest version/);
|
|
202
|
+
const none = boot({ rows: [{ id: '14', slug: 'toast', domain: null }] });
|
|
203
|
+
await settle(60);
|
|
204
|
+
assert.equal(none.told.length, 0, 'nobody to say "ready" to');
|
|
205
|
+
assert.match(none.body.innerHTML, /id="ho-go"/, 'the press is still there');
|
|
206
|
+
});
|
|
@@ -328,6 +328,21 @@ const ADDED_SINCE_R101 = [
|
|
|
328
328
|
why: 'split out of the protected ship.js: the ship-mode step every ship runs '
|
|
329
329
|
+ 'before it lands, so an edit here could change how work reaches main.',
|
|
330
330
|
},
|
|
331
|
+
// task 1004546: the Autobongos self-edit wall. The grade of task 1004540 refused a
|
|
332
|
+
// version whose guard lived in unprotected shared files, so it moved into these two
|
|
333
|
+
// small ones and they sit beside the rest of the publish pipeline.
|
|
334
|
+
{
|
|
335
|
+
after: 'modules/lifecycle/publish-reconciler.js',
|
|
336
|
+
glob: 'modules/lifecycle/runner-self.js',
|
|
337
|
+
why: 'the one list of files an unattended runner may never change, and the shape of '
|
|
338
|
+
+ "a runner's claim; an edit here could let a runner loosen its own fence.",
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
after: 'modules/lifecycle/runner-self.js',
|
|
342
|
+
glob: 'modules/lifecycle/runner-self-guard.js',
|
|
343
|
+
why: "the server's refusal to publish a runner's change to its own supervisor, safety "
|
|
344
|
+
+ 'checks or kill switch, read against the pull request\'s real files.',
|
|
345
|
+
},
|
|
331
346
|
];
|
|
332
347
|
|
|
333
348
|
// Surfaces RENAMED since R101 — still protected, under a new path. Distinct from
|
|
@@ -77,7 +77,7 @@ test('the body: a door, a loading line, the inactive and unreadable lines, then
|
|
|
77
77
|
assert.match(MAIN, /<h1>connections<\/h1>/);
|
|
78
78
|
assert.match(MAIN, /it grants no rank and no access to anything/, 'a connection is never authority (ADR 0016, ADR 0202)');
|
|
79
79
|
assert.match(MAIN, /<section class="aDoor" id="door" hidden aria-labelledby="door-h">[\s\S]*?href="\/api\/bongos\/auth\/web\/start\?return=\/connections&repos=1"/, 'signed out, the door returns here');
|
|
80
|
-
assert.match(MAIN, /<p class="loading" id="loading">/);
|
|
80
|
+
assert.match(MAIN, /<p class="loading loading-mark loading-mark--page" id="loading" role="status">/); // the Bongos-mark loader (task 1005053)
|
|
81
81
|
assert.match(MAIN, /<p class="mineEmpty" id="inactive" hidden>/);
|
|
82
82
|
assert.match(MAIN, /<p class="projMsg err" id="readFailed" role="alert" hidden>/);
|
|
83
83
|
assert.match(MAIN, /<p class="projMsg" id="connMsg" role="status" aria-live="polite" hidden><\/p>/);
|
|
@@ -56,7 +56,7 @@ test('the page wears the hub’s bar, with Invitations as the one current place;
|
|
|
56
56
|
test('the body: a door, a loading line, the four lists in order under one heading, the empty line, and the join field', () => {
|
|
57
57
|
assert.match(MAIN, /<h1>invitations<\/h1>/);
|
|
58
58
|
assert.match(MAIN, /<section class="aDoor" id="door" hidden aria-labelledby="door-h">[\s\S]*?href="\/api\/bongos\/auth\/web\/start\?return=\/invitations&repos=1"/, 'signed out, the door returns here');
|
|
59
|
-
assert.match(MAIN, /<p class="loading" id="loading">/);
|
|
59
|
+
assert.match(MAIN, /<p class="loading loading-mark loading-mark--page" id="loading" role="status">/); // the Bongos-mark loader (task 1005053)
|
|
60
60
|
const section = (MAIN.match(/<section id="mineInvites" aria-labelledby="mineInvitesH">[\s\S]*?<\/section>/) || [''])[0];
|
|
61
61
|
assert.ok(section, 'the invitations section ships as markup, never hidden on its own: it is the page');
|
|
62
62
|
assert.match(section, /<p class="mineEmpty" id="invitesNone" hidden>[^<]+<\/p>/, 'one empty line, down until the reads have answered');
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// tests/loading_mark.mjs — the Bongos-mark loader on the apex pages and the status page
|
|
2
|
+
// (task 1005053, goal 1000095; the hall's own is task 1005043).
|
|
3
|
+
//
|
|
4
|
+
// Owner ask, 2026-11-03: a smooth load screen with the Bongos logo anywhere the
|
|
5
|
+
// software waits for data, in the spirit of monday.com's Vibe data loaders. The
|
|
6
|
+
// apex pages (cosmos.css) and the status page (its own sheet) each carry the same
|
|
7
|
+
// CSS component, .loading-mark, in their own tokens; a page opts in by putting the
|
|
8
|
+
// class on the placeholder it already had, whose words stay as the caption. These
|
|
9
|
+
// cases read the real sheets and pages, per surface: the loader keeps DESIGN.md's
|
|
10
|
+
// motion and colour rules, its rest frame is the logo, and no waiting placeholder
|
|
11
|
+
// is left as a bare line of text.
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { test } from 'node:test';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import vm from 'node:vm';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
const LANDING = path.join(ROOT, 'modules', 'public-landing', 'public');
|
|
21
|
+
const STATUS = path.join(ROOT, 'modules', 'status-ui', 'public');
|
|
22
|
+
const read = (dir, f) => fs.readFileSync(path.join(dir, f), 'utf8').replace(/\r\n/g, '\n');
|
|
23
|
+
|
|
24
|
+
const COSMOS = read(LANDING, 'assets/cosmos.css');
|
|
25
|
+
const STATUS_CSS = read(STATUS, 'style.css');
|
|
26
|
+
const APEX_PAGES = ['account', 'community', 'connections', 'guild', 'invitations', 'projects'];
|
|
27
|
+
|
|
28
|
+
// The loader's section of a sheet: from its banner to the last of its keyframes.
|
|
29
|
+
function section(css, banner) {
|
|
30
|
+
const start = css.indexOf(banner);
|
|
31
|
+
assert.ok(start > -1, `the loader's banner is in the sheet: ${banner}`);
|
|
32
|
+
const end = css.indexOf('@keyframes loading-mark-breathe', start);
|
|
33
|
+
assert.ok(end > start, 'the loader section ends on its breath');
|
|
34
|
+
return css.slice(start, css.indexOf('}', css.indexOf('}', end) + 1) + 1);
|
|
35
|
+
}
|
|
36
|
+
const SURFACES = {
|
|
37
|
+
apex: { css: COSMOS, loader: section(COSMOS, '/* ── the loader (task 1005053') },
|
|
38
|
+
status: { css: STATUS_CSS, loader: section(STATUS_CSS, '/* ---------- The loader (task 1005053') },
|
|
39
|
+
};
|
|
40
|
+
const flat = (s) => s.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\s+/g, ' ');
|
|
41
|
+
const masksOf = (css) => [...css.matchAll(/url\("data:image\/svg\+xml,([^"]+)"\)/g)].map((m) => decodeURIComponent(m[1]));
|
|
42
|
+
function keyframes(css, name) {
|
|
43
|
+
const m = css.match(new RegExp(`@keyframes ${name} ?\\{`));
|
|
44
|
+
assert.ok(m, `@keyframes ${name} exists`);
|
|
45
|
+
let depth = 1, i = m.index + m[0].length;
|
|
46
|
+
for (; i < css.length && depth; i++) depth += css[i] === '{' ? 1 : css[i] === '}' ? -1 : 0;
|
|
47
|
+
return css.slice(m.index + m[0].length, i - 1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test('the apex loader is the bar\'s own Bongos mark, and the status loader is the same mark', () => {
|
|
51
|
+
// The apex bar draws the mark inline (account.html's .bongosMark); the loader's
|
|
52
|
+
// two masks carry exactly its arc, core and dot, so the loader IS the logo.
|
|
53
|
+
const bar = read(LANDING, 'account.html');
|
|
54
|
+
const arc = bar.match(/<path class="mr" d="([^"]+)"/)[1];
|
|
55
|
+
assert.match(bar, /<circle class="mp" cx="100" cy="100" r="55"/);
|
|
56
|
+
assert.match(bar, /<circle cx="156" cy="46" r="12" style="fill:var\(--accent\)"\/>/);
|
|
57
|
+
const [ring, dot] = masksOf(SURFACES.apex.loader);
|
|
58
|
+
assert.ok(ring.includes(`d='${arc}'`), 'the ring is the bar mark\'s arc');
|
|
59
|
+
assert.ok(ring.includes("<circle cx='100' cy='100' r='55'/>"), 'the core is the bar mark\'s core');
|
|
60
|
+
assert.ok(dot.includes("<circle cx='156' cy='46' r='12'/>"), 'the dot is the bar mark\'s dot');
|
|
61
|
+
// The status page has no Bongos mark of its own: its loader carries the apex's two
|
|
62
|
+
// masks byte for byte, so the two surfaces cannot drift into two logos.
|
|
63
|
+
assert.deepEqual(masksOf(SURFACES.status.loader), masksOf(SURFACES.apex.loader));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
for (const [name, { css, loader }] of Object.entries(SURFACES)) {
|
|
67
|
+
test(`${name}: it moves in CSS only, on transform and filter only, and rests on the logo`, () => {
|
|
68
|
+
for (const frame of ['loading-mark-arrive', 'loading-mark-turn', 'loading-mark-breathe']) {
|
|
69
|
+
const body = keyframes(css, frame);
|
|
70
|
+
const props = [...body.matchAll(/([a-z-]+)\s*:/g)].map((m) => m[1]);
|
|
71
|
+
assert.ok(props.length, `${frame} animates something`);
|
|
72
|
+
for (const p of props) assert.ok(['transform', 'filter'].includes(p), `${frame} animates ${p}, not transform or filter`);
|
|
73
|
+
}
|
|
74
|
+
// The turn has no `from`, so 0deg (the logo's own pose, the dot top right) is where
|
|
75
|
+
// it starts and where the Kill Switch leaves it; the arrival and the breath have no
|
|
76
|
+
// `to`, so their resting value is the element as styled, fully shown.
|
|
77
|
+
assert.match(keyframes(css, 'loading-mark-turn'), /^\s*to ?\{ ?transform: ?rotate\(1turn\); ?\}\s*$/);
|
|
78
|
+
assert.match(keyframes(css, 'loading-mark-arrive'), /^\s*from ?\{/);
|
|
79
|
+
assert.match(keyframes(css, 'loading-mark-breathe'), /^\s*from ?\{/);
|
|
80
|
+
// It arrives 240ms late, so a read that answers faster never shows it.
|
|
81
|
+
assert.match(flat(loader), /\.loading-mark ?\{[^}]*animation: ?loading-mark-arrive 500ms cubic-bezier\(\.16, ?1, ?\.3, ?1\) 240ms both;/);
|
|
82
|
+
// The Kill Switch stops every animation, the pseudo-elements included.
|
|
83
|
+
assert.match(flat(css), /@media \(prefers-reduced-motion: ?reduce\) ?\{ ?\*, ?\*::before, ?\*::after ?\{ ?animation: ?none !important; ?transition: ?none !important; ?\} ?\}/);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test(`${name}: every colour is a token; the black in the masks is alpha, not paint`, () => {
|
|
87
|
+
const css = flat(loader);
|
|
88
|
+
assert.doesNotMatch(css, /#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\(/, 'no colour literal');
|
|
89
|
+
const paints = [...css.matchAll(/(?:background|color): ?([^;]+);/g)].map((m) => m[1].trim());
|
|
90
|
+
assert.equal(paints.length, 3, 'the ink, the dot and the caption are painted');
|
|
91
|
+
for (const p of paints) assert.match(p, /^(var\(--[a-z-]+\)|color-mix\(in srgb, ?var\(--ink\) 30%, ?var\(--bg-card\)\))$/, `${p} is a token or a mix of one`);
|
|
92
|
+
// The one accent is the dot: the live thing on a waiting page (the One Warm Thing Rule).
|
|
93
|
+
assert.equal((css.match(/var\(--accent\)/g) || []).length, 1, 'the accent paints the dot alone');
|
|
94
|
+
const masks = masksOf(loader);
|
|
95
|
+
assert.equal(masks.length, 2, 'two masks: the ring and core, and the dot');
|
|
96
|
+
for (const m of masks) assert.doesNotMatch(m.replace(/stroke='black'|fill='none'/g, ''), /fill='|stroke='|#/, 'a mask carries no colour of its own');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test(`${name}: the dot shares the ring's grid cell, so no page padding can knock it off`, () => {
|
|
100
|
+
const css = flat(loader);
|
|
101
|
+
assert.match(css, /\.loading-mark ?\{[^}]*display: ?grid;/);
|
|
102
|
+
assert.match(css, /\.loading-mark::before, ?\.loading-mark::after ?\{[^}]*grid-area: ?1 ?\/ ?1;/);
|
|
103
|
+
assert.doesNotMatch(css, /position: ?absolute|top: ?|left: ?/, 'nothing is positioned by hand');
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
test('apex: three sizes, and a hidden placeholder stays hidden', () => {
|
|
108
|
+
const css = flat(SURFACES.apex.loader);
|
|
109
|
+
assert.match(css, /\.loading-mark--page ?\{[^}]*--lm-size: ?64px;/);
|
|
110
|
+
assert.match(css, /\.loading-mark--inline ?\{[^}]*--lm-size: ?16px;[^}]*grid-template-columns: ?var\(--lm-size\) minmax\(0, ?1fr\);/);
|
|
111
|
+
// The loader is display: grid, which outranks the browser's own [hidden]; the
|
|
112
|
+
// apex pages put a placeholder away with .hidden, so cosmos's rule must win.
|
|
113
|
+
assert.match(COSMOS, /\[hidden\]\{display:none !important;\}/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('apex: every waiting placeholder wears the loader, in the markup and as the scripts write it', () => {
|
|
117
|
+
// A status line rewritten IN PLACE (its words become the answer) keeps its plain
|
|
118
|
+
// words: the loader belongs to a placeholder that is hidden or replaced when the
|
|
119
|
+
// data lands. Each one is named here with why.
|
|
120
|
+
const IN_PLACE = new Set([
|
|
121
|
+
"community.html:say('Searching…')", // the search's status line, rewritten with the count
|
|
122
|
+
"projects.html:hint('h-renderKey', 'Checking with Render…')", // the Render key's hint, rewritten with the answer
|
|
123
|
+
"projects.html:say('Reading your organisations from GitHub…')", // the org picker's status line
|
|
124
|
+
"projects.html:say('Checking with GitHub…')", // the same line
|
|
125
|
+
"projects.html:inviteSay(id, 'Finding your project’s invite page…'", // the invite box's message, after a press
|
|
126
|
+
"projects.html:inviteSay(id, 'Finding the guild’s members…'", // the same, for a guild
|
|
127
|
+
]);
|
|
128
|
+
const WAIT_WORD = '(?:Loading|Reading|Opening|Checking|Finding|Searching|Looking|Fetching)';
|
|
129
|
+
const ELEMENT = new RegExp(`<(p|div|li|span|td)\\b([^>]*)>\\s*${WAIT_WORD}\\b[^<]*…\\s*<\\/\\1>`, 'g');
|
|
130
|
+
const LITERAL = new RegExp(`[a-zA-Z]+\\((?:id, )?'${WAIT_WORD}[^']*…'`, 'g');
|
|
131
|
+
const misses = [];
|
|
132
|
+
for (const page of APEX_PAGES) {
|
|
133
|
+
const html = read(LANDING, `${page}.html`);
|
|
134
|
+
for (const m of html.matchAll(ELEMENT)) {
|
|
135
|
+
if (!/\bloading-mark\b/.test(m[2])) misses.push(`${page}.html: ${m[0].slice(0, 100)}`);
|
|
136
|
+
}
|
|
137
|
+
for (const line of html.split('\n')) {
|
|
138
|
+
for (const m of line.matchAll(LITERAL)) {
|
|
139
|
+
if (/loading-mark/.test(line) || /pickNote\('[^']*', true\)/.test(line)) continue;
|
|
140
|
+
const key = `${page}.html:${m[0]}`;
|
|
141
|
+
if (!IN_PLACE.has(key) && !IN_PLACE.has(`${key})`)) misses.push(key);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
assert.deepEqual(misses, [], 'every waiting placeholder wears the loader');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('apex: each page waits with the size that fits it', () => {
|
|
149
|
+
const html = Object.fromEntries(APEX_PAGES.map((p) => [p, read(LANDING, `${p}.html`)]));
|
|
150
|
+
// a page with nothing else to show yet: the page size
|
|
151
|
+
for (const [page, words] of [['account', 'Reading your account…'], ['connections', 'Reading your connections…'], ['guild', 'Reading this guild…'], ['invitations', 'Reading your invitations…']]) {
|
|
152
|
+
assert.ok(html[page].includes(`<p class="loading loading-mark loading-mark--page" id="loading" role="status">${words}</p>`), `${page} waits on the page loader`);
|
|
153
|
+
}
|
|
154
|
+
assert.ok(html.projects.includes('<div class="loading loading-mark loading-mark--page" id="loading" role="status">Reading your projects…</div>'));
|
|
155
|
+
assert.ok(html.projects.includes('<div class="loading loading-mark loading-mark--page" id="mLoading" role="status">Reading the project…</div>'));
|
|
156
|
+
// a section of a page: the default
|
|
157
|
+
for (const id of ['homeLoading', 'boardLoading', 'weekLoading', 'recruitLoading']) {
|
|
158
|
+
assert.match(html.community, new RegExp(`<p class="commMsg loading-mark" id="${id}" role="status">`), `community's ${id}`);
|
|
159
|
+
}
|
|
160
|
+
// a line in a band or a list: inline. The manage page's thirteen sections each wait on
|
|
161
|
+
// their own read, so a mark the height of the line keeps the page from jumping.
|
|
162
|
+
assert.equal((html.projects.match(/><p class="finePrint loading-mark loading-mark--inline" role="status">(?:Reading the project…|Reading the project’s settings…|Checking the repository on GitHub…)<\/p><\/div>/g) || []).length, 14);
|
|
163
|
+
assert.ok(html.account.includes('<p class="aWhat loading-mark loading-mark--inline" id="waysLoading" role="status">'));
|
|
164
|
+
assert.ok(html.account.includes('<p class="aWhat loading-mark loading-mark--inline" id="buildingLoading" role="status">'));
|
|
165
|
+
assert.ok(html.projects.includes('<div id="ghBusy" class="hint loading-mark loading-mark--inline" role="status" hidden>'));
|
|
166
|
+
// the scripts write them back the same way, and the picker's first open wears it too
|
|
167
|
+
for (const call of ['repoVisMsg(', 'visMsg(', 'joinMsg(', 'doorMsg(', 'envMsg(']) {
|
|
168
|
+
assert.ok(html.projects.includes(`${call}'<p class="finePrint loading-mark loading-mark--inline" role="status">`), `${call} writes the loader`);
|
|
169
|
+
}
|
|
170
|
+
for (const call of ['visRuns(', 'joinRuns(']) {
|
|
171
|
+
assert.ok(html.projects.includes(`${call}'<span class="loading-mark loading-mark--inline" role="status">Checking what the project is running…</span>');`), `${call} writes the loader`);
|
|
172
|
+
}
|
|
173
|
+
assert.match(html.projects, /wait\.className = 'finePrint loading-mark loading-mark--inline'; wait\.setAttribute\('role', 'status'\);/);
|
|
174
|
+
assert.match(html.invitations, /li\.className = 'joinList__note' \+ \(waiting \? ' loading-mark loading-mark--inline' : ''\);/);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('status: its two waiting tablets wear the loader, and a failed or empty read stops it', () => {
|
|
178
|
+
const html = read(STATUS, 'index.html');
|
|
179
|
+
assert.ok(html.includes('<div id="versions" class="versions">\n <p class="empty loading-mark" role="status">The masons are at their tools…</p>'));
|
|
180
|
+
assert.ok(html.includes('<div id="world-snapshot" class="world-snapshot">\n <p class="empty loading-mark" role="status">The cartographers are still at their survey…</p>'));
|
|
181
|
+
// the header's "Tablet being consulted…" is rewritten in place before any read
|
|
182
|
+
// starts (stampRefreshTime), so it is never a wait and keeps its plain words
|
|
183
|
+
assert.match(html, /<span class="herm__updated" id="last-updated">Tablet being consulted…<\/span>/);
|
|
184
|
+
// The Kill Switch is the last thing in the status sheet, so nothing can outrank it.
|
|
185
|
+
assert.match(STATUS_CSS.trimEnd(), /@media \(prefers-reduced-motion: reduce\) \{\n {2}\*, \*::before, \*::after \{ animation: none !important; transition: none !important; \}\n\}$/);
|
|
186
|
+
|
|
187
|
+
const js = read(STATUS, 'status.js');
|
|
188
|
+
// the three ways a tablet can end its wait without data each stop the loader
|
|
189
|
+
assert.match(js, /\} catch \(_\) \{\n {4}stopWaiting\(els\.worldSnapshot\);\n {4}return;\n {2}\}/);
|
|
190
|
+
assert.match(js, /if \(snapshots\.length === 0\) \{ stopWaiting\(els\.worldSnapshot\); return; \}/);
|
|
191
|
+
assert.match(js, /loadVersions\(\)\.catch\(\(e\) => \{ stopWaiting\(els\.versions\); console\.error\('versions', e\); \}\)/);
|
|
192
|
+
// ...and stopWaiting itself, run: it takes the class off and leaves the words alone.
|
|
193
|
+
const src = js.match(/function stopWaiting\(el\) \{[\s\S]*?\n\}/)[0];
|
|
194
|
+
const removed = [];
|
|
195
|
+
const wait = { textContent: 'The masons are at their tools…', classList: { remove: (c) => removed.push(c) } };
|
|
196
|
+
const host = { querySelectorAll: (sel) => (sel === '.loading-mark' ? [wait] : []) };
|
|
197
|
+
const ctx = vm.createContext({});
|
|
198
|
+
vm.runInContext(`${src}; this.stopWaiting = stopWaiting;`, ctx);
|
|
199
|
+
ctx.stopWaiting(host);
|
|
200
|
+
ctx.stopWaiting(null);
|
|
201
|
+
assert.deepEqual(removed, ['loading-mark']);
|
|
202
|
+
assert.equal(wait.textContent, 'The masons are at their tools…');
|
|
203
|
+
});
|
|
@@ -93,8 +93,9 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
|
|
|
93
93
|
// says a held core version was tried and rolled back; task 1004731: the one-time
|
|
94
94
|
// "Upgraded to core X" notice after a move; task 1004826: Standing is Accomplishments;
|
|
95
95
|
// task 1004846: the new logo (its bump landed late, in task 1004852); task 1004938: the
|
|
96
|
-
// Market Watch settings nav item and its fuel glyph
|
|
97
|
-
|
|
96
|
+
// Market Watch settings nav item and its fuel glyph; task 1005041: the Update pill replaced the
|
|
97
|
+
// core-update banner.
|
|
98
|
+
const SHELL_SHA = '9e32caf8aa17dd36';
|
|
98
99
|
|
|
99
100
|
test('shell.js has not changed without its cache-bust being bumped', () => {
|
|
100
101
|
const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
|
|
@@ -345,11 +345,15 @@ test('read only when its group opens — most visits to Settings are for somethi
|
|
|
345
345
|
|
|
346
346
|
const answerWith = (over) => ok({ ...FIXTURE, ...over, update: { ...FIXTURE.update, ...(over.update || {}) } });
|
|
347
347
|
|
|
348
|
+
// task 1005041: the button runs the Update pill's in-place flow, so it no longer reads as a
|
|
349
|
+
// trip off-site; its href stays the hub's door only for a page whose pill script did not load,
|
|
350
|
+
// and the hint names the hub whose small window opens for a moment.
|
|
348
351
|
test('a hosted project\'s button names the hub it opens; a self-hosted one names the command', async () => {
|
|
349
352
|
const hub = bootPanel(() => answerWith({ door: { kind: 'hub', url: 'https://cloudbongos.com/deploy?project=mercury' } }));
|
|
350
353
|
await hub.settle();
|
|
351
|
-
assert.match(hub.body(), /href="https:\/\/cloudbongos\.com\/deploy\?project=mercury">Update to [\d.]
|
|
352
|
-
assert.
|
|
354
|
+
assert.match(hub.body(), /href="https:\/\/cloudbongos\.com\/deploy\?project=mercury">Update to [\d.]+</);
|
|
355
|
+
assert.doesNotMatch(hub.body(), /↗/, 'it updates in place; it is not a link off-site any more');
|
|
356
|
+
assert.match(hub.body(), /A small window from cloudbongos\.com opens for a moment/);
|
|
353
357
|
|
|
354
358
|
const cli = bootPanel(() => answerWith({ door: { kind: 'cli', url: null } }));
|
|
355
359
|
await cli.settle();
|