@bongos/core 1.21.145 → 1.21.147
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 +98 -68
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +6 -2
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +90 -7
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +17 -15
- package/docs/copy-registry.json +37 -19
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +85 -85
- package/modules/builder-settings/core-move-seen.js +41 -0
- package/modules/builder-settings/routes/settings.js +22 -0
- package/modules/builder-settings/settings.js +5 -0
- 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 +70 -0
- package/modules/hall-ui/public/deploy.html +1 -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/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 +38 -5
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/shell.js +18 -6
- 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 +11 -0
- 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/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/provision-auto-upgrade.js +30 -7
- package/scripts/gds/provision-core-upgrade.js +9 -3
- package/src/bongos/core-moves.js +155 -0
- package/src/bongos/routes/core-update.js +55 -3
- package/src/bongos/upgrade-hold.js +96 -0
- package/src/module-api.js +1 -1
- package/tests/core_auto_upgrade.mjs +64 -3
- package/tests/core_move_notice.mjs +356 -0
- package/tests/core_upgrade_owner_door.mjs +25 -2
- package/tests/nav_permission_atoms.mjs +4 -2
- package/tests/upgrade_hold.mjs +207 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// tests/upgrade_hold.mjs — a failed core move is told to the project's OWN owner, in their
|
|
2
|
+
// own hall (task 1004730).
|
|
3
|
+
//
|
|
4
|
+
// The move is `bongos upgrade` run against THIS project's database; its auto-rollback
|
|
5
|
+
// writes `rolled_back: <why> (attempted A → B, restored A)` to this project's own
|
|
6
|
+
// core_upgrades ledger. src/bongos/upgrade-hold.js reads that row back, and GET
|
|
7
|
+
// /software-update (Settings → Software update) and GET /core-update (the banner) carry
|
|
8
|
+
// it as `held`: which version was tried, that it was rolled back, and that the project is
|
|
9
|
+
// still on its old version and working. A newer release clears it.
|
|
10
|
+
//
|
|
11
|
+
// Run: node tests/upgrade_hold.mjs
|
|
12
|
+
|
|
13
|
+
import { strict as assert } from 'node:assert';
|
|
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
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
22
|
+
const { upgradeHold, readUpgradeHold } = require('../src/bongos/upgrade-hold.js');
|
|
23
|
+
|
|
24
|
+
let passed = 0;
|
|
25
|
+
let failed = 0;
|
|
26
|
+
const t = async (name, fn) => {
|
|
27
|
+
try { await fn(); passed += 1; console.log(` PASS ${name}`); }
|
|
28
|
+
catch (e) { failed += 1; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// The row upgrade.js rollback() writes, byte for byte in its note.
|
|
32
|
+
const rollbackRow = (why = 'migrate failed', from = '1.21.20', to = '1.21.115') => ({
|
|
33
|
+
from_version: from, to_version: to, applied_at: '2026-10-14T09:00:00.000Z',
|
|
34
|
+
note: `rolled_back: ${why} (attempted ${from} → ${to}, restored ${from})`,
|
|
35
|
+
});
|
|
36
|
+
const okRow = { from_version: '1.21.20', to_version: '1.21.69', applied_at: '2026-10-15T09:00:00.000Z', note: null };
|
|
37
|
+
|
|
38
|
+
console.log('upgrade hold, read by the project itself (task 1004730)');
|
|
39
|
+
|
|
40
|
+
await t('a rolled-back move is held, in plain words: tried, rolled back, still on the old one and working', () => {
|
|
41
|
+
const h = upgradeHold({ rows: [rollbackRow()], running: '1.21.20', offered: '1.21.115' });
|
|
42
|
+
assert.equal(h.version, '1.21.115');
|
|
43
|
+
assert.equal(h.still_on, '1.21.20');
|
|
44
|
+
assert.equal(h.reason, 'migrate');
|
|
45
|
+
assert.equal(h.retry, 'next_release');
|
|
46
|
+
assert.equal(h.at, '2026-10-14T09:00:00.000Z');
|
|
47
|
+
assert.match(h.message, /Version 1\.21\.115 was tried on this project/);
|
|
48
|
+
assert.match(h.message, /rolled back/);
|
|
49
|
+
assert.match(h.message, /still on 1\.21\.20 and working/);
|
|
50
|
+
assert.match(h.message, /database changes/, 'the reason, in words an owner reads');
|
|
51
|
+
assert.doesNotMatch(h.message, /[—–]/, 'voice rule: no dashes in copy');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await t('each step that can fail reads as its own plain reason; an unknown one as the general line', () => {
|
|
55
|
+
const cases = [['npm install failed', 'npm_install', /downloaded and installed/, 'later'],
|
|
56
|
+
['restart failed', 'restart', /did not start/, 'next_release'],
|
|
57
|
+
['health check failed after restart', 'health', /did not answer properly/, 'next_release'],
|
|
58
|
+
['served core version did not change (still 1.21.20, expected 1.21.115)', 'health', /did not answer properly/, 'next_release'],
|
|
59
|
+
['integrity pin mismatch', 'integrity_pin', /did not match/, 'next_release'],
|
|
60
|
+
['something nobody wrote down', 'unknown', /did not pass its checks/, 'next_release']];
|
|
61
|
+
for (const [why, reason, words, retry] of cases) {
|
|
62
|
+
const h = upgradeHold({ rows: [rollbackRow(why)], running: '1.21.20', offered: '1.21.115' });
|
|
63
|
+
assert.equal(h.reason, reason, why);
|
|
64
|
+
assert.match(h.message, words, why);
|
|
65
|
+
assert.equal(h.retry, retry, why);
|
|
66
|
+
}
|
|
67
|
+
assert.match(upgradeHold({ rows: [rollbackRow('npm install failed')], running: '1.21.20', offered: null }).message, /may be tried again later/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await t('a newer release clears the hold: it may carry the fix', () => {
|
|
71
|
+
assert.equal(upgradeHold({ rows: [rollbackRow()], running: '1.21.20', offered: '1.21.116' }), null);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await t('the hold stands when nothing, or nothing newer, is offered', () => {
|
|
75
|
+
assert.ok(upgradeHold({ rows: [rollbackRow()], running: '1.21.20', offered: null }));
|
|
76
|
+
assert.ok(upgradeHold({ rows: [rollbackRow()], running: '1.21.20', offered: '1.21.100' }));
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await t('a later move that landed ends it (only the newest ledger row counts)', () => {
|
|
80
|
+
assert.equal(upgradeHold({ rows: [okRow, rollbackRow()], running: '1.21.69', offered: null }), null);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await t('"still on it and working" is said only by the version the rollback restored', () => {
|
|
84
|
+
assert.equal(upgradeHold({ rows: [rollbackRow()], running: '1.21.30', offered: null }), null);
|
|
85
|
+
assert.equal(upgradeHold({ rows: [rollbackRow()], running: null, offered: null }), null);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await t('rows this file did not write are no hold', () => {
|
|
89
|
+
for (const rows of [[], null, [okRow], [{ ...rollbackRow(), note: 'rolled_back: whatever' }], [{ ...rollbackRow(), to_version: 'x' }]]) {
|
|
90
|
+
assert.equal(upgradeHold({ rows, running: '1.21.20', offered: null }), null, JSON.stringify(rows));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await t('readUpgradeHold reads the newest ledger row, and a failed read is no hold, never a throw', async () => {
|
|
95
|
+
let sql = '';
|
|
96
|
+
const pool = { query: async (s) => { sql = s; return { rows: [rollbackRow()] }; } };
|
|
97
|
+
const h = await readUpgradeHold({ running: '1.21.20', offered: '1.21.115', pool });
|
|
98
|
+
assert.equal(h.version, '1.21.115');
|
|
99
|
+
assert.match(sql, /FROM core_upgrades/);
|
|
100
|
+
assert.match(sql, /ORDER BY applied_at DESC/);
|
|
101
|
+
assert.equal(await readUpgradeHold({ running: '1.21.20', pool: { query: async () => { throw new Error('no table'); } } }), null);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ---- the two reads the hall makes ----------------------------------------------------
|
|
105
|
+
|
|
106
|
+
async function drive(p, deps) {
|
|
107
|
+
const auth = require('../src/bongos/auth.js');
|
|
108
|
+
const saved = { b: auth.requireBuilder, p: auth.requirePermission };
|
|
109
|
+
auth.requireBuilder = (req, _res, next) => { req.builder = { id: '7' }; next(); };
|
|
110
|
+
auth.requirePermission = () => (_req, _res, next) => next();
|
|
111
|
+
const build = require('../src/bongos/routes/core-update.js');
|
|
112
|
+
const express = require('express');
|
|
113
|
+
const app = express();
|
|
114
|
+
try {
|
|
115
|
+
app.use(build({ askDeps: () => ({ hub: null, getSetting: async () => null }), canUpdate: async () => true,
|
|
116
|
+
doorInputs: () => ({ provisioning: false, idp: null, hubUpgradeUrl: 'https://cloudbongos.com/deploy?project=toast' }), ...deps }));
|
|
117
|
+
} finally { auth.requireBuilder = saved.b; auth.requirePermission = saved.p; }
|
|
118
|
+
const server = app.listen(0);
|
|
119
|
+
await new Promise((r) => server.once('listening', r));
|
|
120
|
+
try {
|
|
121
|
+
const res = await fetch(`http://127.0.0.1:${server.address().port}${p}`);
|
|
122
|
+
return { status: res.status, body: await res.json() };
|
|
123
|
+
} finally { server.close(); }
|
|
124
|
+
}
|
|
125
|
+
const ledger = { query: async () => ({ rows: [rollbackRow()] }) };
|
|
126
|
+
const realHold = (a) => readUpgradeHold({ ...a, pool: ledger });
|
|
127
|
+
|
|
128
|
+
await t('GET /software-update carries the hold and its plain reason (Settings → Software update)', async () => {
|
|
129
|
+
let asked = null;
|
|
130
|
+
const r = await drive('/software-update', {
|
|
131
|
+
softwareUpdate: async ({ running }) => ({ reading: 'ok', running, offered: '1.21.115' }),
|
|
132
|
+
upgradeHold: (a) => { asked = a; return realHold({ ...a, running: '1.21.20' }); },
|
|
133
|
+
});
|
|
134
|
+
assert.equal(r.status, 200);
|
|
135
|
+
assert.equal(asked.offered, '1.21.115', 'the hold is judged against what this project is offered');
|
|
136
|
+
assert.equal(r.body.held.version, '1.21.115');
|
|
137
|
+
assert.match(r.body.held.message, /still on 1\.21\.20 and working/);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await t('GET /software-update: a newer offer means no hold, and an unread registry still shows it', async () => {
|
|
141
|
+
const newer = await drive('/software-update', { softwareUpdate: async () => ({ reading: 'ok', offered: '1.21.116' }),
|
|
142
|
+
upgradeHold: (a) => realHold({ ...a, running: '1.21.20' }) });
|
|
143
|
+
assert.equal(newer.body.held, null);
|
|
144
|
+
const unread = await drive('/software-update', { softwareUpdate: async () => ({ reading: 'no_answer', offered: null }),
|
|
145
|
+
upgradeHold: (a) => realHold({ ...a, running: '1.21.20' }) });
|
|
146
|
+
assert.equal(unread.body.held.version, '1.21.115');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await t('GET /core-update carries it too, for the banner', async () => {
|
|
150
|
+
const r = await drive('/core-update', {
|
|
151
|
+
coreUpdate: async () => ({ reading: 'ok', running: '1.21.20', latest: '1.21.115', behind: true }),
|
|
152
|
+
selfChannel: async () => null,
|
|
153
|
+
upgradeHold: (a) => realHold({ ...a, running: '1.21.20' }),
|
|
154
|
+
});
|
|
155
|
+
assert.equal(r.status, 200);
|
|
156
|
+
assert.equal(r.body.held.version, '1.21.115');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await t('a hold read that throws never fails either read', async () => {
|
|
160
|
+
const boom = async () => { throw new Error('boom'); };
|
|
161
|
+
const a = await drive('/software-update', { softwareUpdate: async () => ({ reading: 'ok', offered: null }), upgradeHold: boom });
|
|
162
|
+
assert.equal(a.status, 200);
|
|
163
|
+
assert.equal(a.body.held, null);
|
|
164
|
+
const b = await drive('/core-update', { coreUpdate: async () => ({ reading: 'ok', behind: false }), selfChannel: async () => null, upgradeHold: boom });
|
|
165
|
+
assert.equal(b.status, 200);
|
|
166
|
+
assert.equal(b.body.held, null);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ---- the Software update panel draws it ----------------------------------------------
|
|
170
|
+
|
|
171
|
+
const PANEL_SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/settings-software-update.js'), 'utf8');
|
|
172
|
+
const escapeHtml = (s) => String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
173
|
+
|
|
174
|
+
async function panel(data) {
|
|
175
|
+
const el = () => ({ hidden: true, textContent: '', _html: '', dataset: {}, set innerHTML(v) { this._html = String(v); }, get innerHTML() { return this._html; }, querySelectorAll: () => [] });
|
|
176
|
+
const els = new Map([['software-update-scroll', el()], ['su-body', el()], ['su-note', el()], ['settings-groups', el()]]);
|
|
177
|
+
els.get('settings-groups').dataset.active = 'update';
|
|
178
|
+
const windowObj = {
|
|
179
|
+
BongosClient: { createClient: () => ({ request: async () => ({ ok: true, status: 200, data }) }) },
|
|
180
|
+
OTB: { escapeHtml }, location: { assign() {} },
|
|
181
|
+
};
|
|
182
|
+
const sandbox = { window: windowObj, document: { getElementById: (id) => els.get(id) || null, addEventListener() {} }, console, setTimeout, Date, Math, URL, Promise, String, Number };
|
|
183
|
+
vm.createContext(sandbox);
|
|
184
|
+
vm.runInContext(PANEL_SRC, sandbox, { filename: 'settings-software-update.js' });
|
|
185
|
+
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
|
|
186
|
+
return els.get('su-body').innerHTML;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await t('the panel says it in the owner\'s words, and does not offer the held version as "Update ready"', async () => {
|
|
190
|
+
const held = upgradeHold({ rows: [rollbackRow()], running: '1.21.20', offered: '1.21.115' });
|
|
191
|
+
const html = await panel({ ok: true, update: { reading: 'ok', running: '1.21.20', offered: '1.21.115', whats_new: null },
|
|
192
|
+
door: { kind: 'hub', url: 'https://cloudbongos.com/deploy?project=toast' }, can_update: true, ask: {}, held });
|
|
193
|
+
assert.ok(html.includes(escapeHtml(held.message)), 'the plain message is drawn');
|
|
194
|
+
assert.match(html, /On hold/);
|
|
195
|
+
assert.doesNotMatch(html, /Update ready/);
|
|
196
|
+
assert.doesNotMatch(html, /id="su-update"/, 'no button to take the version that just failed');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await t('no hold, no change: the panel draws exactly what it did before', async () => {
|
|
200
|
+
const data = { ok: true, update: { reading: 'ok', running: '1.21.20', offered: '1.21.116', whats_new: null }, door: { kind: 'cli', url: null }, can_update: true, ask: {} };
|
|
201
|
+
const html = await panel({ ...data, held: null });
|
|
202
|
+
assert.match(html, /Update ready/);
|
|
203
|
+
assert.equal(html, await panel(data));
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
207
|
+
process.exit(failed === 0 ? 0 : 1);
|