@bongos/core 1.25.30 → 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/.bongos-core.json +42 -47
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/api/openapi.json +3 -6
- package/docs/copy-inventory.md +13 -13
- package/docs/copy-registry.json +14 -14
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +229 -203
- package/modules/government/docket.js +37 -6
- package/modules/hall-ui/public/city.js +1 -1
- package/modules/hall-ui/public/gov-feeds.js +2 -2
- package/modules/hall-ui/public/index.html +4 -4
- package/modules/hall-ui/public/shell-game.css +2 -3
- package/modules/hall-ui/public/shell-game.js +6 -5
- package/modules/hall-ui/public/welcome-boot.css +18 -31
- package/modules/hall-ui/public/welcome-boot.js +81 -97
- package/modules/hall-ui/records/start-screen.md +6 -6
- package/modules/hall-ui/records/welcome-tower.md +29 -20
- package/modules/lifecycle/routes/tasks.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/module-api.js +1 -1
- package/tests/docket_load_speed.mjs +99 -0
- package/tests/hall_city_living.mjs +1 -1
- package/tests/hall_gov_counts.mjs +1 -1
- package/tests/hall_gov_real_site.mjs +1 -1
- package/tests/hall_sky_time.mjs +3 -2
- package/tests/hall_start_screen.mjs +3 -2
- package/tests/hall_street_deck.mjs +1 -1
- package/tests/hall_street_depth.mjs +2 -2
- package/tests/welcome_boot.mjs +35 -27
- package/tests/welcome_boot_browser.mjs +25 -19
- package/modules/hall-ui/public/welcome-day.webp +0 -0
- package/modules/hall-ui/public/welcome-night.webp +0 -0
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
# The welcome — Home's first visit of the day (tasks 1004785, 1004818)
|
|
2
2
|
|
|
3
3
|
The owner's Builders' Hall idea 1001262, second half (the first is [`home-reshuffle.md`](home-reshuffle.md)).
|
|
4
|
-
Files: `public/welcome-boot.js` + `public/welcome-boot.css`,
|
|
5
|
-
`
|
|
4
|
+
Files: `public/welcome-boot.js` + `public/welcome-boot.css`, on Home only. The scene is drawn by
|
|
5
|
+
Home's own city painters (`sky-time.js`, `city-draw.js`, `home-city-draw.js`).
|
|
6
6
|
|
|
7
7
|
**How the look got here (2026-10-23, one day of owner rounds).** Task 1004785 built v5 "the tower"
|
|
8
8
|
([`docs/design/mocks/hall/welcome/v5.src.html`](../../../docs/design/mocks/hall/welcome/v5.src.html)),
|
|
9
9
|
drawn live from the Repo Atlas. The owner then asked for day and night and a slow way out, then
|
|
10
10
|
swapped the tower for a glowing module city with a lunch crew on a beam, dropped the crew, and
|
|
11
11
|
finally had Gemini paint the city by day and by night and asked for those to be animated (task
|
|
12
|
-
1004818). The atlas-drawn scene is gone
|
|
12
|
+
1004818). The atlas-drawn scene is gone.
|
|
13
|
+
|
|
14
|
+
**The painting is retired too (task 1005308, owner 2026-11-14: "retire it everywhere").** The scene
|
|
15
|
+
is now the city in the clouds from task 1005196, the same city Home and the City page draw, in the
|
|
16
|
+
hour's sky. The two WebP paintings and every light that was pinned to their pixels (the circuit
|
|
17
|
+
towers' pulses, the hover cars, the moon and stars) are deleted. The game's start and load screens
|
|
18
|
+
stand on the same scene (`records/start-screen.md`).
|
|
13
19
|
|
|
14
20
|
- **When.** A signed-in builder's first Home load of the LOCAL day, per builder id
|
|
15
21
|
(`localStorage['hall-welcome-day:<id>']`). Storage that throws is never shown it, because it could not
|
|
@@ -20,15 +26,16 @@ finally had Gemini paint the city by day and by night and asked for those to be
|
|
|
20
26
|
`welcome-boot.js` loads before the `shell-trail.js` / `craft-mode.js` / `shell.js` trio, which
|
|
21
27
|
`tests/craft_mode.mjs` keeps adjacent.
|
|
22
28
|
- **Day and night.** `timeOfDay` makes 06:00 to 17:59 local `day`, the rest `night`; the root wears
|
|
23
|
-
`.wb--day` or `.wb--night
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
`.wb--day` or `.wb--night`, which sets the ink. It opens from a veil (`.wb__veil`), white by day
|
|
30
|
+
and black by night, that lifts two frames after the scene is painted (or after 1.5s). The scene's
|
|
31
|
+
own colours follow the hour more finely (`sky-time.js`: night, dawn, midday, dusk, blended).
|
|
32
|
+
- **The scene is the city, as a backdrop.** `paintScene(host)` asks `OTBHomeCityDraw.citySvg` for an
|
|
33
|
+
empty week (the Cloud Bongos core and the museum, standing in cloud) and rewrites it with
|
|
34
|
+
`sceneShape`: every link becomes a plain group, tooltips and goal hooks are dropped, the faded
|
|
35
|
+
`hc-quiet` look is lifted so the core leads, and every id is prefixed `cs-` so it never meets the
|
|
36
|
+
ids of Home's own city on the same page. It covers the screen, standing on the bottom edge
|
|
37
|
+
(`xMidYMax slice`), over `.cloud-scene`'s sky gradient, with a slow drift and two clouds. The same
|
|
38
|
+
`paintScene` is handed to the game's start screen as `scene`.
|
|
32
39
|
- **Banner.** "Working on the core of <project>" from `__BRANDING__.identity` (never a hardcoded word),
|
|
33
40
|
with `core <version>` under it when the public `GET /version` reports one.
|
|
34
41
|
- **Sequence.** Veil lifts, the city powers up (pulses brighten), the greeting comes up at ~3.4s.
|
|
@@ -40,11 +47,13 @@ finally had Gemini paint the city by day and by night and asked for those to be
|
|
|
40
47
|
Artisan → stays on Home (the Ideator door, once `/thinking`, folded into Artist). A pick also calls
|
|
41
48
|
`OTBCraftMode.choose()` with patron / artist / artisan. **Patron shows only where
|
|
42
49
|
`OTBCraftMode.offered('patron')`** — the patron craft is a seat — and the list is built when "Start with your role" is pressed, after the seats have settled.
|
|
43
|
-
- **No
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
- **No city painters** on the page: the root wears `.wb--bare`, the hour's sky gradient stands alone,
|
|
51
|
+
and everything else works.
|
|
52
|
+
- **The scene's colours are the hour's sky tokens only** (`--sky-now-*`); the welcome's ink, veil and
|
|
53
|
+
panels keep their fixed day/night literals, on `tests/hall_audit.mjs`'s literal exemptions.
|
|
54
|
+
- **Tests.** `tests/welcome_boot.mjs` (the day rule, that the painting and its lights are gone, the
|
|
55
|
+
scene's shapes with no door, no unprefixed id and no typed colour, the banner, the role map, the
|
|
56
|
+
load order); `tests/welcome_boot_browser.mjs` runs Home's markup with the real shell, craft mode,
|
|
57
|
+
welcome and city painters in a browser: once a day, Skip, Esc, no double picker, the seat rule, no
|
|
58
|
+
painters, reduced motion, the city in the hour's sky by day and night, the slow exit, and that the
|
|
59
|
+
painting's lights are gone.
|
|
@@ -219,7 +219,7 @@ module.exports = function buildTasksRouter() {
|
|
|
219
219
|
// RETURNED feed, never the query — lifecycle/CLAUDE.md has the why.
|
|
220
220
|
const claimNeeds = parseNeedsFilter(req.query.needs); if (claimNeeds && claimNeeds.bad) return badNeedsFilter(res); // task 1004950
|
|
221
221
|
const claimLimit = posInt(req.query.limit, 'bad_limit', 1000);
|
|
222
|
-
if (claimLimit && claimLimit.bad) return res.fail('bad_limit', { status: 400, message: 'limit must be a positive integer (max 1000).' });
|
|
222
|
+
if (claimLimit && claimLimit.bad) return res.fail('bad_limit', { status: 400, message: 'limit must be a positive integer (max 1000).' }); if (req.query.view === 'claims') return res.json({ active_claims: await db.listActiveClaims() }); // open claims only, no claimable feed: the street needs just these (task 1005310)
|
|
223
223
|
const [feed, active] = await Promise.all([
|
|
224
224
|
db.listClaimableTasks({
|
|
225
225
|
versionId: version || null, builderId: req.builder.id,
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.32",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.25.
|
|
9
|
+
"version": "1.25.32",
|
|
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.25.
|
|
3
|
+
"version": "1.25.32",
|
|
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
|
@@ -11955,5 +11955,17 @@
|
|
|
11955
11955
|
"id": "1005307",
|
|
11956
11956
|
"text": "Choosing Patron in game mode no longer reloads the hall endlessly; it settles on the city."
|
|
11957
11957
|
}
|
|
11958
|
+
],
|
|
11959
|
+
"1.25.31": [
|
|
11960
|
+
{
|
|
11961
|
+
"id": "1005310",
|
|
11962
|
+
"text": "The governor's street asks the server for less and gets its answers sooner."
|
|
11963
|
+
}
|
|
11964
|
+
],
|
|
11965
|
+
"1.25.32": [
|
|
11966
|
+
{
|
|
11967
|
+
"id": "1005308",
|
|
11968
|
+
"text": "The old circuit-city painting is gone. The welcome screen and the game's start screens now open on the city in the clouds, coloured for the time of day: deep night with lit windows, a warm dawn, a bright midday."
|
|
11969
|
+
}
|
|
11958
11970
|
]
|
|
11959
11971
|
}
|
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.
|
|
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');
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// tests/docket_load_speed.mjs — the governor's street loads faster (task 1005310).
|
|
2
|
+
//
|
|
3
|
+
// Measured on the live hub: the street asked for GET /government/docket and
|
|
4
|
+
// GET /government/counts together, counts rebuilt the same docket, and each build
|
|
5
|
+
// waited for the government reads before it even started its sources. Three
|
|
6
|
+
// properties now hold, and each is driven here against the real docket.js:
|
|
7
|
+
// 1. two asks for the SAME viewer while a build runs share that one build, and
|
|
8
|
+
// nothing is kept once it finishes (no cache: the next ask builds afresh);
|
|
9
|
+
// 2. the sources start reading before the government reads have answered;
|
|
10
|
+
// 3. a government read that fails is never an unhandled rejection while the
|
|
11
|
+
// sources are still reading, and the docket still answers or fails as before.
|
|
12
|
+
// And the street's claims read (GET /tasks/claimable?view=claims) answers only the
|
|
13
|
+
// open claims, without building the claimable feed.
|
|
14
|
+
import { strict as assert } from 'node:assert';
|
|
15
|
+
import { test, after } from 'node:test';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const api = require(path.join(ROOT, 'src', 'module-api.js'));
|
|
24
|
+
const docket = require(path.join(ROOT, 'modules', 'government', 'docket.js'));
|
|
25
|
+
const board = require(path.join(ROOT, 'modules', 'government', 'board.js'));
|
|
26
|
+
const resolver = require(path.join(ROOT, 'modules', 'government', 'resolver.js'));
|
|
27
|
+
const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8');
|
|
28
|
+
|
|
29
|
+
const REAL = { perms: resolver.resolveBuilderPermissions, rules: board.decisionRulesView };
|
|
30
|
+
after(() => { resolver.resolveBuilderPermissions = REAL.perms; board.decisionRulesView = REAL.rules; });
|
|
31
|
+
|
|
32
|
+
const order = [];
|
|
33
|
+
let permReads = 0;
|
|
34
|
+
// The docket's own atom only: no government gate, so no government tables are read.
|
|
35
|
+
resolver.resolveBuilderPermissions = async () => { permReads += 1; return ['board.vote.cast']; };
|
|
36
|
+
board.decisionRulesView = () => new Promise((r) => setTimeout(() => { order.push('decisions'); r(null); }, 40));
|
|
37
|
+
api.contribute(docket.CONTRIBUTION_POINT, {
|
|
38
|
+
source: 'blockers', building: 'planning', gate: { anyBuilder: true },
|
|
39
|
+
read: async () => { order.push('source'); return []; },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('two asks for one viewer while a build runs share it; the next ask builds afresh', async () => {
|
|
43
|
+
permReads = 0;
|
|
44
|
+
const viewer = { id: 7001, rank: 'metic' };
|
|
45
|
+
const [a, b] = await Promise.all([docket.docketFor(viewer), docket.docketFor({ ...viewer })]);
|
|
46
|
+
assert.equal(permReads, 1, 'one build for both asks');
|
|
47
|
+
assert.equal(a, b, 'both asks receive the same answer');
|
|
48
|
+
await docket.docketFor(viewer);
|
|
49
|
+
assert.equal(permReads, 2, 'a finished build is not kept: the next ask builds again');
|
|
50
|
+
await Promise.all([docket.docketFor({ id: 7002 }), docket.docketFor({ id: 7003 })]);
|
|
51
|
+
assert.equal(permReads, 4, 'different viewers never share a build');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('the sources start reading before the government reads answer', async () => {
|
|
55
|
+
order.length = 0;
|
|
56
|
+
await docket.docketFor({ id: 7004, rank: 'metic' });
|
|
57
|
+
assert.deepEqual(order, ['source', 'decisions']);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const VIEWER = { id: 42, rank: 'metic' };
|
|
61
|
+
const NOW = Date.parse('2026-11-14T12:00:00Z');
|
|
62
|
+
const slowSource = (ms) => ({
|
|
63
|
+
sources: new Map([['blockers', { source: 'blockers', building: 'planning', gate: { anyBuilder: true },
|
|
64
|
+
read: () => new Promise((r) => setTimeout(() => r([{ id: 1, title: 'Pick a host', state: 'needs', rule: 'decide' }]), ms)) }]]),
|
|
65
|
+
readings: new Map(), floors: new Map(), histories: new Map(), desks: new Map(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('a government read handed in as a promise answers exactly as its value does', async () => {
|
|
69
|
+
const plain = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: slowSource(1), now: NOW, decisions: [], government: null, seats: null });
|
|
70
|
+
const later = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: slowSource(1), now: NOW,
|
|
71
|
+
decisions: Promise.resolve([]), government: Promise.resolve(null), seats: Promise.resolve(null) });
|
|
72
|
+
assert.deepEqual(later, plain);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('a failed government read is never an unhandled rejection while the sources read', async () => {
|
|
76
|
+
const unhandled = [];
|
|
77
|
+
const onUnhandled = (e) => unhandled.push(e);
|
|
78
|
+
process.on('unhandledRejection', onUnhandled);
|
|
79
|
+
try {
|
|
80
|
+
const boom = new Error('government unreadable');
|
|
81
|
+
await assert.rejects(docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: slowSource(30), now: NOW,
|
|
82
|
+
decisions: Promise.resolve(null), government: Promise.reject(boom), seats: Promise.resolve(null) }), /government unreadable/);
|
|
83
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
84
|
+
assert.deepEqual(unhandled, []);
|
|
85
|
+
} finally {
|
|
86
|
+
process.off('unhandledRejection', onUnhandled);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('the claims read answers the open claims without building the claimable feed', () => {
|
|
91
|
+
const src = read('modules/lifecycle/routes/tasks.js');
|
|
92
|
+
const at = src.indexOf("req.query.view === 'claims'");
|
|
93
|
+
assert.ok(at > 0, 'GET /tasks/claimable has a claims-only view');
|
|
94
|
+
assert.ok(at < src.indexOf('db.listClaimableTasks({', at - 2000), 'it answers before the claimable feed is built');
|
|
95
|
+
assert.match(src.slice(at, at + 200), /active_claims: await db\.listActiveClaims\(\)/);
|
|
96
|
+
for (const f of ['modules/hall-ui/public/gov-feeds.js', 'modules/hall-ui/public/city.js']) {
|
|
97
|
+
assert.match(read(f), /tasks\/claimable\?view=claims/, `${f} asks for the claims alone`);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
@@ -488,7 +488,7 @@ async function bootLiving({ asks = false, answer = null } = {}) {
|
|
|
488
488
|
const feeds = {
|
|
489
489
|
'/api/bongos/government/docket': asks ? withAsks(docketRouted()) : docketRouted(), '/api/bongos/goals': fx('goals'), '/api/bongos/goals/rollups': fx('goals__rollups'),
|
|
490
490
|
'/api/bongos/government/leads': leadsRead(), '/api/bongos/government/treasury': fx('government__treasury'),
|
|
491
|
-
'/api/bongos/government/constitution': fx('government__constitution'), '/api/bongos/tasks/claimable?
|
|
491
|
+
'/api/bongos/government/constitution': fx('government__constitution'), '/api/bongos/tasks/claimable?view=claims': fx('tasks__claimable'),
|
|
492
492
|
'/api/bongos/tasks?status=active&limit=200': fx('tasks__status-active'), '/api/bongos/public/cost-summary': fx('public__cost-summary'),
|
|
493
493
|
'/api/bongos/builders/roster': fx('builders__roster'), '/api/bongos/me': fx('me'), '/api/bongos/blockers?limit=100': fx('blockers'),
|
|
494
494
|
'/api/bongos/autonomy/fence': fx('autonomy__fence'), '/api/bongos/moving': fx('moving'),
|
|
@@ -172,7 +172,7 @@ test('in the harness COUNT_FEEDS, with the docket the page already read, give th
|
|
|
172
172
|
assert.equal(L.countsOf(m, s).on_others, L.countsOf(m).on_others - 1);
|
|
173
173
|
// and nothing the shell reads is a task list
|
|
174
174
|
assert.ok(!F.COUNT_FEEDS.includes('tasks') && !F.COUNT_FEEDS.includes('goals'), 'the tally reads no task or goal list');
|
|
175
|
-
assert.match(F.FEEDS.claims, /
|
|
175
|
+
assert.match(F.FEEDS.claims, /[?&]view=claims\b/, 'the claims ride the claims-only view of the claimable route (task 1005310)');
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
// ── 3. every surface prints the same numbers ────────────────────────────────
|
|
@@ -197,7 +197,7 @@ const FEED = {
|
|
|
197
197
|
'/api/bongos/government/docket': 'government__docket', '/api/bongos/government/counts': 'government__counts', '/api/bongos/goals': 'goals', '/api/bongos/goals/rollups': 'goals__rollups',
|
|
198
198
|
'/api/bongos/me': 'me', '/api/bongos/builders/roster': 'builders__roster', '/api/bongos/blockers': 'blockers',
|
|
199
199
|
'/api/bongos/public/cost-summary': 'public__cost-summary', '/api/bongos/tasks/claimable': 'tasks__claimable',
|
|
200
|
-
'/api/bongos/tasks/claimable?
|
|
200
|
+
'/api/bongos/tasks/claimable?view=claims': 'tasks__claimable', '/api/bongos/tasks?status=active&limit=200': 'tasks__status-active',
|
|
201
201
|
'/api/bongos/blockers?limit=100': 'blockers', '/api/bongos/moving': 'moving', '/api/bongos/autonomy/fence': 'autonomy__fence',
|
|
202
202
|
'/api/bongos/government/constitution': 'government__constitution',
|
|
203
203
|
// the hub's law book, as the route answers it (no law passed yet)
|
package/tests/hall_sky_time.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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();
|
|
@@ -411,7 +411,7 @@ const FEED = {
|
|
|
411
411
|
'/api/bongos/me': 'me', '/api/bongos/builders/roster': 'builders__roster',
|
|
412
412
|
'/api/bongos/blockers': 'blockers', '/api/bongos/public/cost-summary': 'public__cost-summary',
|
|
413
413
|
// the living model's reads, as gov-feeds.js names them (one read each per page)
|
|
414
|
-
'/api/bongos/tasks/claimable?
|
|
414
|
+
'/api/bongos/tasks/claimable?view=claims': 'tasks__claimable', '/api/bongos/tasks?status=active&limit=200': 'tasks__status-active',
|
|
415
415
|
'/api/bongos/blockers?limit=100': 'blockers', '/api/bongos/moving': 'moving', '/api/bongos/autonomy/fence': 'autonomy__fence',
|
|
416
416
|
'/api/bongos/government/constitution': 'government__constitution', '/api/bongos/government/leads': 'government__leads',
|
|
417
417
|
};
|
|
@@ -471,7 +471,7 @@ async function bootPage({ withLiving = true } = {}) {
|
|
|
471
471
|
const F = {
|
|
472
472
|
'/api/bongos/government/docket': docketRouted(), '/api/bongos/goals': fx('goals'), '/api/bongos/goals/rollups': fx('goals__rollups'),
|
|
473
473
|
'/api/bongos/government/leads': leadsRead(),
|
|
474
|
-
'/api/bongos/tasks/claimable?
|
|
474
|
+
'/api/bongos/tasks/claimable?view=claims': fx('tasks__claimable'), '/api/bongos/tasks?status=active': fx('tasks__status-active'),
|
|
475
475
|
'/api/bongos/public/cost-summary': fx('public__cost-summary'), '/api/bongos/builders/roster': fx('builders__roster'), '/api/bongos/me': fx('me'),
|
|
476
476
|
// the living model's reads, as gov-feeds.js names them
|
|
477
477
|
'/api/bongos/tasks?status=active&limit=200': fx('tasks__status-active'), '/api/bongos/blockers?limit=100': fx('blockers'),
|
|
@@ -493,7 +493,7 @@ test('the page draws the living street: the skyline, a pod per claim, the plates
|
|
|
493
493
|
const p = await bootPage();
|
|
494
494
|
const all = (id) => walk(p.byId.get(id));
|
|
495
495
|
const cls = (id, c) => all(id).filter((e) => (e.className || '').split(' ').includes(c));
|
|
496
|
-
for (const u of ['/api/bongos/tasks/claimable?
|
|
496
|
+
for (const u of ['/api/bongos/tasks/claimable?view=claims', '/api/bongos/government/leads']) assert.ok(p.calls.includes(u), u);
|
|
497
497
|
assert.ok(!p.calls.some((u) => /governor-overlay/.test(u)), 'no overlay is read');
|
|
498
498
|
assert.equal(p.calls.filter((u) => u === '/api/bongos/me').length, 1, 'the living feeds are read once');
|
|
499
499
|
assert.equal(cls('streetDepthNear', 'street-depth-sil__face').length, 1);
|
package/tests/welcome_boot.mjs
CHANGED
|
@@ -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
|
|
5
|
-
//
|
|
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
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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(
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
95
|
-
await page.
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
260
|
-
|
|
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
|
|
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)',
|
|
267
|
-
[new Date(2026, 9, 23, 22, 0), { day: false, night: true, veil: 'rgb(0, 0, 0)',
|
|
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 .
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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:
|
|
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
|
|
|
Binary file
|
|
Binary file
|