@bongos/core 1.25.31 → 1.25.32

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/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.25.31'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.25.32'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -112,8 +112,9 @@ test('the sky tokens are art: no control, no text and no chrome spends one', ()
112
112
  const sel = m[1].trim();
113
113
  if (f === 'style.css' && /(^|\n):root$/.test(sel)) continue; // the token block itself
114
114
  // only the two scenes spend them: the City page's frame, stage and cloud; Home's scene, its
115
- // stage, and (in game mode) the ground of the Home view
116
- if (!/^(\.city-frame|\.city-stage|\.city-cloud|\.city-stars|\.hc-svg|\.hc-cloud|\.hc-stage|html\[data-game="on"\] body\[data-page="home"\]$)/.test(sel)) { hits.push(`${f}: ${sel}`); continue; }
115
+ // stage, and (in game mode) the ground of the Home view; and the cloud scene behind the welcome
116
+ // and the game's start screens (task 1005308)
117
+ if (!/^(\.city-frame|\.city-stage|\.city-cloud|\.city-stars|\.hc-svg|\.hc-cloud|\.hc-stage|\.cloud-scene|html\[data-game="on"\] body\[data-page="home"\]$)/.test(sel)) { hits.push(`${f}: ${sel}`); continue; }
117
118
  for (const d of m[2].split(';')) {
118
119
  const prop = d.split(':')[0].trim();
119
120
  if (/--sky-/.test(d) && (prop === 'color' || /^border|^outline|^caret|^text-/.test(prop))) hits.push(`${f}: ${sel} { ${prop} }`);
@@ -95,13 +95,14 @@ test('the load screen fills one segment per week, up to the week it is showing (
95
95
  assert.equal(G.segsDone(0, 0), 0);
96
96
  });
97
97
 
98
- test('a fresh sign-in is handed to the start screen with the still, and the craft picker waits for the world', () => {
98
+ test('a fresh sign-in is handed to the start screen with the city in the clouds, and the craft picker waits for the world', () => {
99
99
  const s = sandbox({ store: { 'hall-welcome-signin': '1' } });
100
100
  const access = { authed: true }, builder = { id: 7, display_name: 'Ada' };
101
101
  assert.equal(s.window.OTBWelcome.onAccess(access, builder), true, 'the shell is told a screen is up, so it asks no picker');
102
102
  assert.equal(s.started.length, 1);
103
103
  assert.equal(s.started[0].name, 'Harbor');
104
- assert.match(s.started[0].art, /^\/builders\/welcome-(day|night)\.webp$/);
104
+ assert.equal(typeof s.started[0].scene, 'function', 'the scene is painted, not a picture (task 1005308)');
105
+ assert.equal(s.started[0].art, undefined, 'the circuit-city painting is retired');
105
106
  assert.equal(s.store['hall-welcome-signin'], undefined, 'the flag is spent');
106
107
  assert.deepEqual(s.asked, [], 'nothing is asked while the start screen is up');
107
108
  s.started[0].done();
@@ -1,8 +1,8 @@
1
1
  // tests/welcome_boot.mjs — the welcome's pure parts (tasks 1004785, 1004818).
2
2
  //
3
3
  // WHY. The welcome decides things a reader would only notice going wrong over days: whether today
4
- // is the first visit, whether it is day or night (and so which painting), what the banner names,
5
- // where each role goes, and whether the motion still sits on the painting. Those are pure, so this
4
+ // is the first visit, whether it is day or night, what the banner names, where each role goes,
5
+ // and that the scene is the city in the clouds with its doors taken out (task 1005308). Those are pure, so this
6
6
  // runs the REAL welcome-boot.js (it is Node-require-safe). The overlay itself is
7
7
  // tests/welcome_boot_browser.mjs.
8
8
  import assert from 'node:assert/strict';
@@ -39,40 +39,48 @@ test('the greeting follows the local hour, and the name falls back sensibly', ()
39
39
  assert.equal(W.nameOf({}), 'builder');
40
40
  });
41
41
 
42
- test('day is 06:00 to 17:59 local, night the rest, and each has its painting (task 1004818)', () => {
42
+ test('day is 06:00 to 17:59 local, night the rest (the ink and the veil follow it)', () => {
43
43
  const at = (h, m = 0) => W.timeOfDay(new Date(2026, 9, 23, h, m));
44
44
  assert.deepEqual([at(5, 59), at(6), at(12), at(17, 59), at(18), at(23, 30), at(0)],
45
45
  ['night', 'day', 'day', 'day', 'night', 'night', 'night']);
46
- assert.equal(W.artFor('day'), '/builders/welcome-day.webp');
47
- assert.equal(W.artFor('night'), '/builders/welcome-night.webp');
48
46
  });
49
47
 
50
- test('both paintings ship, as WebP, in the one shape the motion is laid out for', () => {
51
- for (const url of Object.values(W.ART)) {
52
- const file = path.join(HALL, url.replace(/^\/builders\//, ''));
53
- const buf = fs.readFileSync(file);
54
- assert.equal(buf.toString('latin1', 0, 4) + buf.toString('latin1', 8, 12), 'RIFFWEBP', url + ' is a WebP');
55
- assert.ok(buf.length < 400 * 1024, url + ' stays small enough for a first paint');
56
- // The VP8X header carries the canvas size (24-bit, minus one) at bytes 24..29.
57
- assert.equal(buf.toString('latin1', 12, 16), 'VP8X', url + ' carries its size');
58
- const w = 1 + buf.readUIntLE(24, 3), h = 1 + buf.readUIntLE(27, 3);
59
- assert.deepEqual([w, h], [W.AW, W.AH], url + ' is the painting the lights were placed on');
48
+ // Task 1005308 (owner, 2026-11-14): the circuit-city painting is retired everywhere; the scene is the
49
+ // city in the clouds Home's own painters draw (task 1005196), as a backdrop.
50
+ test('the circuit-city painting is retired: no picture file, no lights pinned to its pixels', () => {
51
+ for (const name of ['welcome-day.webp', 'welcome-night.webp']) assert.equal(fs.existsSync(path.join(HALL, name)), false, name + ' is gone');
52
+ for (const gone of ['ART', 'artFor', 'TOWERS', 'LANES', 'AW', 'AH', 'pulseRuns', 'BEACONS']) assert.equal(W[gone], undefined, gone + ' went with the painting');
53
+ for (const file of ['welcome-boot.js', 'welcome-boot.css', 'shell-game.js', 'shell-game.css', 'index.html']) {
54
+ assert.doesNotMatch(fs.readFileSync(path.join(HALL, file), 'utf8'), /welcome-(day|night)\.webp|wb__img|gs__img|wb-motion/, file + ' names no part of the painting');
60
55
  }
61
56
  });
62
57
 
63
- test('every light sits inside the painting, and every pulse climbs inside its tower', () => {
64
- const inside = ([x, y]) => x >= 0 && x <= W.AW && y >= 0 && y <= W.AH;
65
- assert.equal(W.BEACONS, undefined, 'the blinking mast lights are gone (task 1005087)');
66
- for (const t of W.TOWERS) assert.ok(inside([t.x, t.y]) && inside([t.x + t.w, t.y + t.h]), 'tower inside the painting');
67
- const runs = W.pulseRuns(W.TOWERS, 3, 7);
68
- assert.equal(runs.length, W.TOWERS.length * 3);
69
- for (const r of runs) {
70
- const t = W.TOWERS[r.tower];
71
- assert.ok(r.x >= t.x && r.x <= t.x + t.w, 'a pulse runs up its own tower');
72
- assert.ok(r.y1 > r.y0 && r.y0 >= t.y && r.y1 <= t.y + t.h, 'bottom to top, within the tower');
73
- assert.ok(r.dur > 0 && r.delay >= 0);
58
+ test("the scene is the city in the clouds, drawn by Home's own painters, with every door taken out", async () => {
59
+ const vm = await import('node:vm');
60
+ const ctx = { window: {}, console };
61
+ vm.createContext(ctx);
62
+ vm.runInContext(fs.readFileSync(path.join(HALL, 'city-draw.js'), 'utf8'), ctx);
63
+ const Gov = ctx.window.GovCity;
64
+ const Draw = require(path.join(HALL, 'home-city-draw.js'));
65
+ assert.ok(Gov && Draw, 'the painters load');
66
+ const s = W.sceneShapes(Draw, Gov);
67
+ assert.match(s.viewBox, /^0 0 1000 \d+$/);
68
+ const all = [];
69
+ (function walk(n) { all.push(n); n.kids.forEach(walk); })({ tag: 'root', attrs: {}, kids: s.nodes });
70
+ assert.ok(all.some((n) => /hc-cloud/.test(n.attrs.class || '')), 'it stands in cloud');
71
+ assert.ok(all.filter((n) => n.tag === 'path').length > 50, 'the core and the museum are drawn');
72
+ assert.equal(all.filter((n) => n.tag === 'a').length, 0, 'no link: it is a backdrop');
73
+ for (const n of all) {
74
+ for (const k of ['href', 'tabindex', 'aria-label', 'data-tip', 'data-tip-body', 'data-goal']) assert.equal(n.attrs[k], undefined, k + ' is taken out');
75
+ if (n.attrs.id) assert.ok(n.attrs.id.startsWith(W.SCENE_ID), "its ids never meet Home's own city's");
76
+ for (const v of Object.values(n.attrs)) assert.doesNotMatch(String(v), /url\(#(?!cs-)/, 'every reference points at its own prefixed id');
77
+ assert.doesNotMatch(n.attrs.class || '', /hc-quiet/, 'nothing is faded: the core leads');
74
78
  }
75
- assert.deepEqual(W.pulseRuns(W.TOWERS, 3, 7), runs, 'seeded: every visit draws the same picture');
79
+ const json = JSON.stringify(s);
80
+ assert.doesNotMatch(json, /#[0-9a-f]{6}\b/i, "no typed colour: the hour's sky tokens only");
81
+ assert.match(json, /--sky-now-/, "it paints in the hour's sky");
82
+ assert.equal(W.sceneShapes(null, Gov), null, 'no painters: no city, and the sky stands alone');
83
+ assert.equal(W.paintScene(null), false);
76
84
  });
77
85
 
78
86
  test('the banner is the logo, named from the branding pack, never a hardcoded word (task 1004953)', () => {
@@ -1,9 +1,9 @@
1
1
  // tests/welcome_boot_browser.mjs — the welcome in a real browser (tasks 1004785, 1004818).
2
2
  //
3
3
  // WHY. "Once a day", "Skip", "the craft picker does not also open", "Patron only with a seat", the day
4
- // and night paintings and the slow way out are behaviour across the real shell.js, craft-mode.js and
4
+ // and night veils over the city in the clouds and the slow way out are behaviour across the real shell.js, craft-mode.js and
5
5
  // welcome-boot.js together — the shell is what decides which of the two greets a builder — so this
6
- // serves the REAL Home markup, the REAL hall scripts and the REAL paintings from a routed origin
6
+ // serves the REAL Home markup and the REAL hall scripts (the city's painters too) from a routed origin
7
7
  // (localStorage needs one) in the ui-design kit's launcher. Every wait is on DOM state, never on a
8
8
  // flag that flips before the DOM settles (the task 1004780 CI flake). Skips when the machine has no
9
9
  // system Edge or Chrome, as ui_design_kit_fit does.
@@ -33,7 +33,9 @@ const INDEX = fs.readFileSync(path.join(HALL, 'index.html'), 'utf8')
33
33
  .replace(/<script\b[^>]*>[\s\S]*?<\/script>/g, '')
34
34
  .replace(/\{\{[^}]*\}\}/g, '');
35
35
  const STUB = '<script>window.__BRANDING__ = { identity: { worldName: "Harbor" } }; window.BongosClient = { createClient: () => ({ request: async () => ({ ok: false, status: 404, data: null }) }) };</script>';
36
- const SCRIPTS = ['dom-utils.js', 'shell-trail.js', 'craft-mode.js', 'welcome-boot.js', 'shell.js'] // dom-utils first, as on the real page (task 1004876)
36
+ // The city's three painters draw the scene (task 1005308: the city in the clouds, not a painting).
37
+ const PAINTERS = ['sky-time.js', 'city-draw.js', 'home-city-draw.js'];
38
+ const SCRIPTS = ['dom-utils.js', ...PAINTERS, 'shell-trail.js', 'craft-mode.js', 'welcome-boot.js', 'shell.js'] // dom-utils first, as on the real page (task 1004876)
37
39
  .map((f) => `<script src="/builders/${f}"></script>`).join('');
38
40
  const PAGE = INDEX.replace('</body>', STUB + SCRIPTS + '</body>');
39
41
 
@@ -46,7 +48,7 @@ async function hall({ art = true, reducedMotion = 'no-preference', time = null }
46
48
  if (p === '/api/bongos/auth/web/start') return route.fulfill({ contentType: 'text/html', body: '<!doctype html><title>github</title><p id="auth">sign in</p>' });
47
49
  if (p === '/studio' || p === '/city') return route.fulfill({ contentType: 'text/html', body: '<!doctype html><title>room</title><p id="room">' + p + '</p>' });
48
50
  const ext = path.extname(p);
49
- if (ext === '.webp' && !art) return route.fulfill({ status: 404, body: '' });
51
+ if (!art && PAINTERS.includes(path.basename(p))) return route.fulfill({ status: 404, body: '' });
50
52
  const f = path.join(HALL, p.replace(/^\/builders\//, ''));
51
53
  if (p.startsWith('/builders/') && TYPES[ext] && fs.existsSync(f)) return route.fulfill({ contentType: TYPES[ext], body: fs.readFileSync(f) });
52
54
  return route.fulfill({ status: 404, body: '' });
@@ -91,8 +93,8 @@ test('first visit of the day: the welcome, not the craft picker; Skip brings the
91
93
  'the welcome shows, the picker is marked asked and does not stack on it');
92
94
  assert.equal(await page.evaluate(() => document.querySelector('.wb__banner').tagName), 'DIV',
93
95
  'over a signed-in hall the shell keeps the one <header>; the banner is a plain block (task 1005080)');
94
- // The painting loads and its motion is laid over it.
95
- await page.waitForFunction(() => { const i = document.querySelector('#welcome-boot .wb__img'); return i && i.complete && i.naturalWidth === 1024; });
96
+ // The city in the clouds is drawn behind it (task 1005308).
97
+ await page.waitForSelector('#welcome-boot .cloud-scene .cloud-scene__svg path');
96
98
  assert.equal(await page.isVisible('[data-wb-skip]'), true);
97
99
  await page.click('[data-wb-skip]');
98
100
  await done(page);
@@ -227,17 +229,18 @@ test('roles: a seat holder sees Patron; Artisan stays on Home in Artisan', { ski
227
229
  await ctx.close();
228
230
  });
229
231
 
230
- test('no painting: the greeting and the choice still come up, over the sky colour', { skip }, async () => {
232
+ test('no city painters: the greeting and the choice still come up, over the sky colour', { skip }, async () => {
231
233
  const ctx = await hall({ art: false });
232
234
  const { page } = await open(ctx);
233
235
  await signIn(page);
234
236
  await done(page);
235
237
  const s = await page.evaluate(() => ({
236
238
  bare: document.getElementById('welcome-boot').classList.contains('wb--bare'),
237
- img: document.querySelector('#welcome-boot .wb__img').hidden,
239
+ city: !!document.querySelector('#welcome-boot .cloud-scene__svg'),
240
+ sky: !!document.querySelector('#welcome-boot .cloud-scene'),
238
241
  choices: document.querySelectorAll('.wb__choice').length,
239
242
  }));
240
- assert.deepEqual(s, { bare: true, img: true, choices: 2 });
243
+ assert.deepEqual(s, { bare: true, city: false, sky: true, choices: 2 });
241
244
  await ctx.close();
242
245
  });
243
246
 
@@ -256,15 +259,16 @@ const look = (page) => page.evaluate(() => {
256
259
  return {
257
260
  day: r.classList.contains('wb--day'), night: r.classList.contains('wb--night'),
258
261
  veil: getComputedStyle(r.querySelector('.wb__veil')).backgroundColor,
259
- art: r.querySelector('.wb__img').getAttribute('src'),
260
- beacons: r.querySelectorAll('.wb-beacon').length,
262
+ city: !!r.querySelector('.cloud-scene__svg'),
263
+ hour: document.documentElement.getAttribute('data-sky-hour'),
264
+ img: r.querySelectorAll('img:not(.wb__logo)').length,
261
265
  };
262
266
  });
263
267
 
264
- test('by day the day painting opens from a white veil; by night the night one from black, with its lights', { skip }, async () => {
268
+ test('by day the city opens from a white veil, by night from black, in the sky of that hour (task 1005308)', { skip }, async () => {
265
269
  for (const [time, want] of [
266
- [new Date(2026, 9, 23, 10, 0), { day: true, night: false, veil: 'rgb(255, 255, 255)', art: '/builders/welcome-day.webp', beacons: 0 }],
267
- [new Date(2026, 9, 23, 22, 0), { day: false, night: true, veil: 'rgb(0, 0, 0)', art: '/builders/welcome-night.webp', beacons: 0 }], // task 1005087 took the beacons out
270
+ [new Date(2026, 9, 23, 10, 0), { day: true, night: false, veil: 'rgb(255, 255, 255)', city: true, hour: '10', img: 0 }],
271
+ [new Date(2026, 9, 23, 22, 0), { day: false, night: true, veil: 'rgb(0, 0, 0)', city: true, hour: '22', img: 0 }],
268
272
  ]) {
269
273
  const ctx = await hall({ time });
270
274
  const { page } = await open(ctx);
@@ -305,19 +309,21 @@ test('the banner is the animated logo, the motion is laid on, and the drawn towe
305
309
  const ctx = await hall();
306
310
  const { page } = await open(ctx);
307
311
  await signIn(page);
308
- await page.waitForSelector('#welcome-boot.wb--lit .wb-motion');
312
+ await page.waitForSelector('#welcome-boot.wb--lit .cloud-scene__svg');
309
313
  const s = await page.evaluate(() => ({
310
314
  banner: document.querySelector('.wb__banner').textContent,
311
315
  name: document.querySelector('.wb__banner .wb__name').textContent,
312
316
  mark: !!document.querySelector('.wb__banner svg.wb__mark .wb__moonlet'),
313
317
  ringAnim: getComputedStyle(document.querySelector('.wb__ring')).animationName,
314
- pulses: document.querySelectorAll('#wb-fx .wb-pulse').length,
315
- cars: document.querySelectorAll('#wb-fx .wb-car').length,
316
- old: document.querySelectorAll('.wb-floor, .wb-bldg, .wb-person, #wb-scene, #wb-beam, #wb-city').length,
318
+ // anywhere on the page, under any container: the painting's lights must not come back
319
+ pulses: document.querySelectorAll('.wb-pulse, .wb-pulses').length,
320
+ cars: document.querySelectorAll('.wb-car').length,
321
+ // every earlier scene: the drawn tower and city, the painting and its light layer
322
+ old: document.querySelectorAll('.wb-floor, .wb-bldg, .wb-person, #wb-beam, #wb-city, #wb-fx, .wb-motion, .wb-moon, .wb-star, .wb__img').length,
317
323
  }));
318
324
  assert.doesNotMatch(s.banner, /Working on the core of/, 'task 1004953: the banner text is gone');
319
325
  assert.deepEqual({ name: s.name, mark: s.mark, ringAnim: s.ringAnim, pulses: s.pulses, cars: s.cars, old: s.old },
320
- { name: 'Harbor', mark: true, ringAnim: 'wb-ring', pulses: 21, cars: 3, old: 0 });
326
+ { name: 'Harbor', mark: true, ringAnim: 'wb-ring', pulses: 0, cars: 0, old: 0 }, 'the painting\'s lights went with it (task 1005308)');
321
327
  await ctx.close();
322
328
  });
323
329