@bongos/core 1.24.39 → 1.24.41
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 +87 -102
- package/docs/canonical-permissions.md +2 -1
- package/docs/copy-inventory.md +122 -127
- package/docs/copy-registry.json +179 -244
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +11 -69
- package/docs/page-readings.json +434 -822
- package/modules/hall-ui/public/ai-studio.html +2 -2
- package/modules/hall-ui/public/atlas.html +2 -2
- package/modules/hall-ui/public/blockers.html +2 -2
- package/modules/hall-ui/public/board-room.html +2 -2
- package/modules/hall-ui/public/city.html +2 -2
- package/modules/hall-ui/public/collab.html +2 -2
- package/modules/hall-ui/public/commands.html +2 -2
- package/modules/hall-ui/public/copy-desk.html +2 -2
- package/modules/hall-ui/public/craft-mode.js +1 -1
- package/modules/hall-ui/public/deploy.html +2 -2
- package/modules/hall-ui/public/gate.js +12 -5
- package/modules/hall-ui/public/goal-map.html +2 -2
- package/modules/hall-ui/public/goals.html +2 -2
- package/modules/hall-ui/public/government.html +2 -2
- package/modules/hall-ui/public/idea.html +2 -2
- package/modules/hall-ui/public/ideas.html +2 -2
- package/modules/hall-ui/public/index.html +2 -2
- package/modules/hall-ui/public/market-watch-item.html +2 -2
- package/modules/hall-ui/public/market-watch-rules.html +2 -2
- package/modules/hall-ui/public/market-watch.html +2 -2
- package/modules/hall-ui/public/modules.html +2 -2
- package/modules/hall-ui/public/money.html +2 -2
- package/modules/hall-ui/public/my-agents.html +2 -2
- package/modules/hall-ui/public/palette.js +10 -0
- package/modules/hall-ui/public/primer.html +2 -2
- package/modules/hall-ui/public/profile.html +2 -2
- package/modules/hall-ui/public/project-settings.html +2 -2
- package/modules/hall-ui/public/roadmap.html +2 -2
- package/modules/hall-ui/public/roster.css +10 -0
- package/modules/hall-ui/public/roster.html +6 -3
- package/modules/hall-ui/public/roster.js +168 -9
- package/modules/hall-ui/public/sessions.js +21 -10
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/shell.js +1 -2
- package/modules/hall-ui/public/street.html +2 -2
- package/modules/hall-ui/public/studio.html +2 -2
- package/modules/hall-ui/public/task.html +2 -2
- package/modules/hall-ui/public/thinking.html +2 -2
- package/modules/hall-ui/public/tweak-editor.html +2 -2
- package/modules/hall-ui/public/watch.css +0 -6
- package/modules/hall-ui/public/watch.html +121 -15
- package/modules/hall-ui/public/watch.js +28 -166
- package/modules/hall-ui/public/watch.states.json +49 -1
- package/modules/hall-ui/public/work.html +2 -2
- package/modules/hall-ui/records/builders-roster.md +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/bongos/gen-hall-sitemap.js +2 -0
- package/src/bongos/routes/me.js +3 -1
- package/src/bongos/serve-internal.js +15 -22
- package/src/module-api.js +1 -1
- package/tests/autonomy_builder_scope.mjs +2 -1
- package/tests/hall_audit.mjs +1 -1
- package/tests/hall_loading.mjs +13 -2
- package/tests/hall_nav.mjs +5 -2
- package/tests/hall_oversight_tabs.mjs +162 -0
- package/tests/hall_oversight_world.mjs +23 -9
- package/tests/hall_page_gate_map.mjs +7 -2
- package/tests/hall_roster_world.mjs +14 -9
- package/tests/hall_sessions_ledger.mjs +2 -1
- package/tests/nav_permission_atoms.mjs +3 -2
- package/tests/rank_tier_single_source.mjs +3 -1
- package/tests/roster_government_columns.mjs +369 -0
- package/tests/sessions_kit_pager.mjs +8 -4
- package/tests/watch_sealed_floor.mjs +6 -4
- package/modules/hall-ui/public/gate.html +0 -76
- package/modules/hall-ui/public/gate.states.json +0 -60
- package/modules/hall-ui/public/sessions.html +0 -128
- package/modules/hall-ui/public/sessions.states.json +0 -59
- package/tests/watch_roster_self_row.mjs +0 -296
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// tests/roster_government_columns.mjs — the Builders page's government columns,
|
|
2
|
+
// EXECUTED (task 1005048; the own-row rule is task 1003720, re-filed from task
|
|
3
|
+
// 1002951).
|
|
4
|
+
//
|
|
5
|
+
// Watch kept its own roster of the same builders the Builders page lists. The
|
|
6
|
+
// owner's call (idea 1001313) was one list, on Builders: a viewer the server
|
|
7
|
+
// lets read GET /builders/roster also sees each builder's active claims, the
|
|
8
|
+
// stale-claim flag and standing, and the rank and offboard controls that list
|
|
9
|
+
// carried. Anyone that read refuses sees the public list and nothing more.
|
|
10
|
+
//
|
|
11
|
+
// The own-row rule moved with the controls. The bug, as the owner reported it
|
|
12
|
+
// from Discord #bugs: "I cannot press offboard". Both routes behind a row
|
|
13
|
+
// refuse a self-change categorically and always will:
|
|
14
|
+
//
|
|
15
|
+
// PATCH /builders/:id/rank → 403 cannot_change_own_rank
|
|
16
|
+
// PATCH /builders/:id/status → 403 cannot_change_own_status
|
|
17
|
+
//
|
|
18
|
+
// so the viewer's own row renders no control and the foot line says why.
|
|
19
|
+
//
|
|
20
|
+
// These are assertions about what a human sees and clicks, so this runs the
|
|
21
|
+
// real roster.js inside `vm` against the real roster.html, with a makeLedger
|
|
22
|
+
// that actually calls rowHtml.
|
|
23
|
+
//
|
|
24
|
+
// Run: node tests/roster_government_columns.mjs
|
|
25
|
+
|
|
26
|
+
import { strict as assert } from 'node:assert';
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import vm from 'node:vm';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
import { makeRunner, makeEl, parseInto } from './helpers.mjs';
|
|
32
|
+
|
|
33
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
34
|
+
const HALL = path.join(ROOT, 'modules/hall-ui/public');
|
|
35
|
+
const SRC = fs.readFileSync(path.join(HALL, 'roster.js'), 'utf8');
|
|
36
|
+
const PAGE = fs.readFileSync(path.join(HALL, 'roster.html'), 'utf8');
|
|
37
|
+
const WATCH_JS = fs.readFileSync(path.join(HALL, 'watch.js'), 'utf8');
|
|
38
|
+
const WATCH_HTML = fs.readFileSync(path.join(HALL, 'watch.html'), 'utf8');
|
|
39
|
+
|
|
40
|
+
const { test, summary } = makeRunner();
|
|
41
|
+
|
|
42
|
+
const escapeHtml = (s) => String(s == null ? '' : s).replace(/[&<>"']/g,
|
|
43
|
+
(c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
44
|
+
|
|
45
|
+
const tick = () => new Promise((r) => setTimeout(r, 0));
|
|
46
|
+
async function settle() { for (let i = 0; i < 40; i++) await tick(); }
|
|
47
|
+
|
|
48
|
+
const ok = (data) => ({ status: 200, ok: true, data });
|
|
49
|
+
const denied = { status: 403, ok: false, data: { error: { code: 'forbidden', details: { floors: { 'builder.roster.read': 'metic' } } } } };
|
|
50
|
+
|
|
51
|
+
// The branding pack renames every rank (the neutral pack calls archon "Owner"),
|
|
52
|
+
// which is why a hardcoded rank word in page copy is banned. This stub renames
|
|
53
|
+
// them too, so a test for "the foot line does not spell Archon" is real.
|
|
54
|
+
const RANK_LABELS = { xenos: 'Newcomer', thetes: 'Contributor', metic: 'Trusted', archon: 'Owner' };
|
|
55
|
+
const rankLabel = (r) => RANK_LABELS[r] || String(r || '');
|
|
56
|
+
|
|
57
|
+
// The viewer is builder 1. The public list holds the builders who have earned;
|
|
58
|
+
// the government roster holds every builder, Epsilon (nothing earned) included.
|
|
59
|
+
const VIEWER_ID = '1';
|
|
60
|
+
const PUBLIC = [
|
|
61
|
+
{ id: '90', display_name: 'Alpha', github_login: 'alpha', rank: 'archon', total_credits: 9200, karma: 4, tokens_used: 10 },
|
|
62
|
+
{ id: '1', display_name: 'The Owner', github_login: 'theowner', rank: 'archon', total_credits: 6896, karma: 68, tokens_used: 10 },
|
|
63
|
+
{ id: '92', display_name: 'Gamma', github_login: 'gamma', rank: 'thetes', total_credits: 830, karma: 1, tokens_used: 10 },
|
|
64
|
+
{ id: '93', display_name: 'Delta', github_login: 'delta', rank: 'xenos', total_credits: 40, karma: 0, tokens_used: 10 },
|
|
65
|
+
];
|
|
66
|
+
const ROSTER = [
|
|
67
|
+
{ id: '90', display_name: 'Alpha', github_login: 'alpha', rank: 'archon', total_credits: 9200, karma: 4, active_claim_count: 1, stale_claim: false, status: 'active' },
|
|
68
|
+
{ id: '1', display_name: 'The Owner', github_login: 'theowner', rank: 'archon', total_credits: 6896, karma: 68, active_claim_count: 2, stale_claim: false, status: 'active' },
|
|
69
|
+
{ id: '92', display_name: 'Gamma', github_login: 'gamma', rank: 'thetes', total_credits: 830, karma: 1, active_claim_count: 3, stale_claim: true, status: 'active' },
|
|
70
|
+
{ id: '93', display_name: 'Delta', github_login: 'delta', rank: 'xenos', total_credits: 40, karma: 0, active_claim_count: 0, stale_claim: false, status: 'inactive' },
|
|
71
|
+
{ id: '94', display_name: 'Epsilon', github_login: 'epsilon', rank: 'xenos', total_credits: 0, karma: 0, active_claim_count: 0, stale_claim: false, status: 'active' },
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
// ===========================================================================
|
|
75
|
+
// Boot the real page against a scripted API and hand back handles.
|
|
76
|
+
// ===========================================================================
|
|
77
|
+
|
|
78
|
+
async function boot(opts = {}) {
|
|
79
|
+
const {
|
|
80
|
+
builders = ROSTER,
|
|
81
|
+
publicRows = PUBLIC,
|
|
82
|
+
rosterAnswer = null,
|
|
83
|
+
me = { id: VIEWER_ID, rank: 'archon', github_login: 'theowner' },
|
|
84
|
+
permissions = { 'builder.roster.read': true },
|
|
85
|
+
sharedMe = false,
|
|
86
|
+
search = '',
|
|
87
|
+
} = opts;
|
|
88
|
+
|
|
89
|
+
const requests = [];
|
|
90
|
+
const toasts = [];
|
|
91
|
+
const prompts = [];
|
|
92
|
+
let filterBar = null;
|
|
93
|
+
|
|
94
|
+
const api = {
|
|
95
|
+
async request(method, p, init) {
|
|
96
|
+
const body = init && init.body;
|
|
97
|
+
requests.push({ method, path: p, body });
|
|
98
|
+
const route = String(p).replace('/api/bongos', '').split('?')[0];
|
|
99
|
+
if (method === 'GET' && route === '/me') return ok({ builder: me, permissions });
|
|
100
|
+
if (method === 'GET' && route === '/builders/roster') return rosterAnswer || ok({ builders });
|
|
101
|
+
if (method === 'PATCH' && route.startsWith('/builders/')) return ok({ builder: { id: route.split('/')[2] } });
|
|
102
|
+
return { status: 404, ok: false, data: null };
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const documentObj = {
|
|
107
|
+
readyState: 'complete',
|
|
108
|
+
body: makeEl('body'),
|
|
109
|
+
documentElement: makeEl('html'),
|
|
110
|
+
createElement: (tag) => makeEl(tag),
|
|
111
|
+
getElementById(id) { return this.body.querySelector(`#${id}`); },
|
|
112
|
+
querySelector(sel) { return this.body.querySelector(sel); },
|
|
113
|
+
querySelectorAll(sel) { return this.body.querySelectorAll(sel); },
|
|
114
|
+
addEventListener() {},
|
|
115
|
+
};
|
|
116
|
+
parseInto(documentObj.body, PAGE);
|
|
117
|
+
|
|
118
|
+
const locationObj = { hostname: 'builders.example.test', pathname: '/roster', search, hash: '', assign() {} };
|
|
119
|
+
|
|
120
|
+
const kit = {
|
|
121
|
+
esc: escapeHtml,
|
|
122
|
+
makePager: () => ({ page: 1, perPage: 25 }),
|
|
123
|
+
makeFilterBar: (o) => { filterBar = o; o.mount.innerHTML = '<div class="filters"></div>'; return { el: o.mount, values: () => ({}) }; },
|
|
124
|
+
// A ledger that REALLY renders: the whole question here is which row got
|
|
125
|
+
// which cell, and a no-op render answers none of it.
|
|
126
|
+
makeLedger: ({ mount, columns, rowHtml }) => ({
|
|
127
|
+
render(rows) {
|
|
128
|
+
mount.innerHTML =
|
|
129
|
+
'<table class="ledger"><thead><tr>' +
|
|
130
|
+
columns.map((c) => `<th>${escapeHtml(typeof c === 'string' ? c : c.label)}</th>`).join('') +
|
|
131
|
+
'</tr></thead><tbody>' + rows.map((r) => rowHtml(r)).join('') + '</tbody></table>';
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const windowObj = {
|
|
137
|
+
OTB: { fmtInt: (n) => String(n == null ? 0 : n), rankLabel, toast: (m) => toasts.push(m) },
|
|
138
|
+
OTBKit: kit,
|
|
139
|
+
OTBBranding: { currencyLabel: () => 'Credits' },
|
|
140
|
+
BongosClient: { createClient: () => api },
|
|
141
|
+
location: locationObj,
|
|
142
|
+
prompt: (msg) => { prompts.push(msg); return 'left for a new job'; },
|
|
143
|
+
addEventListener() {},
|
|
144
|
+
};
|
|
145
|
+
// The shell's own /me read, shared on window.__hallReads (shell.js probeMe).
|
|
146
|
+
if (sharedMe) windowObj.__hallReads = { '/me': Promise.resolve({ builder: me, permissions }) };
|
|
147
|
+
|
|
148
|
+
const sandbox = {
|
|
149
|
+
window: windowObj, document: documentObj, location: locationObj,
|
|
150
|
+
fetch: async () => ({ ok: true, json: async () => ({ leaderboard: publicRows.map((b) => ({ ...b })) }) }),
|
|
151
|
+
console: { log() {}, warn() {}, error() {} },
|
|
152
|
+
setTimeout, clearTimeout, Promise, Date, Math, JSON, Error, isNaN, URLSearchParams,
|
|
153
|
+
Number, String, Object, Array, Boolean, Set, Map, RegExp,
|
|
154
|
+
encodeURIComponent, decodeURIComponent, parseInt, parseFloat,
|
|
155
|
+
};
|
|
156
|
+
sandbox.globalThis = sandbox;
|
|
157
|
+
vm.createContext(sandbox);
|
|
158
|
+
vm.runInContext(SRC, sandbox, { filename: 'roster.js' });
|
|
159
|
+
await settle();
|
|
160
|
+
|
|
161
|
+
const rosterReads = () => requests.filter((r) => /\/builders\/roster/.test(r.path));
|
|
162
|
+
const meReads = () => requests.filter((r) => /\/me$/.test(r.path));
|
|
163
|
+
const rosterBody = () => documentObj.getElementById('roster-body');
|
|
164
|
+
const heads = () => rosterBody().querySelectorAll('th').map((th) => th.textContent);
|
|
165
|
+
const rows = () => rosterBody().querySelectorAll('tr').filter((tr) => tr.querySelectorAll('td').length);
|
|
166
|
+
const rowFor = (login) => rows().find((tr) => tr.querySelectorAll('td')[1].textContent.includes('@' + login));
|
|
167
|
+
const patches = () => requests.filter((r) => r.method === 'PATCH');
|
|
168
|
+
const foot = () => documentObj.getElementById('roster-gov-foot');
|
|
169
|
+
const footText = () => (foot().hidden ? '' : foot().textContent);
|
|
170
|
+
|
|
171
|
+
return { rosterReads, meReads, requests, toasts, prompts, documentObj, rosterBody, heads, rows, rowFor, patches, foot, footText, settle, filterBar: () => filterBar };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const cells = (tr) => tr.querySelectorAll('td');
|
|
175
|
+
const GOV_HEADS = ['Active claims', 'Standing', 'Action'];
|
|
176
|
+
|
|
177
|
+
// ===========================================================================
|
|
178
|
+
// 1. One list: a government viewer sees the columns Watch's roster carried.
|
|
179
|
+
// ===========================================================================
|
|
180
|
+
|
|
181
|
+
await test('a government viewer sees active claims, standing and actions', async () => {
|
|
182
|
+
const h = await boot();
|
|
183
|
+
for (const head of GOV_HEADS) assert.ok(h.heads().includes(head), `missing column ${head}: ${h.heads()}`);
|
|
184
|
+
assert.ok(h.heads().includes('Credits'), 'the credits column went missing');
|
|
185
|
+
const gamma = cells(h.rowFor('gamma'));
|
|
186
|
+
assert.match(gamma[7].textContent, /^3/, 'Gamma\'s active claims are not shown');
|
|
187
|
+
assert.match(gamma[7].textContent, /stale/, 'Gamma\'s stale claim is not flagged');
|
|
188
|
+
assert.match(cells(h.rowFor('delta'))[8].textContent, /inactive/, 'Delta\'s standing is not shown');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await test('every builder is on the one list, even one who has earned nothing yet', async () => {
|
|
192
|
+
const h = await boot();
|
|
193
|
+
assert.equal(h.rows().length, ROSTER.length);
|
|
194
|
+
assert.ok(h.rowFor('epsilon'), 'a builder on the government roster but not the public list dropped off');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await test('a stale-claims link lands on the list filtered to them', async () => {
|
|
198
|
+
const h = await boot({ search: '?claims=stale' });
|
|
199
|
+
assert.deepEqual(h.rows().map((tr) => cells(tr)[1].textContent.match(/@(\w+)/)[1]), ['gamma']);
|
|
200
|
+
const control = h.filterBar().primary.find((c) => c.id === 'roster-claims');
|
|
201
|
+
assert.equal(control && control.value, 'stale', 'the claims filter does not show the filter the link asked for');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ===========================================================================
|
|
205
|
+
// 2. A viewer without the seat sees no new columns.
|
|
206
|
+
// ===========================================================================
|
|
207
|
+
|
|
208
|
+
await test('a refused roster read draws the public list and nothing more', async () => {
|
|
209
|
+
const h = await boot({ rosterAnswer: denied });
|
|
210
|
+
for (const head of GOV_HEADS) assert.ok(!h.heads().includes(head), `a refused viewer sees ${head}`);
|
|
211
|
+
assert.equal(h.rows().length, PUBLIC.length);
|
|
212
|
+
assert.equal(h.rosterBody().querySelectorAll('[data-roster-rank]').length, 0);
|
|
213
|
+
assert.equal(h.rosterBody().querySelectorAll('[data-roster-status]').length, 0);
|
|
214
|
+
assert.equal(h.footText(), '', 'the government foot line showed to a refused viewer');
|
|
215
|
+
const claims = h.filterBar().primary.find((c) => c.id === 'roster-claims');
|
|
216
|
+
assert.ok(!claims, 'a refused viewer is offered a claims filter');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
await test('a signed-out viewer sees the public list only', async () => {
|
|
220
|
+
const h = await boot({ rosterAnswer: { status: 401, ok: false, data: null } });
|
|
221
|
+
for (const head of GOV_HEADS) assert.ok(!h.heads().includes(head));
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await test('a viewer without the permission is never asked for the roster', async () => {
|
|
225
|
+
// The public page pays no extra round trip for a viewer /me says lacks the seat.
|
|
226
|
+
const h = await boot({ permissions: { 'builder.roster.read': false } });
|
|
227
|
+
assert.equal(h.rosterReads().length, 0, 'the roster was requested for a viewer without builder.roster.read');
|
|
228
|
+
for (const head of GOV_HEADS) assert.ok(!h.heads().includes(head));
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
await test('a /me with no permission map asks for nothing more', async () => {
|
|
232
|
+
const h = await boot({ permissions: null });
|
|
233
|
+
assert.equal(h.rosterReads().length, 0);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
await test('the shell\'s shared /me read is adopted, not asked twice', async () => {
|
|
237
|
+
const h = await boot({ sharedMe: true });
|
|
238
|
+
assert.equal(h.meReads().length, 0, 'roster.js asked /me although the shell had already');
|
|
239
|
+
for (const head of GOV_HEADS) assert.ok(h.heads().includes(head));
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ===========================================================================
|
|
243
|
+
// 3. The viewer's own row offers neither control (task 1003720).
|
|
244
|
+
// ===========================================================================
|
|
245
|
+
|
|
246
|
+
await test('the viewer\'s own row renders no Offboard button', async () => {
|
|
247
|
+
const h = await boot();
|
|
248
|
+
const mine = h.rowFor('theowner');
|
|
249
|
+
assert.ok(mine, 'the roster did not render the viewer\'s own row');
|
|
250
|
+
assert.equal(mine.querySelectorAll('[data-roster-status]').length, 0,
|
|
251
|
+
'the viewer\'s own row still carries a status button the server refuses outright');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
await test('the viewer\'s own row renders no rank select', async () => {
|
|
255
|
+
const h = await boot();
|
|
256
|
+
assert.equal(h.rowFor('theowner').querySelectorAll('[data-roster-rank]').length, 0,
|
|
257
|
+
'the viewer\'s own row still carries a rank select the server refuses outright');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
await test('the own row still SHOWS the rank and says whose row it is', async () => {
|
|
261
|
+
const h = await boot();
|
|
262
|
+
const c = cells(h.rowFor('theowner'));
|
|
263
|
+
assert.equal(c[3].textContent.trim(), 'Owner', 'the own row lost the rank it should still state');
|
|
264
|
+
assert.match(c[9].textContent, /you/i, 'the own row\'s action cell says nothing about why it is empty');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ===========================================================================
|
|
268
|
+
// 4. Every OTHER row keeps both controls.
|
|
269
|
+
// ===========================================================================
|
|
270
|
+
|
|
271
|
+
await test('every other row keeps its Offboard / Reactivate button', async () => {
|
|
272
|
+
const h = await boot();
|
|
273
|
+
for (const login of ['alpha', 'gamma', 'delta']) {
|
|
274
|
+
assert.equal(h.rowFor(login).querySelectorAll('[data-roster-status]').length, 1, `row @${login} lost its status control`);
|
|
275
|
+
}
|
|
276
|
+
assert.equal(h.rowFor('delta').querySelector('[data-roster-status]').getAttribute('data-roster-status'), 'active');
|
|
277
|
+
assert.equal(h.rowFor('alpha').querySelector('[data-roster-status]').getAttribute('data-roster-status'), 'inactive');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
await test('every other row keeps its rank select', async () => {
|
|
281
|
+
const h = await boot();
|
|
282
|
+
for (const login of ['alpha', 'gamma', 'delta']) {
|
|
283
|
+
assert.equal(h.rowFor(login).querySelectorAll('[data-roster-rank]').length, 1, `row @${login} lost its rank select`);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
await test('another builder\'s Offboard still reaches the route', async () => {
|
|
288
|
+
const h = await boot();
|
|
289
|
+
const btn = h.rowFor('gamma').querySelector('[data-roster-status]');
|
|
290
|
+
// Delegated on #roster-body; the stub's fire() does not bubble, so dispatch
|
|
291
|
+
// on the mount and name the button as the target, the way a real click arrives.
|
|
292
|
+
h.rosterBody().fire('click', { target: btn });
|
|
293
|
+
await h.settle();
|
|
294
|
+
const p = h.patches();
|
|
295
|
+
assert.equal(p.length, 1, `expected one PATCH, got ${p.length}`);
|
|
296
|
+
assert.match(p[0].path, /\/builders\/92\/status$/);
|
|
297
|
+
assert.equal(p[0].body.status, 'inactive');
|
|
298
|
+
assert.equal(p[0].body.reason, 'left for a new job', 'the offboarding reason was dropped');
|
|
299
|
+
assert.equal(h.prompts.length, 1, 'the reason prompt no longer runs');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
await test('a rank change reaches the route', async () => {
|
|
303
|
+
const h = await boot();
|
|
304
|
+
const sel = h.rowFor('gamma').querySelector('[data-roster-rank]');
|
|
305
|
+
sel.value = 'metic';
|
|
306
|
+
h.rosterBody().fire('change', { target: sel });
|
|
307
|
+
await h.settle();
|
|
308
|
+
const p = h.patches();
|
|
309
|
+
assert.equal(p.length, 1);
|
|
310
|
+
assert.match(p[0].path, /\/builders\/92\/rank$/);
|
|
311
|
+
assert.equal(p[0].body.rank, 'metic');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// ===========================================================================
|
|
315
|
+
// 5. The foot line says why, in the pack's own words.
|
|
316
|
+
// ===========================================================================
|
|
317
|
+
|
|
318
|
+
await test('a foot line under the roster explains the empty row', async () => {
|
|
319
|
+
const h = await boot();
|
|
320
|
+
assert.match(h.footText(), /your own row carries no controls/i,
|
|
321
|
+
'nothing under the roster explains why one row has no controls');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
await test('the foot line names the rank from the pack, never a hardcoded "Archon"', async () => {
|
|
325
|
+
const h = await boot();
|
|
326
|
+
const foot = h.footText();
|
|
327
|
+
assert.match(foot, /Owner/, 'the foot line does not name the rank through rankLabel()');
|
|
328
|
+
assert.ok(!/archon/i.test(foot), `the foot line hardcodes a rank word: ${foot}`);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
await test('no own-row sentence when the viewer is not on the roster', async () => {
|
|
332
|
+
// A Metic reading the roster of a project they are not listed on.
|
|
333
|
+
const h = await boot({ builders: ROSTER.filter((b) => b.id !== VIEWER_ID), publicRows: PUBLIC.filter((b) => b.id !== VIEWER_ID) });
|
|
334
|
+
assert.ok(!/your own row/i.test(h.footText()), 'the foot line rendered with no own row on the table');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// ===========================================================================
|
|
338
|
+
// 6. Degrade: an unidentifiable viewer must not mark the WRONG row.
|
|
339
|
+
// ===========================================================================
|
|
340
|
+
|
|
341
|
+
await test('no id from /me leaves every row armed', async () => {
|
|
342
|
+
const h = await boot({ me: { rank: 'archon', github_login: 'theowner' } });
|
|
343
|
+
for (const b of PUBLIC) {
|
|
344
|
+
assert.equal(h.rowFor(b.github_login).querySelectorAll('[data-roster-status]').length, 1,
|
|
345
|
+
`row @${b.github_login} was disarmed although the viewer could not be identified`);
|
|
346
|
+
}
|
|
347
|
+
assert.ok(!/your own row/i.test(h.footText()));
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
await test('the match is by value, not by type — a numeric id still matches', async () => {
|
|
351
|
+
const h = await boot({ me: { id: 1, rank: 'archon', github_login: 'theowner' } });
|
|
352
|
+
assert.equal(h.rowFor('theowner').querySelectorAll('[data-roster-status]').length, 0,
|
|
353
|
+
'a numeric /me id did not match the roster\'s string id — the own row is armed again');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// ===========================================================================
|
|
357
|
+
// 7. Watch has no second list; its tally links here.
|
|
358
|
+
// ===========================================================================
|
|
359
|
+
|
|
360
|
+
await test('Watch carries no roster of its own any more', () => {
|
|
361
|
+
assert.ok(!/id="roster-body"/.test(WATCH_HTML), 'watch.html still has a roster section');
|
|
362
|
+
assert.ok(!/data-roster-rank|data-roster-status/.test(WATCH_JS), 'watch.js still draws roster controls');
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
await test('Watch\'s stale-claims tile links to Builders, filtered', () => {
|
|
366
|
+
assert.match(WATCH_HTML, /<a class="stat-tile" href="\/roster\?claims=stale">[^]*?id="att-stale"/);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
summary('Builders page — the government columns');
|
|
@@ -33,7 +33,8 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
33
33
|
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
|
34
34
|
const KIT_JS = read('modules/hall-ui/public/hall-kit.js');
|
|
35
35
|
const SESSIONS_JS = read('modules/hall-ui/public/sessions.js');
|
|
36
|
-
|
|
36
|
+
// task 1005049: the ledger is Watch's Sessions tab, so its markup lives in watch.html.
|
|
37
|
+
const SESSIONS_HTML = read('modules/hall-ui/public/watch.html');
|
|
37
38
|
|
|
38
39
|
const { test, summary } = makeRunner();
|
|
39
40
|
const tick = () => new Promise((r) => setTimeout(r, 0));
|
|
@@ -147,6 +148,9 @@ function harness({ total = 320, failAfter = null, viewerRank = 'metic', perPageP
|
|
|
147
148
|
vm.runInContext(KIT_JS, sandbox, { filename: 'hall-kit.js' });
|
|
148
149
|
if (dropKit) delete sandbox.window.OTBKit;
|
|
149
150
|
vm.runInContext(SESSIONS_JS, sandbox, { filename: 'sessions.js' });
|
|
151
|
+
// The tab boots when Watch first opens it (task 1005049); a second open is a no-op.
|
|
152
|
+
sandbox.window.OTBWatchTabs.sessions();
|
|
153
|
+
sandbox.window.OTBWatchTabs.sessions();
|
|
150
154
|
|
|
151
155
|
const searches = () => requests.filter((u) => u.includes('/sessions/search'));
|
|
152
156
|
const q = (n) => {
|
|
@@ -161,7 +165,7 @@ function harness({ total = 320, failAfter = null, viewerRank = 'metic', perPageP
|
|
|
161
165
|
// 1. the page markup no longer carries a pager of its own
|
|
162
166
|
// ---------------------------------------------------------------------------
|
|
163
167
|
|
|
164
|
-
await test('
|
|
168
|
+
await test('the Sessions tab carries a mount, not a hand-rolled pager', () => {
|
|
165
169
|
assert.ok(!/id="ledger-pager"/.test(SESSIONS_HTML), 'the bespoke <nav class="pager"> is gone');
|
|
166
170
|
assert.ok(!/id="pager-(prev|next|pos)"/.test(SESSIONS_HTML), 'its bespoke button ids are gone');
|
|
167
171
|
assert.match(SESSIONS_HTML, /id="sessions-pager-mount"/);
|
|
@@ -307,14 +311,14 @@ await test('a missing kit degrades to a named failure, not a blank page', async
|
|
|
307
311
|
await tick(); await tick();
|
|
308
312
|
assert.equal(h.searches().length, 0, 'no corpus request without a pager to place it');
|
|
309
313
|
// in the page body: the head that used to carry it is hidden (task 1004829)
|
|
310
|
-
assert.match(h.getEl('
|
|
314
|
+
assert.match(h.getEl('sess-sealed-body').textContent, /could not be loaded/);
|
|
311
315
|
});
|
|
312
316
|
|
|
313
317
|
await test('the rank gate still runs before any corpus request', async () => {
|
|
314
318
|
const h = harness({ total: 320, viewerRank: 'xenos' });
|
|
315
319
|
await tick(); await tick();
|
|
316
320
|
assert.equal(h.searches().length, 0, 'a xenos must not even ask');
|
|
317
|
-
assert.equal(h.getEl('
|
|
321
|
+
assert.equal(h.getEl('sess-sealed').hidden, false, 'they get the sealed notice');
|
|
318
322
|
});
|
|
319
323
|
|
|
320
324
|
summary();
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
//
|
|
17
17
|
// access_request.review → archon ("Archons only" was CORRECT here)
|
|
18
18
|
// override_request.decide → metic (ADR 0157, was archon)
|
|
19
|
-
// builder.roster.read → metic (ADR 0157, was archon)
|
|
19
|
+
// builder.roster.read → metic (ADR 0157, was archon; its panel is on Builders since task 1005048)
|
|
20
20
|
// session.search → metic (ADR 0157, was archon)
|
|
21
21
|
// audit_log.read → metic (ADR 0157, was archon)
|
|
22
22
|
//
|
|
@@ -188,11 +188,12 @@ await test('two panels denied at DIFFERENT floors say different things', async (
|
|
|
188
188
|
const h = await boot({
|
|
189
189
|
answers: {
|
|
190
190
|
'/access-requests': denied({ 'access_request.review': 'archon' }),
|
|
191
|
-
'/
|
|
191
|
+
'/audit-log': denied({ 'audit_log.read': 'metic' }),
|
|
192
192
|
},
|
|
193
193
|
});
|
|
194
|
+
// The roster panel that used to be the metic half moved to Builders (task 1005048).
|
|
194
195
|
assert.match(h.sealed('gate-body'), /^Archon and up\./);
|
|
195
|
-
assert.match(h.sealed('
|
|
196
|
+
assert.match(h.sealed('audit-body'), /^Metic and up\./);
|
|
196
197
|
});
|
|
197
198
|
|
|
198
199
|
// ===========================================================================
|
|
@@ -228,7 +229,8 @@ await test('no sealed message on this page hardcodes a rank name', async () => {
|
|
|
228
229
|
// The source half of the claim: a future edit that re-writes "Archons only."
|
|
229
230
|
// into a sealedMsg reds here even if it never renders in a test.
|
|
230
231
|
const strings = [...SRC.matchAll(/sealedMsg:\s*'([^']*)'/g)].map((m) => m[1]);
|
|
231
|
-
|
|
232
|
+
// Nine since the roster panel moved to Builders (task 1005048).
|
|
233
|
+
assert.ok(strings.length >= 9, `expected the page's sealed messages, found ${strings.length}`);
|
|
232
234
|
for (const s of strings) {
|
|
233
235
|
assert.doesNotMatch(s, /Archon|Metic|Thetes|Xenos/i,
|
|
234
236
|
`a sealed message names a rank in its own text: "${s}". The rank comes from `
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>Autobongos — {{worldName}}</title>
|
|
7
|
-
<meta name="description" content="The Autobongos runner: switch unattended building on or off, choose the goals it may work, and pause any builder's runner.">
|
|
8
|
-
<link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
|
|
9
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
|
-
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
12
|
-
<link rel="stylesheet" href="/builders/oversight.css">
|
|
13
|
-
<link rel="stylesheet" href="/builders/gate.css">
|
|
14
|
-
</head>
|
|
15
|
-
<body data-page="gate" data-page-title="Autobongos">
|
|
16
|
-
|
|
17
|
-
<div class="app">
|
|
18
|
-
<aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
|
|
19
|
-
<div class="app-scrim" id="app-scrim" hidden></div>
|
|
20
|
-
<div class="app-body">
|
|
21
|
-
<header class="app-topbar" id="app-topbar"></header>
|
|
22
|
-
<div class="app-content">
|
|
23
|
-
<!-- The page's one h1, for screen readers: no page shows its head (task
|
|
24
|
-
1004836), so its sub-line went in task 1004829. -->
|
|
25
|
-
<div class="page-head page-head--board">
|
|
26
|
-
<h1 class="page-head__h">Autobongos</h1>
|
|
27
|
-
</div>
|
|
28
|
-
|
|
29
|
-
<main id="hall-main">
|
|
30
|
-
<!-- task 1005043: the page loader, shown until any section below is revealed. -->
|
|
31
|
-
<p class="hall-loading hall-loading--page hall-loading--wait" role="status">Loading…</p>
|
|
32
|
-
<!-- task 1003469: the sealed state is a panel with a heading, not a bare
|
|
33
|
-
line on the ground. gate.js writes the real floor into the lede from
|
|
34
|
-
the pack's rank label, so the copy cannot drift from ADR 0157. -->
|
|
35
|
-
<section class="scroll" id="gate-sealed" aria-labelledby="gate-sealed-h" hidden>
|
|
36
|
-
<h2 class="scroll__h" id="gate-sealed-h">Restricted</h2>
|
|
37
|
-
<p class="scroll__lede" id="gate-sealed-body">This page opens at a higher rank.</p>
|
|
38
|
-
</section>
|
|
39
|
-
|
|
40
|
-
<section id="gate-body" hidden>
|
|
41
|
-
<!-- The Autobongos fence (task 1003908, ADR 0307 §6): what is allowed to
|
|
42
|
-
reach main unattended. This page held the held-PR list too until task
|
|
43
|
-
1004859 removed it; the URL stays /gate so bookmarks keep working.
|
|
44
|
-
Archon-only — a Metic gets the note below instead, and the routes
|
|
45
|
-
behind the panel are system:true/archon server-side regardless. -->
|
|
46
|
-
<p class="scroll__lede" id="gate-archon-only" hidden></p>
|
|
47
|
-
<section class="scroll" id="fence-panel" aria-labelledby="fence-h" hidden>
|
|
48
|
-
<h2 class="scroll__h" id="fence-h">Autobongos runner</h2>
|
|
49
|
-
<!-- One panel (task 1004509): a checklist while anything the runner
|
|
50
|
-
needs is missing, one status line with the switch once it is all
|
|
51
|
-
ticked. Drawn from /autonomy/runners + /autonomy/fence together. -->
|
|
52
|
-
<div id="fence-body"><span class="kit-skel"></span></div>
|
|
53
|
-
<!-- Every builder's runner (task 1004501): what each one picked to
|
|
54
|
-
work, whether you paused it, and whether it is checking in. -->
|
|
55
|
-
<div id="fence-builders"></div>
|
|
56
|
-
</section>
|
|
57
|
-
</section>
|
|
58
|
-
</main>
|
|
59
|
-
|
|
60
|
-
<footer class="app-foot" id="app-foot"></footer>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
64
|
-
|
|
65
|
-
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
66
|
-
|
|
67
|
-
<script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
|
|
68
|
-
<script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
|
|
69
|
-
<script src="/builders/craft-mode.js?v=2026-11-05-sections"></script>
|
|
70
|
-
<script src="/builders/shell.js?v=2026-11-05-sections"></script>
|
|
71
|
-
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
72
|
-
<script src="/builders/palette.js" defer></script>
|
|
73
|
-
<script src="/builders/hall-kit.js"></script>
|
|
74
|
-
<script src="/builders/gate.js?v=2026-10-25-autobongos"></script>
|
|
75
|
-
</body>
|
|
76
|
-
</html>
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"_": "Autobongos — the owner's runner panel behind requireGovernmentPage, still served at /gate (task 1004859 removed the held-PR list). Measured at three widths in both modes rather than looked at once.",
|
|
3
|
-
"page": "/builders/gate",
|
|
4
|
-
"surface": "hall-ui",
|
|
5
|
-
"stub": {
|
|
6
|
-
"prefix": "/builders/"
|
|
7
|
-
},
|
|
8
|
-
"modeQuery": false,
|
|
9
|
-
"_feeds": "The kit's own stub carries no feed for this page, so against it the page renders its EMPTY and SKELETON states — which is the point: those are states the floors must hold in. Against the hall-preview harness the same states render populated. Every declared 404 below is a fixture the harness does not carry, named rather than hidden.",
|
|
10
|
-
"ignoreRequests": [
|
|
11
|
-
"/api/(gds|bongos)/me$",
|
|
12
|
-
"/api/(gds|bongos)/me/",
|
|
13
|
-
"/api/(gds|bongos)/goals",
|
|
14
|
-
"/api/(gds|bongos)/inbox/",
|
|
15
|
-
"/api/(gds|bongos)/versions",
|
|
16
|
-
"/api/(gds|bongos)/builders",
|
|
17
|
-
"/api/(gds|bongos)/access-requests",
|
|
18
|
-
"/api/(gds|bongos)/autonomy/"
|
|
19
|
-
],
|
|
20
|
-
"ignoreConsole": [
|
|
21
|
-
"/api/(gds|bongos)/",
|
|
22
|
-
"\\b401\\b",
|
|
23
|
-
"\\b403\\b",
|
|
24
|
-
"\\b404\\b",
|
|
25
|
-
"\\b501\\b",
|
|
26
|
-
"Unauthorized",
|
|
27
|
-
"Forbidden",
|
|
28
|
-
"Not Found",
|
|
29
|
-
"Not Implemented",
|
|
30
|
-
"load failed"
|
|
31
|
-
],
|
|
32
|
-
"states": {
|
|
33
|
-
"queue": {
|
|
34
|
-
"auth": true,
|
|
35
|
-
"actions": [
|
|
36
|
-
[
|
|
37
|
-
"wait",
|
|
38
|
-
700
|
|
39
|
-
]
|
|
40
|
-
],
|
|
41
|
-
"expect": {
|
|
42
|
-
"visible": [
|
|
43
|
-
"#gate-body"
|
|
44
|
-
]
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
"out": {
|
|
48
|
-
"auth": false
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
"reducedMotion": {
|
|
52
|
-
"auth": true,
|
|
53
|
-
"actions": [
|
|
54
|
-
[
|
|
55
|
-
"wait",
|
|
56
|
-
600
|
|
57
|
-
]
|
|
58
|
-
]
|
|
59
|
-
}
|
|
60
|
-
}
|