@bongos/core 1.25.18 → 1.25.20
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 +113 -83
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -1
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0408-a-genesis-stage-is-three-doors-onto-one-record.md +1 -1
- package/docs/api/openapi.json +81 -2
- package/docs/api-reference.md +7 -5
- package/docs/copy-inventory.md +219 -216
- package/docs/copy-registry.json +273 -246
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +348 -347
- package/docs/project-context.template.md +1 -1
- package/modules/front-door-guide/handoff.js +120 -0
- package/modules/front-door-guide/module.json +1 -1
- package/modules/front-door-guide/routes/front-door-guide-plan.js +24 -0
- package/modules/hall-ui/public/ai-studio.html +1 -1
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/city.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/genesis-home.js +51 -7
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/home-city-draw.js +14 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/market-watch-item.html +1 -1
- package/modules/hall-ui/public/market-watch-rules.html +1 -1
- package/modules/hall-ui/public/market-watch.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/money.html +1 -1
- package/modules/hall-ui/public/my-agents.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +32 -20
- package/modules/hall-ui/public/street.html +1 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +9 -4
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/lifecycle/home-city.js +17 -3
- package/modules/onboarding/founding-mode.js +45 -1
- package/modules/onboarding/genesis-home.js +70 -7
- package/modules/onboarding/genesis-work.js +68 -0
- package/modules/onboarding/guide-handoff.js +109 -0
- package/modules/onboarding/routes/genesis-work.js +11 -4
- package/modules/provisioning/guide-handoff.js +65 -0
- package/modules/provisioning/provisioning.js +2 -2
- package/modules/provisioning/routes/provisioning.js +4 -2
- package/modules/public-landing/public/projects.html +79 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/scripts/bongos/genesis.js +8 -1
- package/src/bongos/founding-page-gate.js +67 -0
- package/src/bongos/serve-internal.js +3 -0
- package/src/branding.js +4 -0
- package/src/module-api.js +1 -1
- package/tests/founding_mode.mjs +84 -8
- package/tests/founding_page_gate.mjs +105 -0
- package/tests/front_door_guide_existing.mjs +1 -0
- package/tests/genesis_home.mjs +105 -1
- package/tests/genesis_work.mjs +29 -0
- package/tests/genesis_work_routes.mjs +9 -0
- package/tests/guide_handoff.mjs +322 -0
- package/tests/home_city.mjs +52 -0
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/provision_settings_apply.mjs +2 -1
- package/tests/provisioning_settings_env.mjs +2 -2
package/tests/founding_mode.mjs
CHANGED
|
@@ -143,7 +143,8 @@ function makeHall() {
|
|
|
143
143
|
documentElement: root,
|
|
144
144
|
createElement: (t) => new El(t),
|
|
145
145
|
querySelector: (sel) => (sel === '.app-body' ? appBody : null),
|
|
146
|
-
querySelectorAll: (sel) => (sel === '#app-sidebar .nav-item--asleep
|
|
146
|
+
querySelectorAll: (sel) => (sel === '#app-sidebar .nav-item--asleep, #app-sidebar .nav-item--arrived'
|
|
147
|
+
? nav.filter((a) => a.classList.contains('nav-item--asleep') || a.classList.contains('nav-item--arrived')) : []),
|
|
147
148
|
getElementById: (id) => nav.find((a) => a.id === id) || appBody.children.find((c) => c.id === id) || null,
|
|
148
149
|
};
|
|
149
150
|
return { document, nav, appBody, root };
|
|
@@ -164,12 +165,12 @@ function sliceFn(name) {
|
|
|
164
165
|
assert.ok(start > -1 && end > start, name + ' was located in shell.js');
|
|
165
166
|
return SHELL.slice(start, end + 4);
|
|
166
167
|
}
|
|
167
|
-
function loadApplyFounding(document, access, brand = { identity: { productName: 'Kestrel' } }) {
|
|
168
|
+
function loadApplyFounding(document, access, brand = { identity: { productName: 'Kestrel' } }, storage = null) {
|
|
168
169
|
const wakeStart = SHELL.indexOf(' var WAKE_TEXT = {');
|
|
169
170
|
const wakeEnd = SHELL.indexOf('};', wakeStart) + 2;
|
|
170
171
|
assert.ok(wakeStart > -1, 'WAKE_TEXT was located in shell.js');
|
|
171
172
|
const src = [SHELL.slice(wakeStart, wakeEnd), sliceEsc(), sliceFn('applyFounding')].join('\n');
|
|
172
|
-
return new Function('document', 'brand', 'access', 'window', `${src}\nreturn applyFounding;`)(document, brand, access, { OTB: realOTB() });
|
|
173
|
+
return new Function('document', 'brand', 'access', 'window', `${src}\nreturn applyFounding;`)(document, brand, access, { OTB: realOTB(), localStorage: storage || undefined });
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
const snapshot = (h) => JSON.stringify({
|
|
@@ -213,8 +214,8 @@ test('founding ON: the band above the topbar, and the rooms the server named asl
|
|
|
213
214
|
const byId = (id) => h.nav.find((a) => a.id === 'nav-' + id);
|
|
214
215
|
assert.ok(!byId('goals').classList.contains('nav-item--asleep'), 'scope is behind: goals is awake');
|
|
215
216
|
assert.ok(byId('work').classList.contains('nav-item--asleep'), 'strategize is current: tasks still sleeps');
|
|
216
|
-
assert.equal(byId('work').children
|
|
217
|
-
assert.
|
|
217
|
+
assert.equal(byId('work').children.length, 0, 'a hidden room carries no label nobody can see (task 1005112)');
|
|
218
|
+
assert.ok(byId('collab').classList.contains('nav-item--asleep'), 'community sleeps until alive');
|
|
218
219
|
assert.ok(!byId('home').classList.contains('nav-item--asleep'), 'home is never put to sleep');
|
|
219
220
|
});
|
|
220
221
|
|
|
@@ -227,7 +228,7 @@ test('founding OFF after ON restores the hall exactly — OFF removes, it does n
|
|
|
227
228
|
assert.notEqual(snapshot(h), before);
|
|
228
229
|
access.founding = null;
|
|
229
230
|
apply();
|
|
230
|
-
assert.equal(snapshot(h), before, 'band, asleep classes
|
|
231
|
+
assert.equal(snapshot(h), before, 'band, asleep classes and the root attribute are all gone');
|
|
231
232
|
});
|
|
232
233
|
|
|
233
234
|
test('the band escapes what it prints — a project name cannot inject markup', () => {
|
|
@@ -280,13 +281,13 @@ test('a failing waiting-on source never turns the mode off', async () => {
|
|
|
280
281
|
assert.equal(f.waiting_on, null);
|
|
281
282
|
});
|
|
282
283
|
|
|
283
|
-
test('re-applying ON does not stack a second band or
|
|
284
|
+
test('re-applying ON does not stack a second band or add anything to a room', () => {
|
|
284
285
|
const h = makeHall();
|
|
285
286
|
const access = { founding: fm.shapeFoundingMode({ active: 'true', stage: 'decide' }) };
|
|
286
287
|
const apply = loadApplyFounding(h.document, access);
|
|
287
288
|
apply(); apply();
|
|
288
289
|
assert.equal(h.appBody.children.filter((c) => c.id === 'founding-band').length, 1);
|
|
289
|
-
assert.equal(h.nav.find((a) => a.id === 'nav-work').children.length,
|
|
290
|
+
assert.equal(h.nav.find((a) => a.id === 'nav-work').children.length, 0);
|
|
290
291
|
});
|
|
291
292
|
|
|
292
293
|
// ── the wiring ──────────────────────────────────────────────────────────────
|
|
@@ -317,3 +318,78 @@ test('founding mode adds no page, no route and no nav item — it is a mode', ()
|
|
|
317
318
|
const hall = fs.readdirSync(path.join(ROOT, 'modules', 'hall-ui', 'public'));
|
|
318
319
|
assert.ok(!hall.some((f) => /^founding\./.test(f)), 'no founding page file');
|
|
319
320
|
});
|
|
321
|
+
|
|
322
|
+
// ── task 1005112: hidden, not dimmed; the arrival; the honest refusal ─────────
|
|
323
|
+
|
|
324
|
+
test('a sleeping room is hidden by the sheet, and a group of sleeping rooms hides its head', () => {
|
|
325
|
+
const css = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'style.css'), 'utf8');
|
|
326
|
+
assert.match(css, /\.nav-item--asleep \{ display: none; \}/, 'asleep is not drawn at all, never dimmed');
|
|
327
|
+
assert.doesNotMatch(css, /\.nav-item--asleep \{ opacity/);
|
|
328
|
+
assert.match(css, /html\[data-founding="on"\] \.nav-group:not\(:has\(\.nav-item:not\(\.nav-item--asleep\)/);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('a room that woke since this browser last looked arrives marked, once (task 1005112)', () => {
|
|
332
|
+
const store = new Map();
|
|
333
|
+
const storage = { getItem: (k) => (store.has(k) ? store.get(k) : null), setItem: (k, v) => store.set(k, String(v)) };
|
|
334
|
+
const at = (stage) => ({ founding: fm.shapeFoundingMode({ active: 'true', stage }) });
|
|
335
|
+
const h1 = makeHall();
|
|
336
|
+
loadApplyFounding(h1.document, at('scope'), undefined, storage)();
|
|
337
|
+
assert.ok(!h1.nav.some((a) => a.classList.contains('nav-item--arrived')), 'the first look marks nothing');
|
|
338
|
+
const h2 = makeHall();
|
|
339
|
+
loadApplyFounding(h2.document, at('strategize'), undefined, storage)();
|
|
340
|
+
const arrived = h2.nav.filter((a) => a.classList.contains('nav-item--arrived')).map((a) => a.id);
|
|
341
|
+
assert.deepEqual(arrived.sort(), ['nav-goal-map', 'nav-goals'], 'scope finished: Goals and Sky arrive');
|
|
342
|
+
const h3 = makeHall();
|
|
343
|
+
loadApplyFounding(h3.document, at('strategize'), undefined, storage)();
|
|
344
|
+
assert.ok(!h3.nav.some((a) => a.classList.contains('nav-item--arrived')), 'the next page load: no mark again');
|
|
345
|
+
// Switching off removes the mark with everything else.
|
|
346
|
+
const access = at('charter');
|
|
347
|
+
const h4 = makeHall();
|
|
348
|
+
const before = snapshot(h4);
|
|
349
|
+
const apply = loadApplyFounding(h4.document, access, undefined, storage);
|
|
350
|
+
apply();
|
|
351
|
+
access.founding = null;
|
|
352
|
+
apply();
|
|
353
|
+
assert.equal(snapshot(h4), before);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test('the shell says, on Home, what a refused room is and which stage opens it (task 1005112)', () => {
|
|
357
|
+
const navStart = SHELL.indexOf(' var NAV = [');
|
|
358
|
+
const navEnd = SHELL.indexOf(' var NAV_COLLAPSE_KEY');
|
|
359
|
+
const wakeStart = SHELL.indexOf(' var WAKE_TEXT = {');
|
|
360
|
+
const wakeEnd = SHELL.indexOf('};', wakeStart) + 2;
|
|
361
|
+
const src = [SHELL.slice(navStart, navEnd), SHELL.slice(wakeStart, wakeEnd), sliceEsc(),
|
|
362
|
+
sliceFn('navItemById'), sliceFn('renderDeniedNotice')].join('\n');
|
|
363
|
+
let inserted = null;
|
|
364
|
+
let replaced = null;
|
|
365
|
+
const el = { querySelector: () => ({ addEventListener() {} }), setAttribute() {} };
|
|
366
|
+
const document = {
|
|
367
|
+
getElementById: (id) => (id === 'app-topbar' ? { insertAdjacentElement: (_, n) => { inserted = n; } } : null),
|
|
368
|
+
createElement: () => el,
|
|
369
|
+
};
|
|
370
|
+
const run = (search) => {
|
|
371
|
+
inserted = null;
|
|
372
|
+
new Function('document', 'window', 'history', 'brand', 'access', 'icon', 'chevronIcon',
|
|
373
|
+
`${src}\nreturn renderDeniedNotice;`)(document,
|
|
374
|
+
{ location: { search, pathname: '/', hash: '' }, OTB: realOTB() },
|
|
375
|
+
{ replaceState: (a, b, url) => { replaced = url; } }, {}, {}, () => '', () => '')();
|
|
376
|
+
return inserted;
|
|
377
|
+
};
|
|
378
|
+
const n = run('?asleep=goals&opens=scope');
|
|
379
|
+
assert.ok(n, 'a notice is drawn');
|
|
380
|
+
assert.match(n.innerHTML, /Goals isn’t open yet\. It opens after scope\. Finish that stage of founding here on Home\./);
|
|
381
|
+
assert.equal(replaced, '/', 'the query is stripped so a refresh does not re-show it');
|
|
382
|
+
assert.equal(run('?asleep=goals%3Cx&opens=scope'), null, 'a malformed room id draws nothing');
|
|
383
|
+
assert.match(run('?denied=page.view.government').innerHTML, /permission/, 'the old denial still reads the same');
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('the arrival mark survives the shell applying the mode a second time on one page', () => {
|
|
387
|
+
const store = new Map([['hall-founding-asleep', JSON.stringify({ goals: 'scope', 'goal-map': 'scope', 'board-room': 'charter' })]]);
|
|
388
|
+
const storage = { getItem: (k) => (store.has(k) ? store.get(k) : null), setItem: (k, v) => store.set(k, String(v)) };
|
|
389
|
+
const h = makeHall();
|
|
390
|
+
const apply = loadApplyFounding(h.document, { founding: fm.shapeFoundingMode({ active: 'true', stage: 'strategize' }) }, undefined, storage);
|
|
391
|
+
apply();
|
|
392
|
+
apply(); // /me settles, then the page reports: applyAccess runs again
|
|
393
|
+
const arrived = h.nav.filter((a) => a.classList.contains('nav-item--arrived')).map((a) => a.id).sort();
|
|
394
|
+
assert.deepEqual(arrived, ['nav-board-room', 'nav-goal-map', 'nav-goals']);
|
|
395
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// tests/founding_page_gate.mjs — A SLEEPING ROOM IS NOT REACHABLE BY URL (task 1005112).
|
|
2
|
+
//
|
|
3
|
+
// The sidebar hides a room a founding project has not opened; this is the other half,
|
|
4
|
+
// the page. Everything here RUNS the real middleware (src/bongos/founding-page-gate.js)
|
|
5
|
+
// over fake requests, fed by the real founding-mode shape (modules/onboarding/
|
|
6
|
+
// founding-mode.js) — and, in the last cases, through the real port lookup — so the
|
|
7
|
+
// server's /me answer and the page refusal are one rule.
|
|
8
|
+
|
|
9
|
+
import { strict as assert } from 'node:assert';
|
|
10
|
+
import { test } from 'node:test';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const { foundingPageGate } = require('../src/bongos/founding-page-gate.js');
|
|
16
|
+
const seams = require('../src/module-seams.js');
|
|
17
|
+
const fm = require('../modules/onboarding/founding-mode.js');
|
|
18
|
+
|
|
19
|
+
const founding = (stage) => fm.shapeFoundingMode({ active: 'true', stage });
|
|
20
|
+
const ROOMS = [...new Set([...Object.values(fm.WAKES).flat(), ...fm.WAKES_WHEN_ALIVE])];
|
|
21
|
+
|
|
22
|
+
// Drive the middleware once; resolve with what it did.
|
|
23
|
+
function run(pathname, { read, buildersHost = false, skip = false } = {}) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const res = {
|
|
26
|
+
headers: {},
|
|
27
|
+
set(k, v) { this.headers[k] = v; return this; },
|
|
28
|
+
redirect(code, url) { resolve({ redirect: url, code, headers: this.headers }); },
|
|
29
|
+
};
|
|
30
|
+
const opts = { isBuildersHost: () => buildersHost, skip: () => skip };
|
|
31
|
+
if (read) opts.read = read;
|
|
32
|
+
foundingPageGate(opts)({ path: pathname }, res, () => resolve({ next: true }));
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test('every room a genesis stage can put to sleep is refused while it sleeps, and nothing else is', async () => {
|
|
37
|
+
// At 'decide' every room sleeps, so this is the whole list the gate must cover.
|
|
38
|
+
const read = async () => founding('decide');
|
|
39
|
+
for (const id of ROOMS) {
|
|
40
|
+
const out = await run(`/builders/${id}`, { read });
|
|
41
|
+
assert.ok(out.redirect, `${id} is refused while asleep`);
|
|
42
|
+
}
|
|
43
|
+
for (const never of ['primer', 'board-room', 'profile', 'task/12', 'settings', 'blockers', 'studio']) {
|
|
44
|
+
assert.deepEqual(await run(`/builders/${never}`, { read }), { next: true }, `${never} is never gated`);
|
|
45
|
+
}
|
|
46
|
+
// And the gate names no room the server never puts to sleep: a fake answer naming
|
|
47
|
+
// an unknown room changes nothing.
|
|
48
|
+
assert.deepEqual(await run('/builders/blockers', { read: async () => ({ active: true, asleep: { blockers: 'scope' } }) }), { next: true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('a sleeping room\'s page goes home, naming the room and the stage that opens it', async () => {
|
|
52
|
+
const out = await run('/builders/goals', { read: async () => founding('scope') });
|
|
53
|
+
assert.equal(out.code, 302);
|
|
54
|
+
assert.equal(out.redirect, '/builders/?asleep=goals&opens=scope');
|
|
55
|
+
assert.equal(out.headers['Cache-Control'], 'no-store', 'a refusal is never cached past the wake');
|
|
56
|
+
const sub = await run('/builders/work.html', { read: async () => founding('scope'), buildersHost: true });
|
|
57
|
+
assert.equal(sub.redirect, '/?asleep=work&opens=strategize', 'the builders. host form');
|
|
58
|
+
const alive = await run('/builders/roster/', { read: async () => founding('legislation') });
|
|
59
|
+
assert.equal(alive.redirect, '/builders/?asleep=roster&opens=alive');
|
|
60
|
+
const sneaky = await run('/builders//goal-map', { read: async () => founding('decide') });
|
|
61
|
+
assert.equal(sneaky.redirect, '/builders/?asleep=goal-map&opens=scope', 'a doubled slash is normalised, not a way past');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('an awake room opens, and a later stage never takes one away', async () => {
|
|
65
|
+
assert.deepEqual(await run('/builders/goals', { read: async () => founding('strategize') }), { next: true });
|
|
66
|
+
assert.deepEqual(await run('/builders/work', { read: async () => founding('charter') }), { next: true });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('a project that is not founding is untouched, and a failed read opens rather than shuts', async () => {
|
|
70
|
+
assert.deepEqual(await run('/builders/goals', { read: async () => null }), { next: true });
|
|
71
|
+
assert.deepEqual(await run('/builders/collab', { read: async () => ({ active: false }) }), { next: true });
|
|
72
|
+
assert.deepEqual(await run('/builders/goals', { read: async () => { throw new Error('db down'); } }), { next: true },
|
|
73
|
+
'a read that throws opens the page');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('only the five room pages ever cost a founding read', async () => {
|
|
77
|
+
let reads = 0;
|
|
78
|
+
const read = async () => { reads += 1; return founding('decide'); };
|
|
79
|
+
for (const p of ['/builders/', '/builders/primer', '/builders/board-room', '/builders/task/5', '/api/bongos/me', '/goals']) {
|
|
80
|
+
assert.deepEqual(await run(p, { read }), { next: true }, p);
|
|
81
|
+
}
|
|
82
|
+
assert.deepEqual(await run('/builders/goals', { read, skip: true }), { next: true }, 'a module surface host is skipped');
|
|
83
|
+
assert.equal(reads, 0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('the default read is the onboarding port\'s foundingModeFor, the answer /me carries', async () => {
|
|
87
|
+
seams._reset();
|
|
88
|
+
try {
|
|
89
|
+
assert.deepEqual(await run('/builders/goals'), { next: true }, 'onboarding module off: not founding, page opens');
|
|
90
|
+
seams.registerProvider('onboarding', { foundingModeFor: async () => founding('scope') });
|
|
91
|
+
assert.equal((await run('/builders/goals')).redirect, '/builders/?asleep=goals&opens=scope');
|
|
92
|
+
seams._reset();
|
|
93
|
+
seams.registerProvider('onboarding', { foundingModeFor: async () => { throw new Error('settings table gone'); } });
|
|
94
|
+
assert.deepEqual(await run('/builders/goals'), { next: true }, 'a port that throws opens the page');
|
|
95
|
+
} finally {
|
|
96
|
+
seams._reset();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('the server mounts it after the sign-in gate, so a stranger still meets sign-in first', () => {
|
|
101
|
+
const src = fs.readFileSync(new URL('../src/bongos/serve-internal.js', import.meta.url), 'utf8');
|
|
102
|
+
const signIn = src.indexOf('return gdsAuth.requireBuilderPage(req, res, next);');
|
|
103
|
+
const mount = src.indexOf('app.use(foundingPageGate(');
|
|
104
|
+
assert.ok(signIn > 0 && mount > signIn, 'mounted, and after the sign-in gate');
|
|
105
|
+
});
|
|
@@ -222,6 +222,7 @@ test('the guide has no upload, fetch or link-reading path: these are all its rou
|
|
|
222
222
|
assert.deepEqual([...routes(chatRouter), ...routes(planRouter)].sort(), [
|
|
223
223
|
'GET /front-door-guide/conversations/:id',
|
|
224
224
|
'GET /front-door-guide/mine',
|
|
225
|
+
'GET /front-door-guide/mine/handoff.md', // the plan as the hand-off file, a download (task 1005274): reads nothing of theirs
|
|
225
226
|
'GET /front-door-guide/status',
|
|
226
227
|
'POST /front-door-guide/conversations',
|
|
227
228
|
'POST /front-door-guide/conversations/:id/attach',
|
package/tests/genesis_home.mjs
CHANGED
|
@@ -226,7 +226,7 @@ function world({ withGovernment = true } = {}) {
|
|
|
226
226
|
const deps = () => ({
|
|
227
227
|
getSetting: async (k) => (store.has(k) ? { key: k, value: store.get(k) } : null),
|
|
228
228
|
setSetting: async (k, v) => { store.set(k, String(v)); return { key: k, value: String(v) }; },
|
|
229
|
-
lifecycle, government, now,
|
|
229
|
+
lifecycle, government, now, documents: null,
|
|
230
230
|
});
|
|
231
231
|
return {
|
|
232
232
|
store, tasks, board, opened, deps,
|
|
@@ -385,6 +385,67 @@ test('with no Board Room module the founder\'s confirm closes a finished stage',
|
|
|
385
385
|
assert.equal((await gh.readGenesis(w.deps())).stage, 'strategize');
|
|
386
386
|
});
|
|
387
387
|
|
|
388
|
+
test('task 1005115: with no Board Room a finished stage moves on by itself, and the view says so', async () => {
|
|
389
|
+
const w = world({ withGovernment: false });
|
|
390
|
+
await gh.startFounding(1, w.deps());
|
|
391
|
+
await gh.savePlan({}, 1, w.deps());
|
|
392
|
+
let g = await gh.readGenesis(w.deps());
|
|
393
|
+
assert.equal(g.board_room, false);
|
|
394
|
+
assert.equal(g.moved, null, 'nothing has moved past a stage yet');
|
|
395
|
+
w.finish('scope');
|
|
396
|
+
// A ship moves it on; no read and no button needed.
|
|
397
|
+
assert.deepEqual(await gh.onTaskShipped(w.deps()), []);
|
|
398
|
+
assert.equal(w.store.get(fm.KEYS.STAGE), 'strategize', 'the ship that finished scope advanced it');
|
|
399
|
+
g = await gh.readGenesis(w.deps());
|
|
400
|
+
assert.equal(g.stage, 'strategize');
|
|
401
|
+
assert.equal(g.can_close, false, 'there is no close button on this path');
|
|
402
|
+
assert.equal(g.moved.from, 'scope');
|
|
403
|
+
assert.equal(g.moved.to, 'strategize');
|
|
404
|
+
assert.equal(g.moved.how, 'tasks_finished');
|
|
405
|
+
assert.deepEqual(g.moved.opened, fm.WAKES.scope, 'and it names what opened');
|
|
406
|
+
assert.equal(g.stages[0].state, 'done');
|
|
407
|
+
// Every read reconciles too, through several finished stages in order.
|
|
408
|
+
w.finish('strategize'); w.finish('charter');
|
|
409
|
+
g = await gh.readGenesis(w.deps());
|
|
410
|
+
assert.equal(g.stage, 'legalize');
|
|
411
|
+
// Legislation does not pass itself: its finished tasks open the alive door.
|
|
412
|
+
w.finish('legalize'); w.finish('legislation');
|
|
413
|
+
g = await gh.readGenesis(w.deps());
|
|
414
|
+
assert.equal(g.stage, 'legislation');
|
|
415
|
+
assert.equal(g.can_close, false);
|
|
416
|
+
assert.equal(g.can_come_alive, true);
|
|
417
|
+
// The old close call on a stage that already moved answers what happened.
|
|
418
|
+
const again = await gh.closeStage('scope', 1, w.deps());
|
|
419
|
+
assert.equal(again.ok, true);
|
|
420
|
+
assert.equal(again.already, true);
|
|
421
|
+
assert.equal(again.next_stage, 'legislation', 'it names where the project is now, not the stage after scope');
|
|
422
|
+
assert.equal((await gh.comeAlive(1, w.deps())).ok, true);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test('task 1005115: with a Board Room nothing moves by itself; the sitting still decides', async () => {
|
|
426
|
+
const w = world();
|
|
427
|
+
await gh.startFounding(1, w.deps());
|
|
428
|
+
await gh.savePlan({}, 1, w.deps());
|
|
429
|
+
w.finish('scope');
|
|
430
|
+
const g = await gh.readGenesis(w.deps());
|
|
431
|
+
assert.equal(g.stage, 'scope');
|
|
432
|
+
assert.equal(g.board_room, true);
|
|
433
|
+
assert.equal(g.can_close, true);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test('task 1005115: each stage on the genesis home carries its document', async () => {
|
|
437
|
+
const w = world({ withGovernment: false });
|
|
438
|
+
await gh.startFounding(1, w.deps());
|
|
439
|
+
await gh.savePlan({}, 1, w.deps());
|
|
440
|
+
const documents = async () => ({ scope: { title: 'The scope', filled: true, updated_at: null, editable: true } });
|
|
441
|
+
let g = await gh.readGenesis({ ...w.deps(), documents });
|
|
442
|
+
assert.equal(g.stages[0].document.title, 'The scope');
|
|
443
|
+
assert.equal(g.stages[1].document, null);
|
|
444
|
+
g = await gh.readGenesis({ ...w.deps(), documents: async () => { throw new Error('db down'); } });
|
|
445
|
+
assert.equal(g.stage, 'scope', 'the home still renders when the documents cannot be read');
|
|
446
|
+
assert.equal(g.stages[0].document, null);
|
|
447
|
+
});
|
|
448
|
+
|
|
388
449
|
test('the view: states, wakes, the life plates and a due date from the plan', () => {
|
|
389
450
|
const mode = { stage: 'strategize', stage_number: 2, stage_count: 5, day: 9 };
|
|
390
451
|
const g = gh.shapeGenesis({
|
|
@@ -592,6 +653,8 @@ test('the stage reads as a checklist: done, open and locked, and a locked row of
|
|
|
592
653
|
assert.match(row('302'), /class="genesis__task is-locked"/);
|
|
593
654
|
assert.match(row('302'), /after Gather what already exists/, 'a locked row says what it waits on');
|
|
594
655
|
assert.doesNotMatch(row('302'), /data-genesis-copy/, 'a locked row offers no prompt for work it cannot claim');
|
|
656
|
+
assert.doesNotMatch(row('301'), /sr-only/, 'an open row adds no hidden word its status pill could contradict');
|
|
657
|
+
assert.match(row('302'), /class="sr-only">Locked: /);
|
|
595
658
|
assert.doesNotMatch(html, /is-finished|line-through/, 'nothing is drawn struck through');
|
|
596
659
|
// The "done" row: the earlier stage is not the one on screen, so check the shape directly.
|
|
597
660
|
const done = gh.shapeGenesis({ mode: { stage: 'scope', stage_number: 1, stage_count: 5, day: 1 }, plan: gh.parsePlan({}).plan,
|
|
@@ -605,6 +668,25 @@ test('the stage reads as a checklist: done, open and locked, and a locked row of
|
|
|
605
668
|
assert.doesNotMatch(r304, /data-genesis-copy/);
|
|
606
669
|
});
|
|
607
670
|
|
|
671
|
+
test('task 1005115: with no Board Room the card says the stage moved by itself, and lists what each stage left', async () => {
|
|
672
|
+
const genesis = gh.shapeGenesis({
|
|
673
|
+
mode: { stage: 'strategize', stage_number: 2, stage_count: 5, day: 3 },
|
|
674
|
+
plan: { days: {} },
|
|
675
|
+
tasks: { scope: [{ id: '1', title: 'Write the scope', status: 'shipped' }], strategize: [{ id: '2', title: 'Write the strategy', status: 'ready' }] },
|
|
676
|
+
stageAt: { strategize: '2026-10-08T00:00:00Z' },
|
|
677
|
+
boardRoom: false,
|
|
678
|
+
documents: { scope: { title: 'The scope', filled: true, updated_at: null, editable: true }, strategize: { title: 'The strategy', filled: false } },
|
|
679
|
+
});
|
|
680
|
+
const { host } = bootCard({ founding: true, data: { genesis, viewer_id: '1' } });
|
|
681
|
+
await new Promise((r) => setImmediate(r));
|
|
682
|
+
await new Promise((r) => setImmediate(r));
|
|
683
|
+
assert.match(host.innerHTML, /data-genesis-moved>Scope is done, so this stage opened by itself\. Now open: /);
|
|
684
|
+
assert.match(host.innerHTML, /moves on by itself/);
|
|
685
|
+
assert.doesNotMatch(host.innerHTML, /put this stage to the board/, 'no board to put it to');
|
|
686
|
+
assert.match(host.innerHTML, /<b>The scope<\/b>/);
|
|
687
|
+
assert.doesNotMatch(host.innerHTML, /The strategy<\/b>/, 'an empty document is not listed');
|
|
688
|
+
});
|
|
689
|
+
|
|
608
690
|
test('the hall carries the five life plates byte-identical to the world\'s', () => {
|
|
609
691
|
const world = path.join(ROOT, 'modules', 'public-landing', 'public', 'assets', 'world');
|
|
610
692
|
const hall = path.join(ROOT, 'modules', 'hall-ui', 'public', 'genesis');
|
|
@@ -694,3 +776,25 @@ test('task 1005121: the close route answers 202 with the whole body for an open
|
|
|
694
776
|
const click = hall.slice(hall.indexOf("t.hasAttribute('data-genesis-close')"), hall.indexOf("t.hasAttribute('data-genesis-confirm')"));
|
|
695
777
|
assert.ok(click.length > 0 && !click.includes('/votes'),'the hall no longer casts a second vote after closing — the server did it');
|
|
696
778
|
});
|
|
779
|
+
|
|
780
|
+
test('a stage that opens no room says what finishing it earns, never nothing (task 1005112)', async () => {
|
|
781
|
+
const genesis = gh.shapeGenesis({ mode: { stage: 'scope', stage_number: 1, stage_count: 5, day: 1 }, plan: gh.parsePlan({}).plan });
|
|
782
|
+
const by = Object.fromEntries(genesis.stages.map((s) => [s.key, s]));
|
|
783
|
+
assert.equal(by.scope.earns, null, 'a stage that wakes rooms names the rooms instead');
|
|
784
|
+
assert.equal(by.strategize.earns, null);
|
|
785
|
+
for (const k of ['charter', 'legalize', 'legislation']) {
|
|
786
|
+
assert.deepEqual(by[k].wakes, [], `${k} wakes no room`);
|
|
787
|
+
assert.ok(by[k].earns && by[k].earns.length > 10, `${k} says what it earns`);
|
|
788
|
+
}
|
|
789
|
+
assert.match(by.legislation.earns, /every room opens/, 'the last stage is the one that opens everything');
|
|
790
|
+
const b = bootCard({ founding: true, data: { genesis, viewer_id: '1' } });
|
|
791
|
+
await mountClickable(b);
|
|
792
|
+
assert.match(b.host.innerHTML, /Wakes Goals, Goal map/);
|
|
793
|
+
assert.match(b.host.innerHTML, /puts your government in force/);
|
|
794
|
+
assert.match(b.host.innerHTML, /settles how contributors are rewarded/);
|
|
795
|
+
// The district rises with the stages (task 1005112): each row also names what it raises.
|
|
796
|
+
assert.match(b.host.innerHTML, /Wakes Goals, Goal map · raises Planning/);
|
|
797
|
+
assert.match(b.host.innerHTML, /Raises Treasury · settles how contributors are rewarded/);
|
|
798
|
+
assert.match(b.host.innerHTML, /Raises Rules, Review, Disputes · brings the project to life/);
|
|
799
|
+
assert.deepEqual(by.legalize.raises, ['treasury']);
|
|
800
|
+
});
|
package/tests/genesis_work.mjs
CHANGED
|
@@ -370,6 +370,35 @@ if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
|
|
|
370
370
|
assert.match(out, /\(empty\)/);
|
|
371
371
|
});
|
|
372
372
|
|
|
373
|
+
await test('task 1005115: a stage\'s document is its record read as one named, editable page', async () => {
|
|
374
|
+
const deps = { store: memStore() };
|
|
375
|
+
let d = (await gw.readDocument('strategize', deps)).document;
|
|
376
|
+
assert.equal(d.title, 'The strategy');
|
|
377
|
+
assert.equal(d.filled, false, 'nothing written yet: an empty document, not a heading');
|
|
378
|
+
assert.equal(d.markdown, '');
|
|
379
|
+
await gw.setAnswer('strategize', 'big_pieces', { text: 'Ledger, then sync' }, 1, deps);
|
|
380
|
+
await gw.setAnswer('strategize', 'risks', { state: 'not_sure', text: 'bank API' }, 1, deps);
|
|
381
|
+
await gw.setPage('strategize', 'We start small.', 1, deps);
|
|
382
|
+
d = (await gw.readDocument('strategize', deps)).document;
|
|
383
|
+
assert.equal(d.filled, true);
|
|
384
|
+
assert.match(d.markdown, /^# The strategy\n/);
|
|
385
|
+
assert.match(d.markdown, /Ledger, then sync/);
|
|
386
|
+
assert.match(d.markdown, /Not sure yet — bank API/);
|
|
387
|
+
assert.match(d.markdown, /We start small\./);
|
|
388
|
+
assert.equal(d.editable, true);
|
|
389
|
+
assert.equal(d.edit.page, 'PUT /api/bongos/founding/work/strategize/page');
|
|
390
|
+
assert.ok(d.updated_at, 'the document says when it last changed');
|
|
391
|
+
// Editing it is editing the record: the same write, seen at once in the document.
|
|
392
|
+
await gw.setAnswer('strategize', 'big_pieces', { text: 'Sync first' }, 1, deps);
|
|
393
|
+
assert.match((await gw.readDocument('strategize', deps)).document.markdown, /Sync first/);
|
|
394
|
+
assert.equal((await gw.readDocument('nope', deps)).reason, 'unknown_stage');
|
|
395
|
+
const all = await gw.readDocuments(deps);
|
|
396
|
+
assert.deepEqual(Object.keys(all), ['scope', 'strategize', 'charter', 'legalize', 'legislation']);
|
|
397
|
+
assert.equal(all.strategize.filled, true);
|
|
398
|
+
assert.equal(all.scope.filled, false);
|
|
399
|
+
assert.equal(all.charter.title, 'The charter');
|
|
400
|
+
});
|
|
401
|
+
|
|
373
402
|
console.log(`\ngenesis_work: ${passed} passed, ${failed} failed`);
|
|
374
403
|
if (failed) process.exit(1);
|
|
375
404
|
}
|
|
@@ -67,6 +67,15 @@ try {
|
|
|
67
67
|
assert.equal(r.data.work.questions.find((q) => q.id === 'big_pieces').answer.text, 'Ledger first.');
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
+
await test('task 1005115: any builder reads a stage\'s document; an unknown stage is 404', async () => {
|
|
71
|
+
assert.equal((await call('GET', '/founding/work/strategize/document', { as: null })).status, 401);
|
|
72
|
+
const r = await call('GET', '/founding/work/strategize/document', { as: OTHER });
|
|
73
|
+
assert.equal(r.status, 200);
|
|
74
|
+
assert.equal(r.data.document.title, 'The strategy');
|
|
75
|
+
assert.match(r.data.document.markdown, /Ledger first\./, 'the answer written above is in the document');
|
|
76
|
+
assert.equal((await call('GET', '/founding/work/nope/document')).status, 404);
|
|
77
|
+
});
|
|
78
|
+
|
|
70
79
|
await test('a builder who is not the owner cannot write', async () => {
|
|
71
80
|
for (const [m, p, body] of [
|
|
72
81
|
['PUT', '/founding/work/strategize/answers/risks', { text: 'x' }],
|