@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
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// tests/projects_hub_look.mjs — the Look step in the create wizard and "Its look" on the
|
|
2
|
+
// manage page (task 1004421, BV2.PS06; ADR 0358; spec D2 + D8).
|
|
3
|
+
//
|
|
4
|
+
// The load-bearing assertions:
|
|
5
|
+
// • the previews paint the REAL looks: every colour lookKit carries equals its
|
|
6
|
+
// pack.json, and the night shares equal src/night-tint.js — a preview that drifted
|
|
7
|
+
// from the hall would promise a look the hall does not wear;
|
|
8
|
+
// • the step sits between App and Review (look 10, review 11, done 12, draft v7), on
|
|
9
|
+
// the rail as a beat of its own, and every kit walk past App passes it;
|
|
10
|
+
// • EXECUTED by the vm-slice idiom: the picker offers the four looks and "make your
|
|
11
|
+
// own", shows the accent only for your own, shows the ADJUSTED colour in the
|
|
12
|
+
// preview and says so, paints a black night when the tint is off, refuses an SVG
|
|
13
|
+
// or oversized logo before reading it, and sends the look then the logo;
|
|
14
|
+
// • Create sends the look right after the project is filed, and the review names it;
|
|
15
|
+
// • the manage page reads the look once per project, never on the minute tick.
|
|
16
|
+
//
|
|
17
|
+
// Run: node --test tests/projects_hub_look.mjs
|
|
18
|
+
|
|
19
|
+
import { test } from 'node:test';
|
|
20
|
+
import assert from 'node:assert/strict';
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import vm from 'node:vm';
|
|
24
|
+
import { createRequire } from 'node:module';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
|
+
const LANDING = path.join(ROOT, 'modules', 'public-landing', 'public');
|
|
30
|
+
const HUB = fs.readFileSync(path.join(LANDING, 'projects.html'), 'utf8').replace(/\r\n/g, '\n');
|
|
31
|
+
const { NIGHT_TINT } = require('../src/night-tint.js');
|
|
32
|
+
const { LIBRARY_LOOKS, lookAdjustAccent } = require('../modules/provisioning/look.js');
|
|
33
|
+
|
|
34
|
+
// ── the lookKit slice, run in a vm under a small DOM stub ───────────────────────
|
|
35
|
+
const KIT_START = ' var lookKit = (function () {';
|
|
36
|
+
const KIT_END = '\n })();\n';
|
|
37
|
+
const kitStart = HUB.indexOf(KIT_START);
|
|
38
|
+
const kitEnd = HUB.indexOf(KIT_END, kitStart);
|
|
39
|
+
const KIT_SRC = HUB.slice(kitStart, kitEnd + KIT_END.length);
|
|
40
|
+
|
|
41
|
+
function makeDom() {
|
|
42
|
+
const all = [];
|
|
43
|
+
function node(tag) {
|
|
44
|
+
const n = {
|
|
45
|
+
tagName: String(tag).toUpperCase(), children: [], parentNode: null, style: { props: {}, setProperty(k, v) { this.props[k] = v; } },
|
|
46
|
+
attrs: {}, listeners: {}, hidden: false, value: '', checked: false, textContent: '', className: '', id: '', type: '', name: '', src: '', files: null,
|
|
47
|
+
get firstChild() { return this.children[0] || null; },
|
|
48
|
+
classList: null,
|
|
49
|
+
appendChild(c) { c.parentNode = this; this.children.push(c); return c; },
|
|
50
|
+
removeChild(c) { this.children = this.children.filter((x) => x !== c); c.parentNode = null; return c; },
|
|
51
|
+
setAttribute(k, v) { this.attrs[k] = String(v); },
|
|
52
|
+
addEventListener(ev, fn) { (this.listeners[ev] ||= []).push(fn); },
|
|
53
|
+
fire(ev) { (this.listeners[ev] || []).forEach((fn) => fn.call(this, { target: this })); },
|
|
54
|
+
querySelector(sel) { return find(this, (x) => x.className.split(' ').includes(sel.replace(/^\./, ''))); },
|
|
55
|
+
};
|
|
56
|
+
n.classList = {
|
|
57
|
+
add(c) { const s = new Set(n.className.split(' ').filter(Boolean)); s.add(c); n.className = [...s].join(' '); },
|
|
58
|
+
remove(c) { n.className = n.className.split(' ').filter((x) => x && x !== c).join(' '); },
|
|
59
|
+
toggle(c, on) { if (on) this.add(c); else this.remove(c); },
|
|
60
|
+
contains(c) { return n.className.split(' ').includes(c); },
|
|
61
|
+
};
|
|
62
|
+
all.push(n);
|
|
63
|
+
return n;
|
|
64
|
+
}
|
|
65
|
+
function find(root, pred) {
|
|
66
|
+
for (const c of root.children) { if (pred(c)) return c; const d = find(c, pred); if (d) return d; }
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const document = { createElement: node };
|
|
70
|
+
const byId = (root, id) => find(root, (x) => x.id === id);
|
|
71
|
+
return { document, node, byId, find };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function bootKit({ fetchImpl, reader } = {}) {
|
|
75
|
+
const dom = makeDom();
|
|
76
|
+
const sandbox = {
|
|
77
|
+
document: dom.document, API: '/api/bongos', JSON, Math, String, Number, Object, Array, Promise, parseInt,
|
|
78
|
+
fetch: fetchImpl || (() => Promise.reject(new Error('no fetch'))),
|
|
79
|
+
FileReader: reader || function FileReader() { this.readAsDataURL = (f) => { this.result = 'data:' + f.type + ';base64,QUJD'; this.onload(); }; },
|
|
80
|
+
};
|
|
81
|
+
vm.createContext(sandbox);
|
|
82
|
+
vm.runInContext(KIT_SRC.replace('var lookKit =', 'this.lookKit ='), sandbox);
|
|
83
|
+
return { kit: sandbox.lookKit, dom };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
test('the slice is findable: a rename must fail here, not silently test nothing', () => {
|
|
87
|
+
assert.ok(kitStart > -1, 'var lookKit = (function () { is in the page');
|
|
88
|
+
assert.ok(kitEnd > kitStart, 'and it closes');
|
|
89
|
+
assert.match(KIT_SRC, /function buildLookPicker\(host, model, onChange, idp, projectName\)/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('every colour the previews paint is its pack\'s, and the night shares are the hall\'s', () => {
|
|
93
|
+
const { kit } = bootKit();
|
|
94
|
+
assert.deepEqual(Object.keys(kit.LOOKS).sort(), [...LIBRARY_LOOKS].sort(), 'one preview per library look');
|
|
95
|
+
for (const name of LIBRARY_LOOKS) {
|
|
96
|
+
const pack = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'styles', name, 'pack.json'), 'utf8')).theme.ui.colors;
|
|
97
|
+
assert.equal(JSON.stringify(kit.LOOKS[name].colors), JSON.stringify(pack), `${name}: the client copy equals pack.json, key for key`);
|
|
98
|
+
}
|
|
99
|
+
const share = (k) => Number(/var\(--accent\) (\d+)%/.exec(NIGHT_TINT[k])[1]);
|
|
100
|
+
assert.equal(kit.NIGHT.bgShare, share('--bg'));
|
|
101
|
+
assert.equal(kit.NIGHT.cardShare, share('--bg-card'));
|
|
102
|
+
assert.match(NIGHT_TINT['--bg'], /#000000\)$/);
|
|
103
|
+
assert.match(NIGHT_TINT['--bg-card'], /#101012\)$/);
|
|
104
|
+
assert.equal(kit.NIGHT.bg, '#000000');
|
|
105
|
+
assert.equal(kit.NIGHT.card, '#101012');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('the step sits between App and Review: look 10, review 11, done 12, draft v7, a beat on the rail', () => {
|
|
109
|
+
assert.match(HUB, /9: 'panel-app', 10: 'panel-look', 11: 'panel-review', 12: 'panel-done' \};/);
|
|
110
|
+
assert.match(HUB, /var DRAFT_KEY = 'cb-create-draft-v7';/);
|
|
111
|
+
assert.match(HUB, /<li data-step="10"><span class="snum">8<\/span><span class="slbl">Look<\/span><\/li>\n\s*<li data-step="11"><span class="snum">9<\/span><span class="slbl">Create<\/span><\/li>/);
|
|
112
|
+
const panel = (HUB.match(/<section class="panel" id="panel-look"[\s\S]*?<\/section>/) || [''])[0];
|
|
113
|
+
assert.match(panel, /<h1 id="look-h">Give it a look<\/h1>/);
|
|
114
|
+
assert.match(panel, /<form data-next="11">/);
|
|
115
|
+
assert.match(panel, /data-back="9"/);
|
|
116
|
+
assert.match(panel, /<div class="lookPick" id="wizLook"><\/div>/);
|
|
117
|
+
assert.ok(HUB.indexOf('id="panel-app"') < HUB.indexOf('id="panel-look"') && HUB.indexOf('id="panel-look"') < HUB.indexOf('id="panel-review"'), 'in document order too');
|
|
118
|
+
assert.match(HUB, /if \(step === 10\) renderLook\(\);\n\s*if \(step === 11\) renderReview\(\);/);
|
|
119
|
+
assert.match(fs.readFileSync(path.join(LANDING, 'assets', 'cosmos.css'), 'utf8'), /#view-new \.steps\{display:grid;grid-template-columns:repeat\(9,1fr\);/, 'a track per numbered step');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('every kit walk past the App step passes the Look step, and the kit has a state on it', () => {
|
|
123
|
+
const states = JSON.parse(fs.readFileSync(path.join(LANDING, 'projects.states.json'), 'utf8')).states;
|
|
124
|
+
const probes = JSON.parse(fs.readFileSync(path.join(LANDING, 'projects.probes.json'), 'utf8')).probes;
|
|
125
|
+
const walks = [...Object.entries(states).map(([k, v]) => [k, v.actions]), ...probes.map((p) => [p.name, p.steps])];
|
|
126
|
+
let past = 0;
|
|
127
|
+
for (const [name, steps] of walks) {
|
|
128
|
+
const at = (steps || []).findIndex((a) => a[1] === '#panel-app button[type=submit]');
|
|
129
|
+
if (at < 0 || /^in-new-look/.test(name)) continue; // the Look states stop ON the step
|
|
130
|
+
past++;
|
|
131
|
+
const look = steps.findIndex((a) => a[1] === '#panel-look button[type=submit]');
|
|
132
|
+
assert.ok(look > at, `${name}: submits the Look step after the App step`);
|
|
133
|
+
}
|
|
134
|
+
assert.ok(past >= 5, `sanity: the walks that reach the review were found (${past})`);
|
|
135
|
+
assert.deepEqual(states['in-new-look'].expect.visible.slice(0, 2), ['#panel-look', '#wizLook']);
|
|
136
|
+
assert.ok(states['in-new-look-custom'].actions.some((a) => a[1] === '#wizLook-custom'), 'a state pictures "make your own"');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('executed: the picker offers the four looks and your own, and the accent shows only for your own', () => {
|
|
140
|
+
const { kit, dom } = bootKit();
|
|
141
|
+
const host = dom.node('div');
|
|
142
|
+
const model = { look: 'chrome-world', nightTint: true, accent: '#b48cf2', logo: null };
|
|
143
|
+
let changes = 0;
|
|
144
|
+
kit.buildLookPicker(host, model, () => { changes++; }, 'wiz', () => 'Kestrel');
|
|
145
|
+
const radios = ['chrome-world', 'expedition', 'grove', 'blueprint', 'custom'].map((k) => dom.byId(host, 'wizLook-' + k));
|
|
146
|
+
assert.ok(radios.every(Boolean), 'five radios, one per choice');
|
|
147
|
+
assert.equal(radios[0].checked, true, 'the current look is ticked');
|
|
148
|
+
const accentWrap = dom.byId(host, 'wizAccent').parentNode.parentNode;
|
|
149
|
+
assert.equal(accentWrap.hidden, true, 'no accent for a library look');
|
|
150
|
+
radios[4].checked = true; radios[4].fire('change');
|
|
151
|
+
assert.equal(model.look, 'custom');
|
|
152
|
+
assert.equal(accentWrap.hidden, false, 'the accent appears for your own');
|
|
153
|
+
assert.equal(changes, 1, 'onChange ran');
|
|
154
|
+
const name = dom.find(host, (x) => x.className === 'mhName');
|
|
155
|
+
assert.equal(name.textContent, 'Kestrel', 'the preview names the project');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('executed: an unreadable accent is shown ADJUSTED in the preview, with a sentence saying so', () => {
|
|
159
|
+
const { kit, dom } = bootKit();
|
|
160
|
+
const host = dom.node('div');
|
|
161
|
+
const model = { look: 'custom', nightTint: true, accent: '#112233', logo: null };
|
|
162
|
+
kit.buildLookPicker(host, model, null, 'wiz');
|
|
163
|
+
const kept = lookAdjustAccent('#112233').hex;
|
|
164
|
+
const note = dom.byId(host, 'wizAccentNote');
|
|
165
|
+
assert.match(note.textContent, new RegExp(`Adjusted to ${kept} so text stays readable in both modes`));
|
|
166
|
+
const light = dom.find(host, (x) => x.className === 'mh light');
|
|
167
|
+
assert.equal(light.style.props['--mh-accent'], kept, 'the preview paints the colour the server will keep');
|
|
168
|
+
model.accent = '#b48cf2';
|
|
169
|
+
const picker = kit.buildLookPicker(host, model, null, 'wiz');
|
|
170
|
+
picker.repaint();
|
|
171
|
+
assert.equal(dom.byId(host, 'wizAccentNote').textContent, ' ', 'a readable colour needs no sentence');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('executed: the dark preview is the look\'s tinted night, or pure black with the tint off', () => {
|
|
175
|
+
const { kit, dom } = bootKit();
|
|
176
|
+
const host = dom.node('div');
|
|
177
|
+
const model = { look: 'grove', nightTint: true, accent: '#b48cf2', logo: null };
|
|
178
|
+
kit.buildLookPicker(host, model, null, 'wiz');
|
|
179
|
+
const dark = () => dom.find(host, (x) => x.className === 'mh dark');
|
|
180
|
+
const accent = kit.LOOKS.grove.colors.accent;
|
|
181
|
+
const mix = (hex, p, base) => '#' + [16, 8, 0].map((sh) => Math.round(((parseInt(hex.slice(1), 16) >> sh) & 255) * p / 100 + ((parseInt(base.slice(1), 16) >> sh) & 255) * (100 - p) / 100).toString(16).padStart(2, '0')).join('');
|
|
182
|
+
assert.equal(dark().style.props['--mh-bg'], mix(accent, 6, '#000000'));
|
|
183
|
+
assert.equal(dark().style.props['--mh-card'], mix(accent, 5, '#101012'));
|
|
184
|
+
const tint = dom.byId(host, 'wizTint');
|
|
185
|
+
tint.checked = false; tint.fire('change');
|
|
186
|
+
assert.equal(model.nightTint, false);
|
|
187
|
+
assert.equal(dark().style.props['--mh-bg'], '#000000');
|
|
188
|
+
assert.equal(dark().style.props['--mh-card'], '#101012');
|
|
189
|
+
// a project on its own colours (manage page) never shows a tinted night
|
|
190
|
+
const own = { look: 'own', nightTint: true, accent: '#b48cf2', logo: null };
|
|
191
|
+
const h2 = dom.node('div');
|
|
192
|
+
kit.buildLookPicker(h2, own, null, 'mLk');
|
|
193
|
+
assert.equal(dom.find(h2, (x) => x.className === 'mh dark').style.props['--mh-bg'], '#000000');
|
|
194
|
+
assert.ok(['chrome-world', 'expedition', 'grove', 'blueprint', 'custom'].every((k) => !dom.byId(h2, 'mLkLook-' + k).checked), 'no look is ticked for a project that never chose');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('executed: a logo is refused before it is read unless it is a small PNG, JPEG or WebP', () => {
|
|
198
|
+
let reads = 0;
|
|
199
|
+
const reader = function FileReader() { this.readAsDataURL = (f) => { reads++; this.result = 'data:' + f.type + ';base64,QUJD'; this.onload(); }; };
|
|
200
|
+
const { kit, dom } = bootKit({ reader });
|
|
201
|
+
const host = dom.node('div');
|
|
202
|
+
const model = { look: 'grove', nightTint: true, accent: '#b48cf2', logo: null };
|
|
203
|
+
kit.buildLookPicker(host, model, null, 'wiz');
|
|
204
|
+
const file = dom.byId(host, 'wizLogo');
|
|
205
|
+
const note = file.parentNode.parentNode.children.find((c) => c.attrs.role === 'status');
|
|
206
|
+
file.files = [{ type: 'image/svg+xml', size: 100, name: 'k.svg' }]; file.fire('change');
|
|
207
|
+
assert.match(note.textContent, /PNG, JPEG or WebP/);
|
|
208
|
+
file.files = [{ type: 'image/png', size: 300 * 1024, name: 'k.png' }]; file.fire('change');
|
|
209
|
+
assert.match(note.textContent, /at most 256 KB/);
|
|
210
|
+
assert.equal(reads, 0, 'neither was read');
|
|
211
|
+
assert.equal(model.logo, null);
|
|
212
|
+
file.files = [{ type: 'image/png', size: 2000, name: 'k.png' }]; file.fire('change');
|
|
213
|
+
assert.equal(reads, 1);
|
|
214
|
+
assert.deepEqual(JSON.parse(JSON.stringify(model.logo)), { type: 'image/png', b64: 'QUJD', name: 'k.png' });
|
|
215
|
+
const mark = dom.find(host, (x) => x.className === 'mhMark logo');
|
|
216
|
+
assert.ok(mark && mark.children[0].src.startsWith('data:image/png;base64,'), 'the preview\'s hall mark shows the logo');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('executed: sendLook sends the look, then the logo — and a removed logo is deleted', async () => {
|
|
220
|
+
const calls = [];
|
|
221
|
+
const fetchImpl = (url, opts) => {
|
|
222
|
+
calls.push({ url, method: opts.method, body: opts.body ? JSON.parse(opts.body) : null });
|
|
223
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ ok: true, look: { look: 'custom' }, push: { message: 'Saved.' }, adjusted: null }) });
|
|
224
|
+
};
|
|
225
|
+
const { kit } = bootKit({ fetchImpl });
|
|
226
|
+
await kit.sendLook(42, { look: 'custom', nightTint: false, accent: '#b48cf2', logo: { type: 'image/webp', b64: 'QUJD', name: 'k.webp' } });
|
|
227
|
+
assert.deepEqual(JSON.parse(JSON.stringify(calls)), [
|
|
228
|
+
{ url: '/api/bongos/provisioning/instances/42/look', method: 'PATCH', body: { look: 'custom', night_tint: 'off', accent: '#b48cf2' } },
|
|
229
|
+
{ url: '/api/bongos/provisioning/instances/42/logo', method: 'PUT', body: { image_b64: 'QUJD', content_type: 'image/webp' } },
|
|
230
|
+
]);
|
|
231
|
+
calls.length = 0;
|
|
232
|
+
await kit.sendLook(42, { look: 'grove', nightTint: true, accent: '#b48cf2', logo: null, logoDropped: true });
|
|
233
|
+
assert.deepEqual(calls.map((c) => [c.method, c.url.split('/').pop(), c.body]), [['PATCH', 'look', { look: 'grove', night_tint: 'on' }], ['DELETE', 'logo', null]]);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('Create sends the look right after the project is filed, and the review names it', () => {
|
|
237
|
+
assert.match(HUB, /sendRenderSetup\(data\.instance && data\.instance\.id\);[^\n]*\n\s*sendLookSetup\(data\.instance && data\.instance\.id\);/);
|
|
238
|
+
assert.match(HUB, /lookKit\.sendLook\(instanceId, state\.look \|\| defaultLook\(\)\)/);
|
|
239
|
+
assert.match(HUB, /function defaultLook\(\) \{ return \{ look: 'chrome-world', nightTint: true, accent: '#b48cf2', logo: null \}; \}/, 'the default is the platform\'s look with the tint ON (owner D2)');
|
|
240
|
+
assert.match(HUB, /\['Look', lookRow\(\)\]/);
|
|
241
|
+
assert.match(HUB, /<p class="hint" id="doneLook" role="status" hidden><\/p>/, 'a refused look is said on the done panel');
|
|
242
|
+
assert.match(HUB, /look: null, termsAck: false \};/, 'the draft carries the look (and its reset clears it)');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('the manage page has "Its look", read once per project — never on the minute tick', () => {
|
|
246
|
+
const sec = (HUB.match(/<div class="card mSec" id="mLook">[\s\S]*?\n {6}<\/div>/) || [''])[0];
|
|
247
|
+
assert.match(sec, /<h2>Its look<\/h2>/);
|
|
248
|
+
assert.match(sec, /<div id="mLookPick">/);
|
|
249
|
+
assert.match(sec, /id="mLookSave">Save the look<\/button>/);
|
|
250
|
+
assert.match(sec, /Saving restarts your project once/);
|
|
251
|
+
assert.match(HUB, /renderCardPick\(inst\);\n\s*renderLookCard\(inst\);/);
|
|
252
|
+
assert.match(HUB, /function renderLookCard\(inst\) \{\n\s*if \(mLookFor === inst\.id\) return;/, 'the tick does not re-read or rebuild it');
|
|
253
|
+
assert.match(HUB, /lookKit\.buildLookPicker\(host, mLookModel,/, 'the wizard\'s own picker');
|
|
254
|
+
assert.match(HUB, /lookKit\.sendLook\(manageId, mLookModel\)/, 'and its own send');
|
|
255
|
+
});
|
|
@@ -415,7 +415,7 @@ test('the review names the set even when the picker was never opened (task 10023
|
|
|
415
415
|
// and the module NAMES are not known. "None — just the core" would be a false
|
|
416
416
|
// promise on the last screen before Create: the server is about to apply the
|
|
417
417
|
// type's bundle. The row says that instead, then repaints when the read lands.
|
|
418
|
-
const h = boot({ state: { projType: 'game', step:
|
|
418
|
+
const h = boot({ state: { projType: 'game', step: 11 } }); // 11 = the review since task 1004421 (10 since 1004417, 8 since 1004351)
|
|
419
419
|
|
|
420
420
|
// Arrange + Act — the review asks for the value before the read can answer
|
|
421
421
|
const first = h.extrasRow();
|
|
@@ -821,7 +821,7 @@ test('with nothing switched on the count says only what is shown', async () => {
|
|
|
821
821
|
});
|
|
822
822
|
|
|
823
823
|
test('the review names the hidden picks too — a filter never reaches the create request', async () => {
|
|
824
|
-
const h = boot({ state: { projType: 'game', step:
|
|
824
|
+
const h = boot({ state: { projType: 'game', step: 11 } }); // 11 = the review since task 1004421 (10 since 1004417, 8 since 1004351)
|
|
825
825
|
await h.open();
|
|
826
826
|
h.click('agents');
|
|
827
827
|
assert.deepEqual(h.forCreate(), ['agents', 'npm-release', 'discord']);
|
|
@@ -70,7 +70,11 @@ ${NEUTRAL_PREFIX}_VISIBILITY=public
|
|
|
70
70
|
${NEUTRAL_PREFIX}_JOIN_GRANT=full
|
|
71
71
|
${NEUTRAL_PREFIX}_ARTIST_GATE=strict
|
|
72
72
|
${NEUTRAL_PREFIX}_SPEND_PAYER=none
|
|
73
|
+
${NEUTRAL_PREFIX}_LOOK=own
|
|
74
|
+
${NEUTRAL_PREFIX}_NIGHT_TINT=on
|
|
73
75
|
${NEUTRAL_PREFIX}_ARTIST_GATE_SINCE=
|
|
76
|
+
${NEUTRAL_PREFIX}_LOOK_ACCENT=
|
|
77
|
+
${NEUTRAL_PREFIX}_LOGO_URL=
|
|
74
78
|
`;
|
|
75
79
|
|
|
76
80
|
t('replaces an existing KEY= line where it stands; every other line survives byte-for-byte', () => {
|
|
@@ -134,6 +138,10 @@ t('settingsEnvVarsFor: the runner pushes the SAME pair webEnvBody writes at stan
|
|
|
134
138
|
// must already hold it in web.env before its first require
|
|
135
139
|
[`${NEUTRAL_PREFIX}_SPEND_PAYER`]: 'none',
|
|
136
140
|
[`${NEUTRAL_PREFIX}_ARTIST_GATE_SINCE`]: '',
|
|
141
|
+
// the look a hall wears and its night tint (task 1004421, ADR 0358), with the
|
|
142
|
+
// custom accent and the logo address as companions, empty when there is none
|
|
143
|
+
[`${NEUTRAL_PREFIX}_LOOK`]: 'own', [`${NEUTRAL_PREFIX}_NIGHT_TINT`]: 'on',
|
|
144
|
+
[`${NEUTRAL_PREFIX}_LOOK_ACCENT`]: '', [`${NEUTRAL_PREFIX}_LOGO_URL`]: '',
|
|
137
145
|
});
|
|
138
146
|
const body = P.webEnvBody(inst, {});
|
|
139
147
|
for (const [k, v] of Object.entries(vars)) assert.match(body, new RegExp(`^${k}=${v}$`, 'm'), `${k} at standup`);
|
|
@@ -145,7 +153,7 @@ const GATED = { settings: { platform_visibility: 'gated', joinability: 'invite_o
|
|
|
145
153
|
const manifestWith = (project) => JSON.stringify({ schema: 'instance-manifest/v1', brand: project === undefined ? {} : { project } });
|
|
146
154
|
|
|
147
155
|
t('true when every pushed key matches brand.project (camelCase, the ENV_OVERRIDES targets)', () => {
|
|
148
|
-
assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none' }), GATED), true);
|
|
156
|
+
assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none', look: 'own', nightTint: 'on' }), GATED), true);
|
|
149
157
|
});
|
|
150
158
|
t('false when the instance still reports a different value', () => {
|
|
151
159
|
assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'public', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none' }), GATED), false);
|
|
@@ -166,7 +174,7 @@ t('instanceManifestCmd asks the instance on loopback, like healthzCmd', () => {
|
|
|
166
174
|
function recorder({ apply = false, envBody = LIVE_ENV, readFails = false, healthzFails = false, privileged = false,
|
|
167
175
|
manifest = 'match', restartThrows = false, writeThrows = false } = {}) {
|
|
168
176
|
const cmds = [], writes = [], events = [], probes = [], cleared = [], logs = [];
|
|
169
|
-
const manifestOut = manifest === 'match' ? manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none' })
|
|
177
|
+
const manifestOut = manifest === 'match' ? manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none', look: 'own', nightTint: 'on' })
|
|
170
178
|
: manifest === 'mismatch' ? manifestWith({ platformVisibility: 'public', joinability: 'apply', visibility: 'public' })
|
|
171
179
|
: manifest === 'none' ? manifestWith(undefined) : 'nope';
|
|
172
180
|
const exec = (cmd, opts = {}) => {
|
|
@@ -234,7 +242,7 @@ await ta('dry-run PLANS the patch: reads nothing real, writes NOTHING (no synthe
|
|
|
234
242
|
assert.equal(r.ok, true);
|
|
235
243
|
assert.equal(r.healthy, null, 'nothing ran, nothing observed');
|
|
236
244
|
assert.equal(writes.length, 0, 'a dry-run must not plan a two-line overwrite of web.env');
|
|
237
|
-
assert.ok(logs.some((l) => /would patch .*_PLATFORM_VISIBILITY, .*_JOINABILITY, .*_VISIBILITY, .*_JOIN_GRANT, .*_ARTIST_GATE, .*_SPEND_PAYER, .*_ARTIST_GATE_SINCE in \/etc\/spike\/web\.env in place/.test(l)), logs.join('\n'));
|
|
245
|
+
assert.ok(logs.some((l) => /would patch .*_PLATFORM_VISIBILITY, .*_JOINABILITY, .*_VISIBILITY, .*_JOIN_GRANT, .*_ARTIST_GATE, .*_SPEND_PAYER, .*_LOOK, .*_NIGHT_TINT, .*_ARTIST_GATE_SINCE, .*_LOOK_ACCENT, .*_LOGO_URL in \/etc\/spike\/web\.env in place/.test(l)), logs.join('\n'));
|
|
238
246
|
assert.ok(cmds.some((c) => /restart spike/.test(c)), 'the restart is still in the plan');
|
|
239
247
|
assert.ok(logs.some((l) => /dry-run — nothing ran/.test(l)), 'the closing line does not claim a restart happened');
|
|
240
248
|
});
|
|
@@ -354,7 +362,7 @@ await ta('--apply, the control-plane row: its OWN /etc/<slug>/web.env on the con
|
|
|
354
362
|
const mk = (bucket) => (cmd) => {
|
|
355
363
|
seen[bucket].push(cmd);
|
|
356
364
|
if (/\bcat /.test(cmd)) return { ok: true, stdout: selfEnv };
|
|
357
|
-
if (/api\/bongos\/instance/.test(cmd)) return { ok: true, stdout: manifestWith({ platformVisibility: 'public', joinability: 'apply', visibility: 'public', joinGrant: 'full', artistGate: 'off', spendPayer: 'none' }) };
|
|
365
|
+
if (/api\/bongos\/instance/.test(cmd)) return { ok: true, stdout: manifestWith({ platformVisibility: 'public', joinability: 'apply', visibility: 'public', joinGrant: 'full', artistGate: 'off', spendPayer: 'none', look: 'own', nightTint: 'on' }) };
|
|
358
366
|
return { ok: true, stdout: '{"ok":true}' };
|
|
359
367
|
};
|
|
360
368
|
try {
|