@bongos/core 1.24.51 → 1.24.53
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 +125 -95
- package/docs/copy-inventory.md +225 -242
- package/docs/copy-registry.json +278 -452
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +33 -0
- package/docs/page-readings.json +515 -531
- package/modules/discord/webhook.js +28 -6
- package/modules/hall-ui/public/ai-studio.html +1 -0
- package/modules/hall-ui/public/approval-queue.css +3 -11
- package/modules/hall-ui/public/approval-queue.js +13 -20
- package/modules/hall-ui/public/atlas.html +1 -0
- package/modules/hall-ui/public/blockers.html +1 -0
- package/modules/hall-ui/public/board-room.html +1 -0
- package/modules/hall-ui/public/bulk-toolbar.js +5 -1
- package/modules/hall-ui/public/city.html +1 -0
- package/modules/hall-ui/public/collab.html +1 -0
- package/modules/hall-ui/public/commands.html +1 -0
- package/modules/hall-ui/public/controls.css +100 -0
- package/modules/hall-ui/public/copy-desk.css +7 -11
- package/modules/hall-ui/public/copy-desk.html +17 -16
- package/modules/hall-ui/public/copy-desk.js +13 -9
- package/modules/hall-ui/public/deploy.html +1 -0
- package/modules/hall-ui/public/goal-inbox.js +32 -17
- package/modules/hall-ui/public/goal-map.html +1 -0
- package/modules/hall-ui/public/goals-page.js +7 -1
- package/modules/hall-ui/public/goals.html +1 -0
- package/modules/hall-ui/public/government.html +1 -0
- package/modules/hall-ui/public/government.js +10 -2
- package/modules/hall-ui/public/hall-dialog.js +255 -0
- package/modules/hall-ui/public/hall-kit.js +9 -2
- package/modules/hall-ui/public/idea.html +1 -0
- package/modules/hall-ui/public/ideas.html +1 -0
- package/modules/hall-ui/public/index.html +1 -0
- package/modules/hall-ui/public/market-watch-item.html +2 -1
- package/modules/hall-ui/public/market-watch-lib.js +16 -9
- package/modules/hall-ui/public/market-watch-rules.html +1 -0
- package/modules/hall-ui/public/market-watch.css +2 -3
- package/modules/hall-ui/public/market-watch.html +2 -1
- package/modules/hall-ui/public/modules.html +1 -0
- package/modules/hall-ui/public/money.html +1 -0
- package/modules/hall-ui/public/my-agents.html +1 -0
- package/modules/hall-ui/public/oversight.css +3 -33
- package/modules/hall-ui/public/primer.html +1 -0
- package/modules/hall-ui/public/profile.html +1 -0
- package/modules/hall-ui/public/project-settings.html +1 -0
- package/modules/hall-ui/public/roster.html +1 -0
- package/modules/hall-ui/public/settings-ai-keys.js +5 -1
- package/modules/hall-ui/public/settings-passkeys.js +5 -1
- package/modules/hall-ui/public/settings-sessions.js +10 -2
- package/modules/hall-ui/public/settings.html +1 -0
- package/modules/hall-ui/public/settings.js +5 -1
- package/modules/hall-ui/public/street.html +1 -0
- package/modules/hall-ui/public/studio.css +6 -32
- package/modules/hall-ui/public/studio.html +4 -3
- package/modules/hall-ui/public/studio.js +40 -35
- package/modules/hall-ui/public/style.css +2 -8
- package/modules/hall-ui/public/task.html +1 -0
- package/modules/hall-ui/public/task.js +26 -12
- package/modules/hall-ui/public/thinking.html +1 -0
- package/modules/hall-ui/public/tweak-editor.html +1 -0
- package/modules/hall-ui/public/watch.html +1 -0
- package/modules/hall-ui/public/work.html +1 -0
- package/modules/hall-ui/public/work.js +7 -1
- 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/bulk_toolbar.mjs +1 -1
- package/tests/discord_board_broadcast.mjs +23 -10
- package/tests/docs_openapi_route.mjs +2 -1
- package/tests/hall_approval_queue.mjs +8 -2
- package/tests/hall_audit.mjs +1 -1
- package/tests/hall_city.mjs +1 -1
- package/tests/hall_collab_world.mjs +9 -6
- package/tests/hall_dialog.mjs +401 -0
- package/tests/hall_error_envelope.mjs +1 -1
- package/tests/hall_oversight_world.mjs +9 -7
- package/tests/hall_record_world.mjs +1 -1
- package/tests/hall_street.mjs +1 -1
- package/tests/hall_studio_home.mjs +1 -1
- package/tests/home_attention_cards.mjs +2 -0
- package/tests/kind_axis_unset.mjs +1 -1
- package/tests/market_watch_pages.mjs +3 -1
|
@@ -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
|
+
});
|
package/tests/bulk_toolbar.mjs
CHANGED
|
@@ -46,7 +46,7 @@ function stubPage(present) {
|
|
|
46
46
|
const els = {};
|
|
47
47
|
for (const id of present) els[id] = { hidden: true, disabled: false, textContent: '', title: '', listeners: {}, addEventListener(t, fn) { this.listeners[t] = fn; } };
|
|
48
48
|
globalThis.document = { getElementById: (id) => els[id] || null };
|
|
49
|
-
globalThis.window = { confirm: () => true };
|
|
49
|
+
globalThis.window = { OTBDialog: { confirm: async () => true } }; // the branded confirm (task 1005085)
|
|
50
50
|
globalThis.location = loc;
|
|
51
51
|
return els;
|
|
52
52
|
}
|
|
@@ -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
|
|
|
@@ -90,6 +90,9 @@ function bootPanel({ answers = {}, writes = null, mode = 'light' } = {}) {
|
|
|
90
90
|
getElementById: byId,
|
|
91
91
|
createElement: (tag) => augment(makeEl(tag, {})),
|
|
92
92
|
get activeElement() { return boot.focused || null; },
|
|
93
|
+
// the shared dialog (hall-dialog.js, task 1005085) hears its keys on the document
|
|
94
|
+
listeners: {},
|
|
95
|
+
addEventListener(type, fn) { (this.listeners[type] = this.listeners[type] || []).push(fn); },
|
|
93
96
|
};
|
|
94
97
|
const api = {
|
|
95
98
|
// The REAL client's shape: request(method, path, { body }). This fake once
|
|
@@ -110,6 +113,7 @@ function bootPanel({ answers = {}, writes = null, mode = 'light' } = {}) {
|
|
|
110
113
|
win.window = win;
|
|
111
114
|
vm.runInContext(read('brand-holes.js'), ctx, { filename: 'brand-holes.js' });
|
|
112
115
|
vm.runInContext(read('approval-queue-lib.js'), ctx, { filename: 'approval-queue-lib.js' });
|
|
116
|
+
vm.runInContext(read('hall-dialog.js'), ctx, { filename: 'hall-dialog.js' }); // the shared dialog behaviour (task 1005085)
|
|
113
117
|
vm.runInContext(read('approval-queue.js'), ctx, { filename: 'approval-queue.js' });
|
|
114
118
|
const panel = win.OTBApprovalQueue.mount({
|
|
115
119
|
api,
|
|
@@ -124,7 +128,9 @@ function bootPanel({ answers = {}, writes = null, mode = 'light' } = {}) {
|
|
|
124
128
|
const q = (sel) => scrim.querySelector(sel);
|
|
125
129
|
const qa = (sel) => scrim.querySelectorAll(sel);
|
|
126
130
|
const click = async (el) => { assert.ok(el, 'the control exists'); scrim.fire('click', { target: el }); await settle(); await settle(); };
|
|
127
|
-
boot = { body, scrim, q, qa, click, requests, counts, panel, classes, win, focused: null
|
|
131
|
+
boot = { body, scrim, q, qa, click, requests, counts, panel, classes, win, focused: null,
|
|
132
|
+
// a key pressed anywhere in the page reaches the document, where the shared dialog listens
|
|
133
|
+
key: (ev) => (documentObj.listeners.keydown || []).forEach((fn) => fn({ preventDefault() {}, stopPropagation() {}, ...ev })) };
|
|
128
134
|
return boot;
|
|
129
135
|
}
|
|
130
136
|
|
|
@@ -330,7 +336,7 @@ await test('empty, closed with Escape, and focus goes back where it was', async
|
|
|
330
336
|
await b.panel.open(); await settle();
|
|
331
337
|
assert.equal(text(b.q('.aq-title')), 'Nothing waiting');
|
|
332
338
|
assert.match(text(b.q('.aq-frame__note')), /Nothing is waiting for you/);
|
|
333
|
-
b.
|
|
339
|
+
b.key({ key: 'Escape', target: b.scrim });
|
|
334
340
|
assert.equal(b.scrim.hidden, true);
|
|
335
341
|
assert.equal(b.focused, opener);
|
|
336
342
|
assert.ok(!b.classes.has('aq-open'));
|
package/tests/hall_audit.mjs
CHANGED
|
@@ -510,7 +510,7 @@ test('the verdict classes and the scrim exist in the token tier, in both modes',
|
|
|
510
510
|
assert.match(css, /\.ledger__verdict--danger\s*\{[^}]*var\(--danger\)/, 'fell short → --danger');
|
|
511
511
|
assert.match(css, /\.ledger__verdict--none\s*\{[^}]*var\(--ink-soft\)/, 'not graded → --ink-soft');
|
|
512
512
|
assert.match(css, /--scrim:/, 'the modal scrim is a token, not a sheet-local literal');
|
|
513
|
-
assert.match(read('
|
|
513
|
+
assert.match(read('controls.css'), /\.hall-dialog::backdrop \{ background: var\(--scrim\); \}/, 'the one dialog backdrop takes it (task 1005085)');
|
|
514
514
|
});
|
|
515
515
|
|
|
516
516
|
// ---- 10. an outline: none always names its replacement (task 1003496) -----
|
package/tests/hall_city.mjs
CHANGED
|
@@ -288,7 +288,7 @@ test('the city sits in the hall chrome, behind the shell\'s full-screen switch',
|
|
|
288
288
|
// task 1004685 (the living city): gov-living.js, the model under the drawing, and
|
|
289
289
|
// gov-feeds.js, its one reader per page (shared with the shell's tally), load before it
|
|
290
290
|
// task 1004702: the card answers its first ask in place, so the deck's card and sender (and the desk rows under them) load before the page
|
|
291
|
-
assert.deepEqual(scripts, ['dom-utils', 'shell-trail', 'craft-mode', 'shell', 'palette', 'gov-living', 'gov-feeds', 'city-draw', 'hall-kit', 'street-draw', 'street-card', 'street-inside', 'street-desk', 'street-deck', 'city-decide', 'city-switchboard', 'city'], 'the shell first, as on every hall page');
|
|
291
|
+
assert.deepEqual(scripts, ['dom-utils', 'shell-trail', 'craft-mode', 'shell', 'hall-dialog', 'palette', 'gov-living', 'gov-feeds', 'city-draw', 'hall-kit', 'street-draw', 'street-card', 'street-inside', 'street-desk', 'street-deck', 'city-decide', 'city-switchboard', 'city'], 'the shell first, then the shared dialog (task 1005085), as on every hall page');
|
|
292
292
|
// task 1004925: every script but dom-utils.js is deferred, so none holds up the
|
|
293
293
|
// parser; defer runs them in document order, which is why the order above still
|
|
294
294
|
// holds. dom-utils.js stays synchronous: the one-escaper guard
|
|
@@ -53,7 +53,9 @@ test('the page sheets spend only the fifteen, in px, and never opacity', () => {
|
|
|
53
53
|
const cd = read('copy-desk.css');
|
|
54
54
|
const literals = cd.match(/rgba?\([^)]*\)/g) || [];
|
|
55
55
|
assert.deepEqual(literals, [], 'not even the scrim is a literal here any more');
|
|
56
|
-
|
|
56
|
+
// task 1005085: the dialogs are the hall's one dialog, whose backdrop lives in controls.css
|
|
57
|
+
assert.doesNotMatch(cd, /::backdrop/, 'no backdrop of its own');
|
|
58
|
+
assert.match(read('controls.css'), /\.hall-dialog::backdrop \{ background: var\(--scrim\); \}/, 'the shared backdrop spends the token');
|
|
57
59
|
});
|
|
58
60
|
|
|
59
61
|
test('every list is the family\'s row, and the card-in-a-card is gone', () => {
|
|
@@ -335,15 +337,16 @@ test('the filters and the search are the kit\'s bar; the duplicates are paged th
|
|
|
335
337
|
assert.match(read('copy-desk.html'), /src="\/builders\/hall-kit\.js/, 'the page loads the kit');
|
|
336
338
|
});
|
|
337
339
|
|
|
338
|
-
test('the three dialogs are the
|
|
340
|
+
test('the three dialogs are the hall\'s one dialog on a native <dialog>, with the number input in a field', () => {
|
|
339
341
|
const html = read('copy-desk.html');
|
|
340
|
-
// Three since task 1003114 (R03): flag, close, and the proposal composer
|
|
341
|
-
|
|
342
|
+
// Three since task 1003114 (R03): flag, close, and the proposal composer; the
|
|
343
|
+
// shared .hall-dialog since task 1005085, each named by its own title.
|
|
344
|
+
assert.equal((html.match(/<dialog id="cd-(close-dialog|dialog|propose-dialog)" class="hall-dialog cd-dialog" aria-labelledby="cd-(close|dialog|propose)-h">/g) || []).length, 3);
|
|
342
345
|
assert.match(html, /<div class="ov-field ov-field--wide">\s*<label class="ov-field__l" for="cd-close-task">[^<]*<\/label>\s*<input type="number" id="cd-close-task"/, 'the number input sits in the family field, which restates the 30px pill');
|
|
343
346
|
assert.ok(!/<label[^>]*>[^<]*<span class="cd-dialog__hint"/.test(html), 'no hint lives inside a label (the label voice would uppercase it)');
|
|
344
347
|
assert.equal((html.match(/class="ov-form__why"/g) || []).length, 4, 'the four hints are the form\'s own explainer line');
|
|
345
|
-
assert.match(html, /class="btn
|
|
346
|
-
assert.match(html, /class="btn
|
|
348
|
+
assert.match(html, /class="btn" id="cd-submit"/, 'the accent door files the flag');
|
|
349
|
+
assert.match(html, /class="btn" id="cd-propose-submit"/, 'and the proposal composer has its own');
|
|
347
350
|
});
|
|
348
351
|
|
|
349
352
|
test('the proposal composer is prefilled, and says what filing it does and does not do', () => {
|