@bongos/core 1.25.19 → 1.25.21
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 +237 -112
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +12 -0
- package/clients/bongos-client/index.cjs +12 -0
- package/clients/bongos-client/index.d.ts +26 -9
- package/clients/bongos-client/index.mjs +12 -0
- package/docs/adr/0406-every-project-gets-a-sandbox-that-follows-main-and-live-moves-only-when-a-person-promotes.md +10 -0
- package/docs/adr/0407-a-project-grant-is-made-by-the-owner-not-witnessed-at-sign-in.md +3 -1
- package/docs/api/openapi.json +444 -37
- package/docs/api-reference.md +11 -5
- package/docs/copy-inventory.md +223 -221
- package/docs/copy-registry.json +262 -244
- 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/government/board.js +8 -7
- package/modules/government/charter-advice.js +10 -3
- package/modules/government/charter.js +88 -4
- package/modules/government/migrations/government_039_vote_cast_via_filed.sql +22 -0
- package/modules/government/routes/government.js +12 -12
- package/modules/government/sole-member.js +75 -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 +35 -9
- 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 +16 -2
- package/modules/onboarding/guide-handoff.js +109 -0
- package/modules/onboarding/routes/genesis-work.js +9 -5
- package/modules/platform-identity/module.json +1 -1
- package/modules/platform-identity/project-standing.js +49 -0
- package/modules/platform-identity/routes/my-projects.js +8 -0
- package/modules/provisioning/grants.js +126 -0
- package/modules/provisioning/guide-handoff.js +65 -0
- package/modules/provisioning/migrations/provisioning_038_render_promote.sql +43 -0
- package/modules/provisioning/migrations/provisioning_039_project_grants.sql +40 -0
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/project-refusal.js +107 -0
- package/modules/provisioning/provisioning.js +4 -4
- package/modules/provisioning/render-lookup.js +36 -1
- package/modules/provisioning/render-promote.js +80 -0
- package/modules/provisioning/route-helpers.js +70 -16
- package/modules/provisioning/routes/env-manifest.js +21 -5
- package/modules/provisioning/routes/grants.js +141 -0
- package/modules/provisioning/routes/provisioning.js +12 -9
- package/modules/provisioning/routes/render-standup.js +82 -3
- package/modules/public-landing/public/projects.html +178 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +28 -0
- package/scripts/bongos/genesis.js +11 -1
- package/scripts/bongos/government-walk-sole.js +41 -0
- package/scripts/bongos/government-walk.js +15 -16
- package/scripts/bongos/provision-render-common.js +48 -0
- package/scripts/bongos/provision-render-promote.js +93 -0
- package/scripts/bongos/provision-render-sandbox.js +14 -2
- package/scripts/bongos/provision-render.js +4 -36
- package/scripts/bongos/provision.js +1 -0
- package/scripts/bongos/render-api.js +17 -2
- package/scripts/gds/government-walk-sole.js +6 -0
- package/scripts/gds/provision-render-common.js +6 -0
- package/scripts/gds/provision-render-promote.js +6 -0
- 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/fleet_cost_view.mjs +2 -0
- 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_government.mjs +5 -1
- package/tests/genesis_home.mjs +41 -0
- package/tests/government_charter_advice.mjs +49 -0
- package/tests/government_charter_forms.mjs +14 -3
- package/tests/government_seed.mjs +1 -0
- package/tests/government_sole_member.mjs +153 -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/project_catalog_routes.mjs +2 -0
- package/tests/provision_settings_apply.mjs +2 -1
- package/tests/provisioning_env_manifest.mjs +4 -0
- package/tests/provisioning_env_manifest_edit_delete.mjs +2 -0
- package/tests/provisioning_project_grants.mjs +458 -0
- package/tests/provisioning_project_owner_gate.mjs +3 -1
- package/tests/provisioning_project_refusal.mjs +316 -0
- package/tests/provisioning_render_route.mjs +1 -1
- package/tests/provisioning_restart.mjs +2 -0
- package/tests/provisioning_settings.mjs +2 -0
- package/tests/provisioning_settings_apply.mjs +1 -1
- package/tests/provisioning_settings_env.mjs +2 -2
- package/tests/provisioning_teardown_intent.mjs +2 -2
- package/tests/render_promote.mjs +309 -0
- package/tests/render_sandbox.mjs +9 -4
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',
|
|
@@ -131,7 +131,11 @@ test('picking Monarchy files nothing: it is already the government in force', as
|
|
|
131
131
|
const seen = await underMonarchy(async () => {
|
|
132
132
|
const { out } = await decide({ government: 'monarchy' });
|
|
133
133
|
assert.equal(out.ok, true);
|
|
134
|
-
assert.
|
|
134
|
+
assert.equal(out.charter.state, 'in_force');
|
|
135
|
+
assert.equal(out.charter.form, 'monarchy');
|
|
136
|
+
// task 1005130: said plainly, and counted as done — never a silent no-op
|
|
137
|
+
assert.equal(out.charter.complete, true);
|
|
138
|
+
assert.match(out.charter.message, /already a Monarchy \/ BDFL\. Nothing to change/);
|
|
135
139
|
});
|
|
136
140
|
assert.equal(seen.filed, null);
|
|
137
141
|
});
|
package/tests/genesis_home.mjs
CHANGED
|
@@ -573,6 +573,23 @@ test('a demo\'s genesis card: no decide form, the settled plan in one line, each
|
|
|
573
573
|
assert.doesNotMatch(host.innerHTML, /data-genesis-plan/, 'a demo never shows the decide form');
|
|
574
574
|
});
|
|
575
575
|
|
|
576
|
+
test('the plan picker says what is in force, marks it, and says what each form asks day to day (task 1005130)', async () => {
|
|
577
|
+
const charter = require('../modules/government/charter.js');
|
|
578
|
+
const reco = require('../modules/government/recommendations.js');
|
|
579
|
+
const MONARCHY = { membership: 'rank:archon', pass_rule: 'first_ratifier', window_minutes: null, close_early_on_full_turnout: true };
|
|
580
|
+
const genesis = gh.shapeGenesis({ mode: { stage: 'decide', stage_number: 0, stage_count: 5, day: 1 }, plan: null });
|
|
581
|
+
// One stub answers every read: the genesis read and the constitution read.
|
|
582
|
+
const { host } = bootCard({ founding: true, data: {
|
|
583
|
+
genesis, viewer_id: '1', headcount: 1, charter: charter.charterView(MONARCHY), recommended: reco.recommendedCharter(1),
|
|
584
|
+
} });
|
|
585
|
+
for (let i = 0; i < 6; i++) await new Promise((r) => setImmediate(r));
|
|
586
|
+
assert.match(host.innerHTML, /This project runs as Monarchy \/ BDFL now\. Every project opens as one/, 'what is in force is said first');
|
|
587
|
+
assert.match(host.innerHTML, /Monarchy \/ BDFL · in force now · recommended/, 'the form in force is marked');
|
|
588
|
+
assert.match(host.innerHTML, /You decide\. Each decision takes one yes from you\./, 'monarchy says one yes is still needed');
|
|
589
|
+
assert.match(host.innerHTML, /Every builder on the project decides\./, 'every form says who decides');
|
|
590
|
+
assert.equal((host.innerHTML.match(/in force now/g) || []).length, 1, 'only the form in force is marked');
|
|
591
|
+
});
|
|
592
|
+
|
|
576
593
|
test('the card lists every stage vote waiting on the viewer, and votes yes on all in order (task 1005199)', async () => {
|
|
577
594
|
const me = [{ builder_id: '1', display_name: 'Founder' }];
|
|
578
595
|
const genesis = gh.shapeGenesis({
|
|
@@ -653,6 +670,8 @@ test('the stage reads as a checklist: done, open and locked, and a locked row of
|
|
|
653
670
|
assert.match(row('302'), /class="genesis__task is-locked"/);
|
|
654
671
|
assert.match(row('302'), /after Gather what already exists/, 'a locked row says what it waits on');
|
|
655
672
|
assert.doesNotMatch(row('302'), /data-genesis-copy/, 'a locked row offers no prompt for work it cannot claim');
|
|
673
|
+
assert.doesNotMatch(row('301'), /sr-only/, 'an open row adds no hidden word its status pill could contradict');
|
|
674
|
+
assert.match(row('302'), /class="sr-only">Locked: /);
|
|
656
675
|
assert.doesNotMatch(html, /is-finished|line-through/, 'nothing is drawn struck through');
|
|
657
676
|
// The "done" row: the earlier stage is not the one on screen, so check the shape directly.
|
|
658
677
|
const done = gh.shapeGenesis({ mode: { stage: 'scope', stage_number: 1, stage_count: 5, day: 1 }, plan: gh.parsePlan({}).plan,
|
|
@@ -774,3 +793,25 @@ test('task 1005121: the close route answers 202 with the whole body for an open
|
|
|
774
793
|
const click = hall.slice(hall.indexOf("t.hasAttribute('data-genesis-close')"), hall.indexOf("t.hasAttribute('data-genesis-confirm')"));
|
|
775
794
|
assert.ok(click.length > 0 && !click.includes('/votes'),'the hall no longer casts a second vote after closing — the server did it');
|
|
776
795
|
});
|
|
796
|
+
|
|
797
|
+
test('a stage that opens no room says what finishing it earns, never nothing (task 1005112)', async () => {
|
|
798
|
+
const genesis = gh.shapeGenesis({ mode: { stage: 'scope', stage_number: 1, stage_count: 5, day: 1 }, plan: gh.parsePlan({}).plan });
|
|
799
|
+
const by = Object.fromEntries(genesis.stages.map((s) => [s.key, s]));
|
|
800
|
+
assert.equal(by.scope.earns, null, 'a stage that wakes rooms names the rooms instead');
|
|
801
|
+
assert.equal(by.strategize.earns, null);
|
|
802
|
+
for (const k of ['charter', 'legalize', 'legislation']) {
|
|
803
|
+
assert.deepEqual(by[k].wakes, [], `${k} wakes no room`);
|
|
804
|
+
assert.ok(by[k].earns && by[k].earns.length > 10, `${k} says what it earns`);
|
|
805
|
+
}
|
|
806
|
+
assert.match(by.legislation.earns, /every room opens/, 'the last stage is the one that opens everything');
|
|
807
|
+
const b = bootCard({ founding: true, data: { genesis, viewer_id: '1' } });
|
|
808
|
+
await mountClickable(b);
|
|
809
|
+
assert.match(b.host.innerHTML, /Wakes Goals, Goal map/);
|
|
810
|
+
assert.match(b.host.innerHTML, /puts your government in force/);
|
|
811
|
+
assert.match(b.host.innerHTML, /settles how contributors are rewarded/);
|
|
812
|
+
// The district rises with the stages (task 1005112): each row also names what it raises.
|
|
813
|
+
assert.match(b.host.innerHTML, /Wakes Goals, Goal map · raises Planning/);
|
|
814
|
+
assert.match(b.host.innerHTML, /Raises Treasury · settles how contributors are rewarded/);
|
|
815
|
+
assert.match(b.host.innerHTML, /Raises Rules, Review, Disputes · brings the project to life/);
|
|
816
|
+
assert.deepEqual(by.legalize.raises, ['treasury']);
|
|
817
|
+
});
|
|
@@ -148,3 +148,52 @@ test('the port entry reads the head count and the law book', async () => {
|
|
|
148
148
|
assert.equal(expected.headcount, 2, 'the founder\'s expected number wins over the live count');
|
|
149
149
|
assert.equal(expected.charter.key, 'monarchy');
|
|
150
150
|
});
|
|
151
|
+
|
|
152
|
+
// ── task 1005130: every project is already a monarchy, and is told so ────────
|
|
153
|
+
const MONARCHY = { membership: 'rank:archon', pass_rule: 'first_ratifier', window_minutes: null, close_early_on_full_turnout: true };
|
|
154
|
+
const config = require(path.join(ROOT, 'modules', 'government', 'config.js'));
|
|
155
|
+
|
|
156
|
+
test('the default constitution reads as Monarchy, and the stage says so on arrival', () => {
|
|
157
|
+
const live = config.loadGovernmentConfig().board;
|
|
158
|
+
const v = charter.charterView(live);
|
|
159
|
+
assert.equal(v.in_force.form, 'monarchy', 'every project boots as a monarchy');
|
|
160
|
+
assert.match(v.in_force.line, /runs as Monarchy \/ BDFL now\. Every project opens as one/);
|
|
161
|
+
assert.match(v.in_force.line, /You decide\. Each decision takes one yes from you\./);
|
|
162
|
+
assert.deepEqual(v.forms.filter((f) => f.current).map((f) => f.key), ['monarchy'], 'exactly the form in force is marked');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('every form says who decides and what closing takes, derived from its own settings', () => {
|
|
166
|
+
const v = charter.charterView(MONARCHY);
|
|
167
|
+
for (const f of v.forms.filter((x) => x.available)) {
|
|
168
|
+
const d = charter.dutyOf(f.franchise, f.pass_rule);
|
|
169
|
+
assert.equal(f.who_decides, d.who_decides, f.key);
|
|
170
|
+
assert.equal(f.to_close, d.to_close, f.key);
|
|
171
|
+
assert.ok(f.who_decides && f.to_close, `${f.key} says both`);
|
|
172
|
+
}
|
|
173
|
+
assert.equal(charter.dutyOf('founder', 'first_ratifier').to_close, 'Each decision takes one yes from you.');
|
|
174
|
+
assert.match(charter.dutyOf('everyone', 'majority').to_close, /more than half/);
|
|
175
|
+
assert.notEqual(charter.dutyOf('trusted', 'consent').to_close, charter.dutyOf('trusted', 'majority').to_close,
|
|
176
|
+
'the sentence follows the pass rule, so a tuned rule is described as it runs');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('choosing what is in force changes nothing and is done; choosing another says what happens next', () => {
|
|
180
|
+
const same = charter.choiceOutcome(MONARCHY, 'monarchy');
|
|
181
|
+
assert.equal(same.changes, false);
|
|
182
|
+
assert.equal(same.complete, true);
|
|
183
|
+
assert.match(same.line, /already a Monarchy \/ BDFL\. Nothing to change/);
|
|
184
|
+
const other = charter.choiceOutcome(MONARCHY, 'direct-democracy');
|
|
185
|
+
assert.equal(other.changes, true);
|
|
186
|
+
assert.match(other.line, /filed as a change to the rules and decided under the government in force \(Monarchy \/ BDFL\)\. Under it, your own one yes applies it\./);
|
|
187
|
+
const underCircle = charter.choiceOutcome({ ...MONARCHY, membership: 'rank:xenos+', pass_rule: 'consent' }, 'monarchy');
|
|
188
|
+
assert.match(underCircle.line, /passes unless one of them objects/, 'under another government the rule in force is named');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('the charter stage advice carries what is in force and what choosing the recommendation does', () => {
|
|
192
|
+
const a = advice.buildAdvice({ answers: {}, headcount: 1, board: MONARCHY });
|
|
193
|
+
assert.equal(a.in_force.form, 'monarchy');
|
|
194
|
+
assert.equal(a.charter.key, 'monarchy');
|
|
195
|
+
assert.equal(a.charter.on_choose.changes, false, 'a solo founder is told the recommendation is what they already have');
|
|
196
|
+
const b = advice.buildAdvice({ answers: { final_say: 'everyone' }, headcount: 1, board: MONARCHY });
|
|
197
|
+
assert.equal(b.charter.on_choose.changes, true);
|
|
198
|
+
assert.ok(b.forms.find((f) => f.key === 'monarchy').current);
|
|
199
|
+
});
|
|
@@ -254,8 +254,12 @@ test('a charter that changes nothing files nothing, and a council must be a rank
|
|
|
254
254
|
getRankByKey: async (key) => (key === 'trustees' ? { rank_key: 'trustees' } : null),
|
|
255
255
|
builderHoldsRankActive: async () => true,
|
|
256
256
|
}, async () => {
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
const same = await board.proposeCharter({ form: 'monarchy', proposedBy: '42' });
|
|
258
|
+
assert.equal(same.reason, 'no_change', 'a sitting on the constitution already in force would decide nothing');
|
|
259
|
+
// task 1005130: the refusal says what is true in words and counts the choice done
|
|
260
|
+
assert.equal(same.in_force, 'Monarchy / BDFL');
|
|
261
|
+
assert.equal(same.complete, true);
|
|
262
|
+
assert.match(same.message, /You are already a Monarchy \/ BDFL\. Nothing to change/);
|
|
259
263
|
assert.equal((await board.proposeCharter({ form: 'foundation', councilRank: 'ghosts', proposedBy: '42' })).reason, 'unknown_council_rank');
|
|
260
264
|
assert.equal((await board.proposeCharter({ form: 'direct-democracy', proposedBy: null })).reason, 'missing_proposer');
|
|
261
265
|
});
|
|
@@ -272,7 +276,14 @@ test('the constitution view carries the charter: forms, variables, and what the
|
|
|
272
276
|
});
|
|
273
277
|
const routes = fs.readFileSync(path.join(GOV, 'routes', 'government.js'), 'utf8');
|
|
274
278
|
assert.match(routes, /router\.post\('\/government\/board\/charter', api\.requireBuilder, api\.requirePermission\('board\.item\.open'\)/);
|
|
275
|
-
|
|
279
|
+
// The port's charter filing IS board.proposeCharter, followed by the board-of-one
|
|
280
|
+
// confirm (task 1005130) — called, not read: a refusal from board comes back as is.
|
|
281
|
+
const { governmentPort } = require(path.join(GOV, 'routes', 'government.js'));
|
|
282
|
+
const real = board.proposeCharter;
|
|
283
|
+
board.proposeCharter = async (a) => ({ ok: false, reason: 'from_board', seen: a.form });
|
|
284
|
+
try {
|
|
285
|
+
assert.deepEqual(await governmentPort.proposeCharter({ form: 'monarchy', proposedBy: '42' }), { ok: false, reason: 'from_board', seen: 'monarchy' });
|
|
286
|
+
} finally { board.proposeCharter = real; }
|
|
276
287
|
const pins = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'), 'utf8');
|
|
277
288
|
assert.match(pins, /'POST \/government\/board\/charter': 'perm:board\.item\.open'/);
|
|
278
289
|
});
|
|
@@ -55,6 +55,7 @@ const NON_GRANT_MIGRATIONS = new Set([
|
|
|
55
55
|
'government_036_access_holders.sql', // who holds the keys outside Bongos + the access:<key> ask seat (task 1005000)
|
|
56
56
|
'government_037_constitution.sql', // the constitution in force as a durable row (task 1005140)
|
|
57
57
|
'government_038_vote_cast_via.sql', // HOW a vote was cast: a ballot, or a stage-close confirm (task 1005121)
|
|
58
|
+
'government_039_vote_cast_via_filed.sql', // a board of one's confirm given by filing (task 1005130)
|
|
58
59
|
]);
|
|
59
60
|
|
|
60
61
|
// Permission keys RENAMED after the grant migrations that first granted them.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// tests/government_sole_member.mjs — a board of ONE decides by filing (task 1005130,
|
|
2
|
+
// part C; the ruling task 1005121 shipped for closing a genesis stage, applied
|
|
3
|
+
// wherever it holds).
|
|
4
|
+
//
|
|
5
|
+
// The rule held here, for a charter change, a laws change and a decision-rules change:
|
|
6
|
+
// • the sole member of a first_ratifier board files → their yes is cast in the same
|
|
7
|
+
// call, recorded as THEIR vote (attributed, cast_via 'filed'), and the sitting
|
|
8
|
+
// closes through the ordinary close path;
|
|
9
|
+
// • a second member on the board, a consent or majority rule, or a caller who is not
|
|
10
|
+
// the sole member → the filing comes back exactly as it was, and nothing is cast;
|
|
11
|
+
// • the yes goes through the real castVote — its membership re-check, and a
|
|
12
|
+
// 'filed' cast is refused on anything but an amendment, or as a 'no';
|
|
13
|
+
// • a filing the filer was refused comes back untouched, and a failing confirm
|
|
14
|
+
// leaves the sitting open rather than failing the filing.
|
|
15
|
+
// The real routes (permission gate first, a non-member's filing left open, a lower
|
|
16
|
+
// rank refused before anything is filed) are walked end to end in
|
|
17
|
+
// scripts/bongos/government-walk.js (rows G3, G5, B*, L2–L4, L11–L15).
|
|
18
|
+
import assert from 'node:assert/strict';
|
|
19
|
+
import { test } from 'node:test';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
27
|
+
const db = require(path.join(GOV, 'db.js'));
|
|
28
|
+
const board = require(path.join(GOV, 'board.js'));
|
|
29
|
+
const sole = require(path.join(GOV, 'sole-member.js'));
|
|
30
|
+
|
|
31
|
+
const MONARCHY = { membership: 'rank:archon', pass_rule: 'first_ratifier', window_minutes: null, close_early_on_full_turnout: true };
|
|
32
|
+
const FILERS = ['proposeCharter', 'proposeLaws', 'proposeDecisionRules'];
|
|
33
|
+
|
|
34
|
+
// A world: who holds which rank, the one open item a filing opened, and every vote
|
|
35
|
+
// written. db is stubbed underneath the REAL castVote; the filers and the close are
|
|
36
|
+
// stubbed on board (looked up at call time), so this exercises the decision alone.
|
|
37
|
+
async function world({ members = ['42'], constitution = MONARCHY, subjectType = 'constitutional_amendment', filedOk = true } = {}, fn) {
|
|
38
|
+
const votes = [];
|
|
39
|
+
const closes = [];
|
|
40
|
+
const item = { id: 7, subject_type: subjectType, constitution, closed_at: null, expired: false };
|
|
41
|
+
const stubs = {
|
|
42
|
+
getActiveBuildersWithRank: async () => members.map((id) => ({ builder_id: id, github_login: `b${id}`, display_name: `B${id}` })),
|
|
43
|
+
builderHoldsRankActive: async (id) => members.includes(String(id)),
|
|
44
|
+
getBoardItemForVote: async () => ({ ...item }),
|
|
45
|
+
getBoardItemAuthor: async () => '42',
|
|
46
|
+
insertBoardVote: async (v) => { const row = { item_id: String(v.itemId), voter_id: String(v.voterId), direction: v.direction, cast_via: v.castVia || 'ballot', cast_at: '2026-11-13T12:00:00.000Z' }; votes.push(row); return row; },
|
|
47
|
+
getBoardVote: async () => null,
|
|
48
|
+
insertVoteKarma: async () => { throw new Error('a confirm given by filing earns no vote karma'); },
|
|
49
|
+
};
|
|
50
|
+
const boardStubs = {
|
|
51
|
+
maybeCloseAfterVote: async (id) => { closes.push(id); return { closed: true, outcome: 'passed', item: { id } }; },
|
|
52
|
+
};
|
|
53
|
+
for (const f of FILERS) boardStubs[f] = async () => (filedOk ? { ok: true, amendment: { id: 3 }, item: { ...item } } : { ok: false, reason: 'proposer_would_lose_their_seat' });
|
|
54
|
+
const saved = {};
|
|
55
|
+
for (const [k, v] of Object.entries(stubs)) { saved[`db.${k}`] = db[k]; db[k] = v; }
|
|
56
|
+
for (const [k, v] of Object.entries(boardStubs)) { saved[`board.${k}`] = board[k]; board[k] = v; }
|
|
57
|
+
try {
|
|
58
|
+
return await fn({ votes, closes });
|
|
59
|
+
} finally {
|
|
60
|
+
for (const [k, v] of Object.entries(saved)) {
|
|
61
|
+
const [obj, key] = k.split('.');
|
|
62
|
+
(obj === 'db' ? db : board)[key] = v;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const filer of FILERS) {
|
|
68
|
+
test(`${filer}: the sole member of a one-yes board decides by filing, and the vote is theirs on the record`, async () => {
|
|
69
|
+
await world({}, async ({ votes, closes }) => {
|
|
70
|
+
const out = await sole[filer]({ proposedBy: '42' });
|
|
71
|
+
assert.equal(out.ok, true);
|
|
72
|
+
assert.deepEqual(out.decided, { closed: true, outcome: 'passed', cast_via: 'filed' });
|
|
73
|
+
assert.equal(votes.length, 1);
|
|
74
|
+
assert.equal(votes[0].voter_id, '42', 'attributed to the member who filed');
|
|
75
|
+
assert.equal(votes[0].direction, 'yes');
|
|
76
|
+
assert.equal(votes[0].cast_via, 'filed', 'the record says how it was cast');
|
|
77
|
+
assert.ok(votes[0].cast_at, 'and when');
|
|
78
|
+
assert.deepEqual(closes, [7], 'closed through the ordinary close path');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test(`${filer}: a second member on the board — nothing is cast, the sitting waits`, async () => {
|
|
83
|
+
await world({ members: ['42', '43'] }, async ({ votes, closes }) => {
|
|
84
|
+
const out = await sole[filer]({ proposedBy: '42' });
|
|
85
|
+
assert.equal(out.decided, undefined);
|
|
86
|
+
assert.deepEqual(votes, []);
|
|
87
|
+
assert.deepEqual(closes, []);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
for (const rule of ['consent', 'majority', 'unanimous']) {
|
|
92
|
+
test(`${filer}: under ${rule} a sole member's filing still waits for their vote`, async () => {
|
|
93
|
+
await world({ constitution: { ...MONARCHY, pass_rule: rule } }, async ({ votes, closes }) => {
|
|
94
|
+
const out = await sole[filer]({ proposedBy: '42' });
|
|
95
|
+
assert.equal(out.decided, undefined);
|
|
96
|
+
assert.deepEqual(votes, []);
|
|
97
|
+
assert.deepEqual(closes, []);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
test(`${filer}: a caller who is not the sole member — the sitting is left open, exactly as today`, async () => {
|
|
103
|
+
await world({ members: ['42'] }, async ({ votes, closes }) => {
|
|
104
|
+
const out = await sole[filer]({ proposedBy: '43' });
|
|
105
|
+
assert.equal(out.ok, true);
|
|
106
|
+
assert.equal(out.decided, undefined);
|
|
107
|
+
assert.deepEqual(votes, []);
|
|
108
|
+
assert.deepEqual(closes, []);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test(`${filer}: a refused filing comes back untouched, and nothing is cast`, async () => {
|
|
113
|
+
await world({ filedOk: false }, async ({ votes }) => {
|
|
114
|
+
assert.deepEqual(await sole[filer]({ proposedBy: '42' }), { ok: false, reason: 'proposer_would_lose_their_seat' });
|
|
115
|
+
assert.deepEqual(votes, []);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
test('the yes goes through the real castVote: membership is re-checked under the item\'s snapshot', async () => {
|
|
121
|
+
// resolve says 42 is the one member, but the per-request re-check says 42 no longer
|
|
122
|
+
// holds the rank (demoted between the two reads): the cast is refused, nothing closes.
|
|
123
|
+
await world({}, async ({ votes, closes }) => {
|
|
124
|
+
db.builderHoldsRankActive = async () => false;
|
|
125
|
+
const out = await sole.proposeLaws({ proposedBy: '42' });
|
|
126
|
+
assert.equal(out.decided, undefined);
|
|
127
|
+
assert.deepEqual(votes, []);
|
|
128
|
+
assert.deepEqual(closes, []);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('a cast marked "filed" is refused on anything but an amendment, and as a no', async () => {
|
|
133
|
+
await world({ subjectType: 'full_idea' }, async ({ votes }) => {
|
|
134
|
+
assert.equal((await board.castVote({ itemId: 7, voterId: '42', direction: 'yes', castVia: 'filed' })).reason, 'bad_cast_via');
|
|
135
|
+
assert.deepEqual(votes, []);
|
|
136
|
+
});
|
|
137
|
+
await world({ subjectType: 'genesis_stage' }, async () => {
|
|
138
|
+
assert.equal((await board.castVote({ itemId: 7, voterId: '42', direction: 'yes', castVia: 'filed' })).reason, 'bad_cast_via');
|
|
139
|
+
});
|
|
140
|
+
await world({}, async () => {
|
|
141
|
+
assert.equal((await board.castVote({ itemId: 7, voterId: '42', direction: 'no', reasonMd: 'no', sectionKey: 'purpose', castVia: 'filed' })).reason, 'bad_cast_via');
|
|
142
|
+
assert.equal((await board.castVote({ itemId: 7, voterId: '42', direction: 'yes', castVia: 'by_magic' })).reason, 'bad_cast_via');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('a confirm that fails leaves the filing standing and the sitting open', async () => {
|
|
147
|
+
await world({}, async () => {
|
|
148
|
+
board.maybeCloseAfterVote = async () => { throw new Error('db down'); };
|
|
149
|
+
const out = await sole.proposeCharter({ proposedBy: '42' });
|
|
150
|
+
assert.equal(out.ok, true);
|
|
151
|
+
assert.equal(out.decided, undefined);
|
|
152
|
+
});
|
|
153
|
+
});
|