@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.
@@ -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 a sub-line beside it', () => {
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>\s*<p id="hall-sub" class="page-head__sub" hidden><\/p>/,
251
- 'the h1 is visible (no sr-only), keeps the id brand-apply.js writes into, and the sub-line ships hidden');
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, the public progress and the claimable feed, and builders.js feeds all three', () => {
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 — the count must never cost the whole queue');
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.match(buildersJs, /get lastClaimableCount\(\)/, 'and the count the same way');
319
- assert.equal((buildersJs.match(/renderGreeting\(\)/g) || []).length, 3, 'rendered after /me, after progress and after the claimable feed — each half as it lands');
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, claimable = null, leaderboard = [] }) {
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; }, get lastClaimableCount() { return claimable; },
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 sub-line says how far the version is and what is claimable', () => {
379
- const { api, el } = bootHallRender({ me: ME, progress: PROGRESS, claimable: 5 });
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('hall-sub').textContent, 'BONGOS-V1 is 75 percent complete. Five tasks are claimable right now.');
383
- assert.equal(el('hall-sub').hidden, false);
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('hall-sub').hidden, true, 'nothing to say yet');
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
- await page.waitForFunction(() => !document.getElementById('attention-dialog').open);
101
- const back = await page.evaluate(() => !!document.querySelector('#attention-slices > #attention-blockers'));
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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);