@bongos/core 1.21.165 → 1.21.166
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 +28 -23
- package/docs/copy-inventory.md +61 -61
- package/docs/copy-registry.json +68 -68
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +112 -113
- package/modules/builder-settings/builder-needs.js +5 -0
- package/modules/hall-ui/public/builders.js +3 -17
- package/modules/hall-ui/public/hall-render.js +8 -24
- package/modules/hall-ui/public/index.html +3 -4
- package/modules/hall-ui/public/settings.js +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +10 -0
- package/src/bongos/routes/me.js +3 -0
- package/src/module-api.js +1 -1
- package/tests/hall_board_world.mjs +18 -39
- package/tests/home_attention_cards.mjs +6 -2
- package/tests/settings_art_key_absent.mjs +137 -0
|
@@ -244,12 +244,15 @@ const hallRenderJs = read('hall-render.js');
|
|
|
244
244
|
const buildersJs = read('builders.js');
|
|
245
245
|
const goalInboxJs = read('goal-inbox.js');
|
|
246
246
|
|
|
247
|
-
test('the greeting IS the page\'s one h1, in the board\'s label voice, with
|
|
247
|
+
test('the greeting IS the page\'s one h1, in the board\'s label voice, with no sub-line under it', () => {
|
|
248
248
|
const h1s = indexHtml.match(/<h1\b/g) || [];
|
|
249
249
|
assert.equal(h1s.length, 1, 'one-header-main-h1: the greeting must BECOME the h1, never add a second');
|
|
250
|
-
assert.match(indexHtml, /<div class="page-head page-head--board page-head--hall">\s*<h1 id="hall-h1" class="page-head__h">Builders' Hall<\/h1
|
|
251
|
-
'the h1 is visible (no sr-only)
|
|
250
|
+
assert.match(indexHtml, /<div class="page-head page-head--board page-head--hall">\s*<h1 id="hall-h1" class="page-head__h">Builders' Hall<\/h1>/,
|
|
251
|
+
'the h1 is visible (no sr-only) and keeps the id brand-apply.js writes into');
|
|
252
252
|
assert.doesNotMatch(indexHtml, /id="hall-h1" class="sr-only"/, 'the sr-only h1 is gone');
|
|
253
|
+
// task 1004780 (owner): the status sentence repeated the version tab and Next up, so it went.
|
|
254
|
+
assert.doesNotMatch(indexHtml, /id="hall-sub"/, 'no status line under the greeting');
|
|
255
|
+
assert.doesNotMatch(hallRenderJs, /hall-sub|claimable right now/, 'and nothing renders one');
|
|
253
256
|
});
|
|
254
257
|
|
|
255
258
|
test('the band ends level: no align-items on the grid, so the two cards stretch', () => {
|
|
@@ -300,28 +303,18 @@ test('the system notices are inbox LINES, first in the card, and counted by its
|
|
|
300
303
|
}
|
|
301
304
|
});
|
|
302
305
|
|
|
303
|
-
test('the greeting reads /me
|
|
306
|
+
test('the greeting reads /me and the public progress (for the version tab), and builders.js feeds both', () => {
|
|
304
307
|
assert.match(buildersJs, /getJSON\(`\$\{API\}\/public\/progress`\)/, 'Home fetches the public progress once');
|
|
305
|
-
// The count comes off the SAME Next-up response — one request, not a second
|
|
306
|
-
// fetch. It reads `claimable_total` rather than the length of the returned array,
|
|
307
|
-
// because that request is now bounded (?limit=2, task 1003545): the array holds two
|
|
308
|
-
// rows and the total holds the whole rank-scoped queue. Asserting the derivation
|
|
309
|
-
// rather than one literal expression is deliberate — the previous version pinned
|
|
310
|
-
// `resp.claimable.length`, which read as a requirement to download the feed.
|
|
311
|
-
assert.match(buildersJs, /lastClaimableCount = Number\.isFinite\(claimableTotal\)/,
|
|
312
|
-
'the claimable feed Home already fetches for Next-up answers the count');
|
|
313
|
-
assert.match(buildersJs, /const claimableTotal = Number\(resp && resp\.claimable_total\);/,
|
|
314
|
-
'and it takes the count from the response total, so a bounded read still knows it');
|
|
315
308
|
assert.doesNotMatch(buildersJs, /getJSON\(`\$\{API\}\/tasks\/claimable`/,
|
|
316
|
-
'no unbounded claimable fetch on Home
|
|
309
|
+
'no unbounded claimable fetch on Home');
|
|
317
310
|
assert.match(buildersJs, /get lastProgressState\(\)/, 'the renderer reads progress through a getter, so it cannot freeze at boot');
|
|
318
|
-
assert.
|
|
319
|
-
assert.equal((buildersJs.match(/renderGreeting\(\)/g) || []).length,
|
|
311
|
+
assert.doesNotMatch(buildersJs, /lastClaimableCount/, 'the greeting no longer needs the claimable count (task 1004780)');
|
|
312
|
+
assert.equal((buildersJs.match(/renderGreeting\(\)/g) || []).length, 2, 'rendered after /me and after progress — each as it lands');
|
|
320
313
|
});
|
|
321
314
|
|
|
322
315
|
// Boot hall-render.js against a stub DOM and a ctx of getters (the way
|
|
323
316
|
// builders.js builds it) and read what the greeting and the card actually say.
|
|
324
|
-
function bootHallRender({ me, progress = null,
|
|
317
|
+
function bootHallRender({ me, progress = null, leaderboard = [] }) {
|
|
325
318
|
const els = new Map();
|
|
326
319
|
const makeEl = () => ({
|
|
327
320
|
dataset: {}, style: {}, textContent: '', hidden: false, _html: '', innerHTML: '',
|
|
@@ -359,7 +352,7 @@ function bootHallRender({ me, progress = null, claimable = null, leaderboard = [
|
|
|
359
352
|
instanceRepo: null, newcomerGated: false,
|
|
360
353
|
get lastMeState() { return me; }, get lastCostState() { return me.cost || null; }, get lastKarmaState() { return me.karma || null; },
|
|
361
354
|
get lastNeedsState() { return me.needs || null; }, get lastRebaseState() { return me.needs_rebase_tasks || null; },
|
|
362
|
-
get lastProgressState() { return progress; },
|
|
355
|
+
get lastProgressState() { return progress; },
|
|
363
356
|
};
|
|
364
357
|
const api = windowObj.OTBHallRender(ctx);
|
|
365
358
|
api.lbState.rows = leaderboard;
|
|
@@ -375,30 +368,16 @@ const ME = {
|
|
|
375
368
|
const LEADERBOARD = [{ github_login: 'a', total_credits: 20000 }, { github_login: 'percy', total_credits: 14441 }, { github_login: 'b', total_credits: 3 }, { github_login: 'c', total_credits: 1 }];
|
|
376
369
|
const PROGRESS = { progress: [{ version_id: 'BONGOS-V1', status: 'building', total_count: 605, percent_complete: 75 }, { version_id: 'V2', status: 'planning', total_count: 9000, percent_complete: 0 }] };
|
|
377
370
|
|
|
378
|
-
test('RUN: the greeting names the builder, and the
|
|
379
|
-
const { api, el } = bootHallRender({ me: ME, progress: PROGRESS
|
|
371
|
+
test('RUN: the greeting names the builder, and the version tab carries how far the version is', () => {
|
|
372
|
+
const { api, el } = bootHallRender({ me: ME, progress: PROGRESS });
|
|
380
373
|
api.renderGreeting();
|
|
381
374
|
assert.match(el('hall-h1').textContent, /^Good (morning|afternoon|evening), Percy Jackson$/);
|
|
382
|
-
assert.equal(el('
|
|
383
|
-
assert.equal(el('
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
test('RUN: each half of the sentence renders as its data lands, and the line hides with neither', () => {
|
|
375
|
+
assert.equal(el('home-version-name').textContent, 'BONGOS-V1', 'the building version, not the bigger planning one');
|
|
376
|
+
assert.equal(el('home-version-pct').textContent, '75% complete');
|
|
377
|
+
assert.equal(el('home-version').hidden, false);
|
|
387
378
|
const none = bootHallRender({ me: ME });
|
|
388
379
|
none.api.renderGreeting();
|
|
389
|
-
assert.equal(none.el('
|
|
390
|
-
const onlyProgress = bootHallRender({ me: ME, progress: PROGRESS });
|
|
391
|
-
onlyProgress.api.renderGreeting();
|
|
392
|
-
assert.equal(onlyProgress.el('hall-sub').textContent, 'BONGOS-V1 is 75 percent complete.');
|
|
393
|
-
const zero = bootHallRender({ me: ME, progress: PROGRESS, claimable: 0 });
|
|
394
|
-
zero.api.renderGreeting();
|
|
395
|
-
assert.match(zero.el('hall-sub').textContent, /Nothing is claimable right now\.$/);
|
|
396
|
-
const one = bootHallRender({ me: ME, claimable: 1 });
|
|
397
|
-
one.api.renderGreeting();
|
|
398
|
-
assert.equal(one.el('hall-sub').textContent, 'One task is claimable right now.');
|
|
399
|
-
const many = bootHallRender({ me: ME, claimable: 63 });
|
|
400
|
-
many.api.renderGreeting();
|
|
401
|
-
assert.equal(many.el('hall-sub').textContent, '63 tasks are claimable right now.');
|
|
380
|
+
assert.equal(none.el('home-version').hidden, true, 'no progress yet: no tab');
|
|
402
381
|
});
|
|
403
382
|
|
|
404
383
|
test('RUN: the Standing card carries the three facts, the place as "2nd of 4", and no banner', () => {
|
|
@@ -97,8 +97,12 @@ test('closing natively (Escape) also returns the slice, so a reopen still finds
|
|
|
97
97
|
const { ctx, page } = await bootInbox();
|
|
98
98
|
await page.click('[data-attn-open="attention-blockers"]');
|
|
99
99
|
await page.keyboard.press('Escape');
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
// `open` turns false at once, but the dialog's `close` EVENT (which moves the slice home) is
|
|
101
|
+
// queued after it, so wait for the slice itself rather than for `open` (task 1004780 — reading
|
|
102
|
+
// straight after `open` flipped raced the event and failed CI about half the time).
|
|
103
|
+
const back = await page.waitForFunction(
|
|
104
|
+
() => !document.getElementById('attention-dialog').open && !!document.querySelector('#attention-slices > #attention-blockers'),
|
|
105
|
+
null, { timeout: 3000 }).then(() => true, () => false);
|
|
102
106
|
assert.equal(back, true, 'the dialog close event moves the slice home');
|
|
103
107
|
await page.click('[data-attn-open="attention-blockers"]');
|
|
104
108
|
const rows = await page.evaluate(() => document.querySelectorAll('#attention-dialog-body .ginbox__item').length);
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// tests/settings_art_key_absent.mjs — the image-generation key card on a project
|
|
2
|
+
// with no art pipeline (task 1004786).
|
|
3
|
+
//
|
|
4
|
+
// The key routes (/me/art-key/*) left the core with the art-pipeline module, but
|
|
5
|
+
// the Settings card and the art_key need stayed. On cloudbongos.com — which has
|
|
6
|
+
// no art pipeline — every builder past Xenos was told "your free art coverage has
|
|
7
|
+
// ended, add your key", and the card the nudge pointed at could never load, so
|
|
8
|
+
// there was nowhere to add one. Two halves, both pinned here:
|
|
9
|
+
//
|
|
10
|
+
// - builder-needs: with the art pipeline absent (ctx.artPipeline === false) the
|
|
11
|
+
// art_key need is not raised at all — nothing to nag about, nothing to add;
|
|
12
|
+
// - settings.js: a 404 from GET /me/art-key/own means the routes are not
|
|
13
|
+
// installed, so the section stays hidden instead of showing a reload error.
|
|
14
|
+
// A real failure (500) still shows the error.
|
|
15
|
+
//
|
|
16
|
+
// Pure vm + file-read (no DB/network) — runs in the DB-free unit gate.
|
|
17
|
+
import { strict as assert } from 'node:assert';
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import vm from 'node:vm';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
+
const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/settings.js'), 'utf8');
|
|
26
|
+
const ME = fs.readFileSync(path.join(ROOT, 'src/bongos/routes/me.js'), 'utf8');
|
|
27
|
+
const needs = createRequire(import.meta.url)('../modules/builder-settings/builder-needs.js');
|
|
28
|
+
|
|
29
|
+
let passed = 0, failed = 0;
|
|
30
|
+
async function t(name, fn) {
|
|
31
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
32
|
+
catch (e) { failed++; console.error(` FAIL ${name}\n ${e.message}`); }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeEl() {
|
|
36
|
+
return {
|
|
37
|
+
hidden: false, textContent: '', value: '', disabled: false, href: '', className: '',
|
|
38
|
+
dataset: {}, style: {}, children: [], _html: '',
|
|
39
|
+
set innerHTML(v) { this._html = String(v); }, get innerHTML() { return this._html; },
|
|
40
|
+
addEventListener() {}, appendChild(c) { return c; },
|
|
41
|
+
append() {}, prepend() {}, remove() {}, setAttribute() {}, removeAttribute() {}, getAttribute() { return null; },
|
|
42
|
+
querySelector() { return null; }, querySelectorAll() { return []; }, closest() { return null; },
|
|
43
|
+
classList: { add() {}, remove() {}, toggle() {}, contains() { return false; } },
|
|
44
|
+
focus() {},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Boot the real settings.js; GET /me/art-key/own answers with `artKeyStatus`.
|
|
49
|
+
async function boot(artKeyStatus) {
|
|
50
|
+
const els = new Map();
|
|
51
|
+
const $ = (sel) => { if (!els.has(sel)) els.set(sel, makeEl()); return els.get(sel); };
|
|
52
|
+
// The markup ships both hidden (settings.html), so the stub starts them hidden.
|
|
53
|
+
$('#art-key').hidden = true;
|
|
54
|
+
$('#artkey-banner').hidden = true;
|
|
55
|
+
const builder = { id: 4, github_login: 'b4', rank: 'thetes', preferred_disciplines: ['engineer'] };
|
|
56
|
+
const me = { builder, needs: { items: [needs.artKeyNeed({ rank: 'thetes' })] } };
|
|
57
|
+
const api = {
|
|
58
|
+
async request(method, url) {
|
|
59
|
+
if (method === 'GET' && url === '/api/bongos/me') return { status: 200, ok: true, data: me };
|
|
60
|
+
if (method === 'GET' && url === '/api/bongos/me/art-key/own') {
|
|
61
|
+
return artKeyStatus === 200
|
|
62
|
+
? { status: 200, ok: true, data: { set: false, storage_configured: true } }
|
|
63
|
+
: { status: artKeyStatus, ok: false, data: null };
|
|
64
|
+
}
|
|
65
|
+
return { status: 404, ok: false, data: null };
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
const escapeHtml = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
69
|
+
const document = {
|
|
70
|
+
getElementById: (id) => $(`#${id}`), createElement: () => makeEl(),
|
|
71
|
+
dispatchEvent() {}, addEventListener() {}, querySelector: () => null, querySelectorAll: () => [],
|
|
72
|
+
};
|
|
73
|
+
const window = {
|
|
74
|
+
__BRANDING__: {}, __MODULES__: { enabled: {} },
|
|
75
|
+
BongosClient: { createClient: () => api },
|
|
76
|
+
OTB: {
|
|
77
|
+
$, escapeHtml, toast() {}, disciplines: () => ['engineer'], roleLabel: (d) => d,
|
|
78
|
+
hubOrigin: () => null, settingsPanels: () => [],
|
|
79
|
+
},
|
|
80
|
+
OTBKit: { emptyStateHtml: (m) => m, makeSectionRail: () => ({ refresh() {}, activate() {} }) },
|
|
81
|
+
location: { hash: '', pathname: '/settings', search: '', assign() {} },
|
|
82
|
+
scrollTo() {}, confirm: () => false, addEventListener() {},
|
|
83
|
+
};
|
|
84
|
+
const sandbox = {
|
|
85
|
+
window, document, console: { log() {}, warn() {}, error() {}, info() {} },
|
|
86
|
+
history: { replaceState() {} }, URLSearchParams, setTimeout, clearTimeout,
|
|
87
|
+
};
|
|
88
|
+
sandbox.globalThis = sandbox;
|
|
89
|
+
vm.createContext(sandbox);
|
|
90
|
+
vm.runInContext(SRC, sandbox, { filename: 'settings.js' });
|
|
91
|
+
for (let i = 0; i < 12; i++) await new Promise((r) => setImmediate(r));
|
|
92
|
+
return $;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log('\nthe art_key need without an art pipeline:');
|
|
96
|
+
|
|
97
|
+
await t('artPipeline: false raises no art_key need at any rank', () => {
|
|
98
|
+
for (const rank of ['xenos', 'thetes', 'metic', 'archon']) {
|
|
99
|
+
assert.equal(needs.artKeyNeed({ rank, artPipeline: false }), null, rank);
|
|
100
|
+
const r = needs.computeNeeds({ rank, artPipeline: false });
|
|
101
|
+
assert.ok(!r.items.some((n) => n.id === 'art_key'), rank);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await t('with the pipeline present (or unstated) the need is unchanged', () => {
|
|
106
|
+
assert.equal(needs.artKeyNeed({ rank: 'thetes', artPipeline: true }).state, 'action_needed');
|
|
107
|
+
assert.equal(needs.artKeyNeed({ rank: 'thetes' }).state, 'action_needed');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await t('/me tells computeNeeds whether the art pipeline is installed', () => {
|
|
111
|
+
const call = ME.slice(ME.indexOf('settings.computeNeeds({'), ME.indexOf('isFoundingOwner'));
|
|
112
|
+
assert.match(call, /artPipeline:/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
console.log('\nthe Settings card:');
|
|
116
|
+
|
|
117
|
+
await t('routes not installed (404) → the section stays hidden, no reload error', async () => {
|
|
118
|
+
const $ = await boot(404);
|
|
119
|
+
assert.equal($('#art-key').hidden, true);
|
|
120
|
+
assert.equal($('#artkey-banner').hidden, true);
|
|
121
|
+
assert.doesNotMatch($('#artkey-card').innerHTML, /could not be loaded/);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await t('a real failure (500) still shows the error', async () => {
|
|
125
|
+
const $ = await boot(500);
|
|
126
|
+
assert.equal($('#art-key').hidden, false);
|
|
127
|
+
assert.match($('#artkey-card').innerHTML, /could not be loaded/);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await t('routes present → the key box renders', async () => {
|
|
131
|
+
const $ = await boot(200);
|
|
132
|
+
assert.equal($('#art-key').hidden, false);
|
|
133
|
+
assert.match($('#artkey-card').innerHTML, /id="artkey-input"/);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
137
|
+
process.exit(failed ? 1 : 0);
|