@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
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
// tests/update_pill.mjs — the Update pill: a newer core, taken in place from the hall (task 1005041).
|
|
2
|
+
//
|
|
3
|
+
// The owner, 2026-11-03: the upgrade was "way too obstructing and not obvious what to do"; it
|
|
4
|
+
// should feel like the Claude app's "restart to update". The pill replaced the full-width
|
|
5
|
+
// banner (task 1004045) and the trip to the deploy door, so the rules the banner's suite
|
|
6
|
+
// pinned are re-pinned here against the pill: said only from an answer, only to the viewers
|
|
7
|
+
// the door would admit, never a link to the page you are on, and never "update" to a version
|
|
8
|
+
// that was just rolled back. These EXECUTE the real shell.js and the real update-pill.js in a
|
|
9
|
+
// DOM stub (core_update_banner.mjs's reason: the 1003159 grade's guards that read source text).
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { test, afterEach } from 'node:test';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import vm from 'node:vm';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { realOTB } from './helpers.mjs';
|
|
18
|
+
|
|
19
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
const require_ = createRequire(import.meta.url);
|
|
21
|
+
const PILL_PATH = path.join(ROOT, 'modules/hall-ui/public/update-pill.js');
|
|
22
|
+
const SHELL = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/shell.js'), 'utf8');
|
|
23
|
+
const PILL = fs.readFileSync(PILL_PATH, 'utf8');
|
|
24
|
+
const P = require_(PILL_PATH);
|
|
25
|
+
|
|
26
|
+
const BEHIND = { running: '1.19.861', latest: '1.19.871', behind: true, reading: 'ok' };
|
|
27
|
+
const HUB = 'https://cloudbongos.com/deploy?project=hoodify-finance';
|
|
28
|
+
const SW = (door, extra = {}) => ({
|
|
29
|
+
ok: true,
|
|
30
|
+
update: {
|
|
31
|
+
running: '1.19.861', offered: '1.19.871', reading: 'ok',
|
|
32
|
+
whats_new: { ok: true, count: 2, more: 0, versions: [
|
|
33
|
+
{ version: '1.19.871', entries: [{ id: '1', text: 'Faster boards — and fewer clicks' }, { id: '2', text: 'A fix' }] },
|
|
34
|
+
{ version: '1.19.870', entries: [{ id: '3', text: 'Clearer errors' }] },
|
|
35
|
+
] },
|
|
36
|
+
},
|
|
37
|
+
door, can_update: true, ask: { available: false }, held: null, history: null, ...extra,
|
|
38
|
+
});
|
|
39
|
+
// objects built inside the vm carry its prototypes, so compare their JSON
|
|
40
|
+
const plain = (x) => JSON.parse(JSON.stringify(x));
|
|
41
|
+
const settle = (ms = 0) => new Promise((r) => setTimeout(r, ms));
|
|
42
|
+
// Every timer a booted page arms is cleared after its case: an update left "running" polls
|
|
43
|
+
// forever, and would hold the process open.
|
|
44
|
+
const armed = new Set();
|
|
45
|
+
const timers = {
|
|
46
|
+
setTimeout: (fn, ms) => { const id = setTimeout(() => { armed.delete(id); fn(); }, ms); armed.add(id); return id; },
|
|
47
|
+
clearTimeout: (id) => { armed.delete(id); clearTimeout(id); },
|
|
48
|
+
setInterval: (fn, ms) => { const id = setInterval(fn, ms); armed.add(id); return id; },
|
|
49
|
+
clearInterval: (id) => { armed.delete(id); clearInterval(id); },
|
|
50
|
+
};
|
|
51
|
+
afterEach(() => { for (const id of armed) { clearTimeout(id); clearInterval(id); } armed.clear(); });
|
|
52
|
+
|
|
53
|
+
// ── the pure half ─────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
test('the pill is said only from an answer that is behind', () => {
|
|
56
|
+
assert.equal(P.pillState({ update: BEHIND }), 'ready');
|
|
57
|
+
assert.equal(P.pillState({ update: { ...BEHIND, reading: 'no_answer' } }), 'none', 'no answer is not "update ready"');
|
|
58
|
+
assert.equal(P.pillState({ update: { ...BEHIND, behind: false } }), 'none');
|
|
59
|
+
assert.equal(P.pillState({ update: { ...BEHIND, latest: 'next' } }), 'none', 'only a real version is offered');
|
|
60
|
+
assert.equal(P.pillState({}), 'none');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('a version this project already tried and rolled back is "held", never offered again', () => {
|
|
64
|
+
assert.equal(P.pillState({ update: BEHIND, held: { version: '1.19.871' } }), 'held');
|
|
65
|
+
assert.equal(P.pillState({ update: BEHIND, held: { version: '1.19.800' } }), 'ready', 'an older hold leaves the newer release offered');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('a remembered run is "running" until the server runs its version, then it has landed', () => {
|
|
69
|
+
const run = { to: '1.19.871', from: '1.19.861', at: 1 };
|
|
70
|
+
assert.equal(P.pillState({ update: BEHIND, run }), 'running');
|
|
71
|
+
assert.equal(P.pillState({ update: { ...BEHIND, running: '1.19.871' }, run }), 'landed');
|
|
72
|
+
// mid-restart the read can fail entirely; the run still speaks
|
|
73
|
+
assert.equal(P.pillState({ update: null, run }), 'running');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('progress is read off /version and the hold, never guessed', () => {
|
|
77
|
+
const run = { to: '1.19.871', from: '1.19.861', at: 1000 };
|
|
78
|
+
assert.equal(P.stageOf(run, { version: '1.19.871' }, 2000), 'done');
|
|
79
|
+
assert.equal(P.stageOf(run, { version: '1.19.861', held: '1.19.871' }, 2000), 'failed');
|
|
80
|
+
assert.equal(P.stageOf(run, { version: '1.19.861' }, 2000), 'starting');
|
|
81
|
+
assert.equal(P.stageOf(run, { version: null }, 2000), 'restarting', 'no answer is the restart');
|
|
82
|
+
assert.equal(P.stageOf({ ...run, down: true }, { version: '1.19.861' }, 2000), 'restarting', 'back up on the old version is not "starting" again');
|
|
83
|
+
assert.equal(P.stageOf(run, { version: '1.19.861' }, 1000 + P.SLOW_MS + 1), 'slow');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('what the update brings: the first lines, dashes as commas (V4), and an honest count', () => {
|
|
87
|
+
const h = P.highlights(SW({}).update.whats_new, 2);
|
|
88
|
+
assert.deepEqual(h.lines, ['Faster boards, and fewer clicks', 'A fix']);
|
|
89
|
+
assert.equal(h.count, 3);
|
|
90
|
+
assert.equal(h.more, false);
|
|
91
|
+
const long = P.highlights({ versions: [{ entries: [{ text: 'word '.repeat(60) }] }], more: 4 }, 3);
|
|
92
|
+
assert.ok(long.lines[0].length <= 120 && long.lines[0].endsWith('…'), 'a long line is clipped at a word');
|
|
93
|
+
assert.equal(long.more, true, 'older versions left out make the count a floor');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('the door decides the button: local, hub (by its origin), ask, the command, or none', () => {
|
|
97
|
+
assert.deepEqual(P.doorPlan(SW({ kind: 'local', url: '/deploy' })), { kind: 'local' });
|
|
98
|
+
assert.deepEqual(P.doorPlan(SW({ kind: 'hub', url: HUB })), { kind: 'hub', url: HUB, origin: 'https://cloudbongos.com' });
|
|
99
|
+
assert.equal(P.doorPlan(SW({ kind: 'hub', url: 'https://cloudbongos.com/elsewhere' })).kind, 'none', 'only a hub /deploy address');
|
|
100
|
+
assert.equal(P.doorPlan(SW({ kind: 'hub', url: 'javascript:alert(1)' })).kind, 'none');
|
|
101
|
+
assert.equal(P.doorPlan(SW({ kind: 'cli', url: null })).kind, 'cli');
|
|
102
|
+
assert.equal(P.doorPlan(SW({ kind: 'cli', url: null }, { ask: { available: true } })).kind, 'ask');
|
|
103
|
+
assert.equal(P.doorPlan(SW({ kind: 'local', url: '/deploy' }, { can_update: 'yes' })).kind, 'none', 'can_update is strictly true');
|
|
104
|
+
assert.equal(P.doorPlan(null).kind, 'none');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('a remembered run expires, and storage that throws costs only the memory', () => {
|
|
108
|
+
const box = new Map();
|
|
109
|
+
const storage = { getItem: (k) => box.get(k) || null, setItem: (k, v) => box.set(k, v), removeItem: (k) => box.delete(k) };
|
|
110
|
+
P.keepRun(storage, { to: '1.19.871', from: '1.19.861', at: 1000 });
|
|
111
|
+
assert.equal(P.readRun(storage, 2000).to, '1.19.871');
|
|
112
|
+
assert.equal(P.readRun(storage, 1000 + P.GIVE_UP_MS + 1), null);
|
|
113
|
+
assert.equal(box.has(P.RUN_KEY), false, 'and is forgotten');
|
|
114
|
+
storage.setItem(P.RUN_KEY, JSON.stringify({ to: 'latest', at: 1 }));
|
|
115
|
+
assert.equal(P.readRun(storage, 2), null, 'only a real version is remembered');
|
|
116
|
+
const broken = { getItem() { throw new Error('private'); }, setItem() { throw new Error('private'); }, removeItem() { throw new Error('private'); } };
|
|
117
|
+
assert.equal(P.readRun(broken, 1), null);
|
|
118
|
+
assert.doesNotThrow(() => P.keepRun(broken, { to: '1.0.0', at: 1 }));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ── the real shell + the real pill, in a DOM stub ─────────────────────────────
|
|
122
|
+
|
|
123
|
+
function boot({
|
|
124
|
+
permissions = { 'core.pin.move': true }, update = BEHIND, held = null, status = 200, coreFails = false,
|
|
125
|
+
sw = SW({ kind: 'local', url: '/deploy' }), preview = { instance_id: '41', recommended_version: '1.19.871' },
|
|
126
|
+
post = { ok: true, status: 200, data: { ok: true } }, run = null, version = () => '1.19.861',
|
|
127
|
+
pathname = '/goals', hash = '', openResult = 'popup',
|
|
128
|
+
} = {}) {
|
|
129
|
+
const ids = new Map();
|
|
130
|
+
const inserted = [];
|
|
131
|
+
const requests = [];
|
|
132
|
+
const opened = [];
|
|
133
|
+
const posted = [];
|
|
134
|
+
const store = new Map();
|
|
135
|
+
if (run) store.set('bongos.update.run', JSON.stringify(run));
|
|
136
|
+
let pillLoads = 0;
|
|
137
|
+
const listeners = {};
|
|
138
|
+
|
|
139
|
+
const stub = (id) => {
|
|
140
|
+
const s = { id, textContent: '', disabled: false, _on: {}, addEventListener(ev, fn) { (s._on[ev] ||= []).push(fn); }, focus() {} };
|
|
141
|
+
return s;
|
|
142
|
+
};
|
|
143
|
+
const el = (tag) => {
|
|
144
|
+
const e = {
|
|
145
|
+
tagName: String(tag).toUpperCase(), id: '', className: '', type: '', title: '', hidden: false,
|
|
146
|
+
style: {}, dataset: {}, _attrs: {}, _on: {}, children: [], parentNode: null, _html: '', _stubs: new Map(), textContent: '',
|
|
147
|
+
classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } },
|
|
148
|
+
setAttribute(k, v) { this._attrs[k] = String(v); }, getAttribute(k) { return k in this._attrs ? this._attrs[k] : null; },
|
|
149
|
+
removeAttribute(k) { delete this._attrs[k]; },
|
|
150
|
+
addEventListener(ev, fn) { (this._on[ev] ||= []).push(fn); },
|
|
151
|
+
appendChild(c) { c.parentNode = this; this.children.push(c); return c; },
|
|
152
|
+
insertBefore(c) { c.parentNode = this; this.children.unshift(c); return c; },
|
|
153
|
+
insertAdjacentElement(_pos, x) { inserted.push(x); },
|
|
154
|
+
remove() { if (this.parentNode) this.parentNode.children = this.parentNode.children.filter((k) => k !== this); },
|
|
155
|
+
contains(x) { return x === this; }, focus() {}, click() {}, scrollIntoView() {},
|
|
156
|
+
getBoundingClientRect() { return { left: 400, right: 480, top: 20, bottom: 48 }; },
|
|
157
|
+
querySelector(sel) {
|
|
158
|
+
const m = /^#([\w-]+)$/.exec(sel) || (sel === '.upd-btn--go' ? [null, (this._html.match(/class="upd-btn upd-btn--go" id="([\w-]+)"/) || [])[1]] : null);
|
|
159
|
+
if (!m || !m[1] || !this._html.includes(`id="${m[1]}"`)) return null;
|
|
160
|
+
if (!this._stubs.has(m[1])) this._stubs.set(m[1], stub(m[1]));
|
|
161
|
+
return this._stubs.get(m[1]);
|
|
162
|
+
},
|
|
163
|
+
querySelectorAll() { return []; },
|
|
164
|
+
set innerHTML(v) { this._html = String(v); this._stubs = new Map(); }, get innerHTML() { return this._html; },
|
|
165
|
+
};
|
|
166
|
+
return e;
|
|
167
|
+
};
|
|
168
|
+
const side = el('div');
|
|
169
|
+
const body = el('body');
|
|
170
|
+
body.dataset = { page: 'goals' };
|
|
171
|
+
const documentObj = {
|
|
172
|
+
body,
|
|
173
|
+
head: {
|
|
174
|
+
appendChild(tag) {
|
|
175
|
+
if (/update-pill\.js/.test(tag.src)) { pillLoads++; vm.runInContext(PILL, sandbox, { filename: 'update-pill.js' }); }
|
|
176
|
+
if (tag.onload) tag.onload();
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
documentElement: { dataset: {}, style: {}, setAttribute() {}, classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } } },
|
|
180
|
+
getElementById(id) { if (!ids.has(id)) ids.set(id, el('div')); return ids.get(id); },
|
|
181
|
+
querySelector(sel) { return sel === '#app-topbar .topbar-side' ? side : null; },
|
|
182
|
+
querySelectorAll() { return []; },
|
|
183
|
+
createElement: (tag) => el(tag),
|
|
184
|
+
addEventListener() {}, dispatchEvent() {},
|
|
185
|
+
get cookie() { return ''; }, set cookie(_v) {},
|
|
186
|
+
};
|
|
187
|
+
const location = { hostname: 'h', origin: 'https://h', pathname, hash, search: '', assign() {}, replace() {}, reload() { location.reloaded = true; } };
|
|
188
|
+
const popup = { closed: false, postMessage(msg, origin) { posted.push({ msg, origin }); } };
|
|
189
|
+
const windowObj = {
|
|
190
|
+
OTB: realOTB(), __BRANDING__: {}, __MODULES__: { enabled: { provisioning: true } }, __updatePollMs: 1,
|
|
191
|
+
BongosClient: {
|
|
192
|
+
createClient: () => ({
|
|
193
|
+
request: (method, p, opts) => {
|
|
194
|
+
requests.push({ method, path: p, body: opts && opts.body });
|
|
195
|
+
if (p === '/api/bongos/core-update') {
|
|
196
|
+
if (coreFails) return Promise.reject(new Error('restarting'));
|
|
197
|
+
return Promise.resolve({ status, ok: status === 200, data: update || held ? { ok: true, update, held } : null });
|
|
198
|
+
}
|
|
199
|
+
if (p === '/api/bongos/software-update') return Promise.resolve({ ok: true, status: 200, data: sw });
|
|
200
|
+
if (p === '/api/bongos/provisioning/control-plane') return Promise.resolve({ ok: true, status: 200, data: { ok: true, preview } });
|
|
201
|
+
if (/\/core-upgrade$/.test(p)) return Promise.resolve(post);
|
|
202
|
+
return new Promise(() => {});
|
|
203
|
+
},
|
|
204
|
+
}),
|
|
205
|
+
},
|
|
206
|
+
fetch: async () => { const v = version(); if (v === null) throw new Error('down'); return { ok: true, json: async () => ({ coreVersion: v }) }; },
|
|
207
|
+
open: (url, name, feats) => { opened.push({ url, name, feats }); return openResult === 'popup' ? popup : null; },
|
|
208
|
+
addEventListener: (ev, fn) => { (listeners[ev] ||= []).push(fn); },
|
|
209
|
+
matchMedia: () => ({ matches: false, addEventListener() {} }),
|
|
210
|
+
localStorage: { getItem: (k) => (store.has(k) ? store.get(k) : null), setItem: (k, v) => store.set(k, String(v)), removeItem: (k) => store.delete(k) },
|
|
211
|
+
requestAnimationFrame() {}, scrollTo() {}, location, innerWidth: 1280,
|
|
212
|
+
...timers,
|
|
213
|
+
};
|
|
214
|
+
windowObj.document = documentObj;
|
|
215
|
+
const sandbox = { window: windowObj, document: documentObj, location, console, ...timers, queueMicrotask, Promise, JSON, URL,
|
|
216
|
+
CustomEvent: class { constructor(ty, i) { this.type = ty; Object.assign(this, i); } } };
|
|
217
|
+
sandbox.globalThis = sandbox;
|
|
218
|
+
vm.createContext(sandbox);
|
|
219
|
+
vm.runInContext(SHELL, sandbox, { filename: 'shell.js' });
|
|
220
|
+
windowObj.OTBShell.applyAccess({ authed: true, rank: 'archon', permissions });
|
|
221
|
+
const pill = () => side.children.find((c) => c.id === 'upd-pill');
|
|
222
|
+
const card = () => body.children.find((c) => c.id === 'upd-card');
|
|
223
|
+
return {
|
|
224
|
+
pill, card, side, inserted, requests, opened, posted, popup, store, location, windowObj,
|
|
225
|
+
pillLoads: () => pillLoads,
|
|
226
|
+
press: (node) => { for (const fn of node._on.click || []) fn({ currentTarget: node, target: node, preventDefault() {} }); },
|
|
227
|
+
pressIn: (id) => { const b = card().querySelector('#' + id); assert.ok(b, `#${id} is in the card`); for (const fn of b._on.click || []) fn({ currentTarget: b, target: b, preventDefault() {} }); },
|
|
228
|
+
message: (data, { source = popup, origin = 'https://cloudbongos.com' } = {}) => { for (const fn of listeners.message || []) fn({ data, source, origin }); },
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
test('an owner who is behind sees a quiet pill in the top bar, not a strip across the page', async () => {
|
|
233
|
+
const b = boot();
|
|
234
|
+
await settle();
|
|
235
|
+
assert.equal(b.pillLoads(), 1, 'the pill script is loaded once');
|
|
236
|
+
const pill = b.pill();
|
|
237
|
+
assert.ok(pill, 'the pill is in the top bar');
|
|
238
|
+
assert.match(pill.innerHTML, />Update</);
|
|
239
|
+
assert.equal(b.inserted.length, 0, 'nothing is inserted after the top bar any more');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('no answer from the registry draws nothing, and loads nothing', async () => {
|
|
243
|
+
const b = boot({ update: { running: '1.19.861', latest: null, behind: false, reading: 'no_answer' } });
|
|
244
|
+
await settle();
|
|
245
|
+
assert.equal(b.pill(), undefined);
|
|
246
|
+
assert.equal(b.pillLoads(), 0);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('a builder without the atom is never asked, and sees no pill', async () => {
|
|
250
|
+
const b = boot({ permissions: { 'task.create': true } });
|
|
251
|
+
await settle();
|
|
252
|
+
assert.ok(!b.requests.some((r) => r.path === '/api/bongos/core-update'));
|
|
253
|
+
assert.equal(b.pill(), undefined);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// task 1004118's lesson, kept: derive the map /me really sends rather than writing one here.
|
|
257
|
+
test('the pill renders from the projection /me ACTUALLY sends', async () => {
|
|
258
|
+
const meSrc = fs.readFileSync(path.join(ROOT, 'src/bongos/routes/me.js'), 'utf8');
|
|
259
|
+
const m = meSrc.match(/NAV_PERMISSION_ATOMS = Object\.freeze\(\[([^\]]*)\]\)/);
|
|
260
|
+
assert.ok(m, 'NAV_PERMISSION_ATOMS is readable');
|
|
261
|
+
const permissions = Object.fromEntries(m[1].split(',').map((x) => x.trim().replace(/^'|'$/g, '')).filter(Boolean).map((a) => [a, true]));
|
|
262
|
+
const b = boot({ permissions });
|
|
263
|
+
await settle();
|
|
264
|
+
assert.ok(b.pill(), 'core.pin.move is outside NAV_PERMISSION_ATOMS, so the pill is dead for everyone (task 1004118)');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// task 1004145's lesson: a control that only navigates to the page you are on does nothing.
|
|
268
|
+
// The pill is a button that acts in place on every page, the deploy door's and Settings' included.
|
|
269
|
+
test('the pill is a button that acts, never a link to the page you are on', async () => {
|
|
270
|
+
for (const [pathname, hash] of [['/deploy', ''], ['/deploy/', ''], ['/settings', '#software-update'], ['/goals', '']]) {
|
|
271
|
+
const b = boot({ pathname, hash });
|
|
272
|
+
await settle();
|
|
273
|
+
assert.equal(b.pill().tagName, 'BUTTON', pathname);
|
|
274
|
+
assert.equal(b.pill().getAttribute('href'), null, pathname);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('opening the card reads what the update brings, once, and names both versions', async () => {
|
|
279
|
+
const b = boot();
|
|
280
|
+
await settle();
|
|
281
|
+
b.press(b.pill());
|
|
282
|
+
await settle();
|
|
283
|
+
b.press(b.pill()); b.press(b.pill());
|
|
284
|
+
await settle();
|
|
285
|
+
assert.equal(b.requests.filter((r) => r.path === '/api/bongos/software-update').length, 1);
|
|
286
|
+
const html = b.card().innerHTML;
|
|
287
|
+
assert.match(html, /A new version of Bongos is ready/);
|
|
288
|
+
assert.match(html, /3 improvements:/, 'every line shown when there are only three');
|
|
289
|
+
assert.match(html, /Faster boards, and fewer clicks/, 'quoted notes have their dashes turned to commas');
|
|
290
|
+
assert.match(html, /1\.19\.861<\/span> → <span class="upd-ver">1\.19\.871/);
|
|
291
|
+
assert.match(html, /id="upd-go"/);
|
|
292
|
+
assert.equal(b.pill().getAttribute('aria-expanded'), 'true');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('the platform\'s own hall: Update now moves its own row to the version the card showed', async () => {
|
|
296
|
+
const b = boot();
|
|
297
|
+
await settle();
|
|
298
|
+
b.press(b.pill());
|
|
299
|
+
await settle();
|
|
300
|
+
b.pressIn('upd-go');
|
|
301
|
+
await settle(5);
|
|
302
|
+
const post = b.requests.find((r) => r.method === 'POST');
|
|
303
|
+
assert.equal(post.path, '/api/bongos/provisioning/instances/41/core-upgrade');
|
|
304
|
+
assert.deepEqual(plain(post.body), { to: '1.19.871' });
|
|
305
|
+
assert.equal(JSON.parse(b.store.get('bongos.update.run')).to, '1.19.871', 'the run is remembered for the next page');
|
|
306
|
+
assert.match(b.pill().innerHTML, /Updating…/);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('a door that names another version: it is shown, never sent, and the next press takes it', async () => {
|
|
310
|
+
const b = boot({ preview: { instance_id: '41', recommended_version: '1.19.900' } });
|
|
311
|
+
await settle();
|
|
312
|
+
b.press(b.pill());
|
|
313
|
+
await settle();
|
|
314
|
+
b.pressIn('upd-go');
|
|
315
|
+
await settle(5);
|
|
316
|
+
assert.ok(!b.requests.some((r) => r.method === 'POST'), 'nothing moves to a version nobody was shown');
|
|
317
|
+
assert.match(b.card().innerHTML, /The update door offers version 1\.19\.900/);
|
|
318
|
+
assert.match(b.card().innerHTML, /1\.19\.861<\/span> → <span class="upd-ver">1\.19\.900/, 'the card now names the door\'s version');
|
|
319
|
+
b.pressIn('upd-go');
|
|
320
|
+
await settle(5);
|
|
321
|
+
const post = b.requests.find((r) => r.method === 'POST');
|
|
322
|
+
assert.deepEqual(plain(post.body), { to: '1.19.900' }, 'the version shown, once it was shown');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('a refusal from the door is said in the server\'s own words', async () => {
|
|
326
|
+
const b = boot({ post: { ok: false, status: 409, data: { error: { code: 'intent_conflict', message: 'A restart is running for this project.' } } } });
|
|
327
|
+
await settle();
|
|
328
|
+
b.press(b.pill());
|
|
329
|
+
await settle();
|
|
330
|
+
b.pressIn('upd-go');
|
|
331
|
+
await settle(5);
|
|
332
|
+
assert.match(b.card().innerHTML, /A restart is running for this project\./);
|
|
333
|
+
assert.equal(b.store.has('bongos.update.run'), false);
|
|
334
|
+
assert.match(b.pill().innerHTML, />Update</, 'back to ready, not stuck on "Updating…"');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('a hosted project: Update now opens the hub\'s window and moves nothing from this page', async () => {
|
|
338
|
+
const b = boot({ sw: SW({ kind: 'hub', url: HUB }) });
|
|
339
|
+
await settle();
|
|
340
|
+
b.press(b.pill());
|
|
341
|
+
await settle();
|
|
342
|
+
assert.match(b.card().innerHTML, /A small window from cloudbongos\.com opens for a moment/);
|
|
343
|
+
b.pressIn('upd-go');
|
|
344
|
+
assert.equal(b.opened.length, 1);
|
|
345
|
+
assert.equal(b.opened[0].url, HUB + '&handoff=1');
|
|
346
|
+
assert.ok(!b.requests.some((r) => r.method === 'POST'), 'the project\'s own server never moves its pin');
|
|
347
|
+
|
|
348
|
+
// a message from anywhere but that window, on the hub's origin, is not heard
|
|
349
|
+
b.message({ tag: 'bongos-update', step: 'ready' }, { origin: 'https://evil.example' });
|
|
350
|
+
b.message({ tag: 'bongos-update', step: 'ready' }, { source: {} });
|
|
351
|
+
assert.equal(b.posted.length, 0);
|
|
352
|
+
|
|
353
|
+
b.message({ tag: 'bongos-update', step: 'ready' });
|
|
354
|
+
assert.deepEqual(plain(b.posted), [{ msg: { tag: 'bongos-update', step: 'go', to: '1.19.871' }, origin: 'https://cloudbongos.com' }],
|
|
355
|
+
'the version shown, and only to the hub');
|
|
356
|
+
|
|
357
|
+
b.message({ tag: 'bongos-update', step: 'started', to: '1.19.871' });
|
|
358
|
+
assert.equal(JSON.parse(b.store.get('bongos.update.run')).to, '1.19.871');
|
|
359
|
+
assert.match(b.pill().innerHTML, /Updating…/);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test('a hub refusal is said in its own words, and nothing is remembered', async () => {
|
|
363
|
+
const b = boot({ sw: SW({ kind: 'hub', url: HUB }) });
|
|
364
|
+
await settle();
|
|
365
|
+
b.press(b.pill()); await settle();
|
|
366
|
+
b.pressIn('upd-go');
|
|
367
|
+
b.message({ tag: 'bongos-update', step: 'refused', message: 'Only the project\'s owner can update it.' });
|
|
368
|
+
assert.match(b.card().innerHTML, /Only the project's owner can update it\.|Only the project's owner can update it\./);
|
|
369
|
+
assert.equal(b.store.has('bongos.update.run'), false);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test('a blocked window is said, with a second press that opens it', async () => {
|
|
373
|
+
const b = boot({ sw: SW({ kind: 'hub', url: HUB }), openResult: null });
|
|
374
|
+
await settle();
|
|
375
|
+
b.press(b.pill()); await settle();
|
|
376
|
+
b.pressIn('upd-go');
|
|
377
|
+
assert.match(b.card().innerHTML, /blocked the update window/);
|
|
378
|
+
const again = b.card().querySelector('#upd-open') || b.card().querySelector('#upd-go');
|
|
379
|
+
assert.ok(again, 'a button to try again');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test('a self-hosted project names the command; an operator-run one offers to ask; a viewer who may not, nothing', async () => {
|
|
383
|
+
const cli = boot({ sw: SW({ kind: 'cli', url: null }) });
|
|
384
|
+
await settle(); cli.press(cli.pill()); await settle();
|
|
385
|
+
assert.match(cli.card().innerHTML, /<code>bongos upgrade<\/code>/);
|
|
386
|
+
assert.doesNotMatch(cli.card().innerHTML, /id="upd-go"/);
|
|
387
|
+
|
|
388
|
+
const ask = boot({ sw: SW({ kind: 'cli', url: null }, { ask: { available: true, hub_host: 'cloudbongos.com' } }) });
|
|
389
|
+
await settle(); ask.press(ask.pill()); await settle();
|
|
390
|
+
assert.match(ask.card().innerHTML, /id="upd-ask"/);
|
|
391
|
+
assert.match(ask.card().innerHTML, /the team at cloudbongos\.com/);
|
|
392
|
+
|
|
393
|
+
const no = boot({ sw: SW({ kind: 'local', url: '/deploy' }, { can_update: false }) });
|
|
394
|
+
await settle(); no.press(no.pill()); await settle();
|
|
395
|
+
assert.doesNotMatch(no.card().innerHTML, /id="upd-go"/);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// task 1004730's rule, kept: a version that just failed here is never offered again.
|
|
399
|
+
test('a held version: the pill says so, the card says what happened, and there is no Update now', async () => {
|
|
400
|
+
const held = { version: '1.19.871', still_on: '1.19.861', message: 'Version 1.19.871 was tried on this project and did not work: health check. It is still on 1.19.861 and working.' };
|
|
401
|
+
const b = boot({ held });
|
|
402
|
+
await settle();
|
|
403
|
+
assert.match(b.pill().innerHTML, /Update held/);
|
|
404
|
+
b.press(b.pill()); await settle();
|
|
405
|
+
assert.match(b.card().innerHTML, /tried on this project and did not work/);
|
|
406
|
+
assert.match(b.card().innerHTML, /still on 1\.19\.861 and working/);
|
|
407
|
+
assert.doesNotMatch(b.card().innerHTML, /id="upd-go"/);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test('a remembered run follows the owner to the next page, even while the server restarts', async () => {
|
|
411
|
+
const b = boot({ coreFails: true, run: { to: '1.19.871', from: '1.19.861', at: Date.now() }, version: () => null });
|
|
412
|
+
await settle();
|
|
413
|
+
assert.ok(b.pill(), 'mid-restart the read fails, and the pill still speaks');
|
|
414
|
+
assert.match(b.pill().innerHTML, /Updating…/);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test('done is the Claude moment: the pill becomes Reload, and pressing it reloads', async () => {
|
|
418
|
+
const b = boot({ run: { to: '1.19.871', from: '1.19.861', at: Date.now() }, version: () => '1.19.871' });
|
|
419
|
+
await settle(20);
|
|
420
|
+
assert.match(b.pill().innerHTML, />Reload</);
|
|
421
|
+
assert.equal(b.store.has('bongos.update.run'), false, 'the run is over');
|
|
422
|
+
b.press(b.pill());
|
|
423
|
+
assert.equal(b.location.reloaded, true);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test('a run whose version this page already runs has landed: nothing to say', async () => {
|
|
427
|
+
const b = boot({ update: { ...BEHIND, running: '1.19.871', behind: false }, run: { to: '1.19.871', from: '1.19.861', at: Date.now() } });
|
|
428
|
+
await settle();
|
|
429
|
+
assert.equal(b.pill(), undefined);
|
|
430
|
+
assert.equal(b.store.has('bongos.update.run'), false);
|
|
431
|
+
});
|