@bongos/core 1.25.1 → 1.25.3
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 +37 -27
- package/docs/copy-inventory.md +18 -16
- package/docs/copy-registry.json +46 -27
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +5 -3
- package/docs/page-readings.json +7 -7
- package/modules/hall-ui/public/genesis-home.js +54 -4
- package/modules/hall-ui/public/index-genesis-votes.states.json +61 -0
- package/modules/hall-ui/public/visor.js +67 -20
- package/modules/hall-ui/records/visor.md +7 -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_visor.mjs +63 -3
|
@@ -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.3",
|
|
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.3",
|
|
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.3",
|
|
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
|
@@ -11700,5 +11700,17 @@
|
|
|
11700
11700
|
"id": "1005040",
|
|
11701
11701
|
"text": "Added the Stripe Connect test-mode payment adapter (economy.processor port). Every payout, cash-out, balance and ledger-check call now has a processor to talk to once a test key is set; nothing is live."
|
|
11702
11702
|
}
|
|
11703
|
+
],
|
|
11704
|
+
"1.25.2": [
|
|
11705
|
+
{
|
|
11706
|
+
"id": "1005248",
|
|
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
|
+
}
|
|
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
|
+
}
|
|
11703
11715
|
]
|
|
11704
11716
|
}
|
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.3'; // 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');
|
package/tests/hall_visor.mjs
CHANGED
|
@@ -77,6 +77,64 @@ test("a place's own key: the visor binds it only when the scene asks, and the ch
|
|
|
77
77
|
assert.equal(V.keyFor('artisan', ev('c')).id, 'claim', "the character's key wins a clash");
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
+
// task 1005248 (owner, 2026-11-12): a key the visor shows is a recommendation, and no two
|
|
81
|
+
// things on the glass or in the sheet ever share one. Every key comes from resolveKeys.
|
|
82
|
+
function shownKeys(r) {
|
|
83
|
+
const out = [['a', 'Actions'], ['esc', 'Close']];
|
|
84
|
+
r.actions.forEach((a) => out.push([a.key, a.id]));
|
|
85
|
+
r.place.forEach((k, i) => out.push([k, 'place ' + i]));
|
|
86
|
+
r.lines.forEach((k, i) => out.push([k, 'line ' + i]));
|
|
87
|
+
out.push([r.options.key, 'options']);
|
|
88
|
+
return out.filter(([k]) => k);
|
|
89
|
+
}
|
|
90
|
+
function assertNoClash(V, who) {
|
|
91
|
+
for (const c of ['artisan', 'artist', 'patron']) {
|
|
92
|
+
const keys = shownKeys(V.resolveKeys(c)).map(([k]) => k);
|
|
93
|
+
assert.equal(new Set(keys).size, keys.length, `${who} / ${c}: one owner per key (${keys.join(' ')})`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const SKY = { name: 'sky', place: '', actions: [{ label: 'Write a thought', key: '/', lead: true, run() {} }, { label: 'Jump to a goal or a thought', key: 'j', bind: true, run() {} }, { label: 'List', key: 'L', run() {} }, { label: 'Tour', key: '?', run() {} }, { label: 'Fit', key: '0', run() {} }, { label: 'Clock · off', key: '', run() {} }], keys: [{ label: 'Look around', key: 'drag' }, { label: 'Zoom', key: '+ −' }] };
|
|
97
|
+
const CITY = { name: 'city', place: 'Governor city', actions: [{ label: 'decide 37 in a row', key: 'd', bind: true, lead: true, run() {} }, { label: 'Archived: Street', run() {} }], keys: [{ label: 'On a card: answer', key: '1–9' }, { label: 'On a card: next', key: '← →' }] };
|
|
98
|
+
|
|
99
|
+
test('no key ever means two things: every character over every real scene, and Options never on Escape', () => {
|
|
100
|
+
const V = loadPure();
|
|
101
|
+
assertNoClash(V, 'no scene');
|
|
102
|
+
for (const sc of [SKY, CITY, { name: 'home', place: 'Home' }]) { V.scene(sc); assertNoClash(V, sc.name); }
|
|
103
|
+
for (const c of ['artisan', 'artist', 'patron']) {
|
|
104
|
+
const r = V.resolveKeys(c);
|
|
105
|
+
assert.notEqual(r.options.key, 'esc', `${c}: Options is not on Escape; Escape is Close`);
|
|
106
|
+
assert.ok(r.options.key, `${c}: Options has a key of its own`);
|
|
107
|
+
}
|
|
108
|
+
assert.equal(V.resolveKeys('artist').options.key, 'o', 'O is Options where nothing else holds it');
|
|
109
|
+
assert.equal(V.resolveKeys('patron').options.key, 'p', "the Patron's O is the Board room, so Options moves to its next letter");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('a clash moves the later claimant to a free letter of its own name, and its key follows', () => {
|
|
113
|
+
const V = loadPure();
|
|
114
|
+
const ev = (key) => ({ key, defaultPrevented: false, ctrlKey: false, metaKey: false, altKey: false });
|
|
115
|
+
const run = () => {};
|
|
116
|
+
// the worst a scene can register: its own keys on A, Escape, a character key, and twice on one key
|
|
117
|
+
V.scene({ name: 'x', place: 'X', actions: [
|
|
118
|
+
{ label: 'Grab', key: 'a', run }, { label: 'Exit', key: 'Esc', run }, { label: 'Go', key: 'g', run }, { label: 'Again', key: 'g', run },
|
|
119
|
+
{ label: 'Claim here', key: 'c', bind: true, run }, { label: 'Thing', key: 't', bind: true, run }, { label: 'Open', key: 'o', bind: true, run },
|
|
120
|
+
], keys: [{ label: 'Same as Go', key: 'G' }] });
|
|
121
|
+
assertNoClash(V, 'hostile scene');
|
|
122
|
+
const r = V.resolveKeys('artisan');
|
|
123
|
+
const plain = (x) => JSON.parse(JSON.stringify(x));
|
|
124
|
+
assert.deepEqual(plain(r.place.slice(0, 4)), ['', '', 'g', ''], "A and Escape are the visor's; the scene keeps G once, and a second claim shows no key");
|
|
125
|
+
assert.equal(r.lines[0], '', 'a key line on a taken key shows no key');
|
|
126
|
+
assert.equal(r.actions.find((a) => a.id === 'sky').key, 'o', 'Goal sky lost G to the scene, so it moves to the next letter of its name, O');
|
|
127
|
+
assert.equal(V.keyFor('artisan', ev('o')).id, 'sky', 'and O runs it');
|
|
128
|
+
assert.equal(V.keyFor('artisan', ev('g')), null, "G is the scene's own now, so the visor does not take it");
|
|
129
|
+
assert.ok(!['c', 'l', 'a'].includes(r.place[4]), 'Claim here: C is taken, L and A are never handed out');
|
|
130
|
+
assert.equal(V.keyFor('artisan', ev(r.place[4])).label, 'Claim here');
|
|
131
|
+
assert.equal(V.keyFor('artisan', ev('c')).id, 'claim', "the character's key still wins");
|
|
132
|
+
assert.ok(r.place[6] && r.place[6] !== 'o', "Open asked for O after Goal sky took it, so it moves");
|
|
133
|
+
assert.equal(V.keyFor('artisan', ev(r.place[6])).label, 'Open');
|
|
134
|
+
assert.equal(V.keyFor('artisan', ev(r.options.key)).id, 'options', 'and Options takes the next free letter');
|
|
135
|
+
assert.equal(V.keyFor('artisan', ev('Escape')), null, 'the ambient glass never takes Escape');
|
|
136
|
+
});
|
|
137
|
+
|
|
80
138
|
// task 1005203: the four segments are the version the project is building, in quarters.
|
|
81
139
|
test('progress: the starred version while open, else the one building, else none', () => {
|
|
82
140
|
const V = loadPure();
|
|
@@ -238,7 +296,8 @@ test('in game mode the glass shows the place, the main action, the key hints, th
|
|
|
238
296
|
assert.equal(g.credits, '56,021 credits');
|
|
239
297
|
assert.deepEqual([g.num, g.unit], ['56,021', 'credits'], 'the count and its word are two spans of the same text');
|
|
240
298
|
// task 1005195: every other control is a quiet key hint. Actions kept its key and its sheet; nothing was dropped.
|
|
241
|
-
|
|
299
|
+
// task 1005248: Options is only in the sheet, so Escape is on the glass nowhere.
|
|
300
|
+
assert.deepEqual(g.hints, ['TYour thinking', 'AActions']);
|
|
242
301
|
assert.equal(g.arcs, 2, 'two arcs frame the glass');
|
|
243
302
|
assert.equal(g.sprite, '/builders/characters/artisan-visor.png', 'your picture on the glass is you wearing the visor (task 1005218)');
|
|
244
303
|
assert.deepEqual(errors, []);
|
|
@@ -295,7 +354,7 @@ test('a lead scene action is the first key hint, and its click and its key run i
|
|
|
295
354
|
const { ctx, page, errors } = await open();
|
|
296
355
|
await page.evaluate(() => OTBVisor.scene({ name: 'test', place: 'Test city', actions: [{ label: 'Decide 2 in a row', key: 'd', bind: true, lead: true, run() { window.decided = (window.decided || 0) + 1; } }] }));
|
|
297
356
|
const hints = await page.$$eval('.visor__hints .visor__hint', (h) => h.map((x) => x.textContent));
|
|
298
|
-
assert.deepEqual(hints, ['DDecide 2 in a row', 'TYour thinking', 'AActions'
|
|
357
|
+
assert.deepEqual(hints, ['DDecide 2 in a row', 'TYour thinking', 'AActions']);
|
|
299
358
|
await page.click('.visor__hint--place');
|
|
300
359
|
await page.keyboard.press('d');
|
|
301
360
|
assert.equal(await page.evaluate(() => window.decided), 2, 'the hint and D both press it');
|
|
@@ -311,7 +370,8 @@ test('the sheet lists the character\'s actions, then this place, and runs a scen
|
|
|
311
370
|
await page.click('[data-visor-sheet]');
|
|
312
371
|
const rows = await page.$$eval('.visor__sheet .visor__row', (r) => r.map((x) => [...x.children].map((c) => c.textContent).join(' ')));
|
|
313
372
|
// task 1005200: Claim a task and Blockers open classic pages, so they are off the glass.
|
|
314
|
-
assert.deepEqual(rows, ['Goal sky G', 'Your thinking T', 'Back to centre 0', 'Look around drag', 'Options
|
|
373
|
+
assert.deepEqual(rows, ['Goal sky G', 'Your thinking T', 'Back to centre 0', 'Look around drag', 'Options O']);
|
|
374
|
+
assert.equal(await page.$eval('.visor__sheet [data-visor-close]', (b) => b.textContent), 'Esc', 'Escape is the Close of the sheet, and only that');
|
|
315
375
|
await page.click('[data-visor-scene="0"]');
|
|
316
376
|
assert.deepEqual(await page.evaluate(() => ({ centred: window.centred, open: OTBVisor.isOpen() })), { centred: true, open: false });
|
|
317
377
|
await ctx.close();
|