@bongos/core 1.24.52 → 1.24.54
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 +88 -43
- package/docs/copy-inventory.md +85 -86
- package/docs/copy-registry.json +117 -126
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +258 -293
- package/modules/discord/webhook.js +28 -6
- package/modules/hall-ui/public/controls.css +96 -0
- package/modules/hall-ui/public/dom-utils.js +64 -3
- package/modules/hall-ui/public/genesis-home.js +1 -1
- package/modules/hall-ui/public/goal-forest-render.js +1 -1
- package/modules/hall-ui/public/goals.css +2 -8
- package/modules/hall-ui/public/idea.js +1 -1
- package/modules/hall-ui/public/record.css +3 -20
- package/modules/hall-ui/public/roster.css +0 -2
- package/modules/hall-ui/public/style.css +14 -26
- package/modules/hall-ui/public/task.js +8 -10
- package/modules/hall-ui/public/watch.css +1 -5
- package/modules/hall-ui/specimens/components.css +26 -0
- package/modules/hall-ui/specimens/components.html +115 -0
- package/modules/hall-ui/specimens/components.js +27 -0
- package/modules/hall-ui/specimens/components.states.json +35 -0
- package/modules/public-landing/public/index.html +11 -8
- package/modules/ui-design/public/brand/og-card.png +0 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-docs/index.html +107 -12
- package/release-notes.json +12 -0
- package/scripts/bongos/gen-brand.js +40 -6
- package/scripts/discord/lib.js +17 -3
- package/scripts/status-mirror-sync.js +11 -7
- package/src/bongos/serve-internal.js +3 -2
- package/src/error-page.js +120 -0
- package/src/link-preview.js +71 -0
- package/src/module-api.js +1 -1
- package/src/platform-server.js +22 -8
- package/tests/brand_surfaces.mjs +213 -0
- package/tests/discord_board_broadcast.mjs +23 -10
- package/tests/docs_openapi_route.mjs +2 -1
- package/tests/goal_forest_page.mjs +2 -1
- package/tests/hall_audit.mjs +2 -1
- package/tests/hall_record_world.mjs +10 -6
- package/tests/hall_roster_world.mjs +1 -1
- package/tests/hall_status_pill.mjs +251 -0
- package/tests/helpers.mjs +2 -0
- package/tests/idea_descent_surface.mjs +2 -1
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.24.
|
|
84
|
+
const CORE_VERSION = '1.24.54'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
package/src/platform-server.js
CHANGED
|
@@ -46,6 +46,11 @@ const path = require('path');
|
|
|
46
46
|
const express = require('express');
|
|
47
47
|
const { mountInternalSurfaces, isStatusHost, isBuildersHost, injectBrandingGlobal } = require('./bongos/serve-internal');
|
|
48
48
|
const { isApiShapedPath, apiNotFoundBody } = require('./bongos/api-path-404');
|
|
49
|
+
const { errorHandlers } = require('./error-page');
|
|
50
|
+
const { applyLinkPreview } = require('./link-preview');
|
|
51
|
+
const { clientBranding, branding } = require('./branding');
|
|
52
|
+
// The instance's public origin, for an absolute link-preview image URL ('' when unset).
|
|
53
|
+
const safeOrigin = () => { try { return branding().domains.publicOrigin || ''; } catch { return ''; } };
|
|
49
54
|
const moduleLoader = require('./module-loader/loader');
|
|
50
55
|
const { start: startErrorTally, startDailySend: startErrorReports } = require('./module-loader/error-tally');
|
|
51
56
|
const { isModuleEnabled } = require('./modules');
|
|
@@ -317,9 +322,14 @@ function buildPlatformApp() {
|
|
|
317
322
|
// per request, because modules mount inside mountInternalSurfaces above. No
|
|
318
323
|
// provider (the module is off, as on every instance but the hub) → next(),
|
|
319
324
|
// and /raise falls through to the 404 tail like any unknown path.
|
|
325
|
+
// The module renders its own HTML, so the platform's one link-preview card (task
|
|
326
|
+
// 1005091) is added here, on the way out, to any page it sends.
|
|
320
327
|
app.use('/raise', (req, res, next) => {
|
|
321
328
|
const raise = isApexHost(req) && seams.resolveOptional('raise.web');
|
|
322
329
|
if (!raise) return next();
|
|
330
|
+
const send = res.send.bind(res);
|
|
331
|
+
res.send = (body) => send(typeof body === 'string' && body.includes('</head>')
|
|
332
|
+
? applyLinkPreview(body, clientBranding(), safeOrigin()) : body);
|
|
323
333
|
Promise.resolve(raise.handle(req, res, next)).catch(next);
|
|
324
334
|
});
|
|
325
335
|
// THE ACCOUNT PAGE (task 1004259, ADR 0337 D4.3): where a person edits what
|
|
@@ -402,15 +412,19 @@ function buildPlatformApp() {
|
|
|
402
412
|
// or the builders' hall lands here. There is NO game to fall through to, so:
|
|
403
413
|
// * an API-shaped path gets a self-describing JSON 404 (never HTML), the
|
|
404
414
|
// same contract server.js gives so a mis-prefixed CLI call is legible;
|
|
405
|
-
// *
|
|
406
|
-
//
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}
|
|
412
|
-
|
|
415
|
+
// * a browser asking for a page gets the branded 404 (src/error-page.js,
|
|
416
|
+
// task 1005091), and any other client the plain-text 404 it always had.
|
|
417
|
+
// Then the trailing error net: a page route that throws answers the branded 500
|
|
418
|
+
// to a browser instead of Express's bare "Internal Server Error".
|
|
419
|
+
const { notFound, onError } = errorHandlers({
|
|
420
|
+
brandHtml: injectBrandingGlobal,
|
|
421
|
+
notFoundCopy: () => { try { return clientBranding().copy.notFound; } catch { return 'Page not found.'; } },
|
|
422
|
+
isApiShapedPath,
|
|
423
|
+
apiNotFoundBody,
|
|
424
|
+
log: (err, req) => logger.error({ err, method: req.method, url: req.originalUrl }, 'page request failed'),
|
|
413
425
|
});
|
|
426
|
+
app.use(notFound);
|
|
427
|
+
app.use(onError);
|
|
414
428
|
|
|
415
429
|
return app;
|
|
416
430
|
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// tests/brand_surfaces.mjs — the brand outside the hall (task 1005091, goal 1000138):
|
|
2
|
+
// the link-preview card every public page carries, the branded 404 and 500 pages,
|
|
3
|
+
// the themed API docs, the brand colour on Discord posts, and no maker's name written
|
|
4
|
+
// into the portable landing.
|
|
5
|
+
//
|
|
6
|
+
// Behaviour, not spelling: the server is built and asked for pages over HTTP, the
|
|
7
|
+
// error handlers and the link-preview template are called, and the Discord posters
|
|
8
|
+
// send to a stubbed fetch whose bodies are read back.
|
|
9
|
+
//
|
|
10
|
+
// Run: node --test tests/brand_surfaces.mjs
|
|
11
|
+
|
|
12
|
+
import { test, before, after } from 'node:test';
|
|
13
|
+
import assert from 'node:assert/strict';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import http from 'node:http';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
22
|
+
const { buildPlatformApp } = require('../src/platform-server.js');
|
|
23
|
+
const { linkPreviewTags, applyLinkPreview, CARD_PATH } = require('../src/link-preview.js');
|
|
24
|
+
const { errorPageTemplate, errorHandlers } = require('../src/error-page.js');
|
|
25
|
+
const { applyBrandTokens, injectBrandingGlobal } = require('../src/bongos/serve-internal.js');
|
|
26
|
+
const { clientBranding } = require('../src/branding.js');
|
|
27
|
+
const webhook = require('../modules/discord/webhook.js');
|
|
28
|
+
const discordLib = require('../scripts/discord/lib.js');
|
|
29
|
+
const { ogCardHtml } = require('../scripts/bongos/gen-brand.js');
|
|
30
|
+
|
|
31
|
+
const BRAND = clientBranding();
|
|
32
|
+
const PRODUCT = BRAND.identity.productName;
|
|
33
|
+
|
|
34
|
+
let server;
|
|
35
|
+
let port;
|
|
36
|
+
before(async () => {
|
|
37
|
+
server = http.createServer(buildPlatformApp());
|
|
38
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
39
|
+
port = server.address().port;
|
|
40
|
+
});
|
|
41
|
+
after(async () => { await new Promise((resolve) => server.close(resolve)); });
|
|
42
|
+
|
|
43
|
+
// node:http, not fetch: fetch drops a Host override.
|
|
44
|
+
function get(p, { host = '127.0.0.1', accept = 'text/html,application/xhtml+xml,*/*;q=0.8' } = {}) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const req = http.request({ host: '127.0.0.1', port, path: p, headers: { Host: host, Accept: accept } }, (res) => {
|
|
47
|
+
let body = '';
|
|
48
|
+
res.setEncoding('utf8');
|
|
49
|
+
res.on('data', (c) => { body += c; });
|
|
50
|
+
res.on('end', () => resolve({ status: res.statusCode, type: res.headers['content-type'] || '', body }));
|
|
51
|
+
});
|
|
52
|
+
req.on('error', reject);
|
|
53
|
+
req.end();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── the link-preview card ───────────────────────────────────────────────────────
|
|
58
|
+
test('the card is built from the page\'s own title and description, with the shared image', () => {
|
|
59
|
+
const html = '<html><head><title>Status & more — Acme</title><meta name="description" content="What "shipped"."></head><body></body></html>';
|
|
60
|
+
const brand = { identity: { productName: 'Acme' }, copy: { tagline: 'unused' } };
|
|
61
|
+
const tags = linkPreviewTags(html, brand, 'https://acme.example/');
|
|
62
|
+
assert.ok(tags.includes('<meta property="og:title" content="Status & more — Acme">'), 'escaped once, not twice');
|
|
63
|
+
assert.ok(tags.includes('<meta property="og:description" content="What "shipped".">'));
|
|
64
|
+
assert.ok(tags.includes(`<meta property="og:image" content="https://acme.example${CARD_PATH}">`), 'absolute when the instance has an origin');
|
|
65
|
+
assert.ok(tags.includes('<meta name="twitter:card" content="summary_large_image">'));
|
|
66
|
+
assert.ok(tags.includes('<meta property="og:site_name" content="Acme">'));
|
|
67
|
+
assert.ok(linkPreviewTags('<title>x</title>', {}, '').includes(`content="${CARD_PATH}"`), 'root-relative with no origin');
|
|
68
|
+
// a page that writes its own card keeps it
|
|
69
|
+
assert.equal(linkPreviewTags('<head><meta property="og:title" content="own"></head>', brand, ''), '');
|
|
70
|
+
const applied = applyLinkPreview(html, brand, '');
|
|
71
|
+
assert.ok(applied.indexOf('og:image') < applied.indexOf('</head>'));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('every served public page carries the card: status, docs, and anything branded', async () => {
|
|
75
|
+
const status = await get('/', { host: 'status.example.com' });
|
|
76
|
+
assert.equal(status.status, 200);
|
|
77
|
+
assert.match(status.body, new RegExp(`<meta property="og:image" content="[^"]*${CARD_PATH.replace(/[.]/g, '\\.')}">`));
|
|
78
|
+
assert.match(status.body, /<meta property="og:title" content="[^"]*build status">/);
|
|
79
|
+
const docs = await get('/docs');
|
|
80
|
+
assert.equal(docs.status, 200);
|
|
81
|
+
assert.match(docs.body, /<meta name="twitter:card" content="summary_large_image">/);
|
|
82
|
+
// the landing and hub pages go through the same injection
|
|
83
|
+
const landingLike = injectBrandingGlobal('<html><head><title>{{productName}} — community</title></head><body></body></html>', BRAND, {});
|
|
84
|
+
assert.ok(landingLike.includes(`<meta property="og:title" content="${PRODUCT} — community">`));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('the card image is a 1200x630 PNG drawn from the shared mark', () => {
|
|
88
|
+
const png = fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'public', 'brand', 'og-card.png'));
|
|
89
|
+
assert.equal(png.toString('ascii', 1, 4), 'PNG');
|
|
90
|
+
assert.deepEqual([png.readUInt32BE(16), png.readUInt32BE(20)], [1200, 630]);
|
|
91
|
+
const page = ogCardHtml({ markSvg: '<svg/>', name: 'Acme <Co>', fontLinks: ['https://f.example/x'], fontFamily: '"Manrope", sans-serif' });
|
|
92
|
+
assert.ok(page.includes('text-transform:lowercase'), 'the wordmark is lowercase (DESIGN.md "Naming")');
|
|
93
|
+
assert.ok(page.includes('Acme <Co>'), 'the name is escaped');
|
|
94
|
+
assert.ok(page.includes('data:image/svg+xml;base64,'), 'the mark is the shared file, embedded');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ── the branded 404 and 500 ────────────────────────────────────────────────────
|
|
98
|
+
test('a browser gets the branded 404; an API path keeps JSON; a non-HTML client keeps text', async () => {
|
|
99
|
+
const page = await get('/no-such-page');
|
|
100
|
+
assert.equal(page.status, 404);
|
|
101
|
+
assert.match(page.type, /text\/html/);
|
|
102
|
+
assert.ok(page.body.includes('<use href="/modules/ui-design/public/brand/mark.svg#mark"/>'), 'the shared mark');
|
|
103
|
+
assert.ok(page.body.includes(`<title>${BRAND.copy.notFound} · ${PRODUCT}</title>`), 'the pack\'s words and the product name');
|
|
104
|
+
assert.ok(page.body.includes(BRAND.theme.fonts.links[0].replace(/&/g, '&')), 'the pack\'s faces');
|
|
105
|
+
assert.doesNotMatch(page.body, /\{\{/);
|
|
106
|
+
|
|
107
|
+
const api = await get('/api/bongos/no-such-thing', { accept: 'text/html' });
|
|
108
|
+
assert.equal(api.status, 404);
|
|
109
|
+
assert.match(api.type, /application\/json/);
|
|
110
|
+
|
|
111
|
+
const curl = await get('/no-such-page', { accept: 'text/plain' });
|
|
112
|
+
assert.equal(curl.status, 404);
|
|
113
|
+
assert.match(curl.type, /text\/plain/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('a page route that throws answers the branded 500 to a browser and JSON to the API', () => {
|
|
117
|
+
const logged = [];
|
|
118
|
+
const { onError } = errorHandlers({
|
|
119
|
+
brandHtml: (h) => applyBrandTokens(h, { identity: { productName: 'Acme' } }, '', '', '', '', ''),
|
|
120
|
+
notFoundCopy: () => 'Gone.', isApiShapedPath: (p) => p.startsWith('/api/'), apiNotFoundBody: () => ({}),
|
|
121
|
+
log: (err) => logged.push(err.message),
|
|
122
|
+
});
|
|
123
|
+
const res = () => {
|
|
124
|
+
const r = { headersSent: false, code: 0, ctype: '', body: null };
|
|
125
|
+
r.status = (c) => { r.code = c; return r; };
|
|
126
|
+
r.type = (t) => { r.ctype = t; return r; };
|
|
127
|
+
r.send = (b) => { r.body = b; return r; };
|
|
128
|
+
r.json = (b) => { r.ctype = 'json'; r.body = b; return r; };
|
|
129
|
+
return r;
|
|
130
|
+
};
|
|
131
|
+
const req = (p, accept) => ({ path: p, method: 'GET', accepts: (t) => (Array.isArray(t) ? (accept === 'html' ? 'html' : 'json') : accept === 'html') });
|
|
132
|
+
const page = res();
|
|
133
|
+
onError(new Error('boom'), req('/projects', 'html'), page, () => {});
|
|
134
|
+
assert.equal(page.code, 500);
|
|
135
|
+
assert.equal(page.ctype, 'html');
|
|
136
|
+
assert.ok(page.body.includes('Something went wrong on our side.') && page.body.includes('· Acme</title>'));
|
|
137
|
+
assert.doesNotMatch(page.body, /boom/, 'the error text never reaches the page');
|
|
138
|
+
const api = res();
|
|
139
|
+
onError(new Error('boom'), req('/api/bongos/x', 'html'), api, () => {});
|
|
140
|
+
assert.equal(api.ctype, 'json');
|
|
141
|
+
assert.equal(api.body.error.code, 'internal_error');
|
|
142
|
+
assert.deepEqual(logged, ['boom', 'boom']);
|
|
143
|
+
// the 404 template carries the pack's words for "not found"
|
|
144
|
+
assert.ok(errorPageTemplate(404, { notFound: 'Gone.' }).includes('<h1>Gone.</h1>'));
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('the error page and the docs write their dark tier twice, byte-identical', () => {
|
|
148
|
+
const twins = (src) => {
|
|
149
|
+
const a = src.match(/html\[data-theme="dark"\]\s*\{([^}]*)\}/);
|
|
150
|
+
const b = src.match(/@media \(prefers-color-scheme:\s*dark\)\s*\{\s*html:not\(\[data-theme\]\)\s*\{([^}]*)\}/);
|
|
151
|
+
assert.ok(a && b, 'both twins exist');
|
|
152
|
+
const norm = (s) => s.replace(/\s+/g, ' ').trim();
|
|
153
|
+
assert.equal(norm(b[1]), norm(a[1]));
|
|
154
|
+
};
|
|
155
|
+
twins(errorPageTemplate(404));
|
|
156
|
+
twins(fs.readFileSync(path.join(ROOT, 'public-docs', 'index.html'), 'utf8'));
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ── the API docs ───────────────────────────────────────────────────────────────
|
|
160
|
+
test('the API docs load the brand: the scales, the mark, the pack\'s faces and a token theme for Redoc', async () => {
|
|
161
|
+
const r = await get('/docs');
|
|
162
|
+
assert.equal(r.status, 200);
|
|
163
|
+
assert.ok(r.body.includes('<link rel="stylesheet" href="/modules/ui-design/public/tokens.css">'));
|
|
164
|
+
assert.ok(r.body.includes('<use href="/modules/ui-design/public/brand/mark.svg#mark"/>'));
|
|
165
|
+
assert.ok(r.body.includes(BRAND.theme.fonts.links[0].replace(/&/g, '&')), 'the pack\'s font link is spliced');
|
|
166
|
+
assert.match(r.body, /Redoc\.init\('\/docs\/openapi\.json', \{[^}]*theme: theme \}/);
|
|
167
|
+
assert.doesNotMatch(r.body, /system-ui, -apple-system, Segoe UI, Roboto/, 'the old system-font bar is gone');
|
|
168
|
+
assert.doesNotMatch(r.body, /#2b6cb0|#1a1a1a/, 'no off-pack colour');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ── Discord ────────────────────────────────────────────────────────────────────
|
|
172
|
+
async function capture(fn, envKey) {
|
|
173
|
+
const sent = [];
|
|
174
|
+
const realFetch = globalThis.fetch;
|
|
175
|
+
const prev = process.env[envKey];
|
|
176
|
+
process.env[envKey] = 'https://discord.example/api/webhooks/1/x';
|
|
177
|
+
globalThis.fetch = async (url, init) => { sent.push(JSON.parse(init.body)); return { ok: true, status: 200, json: async () => ({ id: '1' }), text: async () => '' }; };
|
|
178
|
+
try { await fn(); } finally {
|
|
179
|
+
globalThis.fetch = realFetch;
|
|
180
|
+
if (prev === undefined) delete process.env[envKey]; else process.env[envKey] = prev;
|
|
181
|
+
}
|
|
182
|
+
return sent;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
test('a Discord broadcast is an embed in the pack\'s accent (a sample post)', async () => {
|
|
186
|
+
const accent = parseInt(BRAND.theme.colors.accent.slice(1), 16);
|
|
187
|
+
const sample = webhook.embedBody('**Shipped:** the status page now matches the hall.');
|
|
188
|
+
assert.deepEqual(sample, {
|
|
189
|
+
embeds: [{ description: '**Shipped:** the status page now matches the hall.', color: accent }],
|
|
190
|
+
allowed_mentions: { parse: [] },
|
|
191
|
+
});
|
|
192
|
+
assert.equal(webhook.embedBody('x', { theme: { colors: { accent: 'not-a-colour' } } }).embeds[0].color, undefined, 'no bar rather than a wrong one');
|
|
193
|
+
assert.equal(webhook.embedBody('y'.repeat(5000)).embeds[0].description.length, 4096);
|
|
194
|
+
|
|
195
|
+
const [moduleBody] = await capture(() => webhook.postWebhook('ship_news', 'shipped'), 'DISCORD_SHIP_NEWS_WEBHOOK');
|
|
196
|
+
assert.deepEqual(moduleBody, { embeds: [{ description: 'shipped', color: accent }], allowed_mentions: { parse: [] } });
|
|
197
|
+
const [coreBody] = await capture(() => discordLib.postWebhook('ship_news', 'shipped'), 'DISCORD_SHIP_NEWS_WEBHOOK');
|
|
198
|
+
assert.deepEqual(coreBody, moduleBody, 'the core twin posts the same embed');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// ── the portable landing names no maker ─────────────────────────────────────────
|
|
202
|
+
test('the landing names its maker only from the pack, and drops the band when there is none', () => {
|
|
203
|
+
const src = fs.readFileSync(path.join(ROOT, 'modules', 'public-landing', 'public', 'index.html'), 'utf8');
|
|
204
|
+
const code = src.replace(/<!--(?!\/?brand-if)[\s\S]*?-->/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
205
|
+
assert.doesNotMatch(code, /Verro|VERRO/, 'no maker\'s name written into the portable page');
|
|
206
|
+
const stamp = (name) => applyBrandTokens(src, { identity: { productName: 'Acme', worldName: 'Acme', company: { name, site: '', tagline: '' } } }, '', '', '', '', '');
|
|
207
|
+
const named = stamp('Rocket Labs');
|
|
208
|
+
assert.ok(named.includes('<p class="wordmark">Rocket Labs</p>'));
|
|
209
|
+
assert.ok(named.includes('Built by Rocket Labs, Acme is our way'));
|
|
210
|
+
assert.ok(named.includes('held by Rocket Labs or its pilot clients'));
|
|
211
|
+
const none = stamp('');
|
|
212
|
+
assert.doesNotMatch(none, /class="wrap verro"|held by|Built by ,/, 'the band and the line drop whole');
|
|
213
|
+
});
|
|
@@ -132,16 +132,29 @@ test('the emitter fires on FRESH opens only, and the listener is registered (sou
|
|
|
132
132
|
assert.ok(manifest.consumes.includes('government'), 'the port consumption is declared');
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
-
test('#871 survives: the mention poster keeps parse:[] and rings only the explicit allowlist', () => {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
135
|
+
test('#871 survives: the mention poster keeps parse:[] and rings only the explicit allowlist', async () => {
|
|
136
|
+
// Drive the two posters against a stubbed fetch and read the bodies they send
|
|
137
|
+
// (task 1005091 moved the plain poster onto an embed; the rule did not move).
|
|
138
|
+
const sent = [];
|
|
139
|
+
const realFetch = globalThis.fetch;
|
|
140
|
+
const prev = process.env.DISCORD_BOARD_ROOM_WEBHOOK;
|
|
141
|
+
process.env.DISCORD_BOARD_ROOM_WEBHOOK = 'https://discord.example/api/webhooks/1/x';
|
|
142
|
+
globalThis.fetch = async (url, init) => { sent.push(JSON.parse(init.body)); return { ok: true, status: 200, json: async () => ({ id: '9' }) }; };
|
|
143
|
+
try {
|
|
144
|
+
await webhook.postWebhookMentions('board_room', 'vote <@1> @everyone', ['111', '222', 'not-an-id', ...Array.from({ length: 30 }, (_, i) => String(1000 + i))]);
|
|
145
|
+
await webhook.postWebhook('board_room', 'plain @everyone');
|
|
146
|
+
} finally {
|
|
147
|
+
globalThis.fetch = realFetch;
|
|
148
|
+
if (prev === undefined) delete process.env.DISCORD_BOARD_ROOM_WEBHOOK; else process.env.DISCORD_BOARD_ROOM_WEBHOOK = prev;
|
|
149
|
+
}
|
|
150
|
+
const [ring, plain] = sent;
|
|
151
|
+
assert.deepEqual(ring.allowed_mentions.parse, [], 'content can never ping on its own — only the server-resolved ids ring');
|
|
152
|
+
assert.equal(ring.content, 'vote <@1> @everyone', 'the mention post stays message text, so its ids can ring');
|
|
153
|
+
assert.equal(ring.allowed_mentions.users.length, 20, 'the allowlist is sanity-bounded');
|
|
154
|
+
assert.ok(ring.allowed_mentions.users.every((u) => /^\d+$/.test(u)), 'only numeric snowflake ids survive the filter');
|
|
155
|
+
assert.deepEqual(plain.allowed_mentions, { parse: [] }, 'the default poster stays fully suppressed — #871 verbatim');
|
|
156
|
+
assert.equal(plain.content, undefined, 'the default poster sends an embed, not message text');
|
|
157
|
+
assert.equal(plain.embeds[0].description, 'plain @everyone');
|
|
145
158
|
assert.ok(webhook.CHANNELS.has('board_room'), 'the channel key exists');
|
|
146
159
|
});
|
|
147
160
|
|
|
@@ -67,7 +67,8 @@ test('/docs Redoc page points at the same-origin spec URL it now always serves',
|
|
|
67
67
|
const res = await fetch(base + '/docs');
|
|
68
68
|
assert.equal(res.status, 200);
|
|
69
69
|
const html = await res.text();
|
|
70
|
-
|
|
70
|
+
// task 1005091: Redoc is started by Redoc.init with the page's theme, so the URL is its first argument
|
|
71
|
+
assert.match(html, /Redoc\.init\('\/docs\/openapi\.json'/, 'Redoc host page fetches /docs/openapi.json');
|
|
71
72
|
});
|
|
72
73
|
});
|
|
73
74
|
|
|
@@ -19,7 +19,7 @@ import path from 'node:path';
|
|
|
19
19
|
import vm from 'node:vm';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
21
|
import { createRequire } from 'node:module';
|
|
22
|
-
import { makeRunner, makeEl, walk } from './helpers.mjs';
|
|
22
|
+
import { makeRunner, makeEl, walk, realOTB } from './helpers.mjs';
|
|
23
23
|
|
|
24
24
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
25
|
const HALL = path.join(ROOT, 'modules/hall-ui/public');
|
|
@@ -58,6 +58,7 @@ async function boot({ status = 200, fail = false, payload = { nodes: [], edges:
|
|
|
58
58
|
escapeHtml: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])),
|
|
59
59
|
$: (sel) => documentObj.getElementById(String(sel).replace(/^#/, '')),
|
|
60
60
|
fmtNum: (n) => String(n), fmtDate: (iso) => String(iso ?? ''), toast() {}, rankLabel: (r) => String(r ?? ''), categoryChip: () => '',
|
|
61
|
+
statusPill: realOTB().statusPill, // the one status pill (task 1005086)
|
|
61
62
|
},
|
|
62
63
|
OTBTaskLib: {},
|
|
63
64
|
OTBKit: {
|
package/tests/hall_audit.mjs
CHANGED
|
@@ -128,7 +128,8 @@ test('the removed-class guard CATCHES a reintroduction, and tolerates lookalikes
|
|
|
128
128
|
// watch.css legitimately defines badge--sev-low; the guard must not match it.
|
|
129
129
|
assert.equal(re('sev-low').test('.badge--sev-low { color: red }'), false,
|
|
130
130
|
'a different class that merely ends with the name is not a reintroduction');
|
|
131
|
-
|
|
131
|
+
// task 1005086: the severity badges joined the shared status pill's tones in controls.css
|
|
132
|
+
assert.match(read('controls.css'), /\.badge--sev-low/, 'controls.css owns the live severity styles');
|
|
132
133
|
});
|
|
133
134
|
|
|
134
135
|
// ---- 5. the page never scrolls sideways (task 1002446) --------------------
|
|
@@ -59,6 +59,7 @@ import fs from 'node:fs';
|
|
|
59
59
|
import path from 'node:path';
|
|
60
60
|
import vm from 'node:vm';
|
|
61
61
|
import { fileURLToPath } from 'node:url';
|
|
62
|
+
import { realOTB } from './helpers.mjs';
|
|
62
63
|
|
|
63
64
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
64
65
|
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
@@ -403,6 +404,7 @@ function renderTask(task, { lastClaim = null, viewer = null, goal = null, goalsL
|
|
|
403
404
|
kindIsUnset: (k) => !k || k === 'unclassified',
|
|
404
405
|
KIND_UNSET: 'unclassified',
|
|
405
406
|
escapeHtml,
|
|
407
|
+
statusPill: realOTB().statusPill, // the one status pill (task 1005086)
|
|
406
408
|
$: (sel) => documentObj.querySelector(sel),
|
|
407
409
|
fmtDate: (iso) => String(iso).slice(0, 10),
|
|
408
410
|
errCode: (d) => (d && d.error && d.error.code) || null,
|
|
@@ -520,21 +522,23 @@ test('under two figures there is no band at all — one figure is not a row', as
|
|
|
520
522
|
// The status pill is the one place a lifecycle state is coloured, and it must
|
|
521
523
|
// take the status vocabulary rather than the accent (DESIGN.md §Reuse).
|
|
522
524
|
test('the status pill takes the state vocabulary, and backlog is deliberately neutral', async () => {
|
|
525
|
+
// task 1005086: the tone comes from the hall's ONE map (dom-utils.js STATUS_TONE) and the
|
|
526
|
+
// pill is the ONE .status-pill, so the task page reads exactly as every other page does.
|
|
523
527
|
const tones = {
|
|
524
|
-
shipped: '
|
|
525
|
-
active: '
|
|
526
|
-
blocked: '
|
|
528
|
+
shipped: 'status-pill--ok', confirmed: 'status-pill--ok',
|
|
529
|
+
active: 'status-pill--info', completed: 'status-pill--info', ready: 'status-pill--info',
|
|
530
|
+
blocked: 'status-pill--warn',
|
|
527
531
|
};
|
|
528
532
|
for (const [status, cls] of Object.entries(tones)) {
|
|
529
533
|
const r = renderTask({ ...BASE_TASK, status });
|
|
530
534
|
await r.boot(); await settle();
|
|
531
|
-
assert.match(r.html(), new RegExp(`class="
|
|
535
|
+
assert.match(r.html(), new RegExp(`class="status-pill ${cls}">${status}<`), `${status} takes ${cls}`);
|
|
532
536
|
}
|
|
533
537
|
const r = renderTask({ ...BASE_TASK, status: 'backlog' });
|
|
534
538
|
await r.boot(); await settle();
|
|
535
|
-
assert.match(r.html(), /class="
|
|
539
|
+
assert.match(r.html(), /class="status-pill status-pill--neutral">backlog</,
|
|
536
540
|
'backlog is the NEUTRAL pill: the retired inline block painted it amber like blocked, and "not scheduled yet" is not "something is wrong here"');
|
|
537
|
-
assert.doesNotMatch(r.html(), /
|
|
541
|
+
assert.doesNotMatch(r.html(), /status-pill--(ok|warn|danger|info)">backlog/, 'and it takes no state tone at all');
|
|
538
542
|
});
|
|
539
543
|
|
|
540
544
|
test('an open rank floor is said as the absence of one, not "min rank any rank"', async () => {
|
|
@@ -102,7 +102,7 @@ test('goals: rows|cards is a segmented view toggle that still writes #f-goal-vie
|
|
|
102
102
|
assert.match(src, /function setGoalView\(view, opts\)/);
|
|
103
103
|
assert.match(src, /b\.setAttribute\('aria-pressed', String\(b\.dataset\.view === view\)\)/, 'the buttons carry the pressed state');
|
|
104
104
|
assert.match(src, /if \(savedGoalView === 'cards'\) setGoalView\('cards', \{ fire: false \}\)/, 'the saved pref restores the toggle without a render');
|
|
105
|
-
const css = read('
|
|
105
|
+
const css = read('controls.css'); // the shared view toggle since task 1005086
|
|
106
106
|
const pressed = block(css, '.viewtoggle__btn[aria-pressed="true"]');
|
|
107
107
|
assert.match(pressed, /var\(--accent-tint\)/, 'the pressed side takes the current-thing tint');
|
|
108
108
|
assert.doesNotMatch(pressed, /var\(--accent\)/, 'never the accent fill: a view is not a live door');
|