@bongos/core 1.25.2 → 1.25.4
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 +55 -40
- package/docs/copy-inventory.md +30 -29
- package/docs/copy-registry.json +78 -58
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +5 -3
- package/docs/page-readings.json +44 -44
- package/modules/hall-ui/public/builders.js +3 -27
- package/modules/hall-ui/public/genesis-home.js +54 -4
- package/modules/hall-ui/public/hall-render.js +5 -119
- package/modules/hall-ui/public/hall-widgets.js +3 -3
- package/modules/hall-ui/public/index-genesis-votes.states.json +61 -0
- package/modules/hall-ui/public/index.html +0 -7
- package/modules/hall-ui/public/shell-game.js +22 -4
- package/modules/hall-ui/public/style.css +3 -73
- package/modules/hall-ui/public/visor.css +31 -1
- package/modules/hall-ui/public/visor.js +53 -3
- package/modules/hall-ui/records/home-reshuffle.md +1 -1
- package/modules/hall-ui/records/visor.md +10 -0
- package/modules/onboarding/founding-mode.js +4 -1
- package/modules/onboarding/genesis-home.js +57 -2
- package/modules/onboarding/routes/onboarding.js +2 -0
- package/modules/ui-design/kit/fixtures/founding-genesis-votes.json +193 -0
- package/modules/ui-design/kit/fixtures/me-founding-demo.json +0 -1
- package/modules/ui-design/kit/fixtures/me-founding.json +0 -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/founding_mode.mjs +13 -3
- package/tests/genesis_home.mjs +92 -4
- package/tests/hall_board_world.mjs +7 -35
- package/tests/hall_home_ships_bounded.mjs +11 -27
- package/tests/hall_landing_boot.mjs +7 -8
- package/tests/hall_nav.mjs +2 -2
- package/tests/hall_visor.mjs +9 -2
- package/tests/hall_visor_profile.mjs +150 -0
- package/tests/leaderboard_roles.mjs +3 -17
|
@@ -156,6 +156,8 @@ function shapeGenesis({ mode, plan, tasks = {}, board = {}, stageAt = {} }) {
|
|
|
156
156
|
const idx = fm.STAGES.indexOf(key);
|
|
157
157
|
const list = (tasks[key] || []).map((t) => ({ ...t, finished: FINISHED.includes(t.status) }));
|
|
158
158
|
const b = board[key] || null;
|
|
159
|
+
const allFinished = list.length > 0 && list.every((t) => t.finished);
|
|
160
|
+
const onBoard = !!(b && (b.state === 'sitting' || b.state === 'passed'));
|
|
159
161
|
const days = plan && plan.days && plan.days[key] ? plan.days[key] : info.days;
|
|
160
162
|
// A demo's stage is minutes long (task 1004506); its due time is counted in them.
|
|
161
163
|
const minutes = plan && plan.minutes && plan.minutes[key] ? plan.minutes[key] : null;
|
|
@@ -177,6 +179,9 @@ function shapeGenesis({ mode, plan, tasks = {}, board = {}, stageAt = {} }) {
|
|
|
177
179
|
tasks: list,
|
|
178
180
|
finished: list.filter((t) => t.finished).length,
|
|
179
181
|
total: list.length,
|
|
182
|
+
// task 1005199: this stage (the current one or any later one) can be put to
|
|
183
|
+
// the Board Room now — its tasks are all finished and it is not already there.
|
|
184
|
+
can_close: at >= 1 && idx >= at && allFinished && !onBoard,
|
|
180
185
|
board: b ? {
|
|
181
186
|
state: b.state, item_id: b.item_id || null, needs: b.needs || null,
|
|
182
187
|
one_vote_decides: !!b.one_vote_decides, waiting_on: b.waiting_on || [],
|
|
@@ -349,7 +354,15 @@ async function closeStage(stageKey, builderId, deps = {}) {
|
|
|
349
354
|
const s = seams(deps);
|
|
350
355
|
const mode = await reconcile(s);
|
|
351
356
|
if (!mode) return { ok: false, reason: 'not_founding' };
|
|
352
|
-
|
|
357
|
+
// task 1005199: any stage at or AFTER the current one may be put up, so every
|
|
358
|
+
// finished stage can sit in the Board Room at once. Order is kept by reconcile, not
|
|
359
|
+
// here: the project only moves past a stage whose own sitting passed, so a later
|
|
360
|
+
// stage's pass waits for the earlier ones. A stage already behind, or the decide
|
|
361
|
+
// pre-stage, is still refused.
|
|
362
|
+
if (!PROPER.includes(stageKey)) return { ok: false, reason: 'unknown_stage' };
|
|
363
|
+
if (!PROPER.includes(mode.stage) || fm.STAGES.indexOf(stageKey) < fm.STAGES.indexOf(mode.stage)) {
|
|
364
|
+
return { ok: false, reason: 'not_current_stage', stage: mode.stage };
|
|
365
|
+
}
|
|
353
366
|
const all = s.lifecycle && typeof s.lifecycle.genesisTasks === 'function' ? await s.lifecycle.genesisTasks() : {};
|
|
354
367
|
const list = all[stageKey] || [];
|
|
355
368
|
const taskIds = list.map((t) => t.id);
|
|
@@ -357,9 +370,13 @@ async function closeStage(stageKey, builderId, deps = {}) {
|
|
|
357
370
|
const nextOf = (key) => (key === 'legislation' ? null : fm.STAGES[fm.STAGES.indexOf(key) + 1]);
|
|
358
371
|
const closedAnswer = async (itemId, confirm) => {
|
|
359
372
|
const after = await reconcile(s);
|
|
373
|
+
const at = after && after.stage;
|
|
360
374
|
return {
|
|
361
375
|
ok: true, closed: true, stage: stageKey,
|
|
362
|
-
next_stage: stageKey === 'legislation' ? null :
|
|
376
|
+
next_stage: stageKey === 'legislation' && at === 'legislation' ? null : at || nextOf(stageKey),
|
|
377
|
+
// task 1005199: a later stage passed while an earlier one is still open, so
|
|
378
|
+
// the project stays at the earlier stage until that one passes too.
|
|
379
|
+
waits_for_earlier: !!(at && fm.STAGES.indexOf(at) < fm.STAGES.indexOf(stageKey)),
|
|
363
380
|
item_id: itemId, confirm,
|
|
364
381
|
};
|
|
365
382
|
};
|
|
@@ -379,6 +396,9 @@ async function closeStage(stageKey, builderId, deps = {}) {
|
|
|
379
396
|
return sittingOpen(stageKey, r.item ? String(r.item.id) : null, !r.opened, board[stageKey] || null, builderId);
|
|
380
397
|
}
|
|
381
398
|
if (!list.length || !list.every((t) => FINISHED.includes(t.status))) return { ok: false, reason: 'stage_tasks_unfinished' };
|
|
399
|
+
// With no Board Room there is no sitting to hold a later stage, so only the
|
|
400
|
+
// current one can be closed here; a later one waits its turn.
|
|
401
|
+
if (stageKey !== mode.stage) return { ok: false, reason: 'not_current_stage', stage: mode.stage };
|
|
382
402
|
if (stageKey !== 'legislation') {
|
|
383
403
|
await fm.setFoundingMode({ stage: nextOf(stageKey) }, builderId, modeDeps(s));
|
|
384
404
|
}
|
|
@@ -452,9 +472,44 @@ async function onStagePassed(deps = {}) {
|
|
|
452
472
|
try { await reconcile(seams(deps)); } catch (_) { /* recovered on the next read */ }
|
|
453
473
|
}
|
|
454
474
|
|
|
475
|
+
// putFinishedStages — shipping a stage puts it in the Board Room by itself (task
|
|
476
|
+
// 1005199). Every stage at or after the current one whose tasks are all finished and
|
|
477
|
+
// that has NEVER been put to the board is opened as a sitting, with nobody's vote cast:
|
|
478
|
+
// the founder still says yes, but no longer has to press "close" first. A stage the
|
|
479
|
+
// board sent back is left for the founder to put up again on purpose. Opening is the
|
|
480
|
+
// government port's own gate (finished tasks, one sitting per stage), so a race with a
|
|
481
|
+
// manual close opens one sitting, not two. Returns the stage keys it put up.
|
|
482
|
+
async function putFinishedStages(deps = {}) {
|
|
483
|
+
const s = seams(deps);
|
|
484
|
+
const gov = s.government;
|
|
485
|
+
if (!gov || typeof gov.openGenesisStageClose !== 'function') return [];
|
|
486
|
+
const mode = await reconcile(s);
|
|
487
|
+
if (!mode || !PROPER.includes(mode.stage)) return [];
|
|
488
|
+
const all = s.lifecycle && typeof s.lifecycle.genesisTasks === 'function' ? await s.lifecycle.genesisTasks() : {};
|
|
489
|
+
const board = await boardByStage(gov);
|
|
490
|
+
const put = [];
|
|
491
|
+
for (const key of PROPER.slice(PROPER.indexOf(mode.stage))) {
|
|
492
|
+
const list = all[key] || [];
|
|
493
|
+
if (!list.length || !list.every((t) => FINISHED.includes(t.status))) continue;
|
|
494
|
+
const st = board[key] && board[key].state;
|
|
495
|
+
if (st && st !== 'not_put') continue;
|
|
496
|
+
const r = await gov.openGenesisStageClose({ stageKey: key, taskIds: list.map((t) => t.id), openedBy: null });
|
|
497
|
+
if (r && r.opened) put.push(key);
|
|
498
|
+
}
|
|
499
|
+
return put;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// onTaskShipped — the `task.shipped` listener. Any ship may be the last task of a
|
|
503
|
+
// stage; on a project that is not founding this costs the one genesis.enabled read.
|
|
504
|
+
// Best-effort: the founder's own close button is still there if this misses.
|
|
505
|
+
async function onTaskShipped(deps = {}) {
|
|
506
|
+
try { return await putFinishedStages(deps); } catch (_) { return []; }
|
|
507
|
+
}
|
|
508
|
+
|
|
455
509
|
module.exports = {
|
|
456
510
|
PLAN_KEY, STAGE_INFO, PROPER, DEMO_PEOPLE, DEMO_HOURS, intIn, parsePlan, shapeGenesis, demoMinutes, demoPlan,
|
|
457
511
|
readGenesis, startFounding, savePlan, applyGovernmentChoice, closeStage, comeAlive, skipGenesis, onStagePassed,
|
|
512
|
+
putFinishedStages, onTaskShipped,
|
|
458
513
|
// the founding-mode deps this file reads the mode with, for founding-birth.js (task 1004506)
|
|
459
514
|
modeDepsFor: (deps) => modeDeps(seams(deps)),
|
|
460
515
|
};
|
|
@@ -134,6 +134,8 @@ function mountGenesisRoutes(router) {
|
|
|
134
134
|
if (!genesisListening && typeof api.on === 'function') {
|
|
135
135
|
genesisListening = true;
|
|
136
136
|
api.on('board.genesis_stage.passed', () => genesis.onStagePassed());
|
|
137
|
+
// task 1005199: a ship that finishes a stage puts that stage in the Board Room.
|
|
138
|
+
api.on('task.shipped', () => genesis.onTaskShipped());
|
|
137
139
|
}
|
|
138
140
|
const owns = api.requirePermission('project.settings.manage');
|
|
139
141
|
const refuse = (res, r) => res.status(GENESIS_REFUSALS[r.reason] || 409).json({
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "Genesis with several stages in the Board Room at once (task 1005199): scope, strategize and charter sitting on the founder, legalize finished and not yet put up, legislation in progress.",
|
|
3
|
+
"genesis": {
|
|
4
|
+
"stage": "scope",
|
|
5
|
+
"stage_number": 1,
|
|
6
|
+
"stage_count": 5,
|
|
7
|
+
"day": 2,
|
|
8
|
+
"deciding": false,
|
|
9
|
+
"plan": {
|
|
10
|
+
"days": {
|
|
11
|
+
"scope": 7,
|
|
12
|
+
"strategize": 7,
|
|
13
|
+
"charter": 4,
|
|
14
|
+
"legalize": 7,
|
|
15
|
+
"legislation": 3
|
|
16
|
+
},
|
|
17
|
+
"passes": 1,
|
|
18
|
+
"advisors": "none",
|
|
19
|
+
"government": "monarchy",
|
|
20
|
+
"expected_builders": 1,
|
|
21
|
+
"existing": false
|
|
22
|
+
},
|
|
23
|
+
"demo": null,
|
|
24
|
+
"stages": [
|
|
25
|
+
{
|
|
26
|
+
"key": "scope",
|
|
27
|
+
"number": 1,
|
|
28
|
+
"label": "Scope",
|
|
29
|
+
"life": "primordial sea",
|
|
30
|
+
"plate": "r5-life-1-primordial-sea",
|
|
31
|
+
"done_when": "The scope is written: what you are making, for whom, and what is left out.",
|
|
32
|
+
"wakes": [
|
|
33
|
+
"goals",
|
|
34
|
+
"goal-map"
|
|
35
|
+
],
|
|
36
|
+
"days": 7,
|
|
37
|
+
"minutes": null,
|
|
38
|
+
"due": "2026-11-17T09:00:00.000Z",
|
|
39
|
+
"state": "now",
|
|
40
|
+
"tasks": [
|
|
41
|
+
{
|
|
42
|
+
"id": "2001",
|
|
43
|
+
"title": "Scope: write it down",
|
|
44
|
+
"status": "shipped",
|
|
45
|
+
"finished": true
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"finished": 1,
|
|
49
|
+
"total": 1,
|
|
50
|
+
"can_close": false,
|
|
51
|
+
"board": {
|
|
52
|
+
"state": "sitting",
|
|
53
|
+
"item_id": "41",
|
|
54
|
+
"needs": "One yes decides: the first member to say yes closes it.",
|
|
55
|
+
"one_vote_decides": true,
|
|
56
|
+
"waiting_on": [
|
|
57
|
+
{
|
|
58
|
+
"builder_id": "1",
|
|
59
|
+
"display_name": "Founder"
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"key": "strategize",
|
|
66
|
+
"number": 2,
|
|
67
|
+
"label": "Strategize",
|
|
68
|
+
"life": "first cells",
|
|
69
|
+
"plate": "r5-life-2-first-cells",
|
|
70
|
+
"done_when": "The strategy is written out, or the one you have is linked.",
|
|
71
|
+
"wakes": [
|
|
72
|
+
"work"
|
|
73
|
+
],
|
|
74
|
+
"days": 7,
|
|
75
|
+
"minutes": null,
|
|
76
|
+
"due": null,
|
|
77
|
+
"state": "later",
|
|
78
|
+
"tasks": [
|
|
79
|
+
{
|
|
80
|
+
"id": "2002",
|
|
81
|
+
"title": "Strategize: write it down",
|
|
82
|
+
"status": "shipped",
|
|
83
|
+
"finished": true
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
"finished": 1,
|
|
87
|
+
"total": 1,
|
|
88
|
+
"can_close": false,
|
|
89
|
+
"board": {
|
|
90
|
+
"state": "sitting",
|
|
91
|
+
"item_id": "42",
|
|
92
|
+
"needs": "One yes decides: the first member to say yes closes it.",
|
|
93
|
+
"one_vote_decides": true,
|
|
94
|
+
"waiting_on": [
|
|
95
|
+
{
|
|
96
|
+
"builder_id": "1",
|
|
97
|
+
"display_name": "Founder"
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"key": "charter",
|
|
104
|
+
"number": 3,
|
|
105
|
+
"label": "Charter",
|
|
106
|
+
"life": "the genome",
|
|
107
|
+
"plate": "r5-life-3-genome",
|
|
108
|
+
"done_when": "The starting government is chosen and the bylaws are written.",
|
|
109
|
+
"wakes": [],
|
|
110
|
+
"days": 4,
|
|
111
|
+
"minutes": null,
|
|
112
|
+
"due": null,
|
|
113
|
+
"state": "later",
|
|
114
|
+
"tasks": [
|
|
115
|
+
{
|
|
116
|
+
"id": "2003",
|
|
117
|
+
"title": "Charter: write it down",
|
|
118
|
+
"status": "shipped",
|
|
119
|
+
"finished": true
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
"finished": 1,
|
|
123
|
+
"total": 1,
|
|
124
|
+
"can_close": false,
|
|
125
|
+
"board": {
|
|
126
|
+
"state": "sitting",
|
|
127
|
+
"item_id": "43",
|
|
128
|
+
"needs": "One yes decides: the first member to say yes closes it.",
|
|
129
|
+
"one_vote_decides": true,
|
|
130
|
+
"waiting_on": [
|
|
131
|
+
{
|
|
132
|
+
"builder_id": "1",
|
|
133
|
+
"display_name": "Founder"
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"key": "legalize",
|
|
140
|
+
"number": 4,
|
|
141
|
+
"label": "Legalize",
|
|
142
|
+
"life": "the membrane",
|
|
143
|
+
"plate": "r5-life-4-membrane",
|
|
144
|
+
"done_when": "Rewards are decided, and who can be paid.",
|
|
145
|
+
"wakes": [],
|
|
146
|
+
"days": 7,
|
|
147
|
+
"minutes": null,
|
|
148
|
+
"due": null,
|
|
149
|
+
"state": "later",
|
|
150
|
+
"tasks": [
|
|
151
|
+
{
|
|
152
|
+
"id": "2004",
|
|
153
|
+
"title": "Legalize: write it down",
|
|
154
|
+
"status": "shipped",
|
|
155
|
+
"finished": true
|
|
156
|
+
}
|
|
157
|
+
],
|
|
158
|
+
"finished": 1,
|
|
159
|
+
"total": 1,
|
|
160
|
+
"can_close": true,
|
|
161
|
+
"board": null
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"key": "legislation",
|
|
165
|
+
"number": 5,
|
|
166
|
+
"label": "Legislation",
|
|
167
|
+
"life": "the nervous system",
|
|
168
|
+
"plate": "r5-life-5-nervous-system",
|
|
169
|
+
"done_when": "The voting rules are set: who votes, and how many must agree.",
|
|
170
|
+
"wakes": [],
|
|
171
|
+
"days": 3,
|
|
172
|
+
"minutes": null,
|
|
173
|
+
"due": null,
|
|
174
|
+
"state": "later",
|
|
175
|
+
"tasks": [
|
|
176
|
+
{
|
|
177
|
+
"id": "2005",
|
|
178
|
+
"title": "Legislation: write it down",
|
|
179
|
+
"status": "active",
|
|
180
|
+
"finished": false
|
|
181
|
+
}
|
|
182
|
+
],
|
|
183
|
+
"finished": 0,
|
|
184
|
+
"total": 1,
|
|
185
|
+
"can_close": false,
|
|
186
|
+
"board": null
|
|
187
|
+
}
|
|
188
|
+
],
|
|
189
|
+
"can_close": false,
|
|
190
|
+
"can_come_alive": false
|
|
191
|
+
},
|
|
192
|
+
"viewer_id": "1"
|
|
193
|
+
}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.4",
|
|
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.4",
|
|
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.4",
|
|
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
|
@@ -11706,5 +11706,17 @@
|
|
|
11706
11706
|
"id": "1005248",
|
|
11707
11707
|
"text": "In game mode, no two buttons on the visor share a hotkey any more, and Options now sits only inside the Actions menu with its own key."
|
|
11708
11708
|
}
|
|
11709
|
+
],
|
|
11710
|
+
"1.25.3": [
|
|
11711
|
+
{
|
|
11712
|
+
"id": "1005199",
|
|
11713
|
+
"text": "Founding a project is faster: each finished stage goes to the Board Room by itself, every stage can be voted on at once with one \"vote yes on all\" button, and the Board Room no longer looks locked during genesis."
|
|
11714
|
+
}
|
|
11715
|
+
],
|
|
11716
|
+
"1.25.4": [
|
|
11717
|
+
{
|
|
11718
|
+
"id": "1005249",
|
|
11719
|
+
"text": "Your details now sit on your character at the bottom left of the game: hover to see them, click to open your full profile without leaving the scene. The old profile card at the top right is gone."
|
|
11720
|
+
}
|
|
11709
11721
|
]
|
|
11710
11722
|
}
|
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.4'; // 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');
|
package/tests/founding_mode.mjs
CHANGED
|
@@ -38,22 +38,32 @@ test('an unknown stage fails CLOSED — OFF, never a hall stuck half-asleep', ()
|
|
|
38
38
|
assert.equal(fm.shapeFoundingMode({ active: 'true', stage: 'launch' }), null);
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
test('active with no stage yet is the decide pre-stage, and every room sleeps', () => {
|
|
41
|
+
test('active with no stage yet is the decide pre-stage, and every room but the Board Room sleeps', () => {
|
|
42
42
|
const f = fm.shapeFoundingMode({ active: 'true' });
|
|
43
43
|
assert.equal(f.stage, 'decide');
|
|
44
44
|
assert.equal(f.stage_number, 0);
|
|
45
45
|
assert.equal(f.stage_count, 5);
|
|
46
46
|
// task 1005051: Roadmap folded into Goals; scope wakes Goals and its map.
|
|
47
|
-
for (const id of ['goals', 'goal-map', 'work', '
|
|
47
|
+
for (const id of ['goals', 'goal-map', 'work', 'collab', 'roster']) {
|
|
48
48
|
assert.ok(id in f.asleep, `${id} sleeps while deciding`);
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
// task 1005199: every stage, from scope on, is closed by a vote in the Board Room, so
|
|
53
|
+
// the room must never read as locked while genesis has a ballot in it.
|
|
54
|
+
test('the Board Room is awake at every stage of genesis', () => {
|
|
55
|
+
for (const stage of fm.STAGES) {
|
|
56
|
+
const f = fm.shapeFoundingMode({ active: 'true', stage });
|
|
57
|
+
assert.ok(!('board-room' in f.asleep), `board-room is awake at ${stage}`);
|
|
58
|
+
}
|
|
59
|
+
assert.ok(!Object.values(fm.WAKES).some((ids) => ids.includes('board-room')), 'no stage wakes it, because it never sleeps');
|
|
60
|
+
});
|
|
61
|
+
|
|
52
62
|
test('a room wakes once the stage that wakes it is behind', () => {
|
|
53
63
|
const f = fm.shapeFoundingMode({ active: 'true', stage: 'charter' });
|
|
54
64
|
for (const id of ['goals', 'goal-map', 'work']) assert.ok(!(id in f.asleep), `${id} woke`);
|
|
55
65
|
assert.ok(!('roadmap' in f.asleep), 'the folded Roadmap is no room to wake');
|
|
56
|
-
assert.
|
|
66
|
+
assert.ok(!('board-room' in f.asleep), 'the Board Room never sleeps (task 1005199)');
|
|
57
67
|
// task 1005046: Economy and Ranks were the rooms legalize and legislation woke;
|
|
58
68
|
// they are Primer chapters now, and the Primer never sleeps.
|
|
59
69
|
assert.ok(!('economy' in f.asleep) && !('ranks' in f.asleep), 'no retired Docs page is put to sleep');
|
package/tests/genesis_home.mjs
CHANGED
|
@@ -249,13 +249,65 @@ test('the owner can skip genesis from any stage: no sitting, hall opens, recorde
|
|
|
249
249
|
assert.equal(await gh.readGenesis(mid.deps()), null);
|
|
250
250
|
});
|
|
251
251
|
|
|
252
|
-
test('
|
|
252
|
+
test('a later stage can be put to the board once finished; a stage behind cannot (task 1005199)', async () => {
|
|
253
253
|
const w = world();
|
|
254
254
|
await gh.startFounding(1, w.deps());
|
|
255
255
|
await gh.savePlan({}, 1, w.deps());
|
|
256
|
+
assert.equal((await gh.closeStage('charter', 1, w.deps())).reason, 'stage_tasks_unfinished', 'the finished-tasks gate still holds');
|
|
257
|
+
w.finish('charter');
|
|
256
258
|
const r = await gh.closeStage('charter', 1, w.deps());
|
|
257
|
-
assert.equal(r.reason, '
|
|
258
|
-
assert.
|
|
259
|
+
assert.equal(r.reason, 'sitting_open', 'charter sits while the project is still at scope');
|
|
260
|
+
assert.deepEqual(w.opened.at(-1).stageKey, 'charter');
|
|
261
|
+
assert.equal((await gh.readGenesis(w.deps())).stage, 'scope');
|
|
262
|
+
w.finish('scope'); w.pass('scope'); await gh.onStagePassed(w.deps());
|
|
263
|
+
assert.equal((await gh.closeStage('scope', 1, w.deps())).reason, 'not_current_stage', 'a stage behind is refused');
|
|
264
|
+
assert.equal((await gh.closeStage('decide', 1, w.deps())).reason, 'unknown_stage');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('every finished stage sits at once, and they pass in order (task 1005199)', async () => {
|
|
268
|
+
const w = world();
|
|
269
|
+
await gh.startFounding(1, w.deps());
|
|
270
|
+
await gh.savePlan({}, 1, w.deps());
|
|
271
|
+
for (const s of STAGES5) w.finish(s);
|
|
272
|
+
let g = await gh.readGenesis(w.deps());
|
|
273
|
+
assert.deepEqual(g.stages.map((s) => s.can_close), [true, true, true, true, true], 'every finished stage can be put up');
|
|
274
|
+
for (const s of STAGES5) await gh.closeStage(s, 1, w.deps());
|
|
275
|
+
g = await gh.readGenesis(w.deps());
|
|
276
|
+
assert.deepEqual(g.stages.map((s) => s.board && s.board.state), Array(5).fill('sitting'));
|
|
277
|
+
assert.deepEqual(g.stages.map((s) => s.can_close), [false, false, false, false, false], 'a sitting stage is not put up twice');
|
|
278
|
+
// A later pass does not move the project past an earlier stage still sitting.
|
|
279
|
+
w.pass('charter'); w.pass('legalize');
|
|
280
|
+
await gh.onStagePassed(w.deps());
|
|
281
|
+
assert.equal((await gh.readGenesis(w.deps())).stage, 'scope', 'scope still sits: the project waits for it');
|
|
282
|
+
w.pass('scope'); await gh.onStagePassed(w.deps());
|
|
283
|
+
assert.equal((await gh.readGenesis(w.deps())).stage, 'strategize', 'scope passed; strategize still sits, so the project stops there');
|
|
284
|
+
w.pass('strategize'); await gh.onStagePassed(w.deps());
|
|
285
|
+
assert.equal((await gh.readGenesis(w.deps())).stage, 'legislation', 'strategize passed: charter and legalize had passed already');
|
|
286
|
+
w.pass('legislation'); await gh.onStagePassed(w.deps());
|
|
287
|
+
g = await gh.readGenesis(w.deps());
|
|
288
|
+
assert.equal(g.can_come_alive, true, 'one click left: bring it to life');
|
|
289
|
+
assert.equal((await gh.comeAlive(1, w.deps())).ok, true);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('shipping the last task of a stage puts it in the Board Room by itself (task 1005199)', async () => {
|
|
293
|
+
const w = world();
|
|
294
|
+
assert.deepEqual(await gh.onTaskShipped(w.deps()), [], 'not founding: nothing is put up');
|
|
295
|
+
await gh.startFounding(1, w.deps());
|
|
296
|
+
await gh.savePlan({}, 1, w.deps());
|
|
297
|
+
assert.deepEqual(await gh.onTaskShipped(w.deps()), [], 'no stage is finished yet');
|
|
298
|
+
w.finish('scope'); w.finish('legalize');
|
|
299
|
+
assert.deepEqual(await gh.onTaskShipped(w.deps()), ['scope', 'legalize'], 'every finished stage, current and later');
|
|
300
|
+
assert.equal(w.opened.length, 2);
|
|
301
|
+
assert.deepEqual(await gh.onTaskShipped(w.deps()), [], 'a stage already sitting is not put up again');
|
|
302
|
+
w.board.strategize = { state: 'returned', waiting_on: [] };
|
|
303
|
+
w.finish('strategize');
|
|
304
|
+
assert.deepEqual(await gh.onTaskShipped(w.deps()), [], 'a stage the board sent back waits for the founder');
|
|
305
|
+
assert.deepEqual(await gh.onTaskShipped({ ...w.deps(), government: { genesisStageStatus: async () => { throw new Error('down'); }, openGenesisStageClose: async () => ({}) } }), [], 'a failure never throws into the ship');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('the route factory listens for ships (task 1005199)', () => {
|
|
309
|
+
const src = fs.readFileSync(path.join(ROOT, 'modules', 'onboarding', 'routes', 'onboarding.js'), 'utf8');
|
|
310
|
+
assert.match(src, /api\.on\('task\.shipped', \(\) => genesis\.onTaskShipped\(\)\)/);
|
|
259
311
|
});
|
|
260
312
|
|
|
261
313
|
test('a returned stage stays put and can be put up again', async () => {
|
|
@@ -316,7 +368,7 @@ function bootCard({ founding, data = { genesis: null } }) {
|
|
|
316
368
|
const attrs = founding ? { 'data-founding': 'on' } : {};
|
|
317
369
|
const sandbox = {
|
|
318
370
|
window: {
|
|
319
|
-
OTB: { escapeHtml: (s) => String(s), contributeHallWidget: (w) => { widget = w; } },
|
|
371
|
+
OTB: { escapeHtml: (s) => String(s), statusPill: (s) => `<span>${s}</span>`, contributeHallWidget: (w) => { widget = w; } },
|
|
320
372
|
BongosClient: { createClient: () => ({ request: async (m, p) => { calls.push(`${m} ${p}`); return { ok: true, status: 200, data }; } }) },
|
|
321
373
|
},
|
|
322
374
|
document: { documentElement: { getAttribute: (k) => attrs[k] || null } },
|
|
@@ -371,6 +423,42 @@ test('a demo\'s genesis card: no decide form, the settled plan in one line, each
|
|
|
371
423
|
assert.doesNotMatch(host.innerHTML, /data-genesis-plan/, 'a demo never shows the decide form');
|
|
372
424
|
});
|
|
373
425
|
|
|
426
|
+
test('the card lists every stage vote waiting on the viewer, and votes yes on all in order (task 1005199)', async () => {
|
|
427
|
+
const me = [{ builder_id: '1', display_name: 'Founder' }];
|
|
428
|
+
const genesis = gh.shapeGenesis({
|
|
429
|
+
mode: { stage: 'scope', stage_number: 1, stage_count: 5, day: 1 },
|
|
430
|
+
plan: gh.parsePlan({}).plan,
|
|
431
|
+
tasks: Object.fromEntries(STAGES5.map((s, i) => [s, [{ id: String(100 + i), title: s, status: i < 4 ? 'shipped' : 'ready' }]])),
|
|
432
|
+
board: {
|
|
433
|
+
scope: { state: 'sitting', item_id: '11', waiting_on: me, needs: 'One yes decides.' },
|
|
434
|
+
strategize: { state: 'sitting', item_id: '12', waiting_on: me },
|
|
435
|
+
charter: { state: 'passed', item_id: '13', waiting_on: [] },
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
assert.deepEqual(genesis.stages.map((s) => s.can_close), [false, false, false, true, false], 'only legalize is finished and not yet on the board');
|
|
439
|
+
const b = bootCard({ founding: true, data: { genesis, viewer_id: '1' } });
|
|
440
|
+
let onClick = null;
|
|
441
|
+
b.host.addEventListener = (type, fn) => { if (type === 'click') onClick = fn; };
|
|
442
|
+
b.host.contains = () => true;
|
|
443
|
+
b.widget.mount(b.host); // re-mount so the click handler is captured
|
|
444
|
+
await new Promise((r) => setImmediate(r));
|
|
445
|
+
await new Promise((r) => setImmediate(r));
|
|
446
|
+
const html = b.host.innerHTML;
|
|
447
|
+
assert.match(html, /Waiting for your vote · 2 stages/);
|
|
448
|
+
assert.match(html, /data-genesis-vote-all>Vote yes on all 2</);
|
|
449
|
+
assert.match(html, /passed, waits its turn/, 'a later stage that passed says it waits for the earlier ones');
|
|
450
|
+
assert.match(html, /data-genesis-close="legalize">Put to the board</, 'a finished later stage can be put up from its row');
|
|
451
|
+
assert.doesNotMatch(html, /data-genesis-close="legislation"/, 'an unfinished stage cannot');
|
|
452
|
+
b.calls.length = 0;
|
|
453
|
+
const btn = { hasAttribute: (k) => k === 'data-genesis-vote-all' };
|
|
454
|
+
onClick({ target: { closest: () => btn } });
|
|
455
|
+
for (let i = 0; i < 6; i++) await new Promise((r) => setImmediate(r));
|
|
456
|
+
assert.deepEqual(b.calls.slice(0, 2), [
|
|
457
|
+
'POST /api/bongos/government/board/items/11/votes',
|
|
458
|
+
'POST /api/bongos/government/board/items/12/votes',
|
|
459
|
+
], 'one yes per stage, earliest first');
|
|
460
|
+
});
|
|
461
|
+
|
|
374
462
|
test('the hall carries the five life plates byte-identical to the world\'s', () => {
|
|
375
463
|
const world = path.join(ROOT, 'modules', 'public-landing', 'public', 'assets', 'world');
|
|
376
464
|
const hall = path.join(ROOT, 'modules', 'hall-ui', 'public', 'genesis');
|
|
@@ -270,23 +270,6 @@ test('the band ends level: no align-items on the grid, so the two cards stretch'
|
|
|
270
270
|
'and the single-column fallback still exists below 768px (the task text said 860; the sheet says 768)');
|
|
271
271
|
});
|
|
272
272
|
|
|
273
|
-
test('the Standing card is the mock\'s: an identity line and a three-row fact ledger, no banner', () => {
|
|
274
|
-
const block = hallRenderJs.match(/const homeStanding = document\.getElementById\('home-standing-body'\);[\s\S]*?renderSystemNotices\(/);
|
|
275
|
-
assert.ok(block, 'the Home card render is where it was');
|
|
276
|
-
assert.match(block[0], /class="home-who"/, 'the identity line');
|
|
277
|
-
assert.match(block[0], /<dt>\$\{escapeHtml\(curLabel\(true\)\)\}<\/dt>/,
|
|
278
|
-
'the currency row takes its label from the branding contract at render time — a data-currency-label attribute is rebranded once at load, before this row exists');
|
|
279
|
-
for (const label of ['Leaderboard place', 'Preferred work']) {
|
|
280
|
-
assert.match(block[0], new RegExp(`<dt>${label}</dt>`), `the ledger row: ${label}`);
|
|
281
|
-
}
|
|
282
|
-
assert.match(block[0], /: PREFS_EMPTY\}/, 'the empty-preference copy is the one string both Standing surfaces share');
|
|
283
|
-
assert.doesNotMatch(block[0], /Banner\}/, 'no banner is stacked into the card any more');
|
|
284
|
-
for (const sel of ['.home-who', '.home-facts', '.home-facts dt', '.home-facts dd']) {
|
|
285
|
-
assert.ok(rule(styleCss, sel), `style.css styles ${sel}`);
|
|
286
|
-
}
|
|
287
|
-
assert.match(rule(styleCss, '.home-who__avatar') || '', /var\(--skeleton\)/, 'the avatar placeholder is the skeleton tone, not --bg-deep (pure black in dark)');
|
|
288
|
-
});
|
|
289
|
-
|
|
290
273
|
test('the system notices are inbox LINES, first in the card, and counted by its refresh()', () => {
|
|
291
274
|
// Anchored on the statement AFTER the markup, so a ';' inside the markup or its comments cannot cut it short.
|
|
292
275
|
const mount = goalInboxJs.match(/host\.innerHTML =([\s\S]*?);\n host\.addEventListener/);
|
|
@@ -322,7 +305,7 @@ test('the greeting reads /me only; the version tab and its progress read left Ho
|
|
|
322
305
|
|
|
323
306
|
// Boot hall-render.js against a stub DOM and a ctx of getters (the way
|
|
324
307
|
// builders.js builds it) and read what the greeting and the card actually say.
|
|
325
|
-
function bootHallRender({ me, progress = null,
|
|
308
|
+
function bootHallRender({ me, progress = null, ctxExtra = {} }) {
|
|
326
309
|
const els = new Map();
|
|
327
310
|
const makeEl = () => ({
|
|
328
311
|
dataset: {}, style: {}, textContent: '', hidden: false, _html: '', innerHTML: '',
|
|
@@ -364,7 +347,6 @@ function bootHallRender({ me, progress = null, leaderboard = [], ctxExtra = {} }
|
|
|
364
347
|
...ctxExtra,
|
|
365
348
|
};
|
|
366
349
|
const api = windowObj.OTBHallRender(ctx);
|
|
367
|
-
api.lbState.rows = leaderboard;
|
|
368
350
|
return { api, el: (id) => documentObj.getElementById(id) };
|
|
369
351
|
}
|
|
370
352
|
|
|
@@ -374,7 +356,6 @@ const ME = {
|
|
|
374
356
|
cost: { level: 'warn', spend_mtd_usd: 80, budget_mtd_usd: 100, pct_used: 80 },
|
|
375
357
|
needs_rebase_tasks: [{ id: 1003460 }],
|
|
376
358
|
};
|
|
377
|
-
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 }];
|
|
378
359
|
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 }] };
|
|
379
360
|
|
|
380
361
|
test('RUN: the greeting names the builder, and writes nothing else', () => {
|
|
@@ -386,22 +367,13 @@ test('RUN: the greeting names the builder, and writes nothing else', () => {
|
|
|
386
367
|
assert.equal(el('home-version').textContent || '', '', 'no version tab is drawn');
|
|
387
368
|
});
|
|
388
369
|
|
|
389
|
-
test('RUN:
|
|
390
|
-
const { api, el } = bootHallRender({ me: ME
|
|
370
|
+
test('RUN: Home draws no Standing chip, your details are on the visor now (task 1005249)', () => {
|
|
371
|
+
const { api, el } = bootHallRender({ me: ME });
|
|
372
|
+
assert.equal(api.lbState, undefined, 'no leaderboard rows are kept for a place nobody shows');
|
|
373
|
+
assert.equal(api.setRecentShips, undefined, 'no shipped count is kept for a chip that is gone');
|
|
391
374
|
api.renderProfile(ME.builder);
|
|
392
|
-
|
|
393
|
-
assert.
|
|
394
|
-
assert.match(html, /<p class="home-who__handle">@percy<\/p>/);
|
|
395
|
-
// written from curLabel(true), not a data-currency-label attribute: brand-apply
|
|
396
|
-
// rebrands those once at load, and this row is stamped after /me answers
|
|
397
|
-
assert.match(html, /<dt>Credits<\/dt><dd>14,441<\/dd>/);
|
|
398
|
-
assert.match(html, /<dt>Leaderboard place<\/dt><dd>2nd of 4<\/dd>/);
|
|
399
|
-
assert.match(html, /<dt>Preferred work<\/dt><dd>Not set yet<\/dd>/, 'the mock\'s copy for an empty preference');
|
|
400
|
-
assert.doesNotMatch(html, /profile__(needs|budget|rebase)/, 'the banners are not in the card');
|
|
401
|
-
const withPrefs = bootHallRender({ me: ME, leaderboard: [] });
|
|
402
|
-
withPrefs.api.renderProfile({ ...ME.builder, preferred_disciplines: ['artisan', 'ui'] });
|
|
403
|
-
assert.match(withPrefs.el('home-standing-body').innerHTML, /<dd>artisan, ui<\/dd>/);
|
|
404
|
-
assert.match(withPrefs.el('home-standing-body').innerHTML, /<dt>Leaderboard place<\/dt><dd>—<\/dd>/, 'no roll yet: a dash, not a wrong place');
|
|
375
|
+
// the stub document hands back a blank element for any id, so "drew nothing" is an empty one
|
|
376
|
+
assert.equal(el('home-standing-body').innerHTML || '', '', 'no Standing card is written');
|
|
405
377
|
});
|
|
406
378
|
|
|
407
379
|
// task 1005046: "Where you stand" left the retired Ranks page for Accomplishments,
|