@bongos/core 1.20.58 → 1.20.60
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 +198 -108
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +10 -0
- package/clients/bongos-client/index.cjs +10 -0
- package/clients/bongos-client/index.d.ts +15 -2
- package/clients/bongos-client/index.mjs +10 -0
- package/docs/adr/0358-a-hall-wears-a-look-and-its-night-takes-a-tint.md +52 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +317 -6
- package/docs/api-reference.md +8 -3
- package/docs/architecture.md +1 -0
- package/docs/copy-inventory.md +339 -330
- package/docs/copy-registry.json +524 -439
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +4 -1
- package/docs/page-readings.json +729 -678
- package/migrations/core_268_builders_main_discipline.sql +35 -0
- package/modules/copy-desk/artist-stats.js +56 -0
- package/modules/copy-desk/module.json +1 -1
- package/modules/copy-desk/page-status.js +39 -0
- package/modules/copy-desk/routes/copy-desk.js +7 -16
- package/modules/copy-desk/tests/copy_no_cms.mjs +10 -1
- package/modules/economy/credits-by-craft.js +208 -0
- package/modules/economy/reward.js +8 -0
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/hall-render.js +2 -2
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/profile.js +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +5 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +8 -0
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/ideas/ideator-stats.js +149 -0
- package/modules/ideas/module.json +2 -1
- package/modules/ideas/routes/inbox.js +7 -0
- package/modules/lifecycle/activity.js +5 -2
- package/modules/lifecycle/db-analytics.js +4 -2
- package/modules/lifecycle/db-tasks.js +49 -1
- package/modules/lifecycle/db.js +3 -2
- package/modules/lifecycle/page-tweak-reads.js +23 -5
- package/modules/provisioning/look.js +169 -0
- package/modules/provisioning/migrations/provisioning_034_project_logo.sql +35 -0
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/provisioning.js +4 -3
- package/modules/provisioning/routes/look.js +204 -0
- package/modules/provisioning/routes/provisioning.js +5 -59
- package/modules/provisioning/settings-push.js +76 -0
- package/modules/public-landing/public/assets/cosmos.css +1 -1
- package/modules/public-landing/public/projects.html +499 -28
- package/modules/public-landing/public/projects.probes.json +2 -2
- package/modules/public-landing/public/projects.states.json +6 -3
- package/modules/ui-design/kit/serve.js +50 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/fitness.js +4 -0
- package/scripts/gds/run-unit-tests.js +5 -0
- package/src/bongos/db.js +44 -6
- package/src/bongos/role-stats.js +155 -0
- package/src/bongos/routes/builders.js +15 -2
- package/src/bongos/routes/me.js +11 -3
- package/src/bongos/routes.js +4 -1
- package/src/bongos/serve-internal.js +6 -0
- package/src/branding.js +26 -1
- package/src/hall-look.js +99 -0
- package/src/module-api.js +1 -1
- package/src/night-tint.js +26 -0
- package/tests/copy_desk_page_reads.mjs +1 -1
- package/tests/currency_label.mjs +7 -4
- package/tests/fixtures/page-ledger.mjs +1 -1
- package/tests/hall_tokens.mjs +10 -1
- package/tests/hall_tweak_editor.mjs +1 -1
- package/tests/look_night_tint.mjs +169 -0
- package/tests/main_discipline.mjs +81 -0
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/profile_role_stats.mjs +416 -0
- package/tests/projects_hub.mjs +5 -4
- package/tests/projects_hub_app_status.mjs +1 -1
- package/tests/projects_hub_app_step.mjs +8 -7
- package/tests/projects_hub_look.mjs +255 -0
- package/tests/projects_hub_module_picker.mjs +2 -2
- package/tests/provision_settings_apply.mjs +12 -4
- package/tests/provisioning_look.mjs +376 -0
- package/tests/provisioning_settings.mjs +10 -7
- package/tests/provisioning_settings_apply.mjs +4 -4
- package/tests/provisioning_settings_env.mjs +4 -0
- package/tests/role_stats_db.mjs +237 -0
- package/tests/tweak_replaces_artist_review.mjs +1 -1
- package/tests/wizard_draft_resume.mjs +2 -2
- package/tests/wizard_front_door.mjs +2 -2
- package/tests/wizard_intent_resume.mjs +16 -15
package/src/branding.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
const fs = require('node:fs');
|
|
20
20
|
const path = require('node:path');
|
|
21
21
|
const { resolveCoreRoot, resolveInstanceRoot, resolveEnv, FALLBACK_ENV_PREFIX } = require('./instance-config');
|
|
22
|
+
const { applyLook, readLibraryLook } = require('./hall-look');
|
|
22
23
|
|
|
23
24
|
// ADR 0108 §1: the neutral starter ships WITH the core package; the instance
|
|
24
25
|
// pack is host content. Both resolvers return the repo root today, so this is
|
|
@@ -83,6 +84,16 @@ const ENV_OVERRIDES = [
|
|
|
83
84
|
// door is `open` and this is `none`. Like its neighbours it is memoized for the
|
|
84
85
|
// process, which is why the platform restarts the instance to apply a change.
|
|
85
86
|
['SPEND_PAYER', ['project', 'spendPayer']],
|
|
87
|
+
// The look this hall wears (ADR 0358, task 1004421) — own | a style-library name |
|
|
88
|
+
// custom — and its night tint (on | off). Policy keys like their neighbours: the
|
|
89
|
+
// platform pushes them and a restart applies them. LOOK_ACCENT (a custom look's one
|
|
90
|
+
// accent, already made readable by the platform) and LOGO_URL (the platform's
|
|
91
|
+
// content-addressed logo) are their companions. src/hall-look.js turns them into
|
|
92
|
+
// the fifteen; an unknown or malformed value leaves the hall on its own pack.
|
|
93
|
+
['LOOK', ['project', 'look']],
|
|
94
|
+
['NIGHT_TINT', ['project', 'nightTint']],
|
|
95
|
+
['LOOK_ACCENT', ['project', 'lookAccent']],
|
|
96
|
+
['LOGO_URL', ['project', 'logoUrl']],
|
|
86
97
|
];
|
|
87
98
|
|
|
88
99
|
const isObj = (v) => v && typeof v === 'object' && !Array.isArray(v);
|
|
@@ -115,7 +126,7 @@ function deepFreeze(o) {
|
|
|
115
126
|
// resolver. Decided in ADR 0142 (audit H11 / task 1002224).
|
|
116
127
|
//
|
|
117
128
|
// Pure resolver — all inputs injected, so tests drive it with zero I/O.
|
|
118
|
-
function resolveBranding({ neutral = {}, instance = {}, env = {} } = {}) {
|
|
129
|
+
function resolveBranding({ neutral = {}, instance = {}, env = {}, readLook = readLibraryLook } = {}) {
|
|
119
130
|
// structuredClone so the merged result shares no nested references with the
|
|
120
131
|
// neutral/instance inputs — otherwise the deepFreeze below would also freeze
|
|
121
132
|
// the caller's input objects (deepMerge shallow-copies only the top level).
|
|
@@ -142,6 +153,9 @@ function resolveBranding({ neutral = {}, instance = {}, env = {} } = {}) {
|
|
|
142
153
|
if (missing.length) {
|
|
143
154
|
throw new Error(`branding: missing required field(s): ${missing.map((p) => p.join('.')).join(', ')}`);
|
|
144
155
|
}
|
|
156
|
+
// The chosen look, laid over the pack's theme (ADR 0358). A no-op for a hall that
|
|
157
|
+
// chose none, which is every hall the platform has not pushed a look to.
|
|
158
|
+
applyLook(resolved, readLook);
|
|
145
159
|
return deepFreeze(resolved);
|
|
146
160
|
}
|
|
147
161
|
|
|
@@ -240,6 +254,11 @@ function clientBranding(b = branding()) {
|
|
|
240
254
|
},
|
|
241
255
|
favicon: ui.favicon || '',
|
|
242
256
|
backdrop: ui.backdrop || '',
|
|
257
|
+
// The look (ADR 0358): which one this hall wears ('' = its own pack), whether
|
|
258
|
+
// its night is tinted (only ever true with a look), and its logo address.
|
|
259
|
+
look: ui.look || '',
|
|
260
|
+
nightTint: ui.nightTint === true,
|
|
261
|
+
logo: ui.logo || '',
|
|
243
262
|
},
|
|
244
263
|
copy: {
|
|
245
264
|
tagline: b.copy?.tagline || '',
|
|
@@ -282,6 +301,12 @@ function clientBranding(b = branding()) {
|
|
|
282
301
|
// with?", not the platform's record of what they saved. The fallback is the
|
|
283
302
|
// platform default, which for this key IS today's behaviour.
|
|
284
303
|
spendPayer: b.project?.spendPayer || 'none',
|
|
304
|
+
// The look and its night (ADR 0358, task 1004421). Published for the read-back
|
|
305
|
+
// reason its neighbours are: the settings push compares this manifest with what
|
|
306
|
+
// the platform saved. The fallbacks are the platform defaults — 'own' is today's
|
|
307
|
+
// hall, and 'on' only ever tints a hall that wears a look.
|
|
308
|
+
look: b.project?.look || 'own',
|
|
309
|
+
nightTint: b.project?.nightTint || 'on',
|
|
285
310
|
},
|
|
286
311
|
};
|
|
287
312
|
}
|
package/src/hall-look.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// src/hall-look.js — the look a hall wears, laid over its branding pack (ADR 0358,
|
|
2
|
+
// task 1004421).
|
|
3
|
+
//
|
|
4
|
+
// The platform chooses a project's look (provisioning settings `look`, `night_tint`
|
|
5
|
+
// plus the companion LOOK_ACCENT and LOGO_URL) and pushes only NAMES over web.env;
|
|
6
|
+
// src/branding.js maps them onto project.look / nightTint / lookAccent / logoUrl and
|
|
7
|
+
// hands the merged pack here. This file turns the name into the fifteen, reading the
|
|
8
|
+
// style library the core package ships (a module's styles/<name>/pack.json, ADR 0219),
|
|
9
|
+
// so a palette never travels as env text. The library is found by its SHAPE — any
|
|
10
|
+
// shipped module's styles/ directory — never by a module key, which core may not name
|
|
11
|
+
// (ADR 0083 §Decision #4).
|
|
12
|
+
//
|
|
13
|
+
// own (or absent) the instance's own pack — exactly today's hall.
|
|
14
|
+
// <library name> that look's thirteen colours and two faces.
|
|
15
|
+
// custom the chrome world with ONE accent, already made readable in both
|
|
16
|
+
// modes by the platform (lookAdjustAccent); a value that is not a
|
|
17
|
+
// plain #rrggbb is ignored and the hall stays on its own pack.
|
|
18
|
+
//
|
|
19
|
+
// Favicon and backdrop stay the instance's (ADR 0219 §1) except that a looked hall
|
|
20
|
+
// gets a favicon in its own colours, and a logo, when there is one, is the favicon.
|
|
21
|
+
// PURE given `readLook`; the default reader is the only I/O.
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const fs = require('node:fs');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
|
|
27
|
+
const HEX = /^#[0-9a-f]{6}$/i;
|
|
28
|
+
const LOOK_NAME = /^[a-z][a-z-]{0,39}$/;
|
|
29
|
+
// A logo address is the platform's content-addressed image URL. https only, and no
|
|
30
|
+
// character that could end the attribute or the CSS it is written into.
|
|
31
|
+
const LOGO_URL = /^https:\/\/[^\s"'<>()\\]+$/;
|
|
32
|
+
|
|
33
|
+
// The style library's packs, read once each. null for an unknown name or a pack that
|
|
34
|
+
// does not carry the thirteen as hex — a broken file never half-applies.
|
|
35
|
+
const _looks = new Map();
|
|
36
|
+
function readLibraryLook(name, { root } = {}) {
|
|
37
|
+
if (!LOOK_NAME.test(String(name || ''))) return null;
|
|
38
|
+
const key = `${root || ''}|${name}`;
|
|
39
|
+
if (_looks.has(key)) return _looks.get(key);
|
|
40
|
+
let ui = null;
|
|
41
|
+
try {
|
|
42
|
+
const base = root || require('./instance-config').resolveCoreRoot(); // eslint-disable-line global-require
|
|
43
|
+
const modulesDir = path.join(base, 'modules');
|
|
44
|
+
const file = fs.readdirSync(modulesDir).sort()
|
|
45
|
+
.map((m) => path.join(modulesDir, m, 'styles', name, 'pack.json'))
|
|
46
|
+
.find((f) => fs.existsSync(f));
|
|
47
|
+
const pack = file ? JSON.parse(fs.readFileSync(file, 'utf8')) : null;
|
|
48
|
+
const t = pack && pack.theme && pack.theme.ui;
|
|
49
|
+
if (t && t.colors && Object.values(t.colors).every((v) => HEX.test(v)) && t.fonts) ui = { colors: t.colors, fonts: t.fonts };
|
|
50
|
+
} catch { ui = null; }
|
|
51
|
+
_looks.set(key, ui);
|
|
52
|
+
return ui;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The favicon a looked hall shows without a logo: the chrome world's two-circle mark
|
|
56
|
+
// redrawn in the look's own accent, terracotta and card.
|
|
57
|
+
function lookFavicon(colors) {
|
|
58
|
+
const c = (k) => encodeURIComponent(colors[k]);
|
|
59
|
+
return "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'>" +
|
|
60
|
+
`<circle cx='12' cy='17' r='8' fill='${c('accent')}'/><circle cx='22' cy='17' r='6' fill='${c('terracotta')}'/>` +
|
|
61
|
+
`<circle cx='12' cy='17' r='3.5' fill='${c('bgCard')}'/><circle cx='22' cy='17' r='2.5' fill='${c('bgCard')}'/></svg>`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// What the hall wears: { name, ui } or null for its own pack. PURE given readLook.
|
|
65
|
+
function chosenLook(project = {}, readLook = readLibraryLook) {
|
|
66
|
+
const name = String(project.look || 'own');
|
|
67
|
+
if (name === 'own') return null;
|
|
68
|
+
if (name === 'custom') {
|
|
69
|
+
const accent = String(project.lookAccent || '');
|
|
70
|
+
const base = HEX.test(accent) ? readLook('chrome-world') : null;
|
|
71
|
+
return base ? { name, ui: { colors: { ...base.colors, accent: accent.toLowerCase() }, fonts: base.fonts } } : null;
|
|
72
|
+
}
|
|
73
|
+
const ui = readLook(name);
|
|
74
|
+
return ui ? { name, ui } : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Lay the look over a resolved (still mutable) pack, in place. A logo applies with or
|
|
78
|
+
// without a look; the night tint only with one (ADR 0358 D2), so a hall that never
|
|
79
|
+
// chose a look renders exactly as before whatever its tint setting says.
|
|
80
|
+
function applyLook(resolved, readLook = readLibraryLook) {
|
|
81
|
+
const project = resolved.project || {};
|
|
82
|
+
const look = chosenLook(project, readLook);
|
|
83
|
+
const logo = LOGO_URL.test(String(project.logoUrl || '')) ? project.logoUrl : '';
|
|
84
|
+
if (!look && !logo) return resolved;
|
|
85
|
+
resolved.theme = resolved.theme && typeof resolved.theme === 'object' ? resolved.theme : {};
|
|
86
|
+
const ui = { ...(resolved.theme.ui || {}) };
|
|
87
|
+
if (look) {
|
|
88
|
+
ui.colors = { ...look.ui.colors };
|
|
89
|
+
ui.fonts = { ...look.ui.fonts };
|
|
90
|
+
ui.look = look.name;
|
|
91
|
+
ui.nightTint = project.nightTint !== 'off';
|
|
92
|
+
ui.favicon = lookFavicon(ui.colors);
|
|
93
|
+
}
|
|
94
|
+
if (logo) { ui.logo = logo; ui.favicon = logo; }
|
|
95
|
+
resolved.theme.ui = ui;
|
|
96
|
+
return resolved;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { applyLook, readLibraryLook };
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.20.
|
|
78
|
+
const CORE_VERSION = '1.20.60'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/night-tint.js — the night tint a hall wears with a look (ADR 0358, task 1004421).
|
|
2
|
+
//
|
|
3
|
+
// The hall's dark tier (modules/hall-ui/public/style.css) is pure black. A hall that
|
|
4
|
+
// wears a look takes a hint of that look's own accent into its four dark grounds
|
|
5
|
+
// instead, so two halls stay told apart at night too. It is a FORMULA over the
|
|
6
|
+
// thirteen, never a palette a look authors: no token is added, and a custom accent
|
|
7
|
+
// gets its own night for free. This table is the one copy — themeMarkup() emits it
|
|
8
|
+
// and tests/hall_tokens.mjs (UI_DESIGN_TINT=1) measures it.
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const NIGHT_TINT = Object.freeze({
|
|
12
|
+
'--bg': 'color-mix(in srgb, var(--accent) 6%, #000000)',
|
|
13
|
+
'--bg-deep': 'color-mix(in srgb, var(--accent) 6%, #000000)',
|
|
14
|
+
'--bg-frame': 'color-mix(in srgb, var(--accent) 6%, #08080a)',
|
|
15
|
+
'--bg-card': 'color-mix(in srgb, var(--accent) 5%, #101012)',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// The CSS that applies it: the same two dark twins the sheet writes (the [data-theme]
|
|
19
|
+
// switch shell.js owns, and the media block that covers first paint), at the same
|
|
20
|
+
// specificity. Injected AFTER the sheet, so later wins. PURE.
|
|
21
|
+
function nightTintCss() {
|
|
22
|
+
const decls = Object.entries(NIGHT_TINT).map(([k, v]) => `${k}:${v};`).join('');
|
|
23
|
+
return `html[data-theme="dark"]{${decls}}@media (prefers-color-scheme: dark){html:not([data-theme]){${decls}}}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { NIGHT_TINT, nightTintCss };
|
|
@@ -35,7 +35,7 @@ api.pool = {
|
|
|
35
35
|
async query(sql, params = []) {
|
|
36
36
|
const q = flat(sql);
|
|
37
37
|
seen.push({ q, params });
|
|
38
|
-
if (/FROM tasks t LEFT JOIN task_grades g/.test(q)) {
|
|
38
|
+
if (/FROM pt JOIN tasks t ON t\.id = pt\.id LEFT JOIN task_grades g/.test(q)) {
|
|
39
39
|
assert.match(q, /WHERE t\.source = 'page-tweak'/);
|
|
40
40
|
// The fixture rows stand in for this SELECT, so every column they carry
|
|
41
41
|
// must be one the SELECT really names: a renamed alias would otherwise
|
package/tests/currency_label.mjs
CHANGED
|
@@ -59,11 +59,14 @@ test('clientBranding exposes project policy — the settings read-back (task 100
|
|
|
59
59
|
// is published for a reason of its own: it is the one key that can REFUSE a
|
|
60
60
|
// module, so an owner looking at a module that will not turn on needs the
|
|
61
61
|
// instance's own answer rather than the platform's record of what they saved.
|
|
62
|
+
// `look` and `nightTint` joined in task 1004421 (ADR 0358) — the look the hall wears
|
|
63
|
+
// and whether its night takes a tint. Public because the hall paints them in the
|
|
64
|
+
// browser; their fallbacks ('own', 'on') are what an unconfigured hall shows today.
|
|
62
65
|
assert.deepEqual(clientBranding({}).project,
|
|
63
|
-
{ platformVisibility: 'public', joinability: 'apply', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none' });
|
|
66
|
+
{ platformVisibility: 'public', joinability: 'apply', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none', look: 'own', nightTint: 'on' });
|
|
64
67
|
assert.deepEqual(
|
|
65
|
-
clientBranding({ project: { platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off', spendPayer: 'builder' } }).project,
|
|
66
|
-
{ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off', spendPayer: 'builder' },
|
|
68
|
+
clientBranding({ project: { platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off', spendPayer: 'builder', look: 'grove', nightTint: 'off' } }).project,
|
|
69
|
+
{ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off', spendPayer: 'builder', look: 'grove', nightTint: 'off' },
|
|
67
70
|
'what the instance actually resolved (env override included) is what it publishes');
|
|
68
71
|
});
|
|
69
72
|
|
|
@@ -86,7 +89,7 @@ test('clientBranding is a SMALL allow-list — no server-only field leaks to the
|
|
|
86
89
|
// public for the same read-back reason as its neighbours. Its companion stamp
|
|
87
90
|
// (artistGateSince) deliberately does NOT: that is platform bookkeeping the server
|
|
88
91
|
// reads, and no browser surface has a question it answers.
|
|
89
|
-
assert.deepEqual(Object.keys(projected.project).sort(), ['artistGate', 'joinGrant', 'joinability', 'platformVisibility', 'spendPayer', 'visibility']);
|
|
92
|
+
assert.deepEqual(Object.keys(projected.project).sort(), ['artistGate', 'joinGrant', 'joinability', 'look', 'nightTint', 'platformVisibility', 'spendPayer', 'visibility']);
|
|
90
93
|
for (const leak of ['domains', 'repo', 'db', 'envPrefix', 'llmProjectName']) {
|
|
91
94
|
assert.equal(projected[leak], undefined, `${leak} must not reach the client`);
|
|
92
95
|
}
|
|
@@ -188,7 +188,7 @@ export function installPageLedger(api, { builders }) {
|
|
|
188
188
|
return { rows: db.tasks.filter((t) => t.status === 'shipped' && Date.parse(t.shipped_at) > since && (t.touches || []).some((f) => p[0].includes(f))).map((t) => ({ id: t.id, title: t.title, shipped_at: t.shipped_at })) };
|
|
189
189
|
}
|
|
190
190
|
// The page reads (listPageTweakTasks), computed from the ledger's state.
|
|
191
|
-
if (/FROM tasks t LEFT JOIN task_grades g/.test(q)) {
|
|
191
|
+
if (/FROM pt JOIN tasks t ON t\.id = pt\.id LEFT JOIN task_grades g/.test(q)) {
|
|
192
192
|
assert.match(q, /WHERE t\.source = 'page-tweak'/);
|
|
193
193
|
// The optional page scope (task 1004319): $1 is the round-ref prefix or null.
|
|
194
194
|
assert.match(q, /left\(t\.source_ref, length\(\$1::text\)\) = \$1::text/);
|
package/tests/hall_tokens.mjs
CHANGED
|
@@ -41,6 +41,7 @@ import assert from 'node:assert/strict';
|
|
|
41
41
|
import fs from 'node:fs';
|
|
42
42
|
import path from 'node:path';
|
|
43
43
|
import { fileURLToPath } from 'node:url';
|
|
44
|
+
import { createRequire } from 'node:module';
|
|
44
45
|
|
|
45
46
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
46
47
|
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
@@ -102,9 +103,17 @@ const PACK = Object.fromEntries(
|
|
|
102
103
|
Object.entries(packUi.colors).map(([k, v]) => [camelToVar(k), v]),
|
|
103
104
|
);
|
|
104
105
|
|
|
106
|
+
// UI_DESIGN_TINT=1 measures the NIGHT TINT a hall wears with a look (ADR 0358, task
|
|
107
|
+
// 1004421): themeMarkup() lays src/night-tint.js's four grounds over the dark tier,
|
|
108
|
+
// after the sheet, so they win. tests/look_night_tint.mjs runs this file once per look
|
|
109
|
+
// with it set, and once per custom accent round the hue wheel.
|
|
110
|
+
const NIGHT_TINT = process.env.UI_DESIGN_TINT === '1'
|
|
111
|
+
? createRequire(import.meta.url)('../src/night-tint.js').NIGHT_TINT
|
|
112
|
+
: {};
|
|
113
|
+
|
|
105
114
|
const RAW = {
|
|
106
115
|
light: { ...contractTier, ...PACK },
|
|
107
|
-
dark: { ...contractTier, ...PACK, ...darkExplicit },
|
|
116
|
+
dark: { ...contractTier, ...PACK, ...darkExplicit, ...NIGHT_TINT },
|
|
108
117
|
};
|
|
109
118
|
|
|
110
119
|
// ── color resolution ─────────────────────────────────────────────────────────
|
|
@@ -635,7 +635,7 @@ try {
|
|
|
635
635
|
assert.match(HTML, /<body data-page="tweak-editor"[^>]*data-fullscreen-switch/);
|
|
636
636
|
assert.equal((HTML.match(/<h1\b/g) || []).length, 1);
|
|
637
637
|
assert.ok(HTML.indexOf('tweak-editor-lib.js') < HTML.indexOf('tweak-editor.js?'), 'the lib loads first');
|
|
638
|
-
assert.match(HTML, /shell\.js\?v=2026-
|
|
638
|
+
assert.match(HTML, /shell\.js\?v=2026-10-01-look/);
|
|
639
639
|
assert.equal(/fonts\.googleapis\.com\/css/.test(HTML), false);
|
|
640
640
|
for (const id of ['tw-bar', 'tw-counter', 'tw-open', 'tw-open-more', 'tw-divider', 'tw-openwrap', 'tw-open-menu', 'tw-open-tab', 'tw-open-split', 'tw-frame', 'tw-frame-view', 'tw-split-close', 'tw-download', 'tw-upload', 'tw-file', 'tw-submit', 'tw-paper', 'tw-doc', 'tw-notice', 'tw-done', 'tw-take', 'tw-pick', 'tw-picks', 'tw-refused', 'tw-refused-title', 'tw-refused-list', 'tw-refused-next', 'tw-refused-download', 'tw-refused-close']) {
|
|
641
641
|
assert.ok(new RegExp(`id="${id}"`).test(HTML), `#${id} is in the markup`);
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// tests/look_night_tint.mjs — the look a hall wears, its night tint and its logo, hall
|
|
2
|
+
// side (task 1004421, BV2.PS06; ADR 0358).
|
|
3
|
+
//
|
|
4
|
+
// The load-bearing assertions:
|
|
5
|
+
// • THE BLEND RULE: a hall that never chose a look is exactly today's hall — the
|
|
6
|
+
// same theme, the same markup, pure black at night — whatever its tint setting;
|
|
7
|
+
// • a pushed look name becomes that look's fifteen on the instance, read from the
|
|
8
|
+
// style library the core ships; an unknown name or a malformed accent leaves the
|
|
9
|
+
// hall on its own pack rather than half-applying;
|
|
10
|
+
// • the night tint is emitted only with a look, as both dark twins, after the sheet;
|
|
11
|
+
// • CONTRAST: the hall's own token suite (tests/hall_tokens.mjs) passes under every
|
|
12
|
+
// library look with the tint on, and under custom accents round the hue wheel
|
|
13
|
+
// after the platform's nudge — light, dark and tinted dark;
|
|
14
|
+
// • a logo address reaches the hall mark and the favicon only when it is a plain
|
|
15
|
+
// https URL.
|
|
16
|
+
//
|
|
17
|
+
// Run: node --test tests/look_night_tint.mjs
|
|
18
|
+
|
|
19
|
+
import { test } from 'node:test';
|
|
20
|
+
import assert from 'node:assert/strict';
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import os from 'node:os';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { spawnSync } from 'node:child_process';
|
|
25
|
+
import { createRequire } from 'node:module';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
const require = createRequire(import.meta.url);
|
|
29
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
30
|
+
const { resolveBranding, clientBranding } = require('../src/branding.js');
|
|
31
|
+
const { themeMarkup } = require('../src/bongos/serve-internal.js');
|
|
32
|
+
const { NIGHT_TINT, nightTintCss } = require('../src/night-tint.js');
|
|
33
|
+
const { lookAdjustAccent, LIBRARY_LOOKS } = require('../modules/provisioning/look.js');
|
|
34
|
+
|
|
35
|
+
const neutral = JSON.parse(fs.readFileSync(path.join(ROOT, 'config', 'branding.neutral.json'), 'utf8'));
|
|
36
|
+
const brand = (env = {}) => clientBranding(resolveBranding({ neutral, env }));
|
|
37
|
+
const packOf = (name) => JSON.parse(fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'styles', name, 'pack.json'), 'utf8')).theme.ui;
|
|
38
|
+
|
|
39
|
+
test('the blend rule: no look means today\'s hall, whatever the tint setting says', () => {
|
|
40
|
+
const today = brand();
|
|
41
|
+
for (const env of [{ BONGOS_LOOK: 'own' }, { BONGOS_NIGHT_TINT: 'on' }, { BONGOS_LOOK: 'own', BONGOS_NIGHT_TINT: 'on' }]) {
|
|
42
|
+
const b = brand(env);
|
|
43
|
+
assert.deepEqual(b.theme, today.theme, JSON.stringify(env));
|
|
44
|
+
assert.equal(themeMarkup(b.theme), themeMarkup(today.theme));
|
|
45
|
+
}
|
|
46
|
+
assert.equal(today.theme.nightTint, false);
|
|
47
|
+
assert.equal(today.theme.look, '');
|
|
48
|
+
assert.ok(!themeMarkup(today.theme).includes('data-theme'), 'no dark block is injected for a hall without a look');
|
|
49
|
+
assert.deepEqual(today.theme.colors, neutral.theme.ui.colors);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('a library look becomes its fifteen, and the manifest reads back what was pushed', () => {
|
|
53
|
+
for (const name of LIBRARY_LOOKS) {
|
|
54
|
+
const b = brand({ BONGOS_LOOK: name });
|
|
55
|
+
const ui = packOf(name);
|
|
56
|
+
assert.deepEqual(b.theme.colors, ui.colors, name);
|
|
57
|
+
assert.deepEqual(b.theme.fonts, { display: ui.fonts.display, body: ui.fonts.body, links: ui.fonts.links }, name);
|
|
58
|
+
assert.equal(b.theme.look, name);
|
|
59
|
+
assert.equal(b.project.look, name, 'the settings read-back (settingsConsumed) sees the pushed name');
|
|
60
|
+
assert.equal(b.theme.nightTint, true, 'the tint is on by default with a look');
|
|
61
|
+
assert.match(b.theme.favicon, new RegExp(encodeURIComponent(ui.colors.accent)), 'the tab icon is drawn in the look\'s accent');
|
|
62
|
+
}
|
|
63
|
+
assert.equal(brand({ BONGOS_LOOK: 'grove', BONGOS_NIGHT_TINT: 'off' }).theme.nightTint, false, 'the founder can switch back to pure black');
|
|
64
|
+
assert.equal(brand({ BONGOS_LOOK: 'grove', BONGOS_NIGHT_TINT: 'off' }).project.nightTint, 'off');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('a custom look is the chrome world with one accent; an unusable accent or an unknown look leaves the hall alone', () => {
|
|
68
|
+
const b = brand({ BONGOS_LOOK: 'custom', BONGOS_LOOK_ACCENT: '#B48CF2' });
|
|
69
|
+
assert.deepEqual(b.theme.colors, { ...packOf('chrome-world').colors, accent: '#b48cf2' });
|
|
70
|
+
assert.equal(b.theme.look, 'custom');
|
|
71
|
+
const today = brand();
|
|
72
|
+
for (const env of [{ BONGOS_LOOK: 'custom' }, { BONGOS_LOOK: 'custom', BONGOS_LOOK_ACCENT: 'red;}body{x' }, { BONGOS_LOOK: 'neon' }, { BONGOS_LOOK: '../../etc' }]) {
|
|
73
|
+
assert.deepEqual(brand(env).theme, today.theme, JSON.stringify(env));
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('the night tint is emitted with a look, as both dark twins, and nowhere else', () => {
|
|
78
|
+
const on = themeMarkup(brand({ BONGOS_LOOK: 'expedition' }).theme);
|
|
79
|
+
assert.ok(on.includes(nightTintCss()), 'the tint block is in the markup');
|
|
80
|
+
assert.ok(on.indexOf(':root{') < on.indexOf(nightTintCss()), 'after the palette, so the dark grounds win on order');
|
|
81
|
+
assert.equal(nightTintCss().split('--bg:').length - 1, 2, 'written twice: the [data-theme] switch and the media twin');
|
|
82
|
+
assert.match(nightTintCss(), /html\[data-theme="dark"\]\{/);
|
|
83
|
+
assert.match(nightTintCss(), /@media \(prefers-color-scheme: dark\)\{html:not\(\[data-theme\]\)\{/);
|
|
84
|
+
for (const v of Object.values(NIGHT_TINT)) assert.match(v, /^color-mix\(in srgb, var\(--accent\) \d+%, #[0-9a-f]{6}\)$/, 'derived from the look\'s own accent, never a literal colour');
|
|
85
|
+
assert.ok(!themeMarkup(brand({ BONGOS_LOOK: 'expedition', BONGOS_NIGHT_TINT: 'off' }).theme).includes('data-theme'));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('a logo reaches the hall only as a plain https address, and becomes its favicon', () => {
|
|
89
|
+
const url = 'https://platform.test/api/bongos/provisioning/logos/' + 'a'.repeat(64) + '.png';
|
|
90
|
+
const b = brand({ BONGOS_LOGO_URL: url });
|
|
91
|
+
assert.equal(b.theme.logo, url);
|
|
92
|
+
assert.equal(b.theme.favicon, url);
|
|
93
|
+
assert.equal(b.theme.nightTint, false, 'a logo alone tints nothing');
|
|
94
|
+
assert.deepEqual(b.theme.colors, neutral.theme.ui.colors, 'a logo alone changes no colour');
|
|
95
|
+
for (const bad of ['http://x.test/a.png', 'javascript:alert(1)', 'https://x.test/a.png"><script>', 'https://x.test/a b.png', 'https://x.test/a.png)']) {
|
|
96
|
+
assert.equal(brand({ BONGOS_LOGO_URL: bad }).theme.logo, '', bad);
|
|
97
|
+
}
|
|
98
|
+
const shell = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'shell.js'), 'utf8');
|
|
99
|
+
assert.match(shell, /brand\.theme && brand\.theme\.logo\s*\?\s*'<img class="brand-mark__logo" src="' \+ esc\(brand\.theme\.logo\)/, 'the hall mark shows the logo, escaped, in the dot\'s place');
|
|
100
|
+
assert.match(fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'style.css'), 'utf8'), /\.brand-mark__logo \{/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ── contrast: the hall's own suite, under each look, with the tint on ──────────────
|
|
104
|
+
const childEnv = (extra) => {
|
|
105
|
+
const env = { ...process.env, ...extra };
|
|
106
|
+
delete env.NODE_TEST_CONTEXT; // a nested node --test must run its own tests, not report to ours
|
|
107
|
+
return env;
|
|
108
|
+
};
|
|
109
|
+
function hallSuite(extra) {
|
|
110
|
+
const r = spawnSync(process.execPath, ['--test', '--test-reporter=tap', 'tests/hall_tokens.mjs'], {
|
|
111
|
+
cwd: ROOT, encoding: 'utf8', env: childEnv({ UI_DESIGN_TINT: '1', ...extra }), timeout: 120000,
|
|
112
|
+
});
|
|
113
|
+
const fails = (r.stdout || '').split('\n').filter((l) => /= \d+\.\d+:1/.test(l)).map((l) => l.trim());
|
|
114
|
+
return { status: r.status, fails };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
test('the hall clears AA under every library look with the night tint on', () => {
|
|
118
|
+
for (const name of LIBRARY_LOOKS) {
|
|
119
|
+
const r = hallSuite({ UI_DESIGN_PACK: path.join(ROOT, 'modules', 'ui-design', 'styles', name, 'pack.json') });
|
|
120
|
+
assert.equal(r.status, 0, `${name}, tinted night:\n${r.fails.join('\n')}`);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('the hall clears AA under custom accents round the hue wheel, once the platform has nudged them', () => {
|
|
125
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'look-tint-'));
|
|
126
|
+
const chrome = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'styles', 'chrome-world', 'pack.json'), 'utf8'));
|
|
127
|
+
const hsl = (h, s, l) => {
|
|
128
|
+
const a = s * Math.min(l, 1 - l), k = (n) => (n + h / 30) % 12;
|
|
129
|
+
const f = (n) => Math.round(255 * (l - a * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1))));
|
|
130
|
+
return '#' + [f(0), f(8), f(4)].map((x) => x.toString(16).padStart(2, '0')).join('');
|
|
131
|
+
};
|
|
132
|
+
// Saturated hues at three lightnesses, plus the greys — the corners of the space.
|
|
133
|
+
const raw = [];
|
|
134
|
+
for (let h = 0; h < 360; h += 45) for (const l of [0.25, 0.5, 0.8]) raw.push(hsl(h, 1, l));
|
|
135
|
+
raw.push('#000000', '#808080', '#ffffff');
|
|
136
|
+
const failures = [];
|
|
137
|
+
try {
|
|
138
|
+
for (const hex of raw) {
|
|
139
|
+
const kept = lookAdjustAccent(hex).hex;
|
|
140
|
+
const pack = structuredClone(chrome);
|
|
141
|
+
pack.theme.ui.colors.accent = kept;
|
|
142
|
+
const file = path.join(tmp, `${hex.slice(1)}.json`);
|
|
143
|
+
fs.writeFileSync(file, JSON.stringify(pack));
|
|
144
|
+
for (const tint of ['0', '1']) {
|
|
145
|
+
const r = hallSuite({ UI_DESIGN_PACK: file, UI_DESIGN_TINT: tint });
|
|
146
|
+
if (r.status !== 0) failures.push(`${hex} → ${kept} (${tint === '1' ? 'tinted' : 'black'} night): ${r.fails.join('; ') || 'suite failed'}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} finally { fs.rmSync(tmp, { recursive: true, force: true }); }
|
|
150
|
+
assert.deepEqual(failures, []);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('UI_DESIGN_TINT is honoured, not decorative: a tint that is too strong fails the hall suite', () => {
|
|
154
|
+
// Proves the knob reaches the resolver: the same suite, with the card tinted far past
|
|
155
|
+
// the shipped share, must go red on the pair the 8% draft already failed.
|
|
156
|
+
const src = fs.readFileSync(path.join(ROOT, 'tests', 'hall_tokens.mjs'), 'utf8');
|
|
157
|
+
assert.match(src, /process\.env\.UI_DESIGN_TINT === '1'/);
|
|
158
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'look-tint-strong-'));
|
|
159
|
+
try {
|
|
160
|
+
const strong = path.join(tmp, 'night-tint.js');
|
|
161
|
+
fs.writeFileSync(strong, fs.readFileSync(path.join(ROOT, 'src', 'night-tint.js'), 'utf8').replace(/var\(--accent\) 5%, #101012/, 'var(--accent) 30%, #101012'));
|
|
162
|
+
const probe = path.join(tmp, 'hall_tokens.mjs');
|
|
163
|
+
fs.writeFileSync(probe, src.replace("'../src/night-tint.js'", JSON.stringify(strong)).replace(
|
|
164
|
+
"path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')", JSON.stringify(ROOT)));
|
|
165
|
+
const r = spawnSync(process.execPath, ['--test', '--test-reporter=tap', probe], { cwd: ROOT, encoding: 'utf8', env: childEnv({ UI_DESIGN_TINT: '1' }), timeout: 120000 });
|
|
166
|
+
assert.notEqual(r.status, 0, 'a 30% card tint must fail the hall suite');
|
|
167
|
+
assert.match(r.stdout, /dark --[\w-]+ on --[\w-]+ = \d/);
|
|
168
|
+
} finally { fs.rmSync(tmp, { recursive: true, force: true }); }
|
|
169
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// tests/main_discipline.mjs — the builder's own MAIN role (task 1004433 /
|
|
2
|
+
// WA6.RP03, core_268): what updateBuilderDisciplines sends for each of the
|
|
3
|
+
// three answers, and what it refuses before anything is written.
|
|
4
|
+
//
|
|
5
|
+
// The statement's own behaviour (keep, clear, set) runs against a real Postgres
|
|
6
|
+
// in tests/role_stats_db.mjs; this pins the half before it, DB-free: the
|
|
7
|
+
// "named or not" flag, the value, and the refusal of a main role that is not
|
|
8
|
+
// one of the builder's chosen crafts.
|
|
9
|
+
|
|
10
|
+
import { strict as assert } from 'node:assert';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
|
|
15
|
+
let passed = 0;
|
|
16
|
+
let failed = 0;
|
|
17
|
+
async function t(name, fn) {
|
|
18
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
19
|
+
catch (e) { failed++; console.error(` FAIL ${name}\n ${e.message}`); }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const calls = [];
|
|
23
|
+
const poolPath = require.resolve('../src/bongos/pool.js');
|
|
24
|
+
require.cache[poolPath] = {
|
|
25
|
+
id: poolPath, filename: poolPath, loaded: true,
|
|
26
|
+
exports: {
|
|
27
|
+
pool: {
|
|
28
|
+
async query(sql, params) {
|
|
29
|
+
calls.push({ sql, params });
|
|
30
|
+
return { rows: [{ id: params[0], preferred_disciplines: params[1], main_discipline: params[2] ? params[3] : null }] };
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
const db = require('../src/bongos/db.js');
|
|
36
|
+
const { enabledDisciplines } = require('../src/modules.js');
|
|
37
|
+
const offered = enabledDisciplines();
|
|
38
|
+
const last = () => calls[calls.length - 1];
|
|
39
|
+
|
|
40
|
+
await t('the write goes through the one exported statement', async () => {
|
|
41
|
+
await db.updateBuilderDisciplines(7, ['engineer']);
|
|
42
|
+
assert.equal(last().sql, db.UPDATE_BUILDER_DISCIPLINES_SQL);
|
|
43
|
+
assert.match(db.UPDATE_BUILDER_DISCIPLINES_SQL, /main_discipline = CASE WHEN \$3::boolean THEN \$4::text/);
|
|
44
|
+
assert.match(db.UPDATE_BUILDER_DISCIPLINES_SQL, /WHEN main_discipline = ANY\(\$2::text\[\]\) THEN main_discipline\s+ELSE NULL END/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await t('not mentioned: the flag is false, so the stored main role is left to the statement to keep or clear', async () => {
|
|
48
|
+
await db.updateBuilderDisciplines(7, ['engineer', 'ideator']);
|
|
49
|
+
assert.deepEqual(last().params.slice(2), [false, null]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await t('named: the flag is true and the role travels', async () => {
|
|
53
|
+
await db.updateBuilderDisciplines(7, ['ideator', 'engineer'], { mainDiscipline: 'ideator' });
|
|
54
|
+
assert.deepEqual(last().params.slice(2), [true, 'ideator']);
|
|
55
|
+
assert.deepEqual(last().params[1], offered.filter((d) => ['ideator', 'engineer'].includes(d)),
|
|
56
|
+
'the set is still stored in canonical order: the main role is what carries the choice');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await t('null clears it on purpose', async () => {
|
|
60
|
+
await db.updateBuilderDisciplines(7, ['engineer'], { mainDiscipline: null });
|
|
61
|
+
assert.deepEqual(last().params.slice(2), [true, null]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await t('a main role that is not one of the chosen crafts is refused, and nothing is written', async () => {
|
|
65
|
+
const before = calls.length;
|
|
66
|
+
await assert.rejects(() => db.updateBuilderDisciplines(7, ['engineer'], { mainDiscipline: 'ideator' }),
|
|
67
|
+
(err) => err.code === 'invalid_main_discipline' && err.bad[0] === 'ideator');
|
|
68
|
+
assert.equal(calls.length, before);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await t('the PATCH route hands main_discipline through only when the body names it', async () => {
|
|
72
|
+
const fs = require('node:fs');
|
|
73
|
+
const src = fs.readFileSync(require.resolve('../src/bongos/routes/me.js'), 'utf8');
|
|
74
|
+
const route = src.slice(src.indexOf("router.patch('/me/disciplines'"), src.indexOf("router.patch('/me/disciplines'") + 2500);
|
|
75
|
+
assert.match(route, /hasOwnProperty\.call\(body, 'main_discipline'\)/);
|
|
76
|
+
assert.match(route, /db\.updateBuilderDisciplines\(req\.builder\.id, input, mainOpt\)/);
|
|
77
|
+
assert.match(route, /invalid_main_discipline/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
81
|
+
if (failed) process.exit(1);
|
|
@@ -87,7 +87,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
|
|
|
87
87
|
// the file and left the query alone, so a returning browser kept the cached copy —
|
|
88
88
|
// a fix that reaches nobody looks identical to a fix that does not work.
|
|
89
89
|
|
|
90
|
-
const SHELL_SHA = '
|
|
90
|
+
const SHELL_SHA = '51a13ea563542113';
|
|
91
91
|
|
|
92
92
|
test('shell.js has not changed without its cache-bust being bumped', () => {
|
|
93
93
|
const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
|