@bongos/core 1.21.36 → 1.21.38
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 +90 -70
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +77 -3
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +8 -8
- package/docs/copy-registry.json +8 -8
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +234 -234
- package/modules/government/board-withdraw.js +38 -0
- package/modules/government/board.js +1 -0
- package/modules/government/db.js +20 -0
- package/modules/government/migrations/government_026_board_outcome_withdrawn.sql +29 -0
- package/modules/government/routes/government.js +26 -0
- package/modules/hall-ui/public/atlas.html +3 -3
- package/modules/hall-ui/public/blockers.html +3 -3
- package/modules/hall-ui/public/board-room.html +3 -3
- package/modules/hall-ui/public/city.html +3 -3
- package/modules/hall-ui/public/collab.html +3 -3
- package/modules/hall-ui/public/commands.html +3 -3
- package/modules/hall-ui/public/copy-desk.html +3 -3
- package/modules/hall-ui/public/craft-mode.js +44 -6
- package/modules/hall-ui/public/deploy.html +3 -3
- package/modules/hall-ui/public/diagrams.html +3 -3
- package/modules/hall-ui/public/drachmae.html +3 -3
- package/modules/hall-ui/public/fleet.html +3 -3
- package/modules/hall-ui/public/gate.html +3 -3
- package/modules/hall-ui/public/goal-map.html +3 -3
- package/modules/hall-ui/public/goals.html +3 -3
- package/modules/hall-ui/public/government.html +3 -3
- package/modules/hall-ui/public/idea.html +3 -3
- package/modules/hall-ui/public/ideas.html +3 -3
- package/modules/hall-ui/public/index.html +3 -3
- package/modules/hall-ui/public/modules.html +3 -3
- package/modules/hall-ui/public/primer.html +3 -3
- package/modules/hall-ui/public/profile.html +3 -3
- package/modules/hall-ui/public/project-settings.html +3 -3
- package/modules/hall-ui/public/ranks.html +3 -3
- package/modules/hall-ui/public/roadmap.html +3 -3
- package/modules/hall-ui/public/roster.html +3 -3
- package/modules/hall-ui/public/sessions.html +3 -3
- package/modules/hall-ui/public/settings.html +3 -3
- package/modules/hall-ui/public/street-card.js +16 -0
- package/modules/hall-ui/public/street-desk.js +2 -0
- package/modules/hall-ui/public/street.html +3 -3
- package/modules/hall-ui/public/street.js +1 -1
- package/modules/hall-ui/public/studio.html +3 -3
- package/modules/hall-ui/public/style.css +121 -2
- package/modules/hall-ui/public/task.html +3 -3
- package/modules/hall-ui/public/thinking.html +3 -3
- package/modules/hall-ui/public/tweak-editor.html +3 -3
- package/modules/hall-ui/public/watch.html +3 -3
- package/modules/hall-ui/public/work.html +3 -3
- package/modules/hall-ui/records/craft-mode.md +8 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/bongos/route-rank-check.js +4 -0
- package/src/module-api.js +1 -1
- package/tests/craft_mode_looks.mjs +170 -0
- package/tests/craft_mode_nav.mjs +7 -5
- package/tests/government_board_withdraw.mjs +144 -0
- package/tests/government_seed.mjs +1 -0
- package/tests/hall_street_desk.mjs +37 -0
- package/tests/hall_tweak_editor.mjs +1 -1
package/tests/craft_mode_nav.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Each craft mode shows only the tabs its craft needs; Legacy mode and no mode
|
|
5
5
|
// show every tab, exactly as before. The owner's rule (2026-10-01): some tabs
|
|
6
6
|
// stay in EVERY mode, Builders first among them, written down as ONE shared list
|
|
7
|
-
// that each mode's list adds to. These cases boot the REAL shell-trail.js +
|
|
7
|
+
// that each mode's list adds to; on 2026-10-03 Collab and Profile joined it. These cases boot the REAL shell-trail.js +
|
|
8
8
|
// craft-mode.js + shell.js and read the sidebar the shell actually rendered, so a
|
|
9
9
|
// tab id that drifts from NAV, or a gate pass that runs after the mode and
|
|
10
10
|
// re-shows a tab, reds here.
|
|
@@ -126,13 +126,15 @@ function boot({ session = {}, perms = { 'board.vote.cast': true } } = {}) {
|
|
|
126
126
|
|
|
127
127
|
const CRAFTS = ['building', 'artistry', 'ideating', 'governing'];
|
|
128
128
|
|
|
129
|
-
test('THE ALWAYS-PRESENT LIST: one shared list, Builders first, inside every mode', () => {
|
|
129
|
+
test('THE ALWAYS-PRESENT LIST: one shared list, Builders first, then Collab and Profile, inside every mode', () => {
|
|
130
130
|
const { craft } = boot();
|
|
131
|
-
assert.
|
|
131
|
+
assert.deepEqual([...craft.ALWAYS_TABS], ['roster', 'collab', 'profile'],
|
|
132
|
+
'Builders (NAV id roster) heads the list (owner, 2026-10-01); Collab and Profile join it (owner, 2026-10-03)');
|
|
132
133
|
for (const m of CRAFTS) {
|
|
133
134
|
const tabs = craft.tabsFor(m);
|
|
134
135
|
assert.ok(Array.isArray(tabs), `${m} has a named tab list`);
|
|
135
136
|
for (const id of craft.ALWAYS_TABS) assert.ok(tabs.includes(id), `${m} keeps ${id}`);
|
|
137
|
+
assert.equal(new Set(tabs).size, tabs.length, `${m} names no tab twice`);
|
|
136
138
|
}
|
|
137
139
|
assert.match(CRAFT, /return own \? ALWAYS_TABS\.concat\(own\) : null;/, 'each list is ALWAYS_TABS plus its own, not a copy of it');
|
|
138
140
|
});
|
|
@@ -173,7 +175,7 @@ test('each mode shows its own tabs, and switching updates the nav live', () => {
|
|
|
173
175
|
p.craft.choose(m);
|
|
174
176
|
assert.equal(p.root.dataset.craft, m);
|
|
175
177
|
assert.deepEqual(new Set(p.showing()), new Set(p.craft.tabsFor(m)), `${m} shows exactly its list`);
|
|
176
|
-
assert.ok(p.showing().includes(
|
|
178
|
+
for (const id of ['roster', 'collab', 'profile']) assert.ok(p.showing().includes(id), `${id} stays in ${m}`);
|
|
177
179
|
assert.equal(p.headShowing('government'), m === 'governing', `the Government head shows only where its tabs do (${m})`);
|
|
178
180
|
}
|
|
179
181
|
p.craft.choose('legacy');
|
|
@@ -185,7 +187,7 @@ test('a mode never shows a tab the gate hid, and a later gate pass keeps the mod
|
|
|
185
187
|
p.signIn('thetes');
|
|
186
188
|
assert.ok(!p.showing().includes('watch'), 'a thetes in Governing does not get the metic-floored Watch tab');
|
|
187
189
|
assert.ok(!p.headShowing('government'), 'nor its group head');
|
|
188
|
-
assert.deepEqual(p.showing(), ['blockers', 'board-room', 'roster'], 'what a thetes board member in Governing sees');
|
|
190
|
+
assert.deepEqual(p.showing(), ['blockers', 'board-room', 'profile', 'collab', 'roster'], 'what a thetes board member in Governing sees');
|
|
189
191
|
p.signIn('archon');
|
|
190
192
|
assert.ok(p.showing().includes('watch'), 'promoted, the gate lets it through and the mode keeps it');
|
|
191
193
|
assert.ok(p.headShowing('government'), 'and its group head comes back with it, which only a mode pass after the gate can know');
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// tests/government_board_withdraw.mjs — a proposer withdraws their own open
|
|
2
|
+
// sitting before anyone has voted (task 1004564, goal 1000125).
|
|
3
|
+
//
|
|
4
|
+
// WHAT IT GUARDS.
|
|
5
|
+
// 1. THE TWO WALLS IN withdrawItem (board-withdraw.js) — only the builder who OPENED the
|
|
6
|
+
// sitting, and only while no vote is on it. Each refusal asserts the write
|
|
7
|
+
// was never reached, so deleting either guard reds a test here (the stub
|
|
8
|
+
// write would otherwise "succeed").
|
|
9
|
+
// 2. THE SAME WALLS IN THE WRITE ITSELF — db.withdrawBoardItem's WHERE repeats
|
|
10
|
+
// open + proposer + no vote, so a vote landing between the reads and the
|
|
11
|
+
// write cannot be withdrawn over. Checked on the SQL the db actually sends.
|
|
12
|
+
// 3. THE MIGRATION — government_026 widens the outcome CHECK to carry
|
|
13
|
+
// 'withdrawn' and 'expired', is idempotent, and never narrows it.
|
|
14
|
+
//
|
|
15
|
+
// DB-free: the db functions (and, for 2, the pool's query) are monkeypatched,
|
|
16
|
+
// the sibling board tests' idiom.
|
|
17
|
+
//
|
|
18
|
+
// Run: node --test tests/government_board_withdraw.mjs
|
|
19
|
+
import assert from 'node:assert/strict';
|
|
20
|
+
import { test } from 'node:test';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
29
|
+
const db = require(path.join(GOV, 'db.js'));
|
|
30
|
+
const { withdrawItem } = require(path.join(GOV, 'board-withdraw.js'));
|
|
31
|
+
|
|
32
|
+
const CONSENT = { membership: 'rank:metic', pass_rule: 'consent', window_minutes: 1440, close_early_on_full_turnout: true };
|
|
33
|
+
const openItem = (over = {}) => ({
|
|
34
|
+
id: '21', subject_type: 'constitutional_amendment', subject_id: '4', opened_by: '42',
|
|
35
|
+
closed_at: null, outcome: null, expired: false, constitution: CONSENT, ...over,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
async function withDb(stubs, fn) {
|
|
39
|
+
const originals = {};
|
|
40
|
+
for (const [k, v] of Object.entries(stubs)) { originals[k] = db[k]; db[k] = v; }
|
|
41
|
+
try { return await fn(); }
|
|
42
|
+
finally { for (const [k, v] of Object.entries(originals)) db[k] = v; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A stub set whose write records whether it was reached.
|
|
46
|
+
function stubs(item, votes = []) {
|
|
47
|
+
const calls = { write: null };
|
|
48
|
+
return {
|
|
49
|
+
calls,
|
|
50
|
+
db: {
|
|
51
|
+
getBoardItemForVote: async () => item,
|
|
52
|
+
getVotesForItem: async () => votes,
|
|
53
|
+
withdrawBoardItem: async (args) => { calls.write = args; return { ...item, closed_at: '2026-10-04T10:00:00Z', outcome: 'withdrawn' }; },
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── 1. the walls in withdrawItem (board-withdraw.js) ───────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
test('the proposer withdraws their own open sitting before any vote: closed, outcome withdrawn', async () => {
|
|
61
|
+
const s = stubs(openItem());
|
|
62
|
+
const out = await withDb(s.db, () => withdrawItem({ itemId: 21, builderId: 42 }));
|
|
63
|
+
assert.equal(out.ok, true);
|
|
64
|
+
assert.equal(out.item.outcome, 'withdrawn');
|
|
65
|
+
assert.deepEqual(s.calls.write, { itemId: '21', builderId: 42 }, 'the write carries the proposer, so its WHERE can check them too');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('PROPOSER ONLY: anyone else is refused not_proposer, and the write is never reached', async () => {
|
|
69
|
+
const s = stubs(openItem({ opened_by: '42' }));
|
|
70
|
+
const out = await withDb(s.db, () => withdrawItem({ itemId: 21, builderId: 7 }));
|
|
71
|
+
assert.deepEqual(out, { ok: false, reason: 'not_proposer' });
|
|
72
|
+
assert.equal(s.calls.write, null, 'a refused withdraw writes nothing');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('PROPOSER ONLY: a sitting nobody opened (a Full Idea\'s, opened by its grade) cannot be withdrawn', async () => {
|
|
76
|
+
const s = stubs(openItem({ subject_type: 'full_idea', opened_by: null }));
|
|
77
|
+
const out = await withDb(s.db, () => withdrawItem({ itemId: 21, builderId: 42 }));
|
|
78
|
+
assert.deepEqual(out, { ok: false, reason: 'not_proposer' });
|
|
79
|
+
assert.equal(s.calls.write, null);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('NO VOTES: once anyone has voted — even the proposer — it is refused votes_cast, and the write is never reached', async () => {
|
|
83
|
+
for (const voter of ['7', '42']) {
|
|
84
|
+
const s = stubs(openItem(), [{ id: '1', item_id: '21', voter_id: voter, direction: 'yes' }]);
|
|
85
|
+
const out = await withDb(s.db, () => withdrawItem({ itemId: 21, builderId: 42 }));
|
|
86
|
+
assert.deepEqual(out, { ok: false, reason: 'votes_cast' }, `a vote by ${voter} holds the sitting`);
|
|
87
|
+
assert.equal(s.calls.write, null);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('closed, past its clock, missing, or a vote racing the write: each refused with its own reason', async () => {
|
|
92
|
+
const cases = [
|
|
93
|
+
[null, 'item_not_found'],
|
|
94
|
+
[openItem({ closed_at: '2026-10-01T00:00:00Z', outcome: 'returned' }), 'item_closed'],
|
|
95
|
+
[openItem({ expired: true }), 'vote_window_expired'],
|
|
96
|
+
];
|
|
97
|
+
for (const [item, reason] of cases) {
|
|
98
|
+
const s = stubs(item);
|
|
99
|
+
const out = await withDb(s.db, () => withdrawItem({ itemId: 21, builderId: 42 }));
|
|
100
|
+
assert.deepEqual(out, { ok: false, reason });
|
|
101
|
+
assert.equal(s.calls.write, null, `${reason} writes nothing`);
|
|
102
|
+
}
|
|
103
|
+
// the write's own WHERE matched nothing — a vote landed after the reads
|
|
104
|
+
const s = stubs(openItem());
|
|
105
|
+
s.db.withdrawBoardItem = async () => null;
|
|
106
|
+
const out = await withDb(s.db, () => withdrawItem({ itemId: 21, builderId: 42 }));
|
|
107
|
+
assert.deepEqual(out, { ok: false, reason: 'item_changed' });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ── 2. the same walls in the write ───────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
test('the write repeats every wall in its WHERE: open, opened by this builder, no vote row', async () => {
|
|
113
|
+
const { pool } = require(path.join(ROOT, 'src', 'bongos', 'pool'));
|
|
114
|
+
const original = pool.query;
|
|
115
|
+
let sent = null;
|
|
116
|
+
pool.query = async (sql, params) => { sent = { sql: String(sql).replace(/\s+/g, ' '), params }; return { rows: [] }; };
|
|
117
|
+
try {
|
|
118
|
+
const out = await db.withdrawBoardItem({ itemId: 21, builderId: 42 });
|
|
119
|
+
assert.equal(out, null, 'no row matched → null, the sitting untouched');
|
|
120
|
+
} finally { pool.query = original; }
|
|
121
|
+
assert.match(sent.sql, /UPDATE government_board_items SET closed_at = now\(\), outcome = 'withdrawn'/);
|
|
122
|
+
assert.match(sent.sql, /closed_at IS NULL/);
|
|
123
|
+
assert.match(sent.sql, /opened_by = \$2/);
|
|
124
|
+
assert.match(sent.sql, /NOT EXISTS \(SELECT 1 FROM government_board_votes WHERE item_id = \$1\)/);
|
|
125
|
+
assert.deepEqual(sent.params, [21, 42]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ── 3. the migration ─────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
test('government_026 widens the outcome CHECK to withdrawn and expired, idempotently, never narrowing it', () => {
|
|
131
|
+
const sql = fs.readFileSync(path.join(GOV, 'migrations', 'government_026_board_outcome_withdrawn.sql'), 'utf8');
|
|
132
|
+
const original = fs.readFileSync(path.join(GOV, 'migrations', 'government_003_board_tables.sql'), 'utf8');
|
|
133
|
+
assert.match(original, /outcome\s+text\s+CHECK \(outcome IN \('passed', 'returned'\)\)/, 'sanity: government_003 is the CHECK being widened');
|
|
134
|
+
assert.match(sql, /DROP CONSTRAINT IF EXISTS government_board_items_outcome_check;/, 'idempotent: safe to run twice');
|
|
135
|
+
const check = /ADD CONSTRAINT government_board_items_outcome_check\s+CHECK \(outcome IN \(([^)]*)\)\)/.exec(sql);
|
|
136
|
+
assert.ok(check, 'the widened CHECK is re-added under the same name');
|
|
137
|
+
const words = check[1].split(',').map((w) => w.trim().replace(/'/g, ''));
|
|
138
|
+
assert.deepEqual(words.sort(), ['expired', 'passed', 'returned', 'withdrawn']);
|
|
139
|
+
// the docket already names both new outcomes, so the CHECK and its words agree
|
|
140
|
+
const floors = fs.readFileSync(path.join(GOV, 'docket-floors.js'), 'utf8');
|
|
141
|
+
for (const w of words) assert.match(floors, new RegExp(`\\b${w}:`), `docket-floors OUTCOME_WORDS names '${w}'`);
|
|
142
|
+
// additive only: nothing in it rewrites a row or drops anything else
|
|
143
|
+
assert.doesNotMatch(sql.replace(/--.*$/gm, ''), /\b(UPDATE|DELETE|DROP TABLE|DROP COLUMN)\b/i);
|
|
144
|
+
});
|
|
@@ -45,6 +45,7 @@ const NON_GRANT_MIGRATIONS = new Set([
|
|
|
45
45
|
'government_018_passed_items_index.sql', // BV2.R17 passed-item lookup index (task 1004096)
|
|
46
46
|
'government_020_genesis_stage_subject.sql', // BV2.PS04 genesis_stage board subject CHECK (task 1004429)
|
|
47
47
|
'government_023_decision_rules.sql', // who decides what: the decision-rules table, matters (task 1004529)
|
|
48
|
+
'government_026_board_outcome_withdrawn.sql', // board outcome CHECK widened: withdrawn, expired (task 1004564)
|
|
48
49
|
]);
|
|
49
50
|
|
|
50
51
|
// Permission keys RENAMED after the grant migrations that first granted them.
|
|
@@ -202,6 +202,43 @@ test('a row\'s buttons are the card\'s acts under the board\'s words, and send t
|
|
|
202
202
|
assert.equal(D.rowsFor('charter', 0, dk, { now: NOW })[0].wait, 'reading…');
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
+
// task 1004564: the proposer's own sitting, before anyone has voted, is one row
|
|
206
|
+
// with one button — withdraw — sent to the withdraw route. A vote takes it away.
|
|
207
|
+
test('your own sitting with no vote on it: one row, withdraw, sent to its route; a vote or another viewer takes it away', () => {
|
|
208
|
+
const ms = [matter({ id: 'board:1', source: 'board', building: 'charter', state: 'moving', ref: { type: 'board_item', id: '1' } })];
|
|
209
|
+
const dk = docket(ms);
|
|
210
|
+
const room = (votes) => ({ open: [{ id: '1', subject_type: 'constitutional_amendment', subject: { title: LONG, author_id: '95' }, votes }], closed: [] });
|
|
211
|
+
const mine = D.rowsFor('charter', 0, dk, { room: room([]), now: NOW, viewerId: '95' })[0];
|
|
212
|
+
assert.deepEqual(mine.actions.map((a) => [a.key, a.label]), [['withdraw', 'withdraw']]);
|
|
213
|
+
assert.equal(mine.chips, null, 'withdraw asks nothing: no chips');
|
|
214
|
+
assert.deepEqual(j(C.request(mine.card, 'withdraw')), { method: 'POST', url: '/api/bongos/government/board/items/1/withdraw', body: {} });
|
|
215
|
+
assert.match(C.answer(mine.card, 'withdraw', { item_closed: true, outcome: 'withdrawn' }).toast, /^Withdrawn/);
|
|
216
|
+
assert.equal(C.refusal(409, { error: { code: 'votes_cast' } }), 'Someone has voted on it, so it is the sitting\'s to decide now.');
|
|
217
|
+
// one vote — anyone's — and the row is the sitting's again
|
|
218
|
+
const voted = D.rowsFor('charter', 0, dk, { room: room([{ voter_id: '7', direction: 'yes', name: 'Ana' }]), now: NOW, viewerId: '95' })[0];
|
|
219
|
+
assert.ok(!voted.actions.some((a) => a.key === 'withdraw'), 'no withdraw once a vote is cast');
|
|
220
|
+
// someone else's sitting never offers it
|
|
221
|
+
const theirs = D.rowsFor('charter', 0, dk, { room: room([]), now: NOW, viewerId: '7' })[0];
|
|
222
|
+
assert.ok(!theirs.actions.some((a) => a.key === 'withdraw'), 'only the proposer sees withdraw');
|
|
223
|
+
// a proposer who may also vote keeps yes first, withdraw second — still two buttons
|
|
224
|
+
const needs = D.rowsFor('charter', 0, docket([{ ...ms[0], state: 'needs' }]), { room: room([]), now: NOW, viewerId: '95' })[0];
|
|
225
|
+
assert.deepEqual(needs.actions.map((a) => a.key), ['for', 'withdraw']);
|
|
226
|
+
// and the line rule holds on every floor with the proposer looking
|
|
227
|
+
const own = { ...ROOM, open: ROOM.open.map((it) => it.subject_type === 'full_idea' ? it : { ...it, subject: { ...it.subject, author_id: '95' } }) };
|
|
228
|
+
const desks = everyDesk(docket(longMatters()), { room: own });
|
|
229
|
+
assert.ok(desks.flatMap((d) => d.rows).some((r) => r.actions.some((a) => a.key === 'withdraw')), 'the stress docket reached a withdraw row');
|
|
230
|
+
assert.deepEqual(desks.flatMap((d) => D.breaks(d)), []);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('a withdrawn sitting reads as withdrawn on the card, never as sent back to revise', () => {
|
|
234
|
+
const dk = docket([matter({ id: 'board:1', source: 'board', building: 'charter', state: 'settled', ref: { type: 'board_item', id: '1' } })]);
|
|
235
|
+
const room = { open: [], closed: [{ id: '1', subject_type: 'constitutional_amendment', subject: { title: 'Shorter windows', author_id: '95' }, votes: [], closed_at: '2026-10-03T10:00:00.000Z', outcome: 'withdrawn' }] };
|
|
236
|
+
const cv = (viewerId) => C.model({ docket: dk, key: 'board:1', room, now: NOW, viewerId });
|
|
237
|
+
assert.equal(cv('95').result, 'You withdrew it before anyone voted.');
|
|
238
|
+
assert.match(cv('7').result, /^Withdrawn by .* before anyone voted\.$/);
|
|
239
|
+
assert.equal(cv('95').readOnly, true);
|
|
240
|
+
});
|
|
241
|
+
|
|
205
242
|
// ── the page: the desk acts in place, and the game layer answers ──────────────
|
|
206
243
|
|
|
207
244
|
function fakeDom() {
|
|
@@ -635,7 +635,7 @@ try {
|
|
|
635
635
|
assert.match(HTML, /<body data-page="tweak-editor"[^>]*data-fullscreen-switch/);
|
|
636
636
|
assert.equal((HTML.match(/<h1\b/g) || []).length, 1);
|
|
637
637
|
assert.ok(HTML.indexOf('tweak-editor-lib.js') < HTML.indexOf('tweak-editor.js?'), 'the lib loads first');
|
|
638
|
-
assert.match(HTML, /shell\.js\?v=2026-10-03-
|
|
638
|
+
assert.match(HTML, /shell\.js\?v=2026-10-03-looks/);
|
|
639
639
|
assert.equal(/fonts\.googleapis\.com\/css/.test(HTML), false);
|
|
640
640
|
for (const id of ['tw-bar', 'tw-counter', 'tw-open', 'tw-open-more', 'tw-divider', 'tw-openwrap', 'tw-open-menu', 'tw-open-tab', 'tw-open-split', 'tw-frame', 'tw-frame-view', 'tw-split-close', 'tw-download', 'tw-upload', 'tw-file', 'tw-submit', 'tw-paper', 'tw-doc', 'tw-notice', 'tw-done', 'tw-take', 'tw-pick', 'tw-picks', 'tw-refused', 'tw-refused-title', 'tw-refused-list', 'tw-refused-next', 'tw-refused-download', 'tw-refused-close']) {
|
|
641
641
|
assert.ok(new RegExp(`id="${id}"`).test(HTML), `#${id} is in the markup`);
|