@bongos/core 1.19.865 → 1.19.866

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.
@@ -0,0 +1,324 @@
1
+ // tests/hall_builders_page_pass.mjs — the three parts of task 1004004, each
2
+ // pinned by RUNNING the shipped renderer rather than reading it.
3
+ //
4
+ // The three were filed separately (1002851, 1002837, 1002835) and consolidated
5
+ // because their touches[] overlap serialised them anyway. What they share is one
6
+ // failure mode: a fact the hall already holds was rendered as a footnote, as
7
+ // nothing at all, or not rendered at all, so the reader had to go and look it up
8
+ // somewhere else.
9
+ //
10
+ // A (1002851) — the signed-out landing. Signing in was a footnote LINK, but
11
+ // only on the `apply` door; `open` and `invite_only` already render a real
12
+ // .landing__btn inside the primary door. So the button belongs on `apply`
13
+ // alone — adding one to all three would have made a third scattered path.
14
+ // These tests hold BOTH halves: the button appears on `apply`, and the two
15
+ // doors that already had one do NOT grow a second.
16
+ // B (1002837) — the roster's place cell. A builder's number is the id every
17
+ // task row and ship note cites, and the roster was the one place that knew
18
+ // who the builder was and didn't print it. Degrades to the place alone when
19
+ // the payload carries no id.
20
+ // C (1002835) — the peer-acclaim feed. A karma vote was cast on a title. The
21
+ // grade and the cost now ride along, and ABSENCE RENDERS AS ABSENCE: an
22
+ // ungraded ship says "not graded" and an unlogged cost says "no cost
23
+ // logged", because 0.0 and $0.00 are measurements that were never taken.
24
+ //
25
+ // Pure vm + file-read (no DB, no network) — runs in the DB-free unit gate.
26
+ import test from 'node:test';
27
+ import assert from 'node:assert/strict';
28
+ import fs from 'node:fs';
29
+ import path from 'node:path';
30
+ import vm from 'node:vm';
31
+ import { fileURLToPath } from 'node:url';
32
+
33
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
34
+ const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
35
+ const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8').replace(/\r\n/g, '\n');
36
+
37
+ const escapeHtml = (s) => String(s).replace(/[&<>"']/g, (c) => (
38
+ { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]
39
+ ));
40
+
41
+ // Count real sign-in CONTROLS (a .landing__btn), as opposed to prose links.
42
+ const countButtons = (html) => (html.match(/class="[^"]*landing__btn[^"]*"/g) || []).length;
43
+
44
+ // =========================================================================
45
+ // PART A — the returning builder's door (task 1002851)
46
+ // =========================================================================
47
+
48
+ // Render the signed-out landing for one joinability mode and hand back the HTML
49
+ // the page actually wrote. Runs the shipped hall-render.js in a DOM stub thin
50
+ // enough that anything renderLanding touches beyond the landing root would
51
+ // throw rather than pass silently.
52
+ function hallRender(joinability) {
53
+ let written = '';
54
+ const el = (extra = {}) => ({
55
+ hidden: false,
56
+ textContent: '',
57
+ contains() { return false; },
58
+ addEventListener() {},
59
+ set innerHTML(v) { written = v; },
60
+ get innerHTML() { return written; },
61
+ ...extra,
62
+ });
63
+ const nodes = { '#landing-root': el(), '#welcome-line': el() };
64
+
65
+ const sandbox = {
66
+ window: {
67
+ OTB: {
68
+ escapeHtml,
69
+ $: (sel) => nodes[sel] || null,
70
+ fmtNum: (n) => String(n),
71
+ moduleOn: () => true,
72
+ },
73
+ OTBShell: { applyAccess() {} },
74
+ // hall-render builds its pagers at factory time; the landing uses none.
75
+ OTBKit: { makePager: () => ({ page: 1 }), pagerBarHtml: () => '' },
76
+ __BRANDING__: joinability ? { project: { joinability } } : {},
77
+ },
78
+ document: { querySelectorAll: () => [] },
79
+ console,
80
+ };
81
+ vm.createContext(sandbox);
82
+ vm.runInContext(read('hall-render.js'), sandbox, { filename: 'hall-render.js' });
83
+
84
+ const hall = sandbox.window.OTBHallRender({
85
+ API: '/api/bongos',
86
+ RANK_ORDER: { xenos: 1, thetes: 2, metic: 3, archon: 4 },
87
+ api: { request: () => Promise.resolve({ status: 200, ok: true, data: {} }) },
88
+ brandWorld: () => 'Testworld',
89
+ curLabel: () => 'Credits',
90
+ emptyRow: () => '',
91
+ fmtDate: () => '',
92
+ mins: () => '',
93
+ normalizeRank: (r) => r,
94
+ pct: () => 0,
95
+ postJSON: () => Promise.resolve({}),
96
+ rankLabel: (r) => r,
97
+ rankWeight: () => 0,
98
+ signClasses: () => '',
99
+ toast: () => {},
100
+ usd: (n) => `$${n}`,
101
+ });
102
+ return { hall, html: () => written };
103
+ }
104
+
105
+ // Render the signed-out landing for one joinability mode and hand back the
106
+ // HTML the page actually wrote.
107
+ function renderLandingHtml(joinability) {
108
+ const { hall, html } = hallRender(joinability);
109
+ hall.renderLanding();
110
+ return html();
111
+ }
112
+
113
+ test('A: the apply door gives signing in a real button, not a footnote link', () => {
114
+ const html = renderLandingHtml('apply');
115
+ assert.match(html, /landing__returning/, 'the apply door renders a returning-builder block');
116
+ assert.match(
117
+ html,
118
+ /class="landing__btn landing__btn--quiet" href="\/api\/bongos\/auth\/web\/start"/,
119
+ 'that block carries a real sign-in control in the quiet fill',
120
+ );
121
+ // The whole point of the change: the footnote link is GONE from this door, so
122
+ // there is exactly one sign-in path and it is the button.
123
+ assert.ok(
124
+ !/Already a member\? <a /.test(html),
125
+ 'the apply door no longer repeats sign-in as a prose link under the form',
126
+ );
127
+ assert.match(html, /We store only your GitHub login/, 'the privacy sentence survives the note');
128
+ // Two controls total: request access, and sign in. Not three.
129
+ assert.equal(countButtons(html), 2, 'the apply door carries the request button and the sign-in button');
130
+ });
131
+
132
+ test('A: the two doors that already had a sign-in button do not grow a second', () => {
133
+ for (const mode of ['open', 'invite_only']) {
134
+ const html = renderLandingHtml(mode);
135
+ assert.ok(
136
+ !/landing__returning/.test(html),
137
+ `the ${mode} door does not add a returning block — its primary door already signs you in`,
138
+ );
139
+ assert.equal(countButtons(html), 1, `the ${mode} door carries exactly one sign-in control`);
140
+ assert.match(html, /Already a member\? <a /, `the ${mode} door keeps its prose note for returning builders`);
141
+ }
142
+ });
143
+
144
+ test('A: an older branding pack with no joinability falls back to the apply door', () => {
145
+ // joinabilityMode() treats anything but open/invite_only as 'apply', so the
146
+ // button must follow that fallback — not sit behind a field an older pack
147
+ // never sent.
148
+ assert.match(renderLandingHtml(null), /landing__returning/);
149
+ });
150
+
151
+ test('A: the quiet fill is a defined variant, not an inline colour', () => {
152
+ const css = read('style.css');
153
+ const i = css.indexOf('\n.landing__btn--quiet {');
154
+ assert.ok(i > 0, 'style.css defines .landing__btn--quiet');
155
+ const block = css.slice(i, css.indexOf('}', i));
156
+ assert.deepEqual(block.match(/#[0-9a-f]{3,8}\b/gi) || [], [], 'the variant carries no literal colour');
157
+ // Same geometry as the request button means it inherits .landing__btn's
158
+ // min-height and padding rather than restating them.
159
+ assert.ok(!/min-height|padding/.test(block), 'the variant overrides fill only, never geometry');
160
+ });
161
+
162
+ // =========================================================================
163
+ // PART B — the builder's number on the roster (task 1002837)
164
+ // =========================================================================
165
+
166
+ // roster.js is an IIFE that fetches and mounts. Run it against a stubbed kit,
167
+ // capture the rowHtml it hands makeLedger, and call the real function.
168
+ async function rosterRowHtml(rows) {
169
+ let rowFn = null;
170
+ const elStub = { innerHTML: '', className: '', textContent: '', hidden: false };
171
+ const sandbox = {
172
+ window: {
173
+ OTB: { fmtInt: (n) => String(n == null ? 0 : n), rankLabel: (r) => r },
174
+ OTBBranding: { currencyLabel: () => 'Credits' },
175
+ OTBKit: {
176
+ esc: escapeHtml,
177
+ makePager: () => ({ page: 1 }),
178
+ makeFilterBar: () => {},
179
+ makeLedger: (cfg) => { rowFn = cfg.rowHtml; return { render() {} }; },
180
+ paginate: (xs) => xs,
181
+ },
182
+ },
183
+ document: { getElementById: () => ({ ...elStub }) },
184
+ fetch: () => Promise.resolve({ ok: true, json: () => Promise.resolve({ leaderboard: rows }) }),
185
+ console,
186
+ };
187
+ vm.createContext(sandbox);
188
+ vm.runInContext(read('roster.js'), sandbox, { filename: 'roster.js' });
189
+ // Let the fetch promise chain drain.
190
+ await new Promise((r) => setImmediate(r));
191
+ assert.ok(rowFn, 'roster.js mounted a ledger and handed it a row renderer');
192
+ return rowFn;
193
+ }
194
+
195
+ const ROSTER_ROWS = [
196
+ { id: '1000025', github_login: 'someone', display_name: 'Someone', rank: 'metic', total_credits: 900, karma: 5, tokens_used: 10 },
197
+ { github_login: 'nameless', display_name: 'Nameless', rank: 'xenos', total_credits: 10, karma: 0, tokens_used: 1 },
198
+ ];
199
+
200
+ test('B: the roster prints the builder\'s number under their place', async () => {
201
+ const rowHtml = await rosterRowHtml(ROSTER_ROWS);
202
+ const html = rowHtml({ ...ROSTER_ROWS[0], __place: 1 });
203
+ assert.match(html, /<span class="ledger__id">#1000025<\/span>/, 'the id renders');
204
+ // Under the place, not beside the name — the place cell is what answers
205
+ // "which builder is this row".
206
+ const cell = html.slice(0, html.indexOf('</td>'));
207
+ assert.match(cell, /ledger__place/, 'the place cell is the one that carries it');
208
+ assert.match(cell, /1[\s\S]*ledger__id/, 'the place still comes first');
209
+ });
210
+
211
+ test('B: a payload with no id degrades to the place alone', async () => {
212
+ const rowHtml = await rosterRowHtml(ROSTER_ROWS);
213
+ const html = rowHtml({ ...ROSTER_ROWS[1], __place: 2 });
214
+ assert.ok(!/ledger__id/.test(html), 'no empty "#" is printed for a row the server sent no id for');
215
+ assert.match(html, /<td class="ledger__num ledger__place">2<\/td>/, 'the place renders on its own');
216
+ });
217
+
218
+ test('B: the id wears the same mono/faint voice as the login', () => {
219
+ const css = read('people.css');
220
+ const i = css.indexOf('.ledger__place .ledger__id {');
221
+ assert.ok(i > 0, 'people.css styles the roster id');
222
+ const block = css.slice(i, css.indexOf('}', i));
223
+ for (const token of ['var(--font-mono)', 'var(--ink-faint)']) {
224
+ assert.ok(block.includes(token), `the id spends ${token}, the voice .dir-login already uses`);
225
+ }
226
+ assert.match(block, /display: block/, 'it sits under the place rather than wrapping beside it');
227
+ });
228
+
229
+ // =========================================================================
230
+ // PART C — grade and cost on the peer-acclaim feed (task 1002835)
231
+ // =========================================================================
232
+
233
+ // acclaimEvidenceHtml is a view painter, so it lives in the hall-render carve
234
+ // (the same reason renderLanding does) and builders.js takes it from the
235
+ // factory. Read it off the same factory rather than re-deriving it.
236
+ const acclaimEvidence = () => hallRender().hall.acclaimEvidenceHtml;
237
+ test('C: a graded, costed ship carries both numbers', () => {
238
+ const html = acclaimEvidence()({ grade_score: '8.5', grade_passed: true, cost_usd: '12.3456' });
239
+ assert.match(html, /ledger__verdict--ok">8\.5 \/ 10</, 'a passing grade reads as passing');
240
+ assert.match(html, /acclaim__cost">\$12\.35</, 'the cost rounds to cents');
241
+ });
242
+
243
+ test('C: a failing grade is not dressed as a passing one', () => {
244
+ const html = acclaimEvidence()({ grade_score: '3.0', grade_passed: false, cost_usd: '1' });
245
+ assert.match(html, /ledger__verdict--danger">3\.0 \/ 10</);
246
+ });
247
+
248
+ test('C: absence renders as absence, never as zero', () => {
249
+ const ev = acclaimEvidence();
250
+ // The case that matters: a grader outage arrives as a NULL score (the route
251
+ // nulls it through gradeQualityWhere), and a voter must not read "0.0 / 10".
252
+ const ungraded = ev({ grade_score: null, grade_passed: null, cost_usd: '4' });
253
+ assert.match(ungraded, /ledger__verdict--none">not graded</);
254
+ assert.ok(!/0\.0 \/ 10/.test(ungraded), 'an ungraded ship is never scored 0.0');
255
+
256
+ // cost_usd PRESENT and null is the "logged nothing" case, distinct from the
257
+ // key being absent entirely (covered above) — both must not print $0.00.
258
+ const uncosted = ev({ grade_score: '9.0', grade_passed: true, cost_usd: null });
259
+ assert.match(uncosted, /ledger__verdict--none">no cost logged</);
260
+ assert.ok(!/\$0\.00/.test(uncosted), 'an unlogged cost is never printed as free');
261
+
262
+ // A genuine zero is still a measurement and must survive.
263
+ assert.match(ev({ grade_score: '0.0', grade_passed: false, cost_usd: '0' }), /0\.0 \/ 10/);
264
+ assert.match(ev({ grade_score: '0.0', grade_passed: false, cost_usd: '0' }), /\$0\.00/);
265
+ });
266
+
267
+ test('C: the evidence line hangs under the work, in the feed row', () => {
268
+ const src = read('builders.js');
269
+ assert.match(
270
+ src,
271
+ /<td>\$\{title\}\$\{acclaimEvidenceHtml\(t\)\}<\/td>/,
272
+ 'the row renders it beneath the title rather than adding a column',
273
+ );
274
+ });
275
+
276
+ test('C: a builder who may not vote is shown no cost figure at all', () => {
277
+ // The route OMITS cost_usd below Metic rather than nulling it, because a null
278
+ // means "nothing was logged" — telling a reader a figure is missing is a
279
+ // different (and false) statement from showing them no figure.
280
+ const html = acclaimEvidence()({ grade_score: '8.0', grade_passed: true });
281
+ assert.match(html, /8\.0 \/ 10/, 'the grade is a quality verdict and still shows');
282
+ assert.ok(!/no cost logged/.test(html), 'a withheld cost is never reported as an unlogged one');
283
+ assert.ok(!/acclaim__cost/.test(html), 'and no figure is printed');
284
+ assert.ok(!/ · /.test(html), 'the separator goes with the part it separated');
285
+ });
286
+
287
+ test('C: the route hands the cost only to the ranks that can vote', () => {
288
+ const route = fs.readFileSync(path.join(ROOT, 'modules/lifecycle/routes/tasks.js'), 'utf8');
289
+ const i = route.indexOf("router.get('/tasks/peer-votes/feed'");
290
+ const handler = route.slice(i, route.indexOf('// POST /tasks/peer-votes/tally', i));
291
+ assert.match(
292
+ handler,
293
+ /const tasks = canVote \? rows : rows\.map\(\(\{ cost_usd, \.\.\.rest \}\) => rest\);/,
294
+ 'the cost is stripped for a non-voter, by omission rather than by nulling',
295
+ );
296
+ assert.match(handler, /res\.json\(\{ tasks, can_vote: canVote \}\)/, 'and the gated rows are what is sent');
297
+ assert.ok(!/grade_score, \.\.\.rest/.test(handler), 'the grade is not spend and is not gated with it');
298
+ });
299
+
300
+ test('C: the route supplies both fields, and nulls a non-verdict grade', () => {
301
+ const route = fs.readFileSync(path.join(ROOT, 'modules/lifecycle/routes/tasks.js'), 'utf8');
302
+ const i = route.indexOf("router.get('/tasks/peer-votes/feed'");
303
+ assert.ok(i > 0, 'the feed route is still here');
304
+ const handler = route.slice(i, route.indexOf('// POST /tasks/peer-votes/tally', i));
305
+ for (const field of ['grade_score', 'grade_passed', 'cost_usd']) {
306
+ assert.ok(handler.includes(`AS ${field}`), `the feed selects ${field}`);
307
+ }
308
+ // The honesty half is server-side: a grader outage carries score 0 in
309
+ // task_grades, and only gradeQualityWhere tells it apart from a real 0.
310
+ assert.match(handler, /gradeQualityWhere\('g'\)/, 'an outage row is nulled, not passed through as 0');
311
+ // SUM over no rows is NULL, which is the "no cost logged" the client renders.
312
+ // A COALESCE here would turn every uncosted ship into a free one.
313
+ assert.ok(
314
+ !/COALESCE\(SUM\(amount_usd\)/.test(handler),
315
+ 'the cost sum is left NULL when nothing was logged',
316
+ );
317
+ // Both joins are indexed lookups. Pinned here so a future reader does not have
318
+ // to re-derive it: task_grades.task_id is UNIQUE and cost_log.task_id carries
319
+ // idx_cost_log_task. If either ever stops being true, this reds.
320
+ const g016 = fs.readFileSync(path.join(ROOT, 'migrations/016_pms_task_grades.sql'), 'utf8');
321
+ assert.match(g016, /task_id\s+BIGINT NOT NULL UNIQUE/, 'task_grades.task_id is unique, so indexed');
322
+ const m003 = fs.readFileSync(path.join(ROOT, 'migrations/003_pms.sql'), 'utf8');
323
+ assert.match(m003, /CREATE INDEX IF NOT EXISTS idx_cost_log_task ON cost_log \(task_id\)/, 'cost_log.task_id is indexed');
324
+ });
@@ -177,7 +177,17 @@ test('the hall landing renders one door per mode, and its form collapses on the
177
177
  assert.match(door, /mode === 'invite_only'/);
178
178
  assert.match(door, /there is nothing to apply for here/);
179
179
  assert.match(door, /id="access-request-form"/, 'the apply door keeps the request form');
180
- assert.match(hall, /\$\{landingDoorHtml\(joinabilityMode\(\)\)\}/, 'renderLanding paints the door the pack names');
180
+ // This read `${landingDoorHtml(joinabilityMode())}` inline. Task 1004004 gave
181
+ // the apply door a real sign-in button, so renderLanding resolves the mode
182
+ // ONCE and hands the same value to both doors. The invariant is unchanged and
183
+ // now reaches further: BOTH doors are painted from the pack's own answer, so
184
+ // neither can drift onto a second reading of joinability.
185
+ const landing = (hall.match(/function renderLanding\(\) \{[\s\S]*?\n \}\n/) || [''])[0];
186
+ assert.ok(landing, 'renderLanding() must exist');
187
+ assert.match(landing, /const mode = joinabilityMode\(\);/, 'the pack names the mode, once');
188
+ assert.match(landing, /\$\{landingDoorHtml\(mode\)\}/, 'renderLanding paints the door the pack names');
189
+ assert.match(landing, /\$\{returningDoorHtml\(mode\)\}/, 'and the returning-builder door off the same answer');
190
+ assert.equal((landing.match(/joinabilityMode\(\)/g) || []).length, 1, 'joinability is read once, not per door');
181
191
  assert.match(hall, /code === 'invite_only' \|\| code === 'open_enrollment'/, 'the refusal codes are handled, not shown red');
182
192
  });
183
193