@bongos/core 1.21.165 → 1.21.167
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 +32 -27
- package/docs/copy-inventory.md +61 -61
- package/docs/copy-registry.json +68 -68
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +126 -127
- package/modules/builder-settings/builder-needs.js +5 -0
- package/modules/hall-ui/public/brand-apply.js +3 -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/modules/hall-ui/public/sky-data.js +4 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -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/hall_sky.mjs +18 -0
- package/tests/home_attention_cards.mjs +6 -2
- package/tests/reward_mode_surfacing.mjs +12 -0
- package/tests/settings_art_key_absent.mjs +137 -0
|
@@ -722,20 +722,19 @@
|
|
|
722
722
|
if (window.OTBAttention && typeof window.OTBAttention.refresh === 'function') window.OTBAttention.refresh();
|
|
723
723
|
}
|
|
724
724
|
|
|
725
|
-
// task 1003464: the greeting IS Home's one h1 (the mock opens with it)
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
//
|
|
729
|
-
const SMALL_NUMBERS = ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve'];
|
|
725
|
+
// task 1003464: the greeting IS Home's one h1 (the mock opens with it).
|
|
726
|
+
// task 1004780: it has no sub-line any more. The version tab under the banner
|
|
727
|
+
// shows how far the version is and Next up shows the ready work, so a sentence
|
|
728
|
+
// repeating both was redundant (owner, 2026-10-23).
|
|
730
729
|
const greetingWord = (hour) => (hour < 12 ? 'Good morning' : (hour < 18 ? 'Good afternoon' : 'Good evening'));
|
|
731
|
-
// The version the
|
|
730
|
+
// The version the tab is about: the one being built with the most work in it.
|
|
732
731
|
function currentVersion(progress) {
|
|
733
732
|
const rows = progress && Array.isArray(progress.progress) ? progress.progress : [];
|
|
734
733
|
return rows.filter((r) => r && r.status === 'building')
|
|
735
734
|
.sort((a, b) => (Number(b.total_count) || 0) - (Number(a.total_count) || 0))[0] || null;
|
|
736
735
|
}
|
|
737
|
-
// task 1004774: the version tab under the Home banner
|
|
738
|
-
//
|
|
736
|
+
// task 1004774: the version tab under the Home banner, drawn as a bar.
|
|
737
|
+
// Hidden while no version is being built.
|
|
739
738
|
function renderVersionTab(v) {
|
|
740
739
|
const tab = document.getElementById('home-version');
|
|
741
740
|
if (!tab) return;
|
|
@@ -752,26 +751,11 @@
|
|
|
752
751
|
}
|
|
753
752
|
function renderGreeting() {
|
|
754
753
|
const h1 = document.getElementById('hall-h1');
|
|
755
|
-
const sub = document.getElementById('hall-sub');
|
|
756
754
|
const builder = ctx.lastMeState && ctx.lastMeState.builder;
|
|
757
755
|
if (h1 && builder) {
|
|
758
756
|
h1.textContent = `${greetingWord(new Date().getHours())}, ${builder.display_name || builder.github_login || 'builder'}`;
|
|
759
757
|
}
|
|
760
|
-
|
|
761
|
-
renderVersionTab(v);
|
|
762
|
-
if (!sub) return;
|
|
763
|
-
const parts = [];
|
|
764
|
-
if (v) parts.push(`${v.version_id} is ${Math.round(Number(v.percent_complete) || 0)} percent complete.`);
|
|
765
|
-
const n = ctx.lastClaimableCount;
|
|
766
|
-
if (typeof n === 'number') {
|
|
767
|
-
if (n === 0) parts.push('Nothing is claimable right now.');
|
|
768
|
-
else {
|
|
769
|
-
const word = n < SMALL_NUMBERS.length ? SMALL_NUMBERS[n] : fmtNum(n);
|
|
770
|
-
parts.push(`${word.charAt(0).toUpperCase()}${word.slice(1)} ${n === 1 ? 'task is' : 'tasks are'} claimable right now.`);
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
sub.textContent = parts.join(' ');
|
|
774
|
-
sub.hidden = parts.length === 0;
|
|
758
|
+
renderVersionTab(currentVersion(ctx.lastProgressState));
|
|
775
759
|
}
|
|
776
760
|
const ONBOARDING_STAGE_ORDER = [
|
|
777
761
|
'auth_complete',
|
|
@@ -42,12 +42,11 @@
|
|
|
42
42
|
(one-header-main-h1 — never a second one). The static text is the no-JS /
|
|
43
43
|
signed-out fallback and what brand-apply.js writes the hall's name into;
|
|
44
44
|
hall-render.js renderGreeting turns it into "Good morning, <name>" once
|
|
45
|
-
/me answers
|
|
46
|
-
the version is
|
|
47
|
-
|
|
45
|
+
/me answers. It has no sub-line (task 1004780): the version tab and Next
|
|
46
|
+
up already say how far the version is and what is ready. The label voice
|
|
47
|
+
is the board's own (.page-head--board). -->
|
|
48
48
|
<div class="page-head page-head--board page-head--hall">
|
|
49
49
|
<h1 id="hall-h1" class="page-head__h">Builders' Hall</h1>
|
|
50
|
-
<p id="hall-sub" class="page-head__sub" hidden></p>
|
|
51
50
|
<!-- task 1004774: Standing left the Home band for this chip. At rest it
|
|
52
51
|
shows the avatar and rank. On hover or keyboard focus the chip itself
|
|
53
52
|
widens to the left into the top of the card, and only the facts below
|
|
@@ -1030,6 +1030,10 @@
|
|
|
1030
1030
|
renderArtKeyBanner(artKeyNeedItem(me));
|
|
1031
1031
|
const res = await api.request('GET', `${API}/me/art-key/own`);
|
|
1032
1032
|
if (res.status === 401) { toAuth(); return; }
|
|
1033
|
+
// 404: the art pipeline is not installed, so there is no key to add here —
|
|
1034
|
+
// keep the section (and any banner) hidden rather than show a reload error
|
|
1035
|
+
// (task 1004786).
|
|
1036
|
+
if (res.status === 404) { renderArtKeyBanner(null); return; }
|
|
1033
1037
|
if (!res.ok) throw new Error(`art-key GET → ${res.status}`);
|
|
1034
1038
|
renderArtKeyCard(res.data);
|
|
1035
1039
|
wireArtKeyCard();
|
|
@@ -147,7 +147,10 @@
|
|
|
147
147
|
id: String(g.goal_id), title: g.title || 'A private goal', subtitle: g.subtitle || null, version: g.version_id, status: g.status,
|
|
148
148
|
taskTotal: Number(g.task_total) || 0, taskDone: Number(g.task_done) || 0, createdAt: g.created_at || null,
|
|
149
149
|
crit: (g.criteria || []).map((c) => ({ id: String(c.id), text: c.criterion_md || '', satisfied: c.satisfied === true, tasks: (c.task_ids || []).map(String), pendingReview: c.pending_review === true, uat: c.uat_state || null, uatLabel: uatLabel(c.uat_state), backendOnly: c.backend_only === true })),
|
|
150
|
-
|
|
150
|
+
// task 1004782 — abandoned (or merged-away) work carries no credits: it is listed, at zero, and never adds to a
|
|
151
|
+
// well's mass or a ring. A merged-away task is always abandoned (tasks.merged_into_task_id is CHECK-constrained to
|
|
152
|
+
// abandoned rows), so the status test covers both; /sky does not send the pointer.
|
|
153
|
+
tasks: (g.tasks || []).map((t) => ({ id: String(t.id), title: t.title || 'a task', status: t.status, credits: t.status === 'abandoned' || t.credits_reward == null ? 0 : Number(t.credits_reward), createdAt: t.created_on || null, sourceRef: t.idea_id != null ? String(t.idea_id) : null })),
|
|
151
154
|
};
|
|
152
155
|
}
|
|
153
156
|
const taskRow = (t) => ({ id: String(t.task_id ?? t.id), title: t.title || 'a task', status: t.status, goalId: t.goal_id != null ? String(t.goal_id) : null, origin: t.origin || null });
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.167",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.167",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.167",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -10040,5 +10040,21 @@
|
|
|
10040
10040
|
"id": "1004771",
|
|
10041
10041
|
"text": "Artist review is now a small tab on the right edge of every hall page in artist mode. Hover the tab and the review panel slides out; move the cursor back onto the page and it tucks away, keeping your draft. The page stays scro"
|
|
10042
10042
|
}
|
|
10043
|
+
],
|
|
10044
|
+
"1.21.166": [
|
|
10045
|
+
{
|
|
10046
|
+
"id": "1004780",
|
|
10047
|
+
"text": "Home's greeting stands on its own: the \"BONGOS-V2 is 83 percent complete. 150 tasks are claimable right now.\" line under \"Good morning\" is gone, because the version bar and the ready-work list beside it already say both."
|
|
10048
|
+
},
|
|
10049
|
+
{
|
|
10050
|
+
"id": "1004786",
|
|
10051
|
+
"text": "Settings no longer shows a broken image-generation key card on projects without the art tools. Before, builders were told their free art coverage had ended and to add a key, but the card could never load, so there was nowhere"
|
|
10052
|
+
}
|
|
10053
|
+
],
|
|
10054
|
+
"1.21.167": [
|
|
10055
|
+
{
|
|
10056
|
+
"id": "1004782",
|
|
10057
|
+
"text": "Goals no longer count abandoned work in their credit totals. Will goal drops by 241 credits of dropped tasks; across the project about 20,000 credits of abandoned estimates stop inflating goal sizes in the sky map and goal pag"
|
|
10058
|
+
}
|
|
10043
10059
|
]
|
|
10044
10060
|
}
|
package/src/bongos/routes/me.js
CHANGED
|
@@ -569,6 +569,9 @@ module.exports = function buildMeRouter() {
|
|
|
569
569
|
rank: builder?.rank,
|
|
570
570
|
ownKeySet: !!(geminiKeyRow && geminiKeyRow.enc),
|
|
571
571
|
sharedKeyConfigured: artSharedKeyConfigured(),
|
|
572
|
+
// task 1004786: the art-pipeline module provides this port when enabled;
|
|
573
|
+
// without it there are no key routes, so the need is not raised.
|
|
574
|
+
artPipeline: seams.hasProvider('art.sharedKeyConfigured'),
|
|
572
575
|
// founding grace (task 1002739 / direction §5.6): a newborn instance's
|
|
573
576
|
// founder gets a welcome roster, not a coverage nag. The module owns the
|
|
574
577
|
// policy (window length, copy); we pass the raw facts.
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.167'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -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', () => {
|
package/tests/hall_sky.mjs
CHANGED
|
@@ -216,6 +216,24 @@ await test('the rings are the criteria, closing as each is satisfied and thick w
|
|
|
216
216
|
assert.equal(g.tasks.find((t) => t.id === '500').sourceRef, '10', 'and which thought it was born of');
|
|
217
217
|
});
|
|
218
218
|
|
|
219
|
+
await test('abandoned work is listed at zero and adds nothing to a well or its ring (task 1004782)', async () => {
|
|
220
|
+
const base = well();
|
|
221
|
+
const p = await boot({ sky: sky([well({
|
|
222
|
+
criteria: [{ ...base.criteria[0], task_ids: ['500', '504'] }, { ...base.criteria[1], task_ids: ['501', '502', '503'] }],
|
|
223
|
+
tasks: [
|
|
224
|
+
...base.tasks,
|
|
225
|
+
{ id: '503', title: 'A dropped task', status: 'abandoned', credits_reward: 500, created_on: '2026-08-03', idea_id: null },
|
|
226
|
+
// merged away: the DB only allows merged_into_task_id on an abandoned row, so this is its whole shape
|
|
227
|
+
{ id: '504', title: 'A task folded into 500', status: 'abandoned', merged_into_task_id: '500', credits_reward: 90, created_on: '2026-08-04', idea_id: null },
|
|
228
|
+
],
|
|
229
|
+
})]) });
|
|
230
|
+
const g = p.S().goalById.get('7');
|
|
231
|
+
assert.equal(g.tasks.find((t) => t.id === '503').credits, 0, 'still listed, at zero');
|
|
232
|
+
assert.equal(g.tasks.find((t) => t.id === '504').credits, 0, 'a merged-away task is listed at zero too');
|
|
233
|
+
assert.equal(g.credits, 130, 'the well\'s mass is the live work only (60 + 40 + 30)');
|
|
234
|
+
same(g.crit.map((c) => c.credits), [60, 70], 'and the ring it sat on is not thickened by it');
|
|
235
|
+
});
|
|
236
|
+
|
|
219
237
|
await test('a private goal keeps its structure and none of its prose', async () => {
|
|
220
238
|
const p = await boot({ sky: sky([well({ goal_id: '8', title: null, subtitle: null, criteria: [{ id: '80', criterion_id: 'c1', criterion_md: null, satisfied: false, pending_review: false, task_ids: [] }], tasks: [{ id: '800', title: null, status: 'ready', credits_reward: 10, created_on: '2026-08-01', idea_id: null }] })]) });
|
|
221
239
|
const g = p.S().goalById.get('8');
|
|
@@ -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);
|
|
@@ -164,5 +164,17 @@ test('hall: the tooltip names the mode and what actually pays', () => {
|
|
|
164
164
|
assert.match(note, /guide, not a payout/i);
|
|
165
165
|
});
|
|
166
166
|
|
|
167
|
+
// task 1004782 — abandoned and merged-away tasks are worth 0 wherever a total
|
|
168
|
+
// adds them up, on every reward mode.
|
|
169
|
+
test('hall: an abandoned or merged-away task is worth 0, whatever its estimate', () => {
|
|
170
|
+
for (const mode of [CPO, PAYS, undefined]) {
|
|
171
|
+
const B = loadBrandApply(mode);
|
|
172
|
+
assert.equal(B.taskCreditValue({ status: 'abandoned', credits_reward: 60 }), 0);
|
|
173
|
+
assert.equal(B.taskCreditValue({ status: 'abandoned', credits_reward: 60, credits_awarded: 12 }), 0);
|
|
174
|
+
assert.equal(B.taskCreditValue({ status: 'ready', credits_reward: 60, merged_into_task_id: 9 }), 0);
|
|
175
|
+
assert.equal(B.taskCreditValue(READY_TASK), 60, 'live work keeps its value');
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
167
179
|
console.log(`\nreward_mode_surfacing: ${passed} passed, ${failed} failed`);
|
|
168
180
|
process.exit(failed ? 1 : 0);
|
|
@@ -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);
|