@bongos/core 1.19.89 → 1.19.91
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 +59 -29
- package/config/branding.neutral.json +6 -6
- package/docs/design/landing-page-direction.md +176 -0
- package/docs/design/mocks/_schemes.css +604 -0
- package/docs/design/mocks/landing-page-mock.html +687 -0
- package/docs/design/mocks/landing-palettes.html +103 -0
- package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
- package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
- package/modules/dev-box/app/assets/tray/win-off.png +0 -0
- package/modules/dev-box/app/assets/tray/win-on.png +0 -0
- package/modules/dev-box/app/build-resources/icon.png +0 -0
- package/modules/dev-box/app/renderer/theme.generated.css +11 -11
- package/modules/dev-box/app/scripts/gen-brand.js +12 -12
- package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +3 -3
- package/modules/dev-box/app/scripts/gen-icons.js +3 -3
- package/modules/dev-box/app/src/brand.generated.json +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +129 -70
- package/public-landing/create.html +5 -2
- package/public-landing/index.html +543 -88
- package/scripts/status-mirror-sync.js +12 -6
- package/src/bongos/serve-internal.js +53 -10
- package/src/module-api.js +5 -1
- package/src/platform-server.js +26 -2
- package/tests/cloud_bongos_pack.mjs +15 -9
- package/tests/cosmos_token_contrast.mjs +268 -0
- package/tests/landing_page.mjs +289 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// tests/landing_page.mjs — the apex landing page (task 1002748, goal 1000061).
|
|
2
|
+
//
|
|
3
|
+
// Contract: docs/design/landing-page-direction.md + docs/design/mocks/landing-page-mock.html.
|
|
4
|
+
// These lock the four properties the direction calls load-bearing, each of which
|
|
5
|
+
// fails SILENTLY in a browser and so cannot be caught by looking at the page:
|
|
6
|
+
//
|
|
7
|
+
// * the apex conditional — public-landing/ ships wholesale to every instance,
|
|
8
|
+
// so the platform-only sections must be REMOVED (not hidden) off the hub;
|
|
9
|
+
// * the server-stamped head — social tags are read before JS runs, so they
|
|
10
|
+
// cannot be client-filled, and they must not be baked to one instance;
|
|
11
|
+
// * the branding cascade — a page-local :root silently outranks the pack's
|
|
12
|
+
// injected theme and defeats re-branding (direction C1);
|
|
13
|
+
// * no hardcoded apex — the file is portable or it lies to child instances.
|
|
14
|
+
//
|
|
15
|
+
// Deliberately NOT asserted here: that GET / returns the page. It does not — the
|
|
16
|
+
// apex still 302s into the gate until the hub exists (task 1002743 part 3), which
|
|
17
|
+
// is why tests/platform_boot.mjs stays parked in run-unit-tests KNOWN_FAILING.
|
|
18
|
+
// This file tests the artifact that flip will serve.
|
|
19
|
+
|
|
20
|
+
import { test } from 'node:test';
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
import { createRequire } from 'node:module';
|
|
26
|
+
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
|
+
const HTML = fs.readFileSync(path.join(ROOT, 'public-landing', 'index.html'), 'utf8');
|
|
30
|
+
|
|
31
|
+
const { stripApexOnly } = require('../src/platform-server.js');
|
|
32
|
+
const { applyBrandTokens } = require('../src/bongos/serve-internal.js');
|
|
33
|
+
|
|
34
|
+
// ── the apex conditional ───────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
test('the apex-only sentinels are balanced and each one wraps content', () => {
|
|
37
|
+
const starts = HTML.match(/<!--apex-only:start-->/g) || [];
|
|
38
|
+
const ends = HTML.match(/<!--apex-only:end-->/g) || [];
|
|
39
|
+
assert.equal(starts.length, ends.length, 'every apex-only:start needs its end');
|
|
40
|
+
assert.ok(starts.length >= 4, 'the direction marks four apex-only regions');
|
|
41
|
+
// no start after an unclosed start — a nested pair would make the non-greedy
|
|
42
|
+
// strip close on the wrong sentinel and leak the platform's claims.
|
|
43
|
+
let depth = 0;
|
|
44
|
+
for (const m of HTML.matchAll(/<!--apex-only:(start|end)-->/g)) {
|
|
45
|
+
depth += m[1] === 'start' ? 1 : -1;
|
|
46
|
+
assert.ok(depth === 0 || depth === 1, 'apex-only regions must not nest');
|
|
47
|
+
}
|
|
48
|
+
assert.equal(depth, 0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('the hub keeps every apex-only section', () => {
|
|
52
|
+
assert.equal(stripApexOnly(HTML, true), HTML);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('a child instance is served none of the platform-only claims', () => {
|
|
56
|
+
const child = stripApexOnly(HTML, false);
|
|
57
|
+
assert.doesNotMatch(child, /<!--apex-only:(start|end)-->/, 'no sentinel survives the strip');
|
|
58
|
+
// the four regions, by their load-bearing content
|
|
59
|
+
assert.doesNotMatch(child, /capture-resistant commons/, 'the commons claim is the platform\'s, not a child\'s');
|
|
60
|
+
assert.doesNotMatch(child, /Built on Cloud Bongos/, 'a child has no catalog of other instances');
|
|
61
|
+
assert.doesNotMatch(child, /What you own/, 'the AGPL/ownership pitch is the platform\'s');
|
|
62
|
+
assert.doesNotMatch(child, /See what's building/, 'the catalog CTA has nothing to point at');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('a child instance keeps the whole portable spine', () => {
|
|
66
|
+
const child = stripApexOnly(HTML, false);
|
|
67
|
+
for (const portable of [
|
|
68
|
+
'Building software is laying', // the hero tagline
|
|
69
|
+
'the ledger', 'the gates', 'the grade', // S3, the differentiator
|
|
70
|
+
'How a change reaches your project', // S4
|
|
71
|
+
'Build a project', 'Join as a builder', // S7, both doors
|
|
72
|
+
'Built by agents, on Cloud', // the portable half of the footer
|
|
73
|
+
'id="who"', // the identity chip
|
|
74
|
+
]) {
|
|
75
|
+
assert.ok(child.includes(portable), `a child instance lost "${portable}"`);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('the footer keeps its attribution half and loses only the commons half', () => {
|
|
80
|
+
const child = stripApexOnly(HTML, false);
|
|
81
|
+
const foot = child.match(/<div class="foot">([\s\S]*?)<\/div>/);
|
|
82
|
+
assert.ok(foot, 'the footer survives on a child instance');
|
|
83
|
+
assert.doesNotMatch(foot[1], /non-profit/);
|
|
84
|
+
assert.match(foot[1], /Built by agents/);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ── the server-stamped head ────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
const BRAND = {
|
|
90
|
+
identity: { productName: 'Spike Harbor', worldName: 'Spike Harbor' },
|
|
91
|
+
copy: { tagline: 'A different beat entirely.' },
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
test('every head tag a crawler reads is stamped, and no token survives', () => {
|
|
95
|
+
const out = applyBrandTokens(HTML, BRAND, 'https://spike-harbor.example', 'https://builders.spike-harbor.example');
|
|
96
|
+
assert.doesNotMatch(out, /\{\{[a-zA-Z]+\}\}/, 'an unstamped {{token}} would render literally');
|
|
97
|
+
assert.match(out, /<title>Spike Harbor — A different beat entirely\.<\/title>/);
|
|
98
|
+
assert.match(out, /<link rel="canonical" href="https:\/\/spike-harbor\.example\/">/);
|
|
99
|
+
for (const prop of ['og:type', 'og:site_name', 'og:url', 'og:title', 'og:description', 'og:image']) {
|
|
100
|
+
assert.match(out, new RegExp(`property="${prop}"`), `missing ${prop}`);
|
|
101
|
+
}
|
|
102
|
+
for (const name of ['twitter:card', 'twitter:title', 'twitter:description', 'twitter:image']) {
|
|
103
|
+
assert.match(out, new RegExp(`name="${name}"`), `missing ${name}`);
|
|
104
|
+
}
|
|
105
|
+
// the head must not advertise the authoring instance to a child
|
|
106
|
+
assert.doesNotMatch(out.split('</head>')[0], /cloudbongos\.com/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('every cross-host link is stamped to THIS instance, never the platform', () => {
|
|
110
|
+
const out = applyBrandTokens(HTML, BRAND, 'https://spike-harbor.example', 'https://builders.spike-harbor.example');
|
|
111
|
+
const halls = [...out.matchAll(/data-link-hall href="([^"]*)"/g)].map((m) => m[1]);
|
|
112
|
+
assert.ok(halls.length >= 2, 'the header door and the builders door both point at the hall');
|
|
113
|
+
for (const href of halls) assert.equal(href, 'https://builders.spike-harbor.example');
|
|
114
|
+
// C6: this is the bug the stamping fixes — clientBranding() omits domains, so a
|
|
115
|
+
// client fill can never re-point these and every child sent visitors to the platform.
|
|
116
|
+
assert.doesNotMatch(out, /builders\.cloudbongos\.com/);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('a pack with no hall of its own falls back to its own apex, not the platform', () => {
|
|
120
|
+
const out = applyBrandTokens(HTML, BRAND, 'https://spike-harbor.example', '');
|
|
121
|
+
assert.match(out, /data-link-hall href="https:\/\/spike-harbor\.example"/);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('the source link is stamped from the pack repo, never a baked owner/name', () => {
|
|
125
|
+
// the authorship audit fails a hardcoded owner/name, and a baked one would
|
|
126
|
+
// point every instance at whoever authored this file
|
|
127
|
+
assert.doesNotMatch(HTML, /github\.com\/[a-z0-9-]+\/[a-z0-9-]+/i, 'no baked repo path');
|
|
128
|
+
const out = applyBrandTokens(HTML, BRAND, 'https://o.example', '', 'https://github.com/acme/harbor');
|
|
129
|
+
assert.match(out, /href="https:\/\/github\.com\/acme\/harbor"/);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('an unstamped link is removed, not left dangling', () => {
|
|
133
|
+
const out = applyBrandTokens(HTML, BRAND, 'https://o.example', '', '');
|
|
134
|
+
assert.match(out, /href=""/, 'the token resolves empty when the pack names no repo');
|
|
135
|
+
assert.match(CODE, /a\[href=""\]/, 'and the page drops those links');
|
|
136
|
+
assert.match(CODE, /\.remove\(\)/);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ── the branding cascade + portability ─────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
test('the page declares no :root — a page-local one defeats re-branding (C1)', () => {
|
|
142
|
+
const styles = [...HTML.matchAll(/<style>([\s\S]*?)<\/style>/g)].map((m) => m[1]).join('\n');
|
|
143
|
+
assert.ok(styles.length > 0, 'the page ships a local stylesheet');
|
|
144
|
+
// :root[data-mode="dark"] .bongos{…} is a DESCENDANT rule, not a :root declaration
|
|
145
|
+
// block — it scopes component tokens and cannot outrank the pack's own :root.
|
|
146
|
+
assert.doesNotMatch(styles, /:root\s*\{/, 'a bare :root{ } block outranks the injected pack');
|
|
147
|
+
assert.doesNotMatch(styles, /:root\[data-mode="dark"\]\s*\{/, 'same, for the dark tier');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('every dark-mode rule is written twice — the block and its media twin (C2)', () => {
|
|
151
|
+
const styles = [...HTML.matchAll(/<style>([\s\S]*?)<\/style>/g)].map((m) => m[1]).join('\n');
|
|
152
|
+
const explicit = (styles.match(/:root\[data-mode="dark"\]/g) || []).length;
|
|
153
|
+
const media = (styles.match(/:root:not\(\[data-mode="light"\]\)/g) || []).length;
|
|
154
|
+
assert.equal(explicit, media, 'a dark rule with no prefers-color-scheme twin only half-works');
|
|
155
|
+
assert.ok(explicit >= 2, 'the drum skin and the hero glow both flip');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('the drum head never inherits a card colour — it would turn black in dark', () => {
|
|
159
|
+
const styles = [...HTML.matchAll(/<style>([\s\S]*?)<\/style>/g)].map((m) => m[1]).join('\n');
|
|
160
|
+
const skin = styles.match(/\.bongos\{[^}]*\}/);
|
|
161
|
+
assert.ok(skin, '.bongos carries its own skin tokens');
|
|
162
|
+
assert.match(skin[0], /--skin-hi:/);
|
|
163
|
+
assert.doesNotMatch(styles, /--skin-[a-z]+:\s*var\(--bg-card\)/);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('the page ships the .sky layer the pack backdrop would otherwise show through (C3)', () => {
|
|
167
|
+
assert.match(HTML, /<div class="sky"/);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ── the static pose under reduced motion (direction §7) ───────────────────
|
|
171
|
+
// The hero animates by default, so this is an accessibility constraint that
|
|
172
|
+
// fails SILENTLY — nothing about the page looks wrong to a developer who does
|
|
173
|
+
// not have the OS setting on. It holds via a cross-FILE dependency (cosmos.css
|
|
174
|
+
// carries one global motion kill switch rather than every page repeating it),
|
|
175
|
+
// which is the right shape but an invisible one, so both halves are locked here.
|
|
176
|
+
// Verified in a real browser with reducedMotion emulation: every hero element
|
|
177
|
+
// computes animation-name:none, and the ripples additionally display:none.
|
|
178
|
+
|
|
179
|
+
const COSMOS = fs.readFileSync(path.join(ROOT, 'public-landing', 'assets', 'cosmos.css'), 'utf8');
|
|
180
|
+
const PAGE_STYLES = [...HTML.matchAll(/<style>([\s\S]*?)<\/style>/g)].map((m) => m[1]).join('\n');
|
|
181
|
+
// declarations only — the comments quote the very shapes these tests rule out
|
|
182
|
+
const PAGE_CSS = PAGE_STYLES.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
183
|
+
|
|
184
|
+
test('cosmos.css still carries the global motion kill switch the hero relies on', () => {
|
|
185
|
+
const block = COSMOS.match(/@media\s*\(prefers-reduced-motion:\s*reduce\)\s*\{([\s\S]*?)\n\}/);
|
|
186
|
+
assert.ok(block, 'cosmos.css must keep a prefers-reduced-motion block');
|
|
187
|
+
assert.match(block[1], /\*[^{]*\{[^}]*animation:\s*none\s*!important/,
|
|
188
|
+
'the universal selector + !important is what stops the hero; without it the drums keep beating');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('nothing on the page marks an animation !important and escapes that switch', () => {
|
|
192
|
+
// the ONE way this page could break the constraint on its own
|
|
193
|
+
assert.doesNotMatch(PAGE_CSS, /animation[^;:]*:[^;]*!important/,
|
|
194
|
+
'an !important animation here would outrank the global kill switch');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('the hero animations are plain declarations, so the switch reaches them', () => {
|
|
198
|
+
// every animated part of the drum kit, as the mock defines the beat
|
|
199
|
+
for (const cls of ['drumBig', 'drumSml', 'headBig', 'headSml', 'glow', 'bshadow', 'ring']) {
|
|
200
|
+
assert.match(PAGE_STYLES, new RegExp(`\\.bongos\\s+\\.${cls}\\s*\\{[^}]*animation:`),
|
|
201
|
+
`.${cls} should be animated in the default (non-reduced) case`);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('the ripples are hidden under reduced motion — they rest at full stroke', () => {
|
|
206
|
+
const reduced = PAGE_STYLES.match(/@media\s*\(prefers-reduced-motion:reduce\)\{([^}]*\})*/);
|
|
207
|
+
assert.ok(reduced, 'the page carries its own reduced-motion block');
|
|
208
|
+
assert.match(reduced[0], /\.bongos\s+\.ring\s*\{\s*display:none/);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('the self-typing eyebrow degrades to real text, not an empty span', () => {
|
|
212
|
+
// the script only decorates; markup must already carry the word
|
|
213
|
+
assert.match(HTML, /<span class="typed" id="typed">bongos<\/span>/);
|
|
214
|
+
assert.match(CODE, /matchMedia\('\(prefers-reduced-motion: reduce\)'\)/);
|
|
215
|
+
assert.match(CODE, /if \(!reduce\) \{/, 'the typing loop is skipped under reduced motion');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('nothing about the platform is hardcoded — the file is portable (C5)', () => {
|
|
219
|
+
// the source repo link is a real, deliberate attribution; every OTHER mention
|
|
220
|
+
// of an origin must be a token.
|
|
221
|
+
const withoutSource = HTML.replace(/https:\/\/github\.com\/[^"]*/g, '');
|
|
222
|
+
assert.doesNotMatch(withoutSource, /cloudbongos\.com/);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('the page reads without JavaScript: the spine is markup, not script-built', () => {
|
|
226
|
+
const body = HTML.split('<body>')[1].split('<script>')[0];
|
|
227
|
+
for (const copy of [
|
|
228
|
+
'Building software is laying',
|
|
229
|
+
'Every change is a claimed task',
|
|
230
|
+
'A rank ladder the server enforces',
|
|
231
|
+
'Nothing ships ungraded',
|
|
232
|
+
'How a change reaches your project',
|
|
233
|
+
'Start a project',
|
|
234
|
+
'Enter the Hall',
|
|
235
|
+
]) {
|
|
236
|
+
assert.ok(body.includes(copy), `"${copy}" is only reachable with scripting on`);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('the proof band ships hidden so a failed or empty feed leaves no orphan heading', () => {
|
|
241
|
+
assert.match(HTML, /<section class="band" id="proof" hidden>/);
|
|
242
|
+
// and it is only ever revealed on a non-empty 200
|
|
243
|
+
assert.match(HTML, /if \(!list\.length\) return;/);
|
|
244
|
+
assert.match(HTML, /band\.hidden = false;/);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// The page script minus its comments — these assertions are about what the code
|
|
248
|
+
// DOES, and the comments deliberately quote the shapes being ruled out.
|
|
249
|
+
const CODE = HTML.split('<script>').pop().replace(/\/\*[\s\S]*?\*\//g, '');
|
|
250
|
+
|
|
251
|
+
test('the catalog claims no liveness the public feed cannot back', () => {
|
|
252
|
+
// listFeaturedProjects projects {origin, name, tagline, art_url} — no status,
|
|
253
|
+
// so a verification chip here would be a claim nothing checked.
|
|
254
|
+
assert.doesNotMatch(CODE, /chip (live|maybe|down)/);
|
|
255
|
+
assert.doesNotMatch(CODE, /ago/);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('catalog values are never interpolated as HTML, and hrefs are scheme-checked', () => {
|
|
259
|
+
assert.doesNotMatch(CODE, /innerHTML/, 'the feed carries third-party strings');
|
|
260
|
+
assert.match(CODE, /u\.protocol === 'https:' \|\| u\.protocol === 'http:'/);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// ── the shared layer gained the primitives, and the page did not fork it ───
|
|
264
|
+
|
|
265
|
+
test('the landing links cosmos.css and does not fork the design system', () => {
|
|
266
|
+
assert.match(HTML, /<link rel="stylesheet" href="assets\/cosmos\.css">/);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('the five primitives graduated into cosmos.css, not the page (§6)', () => {
|
|
270
|
+
const css = fs.readFileSync(path.join(ROOT, 'public-landing', 'assets', 'cosmos.css'), 'utf8');
|
|
271
|
+
assert.match(css, /^\.wrap\{/m, 'the page container');
|
|
272
|
+
assert.match(css, /^h2\.sec-h\{/m, 'the type scale below h1');
|
|
273
|
+
assert.match(css, /--pad-xl:/, 'the spacing scale');
|
|
274
|
+
assert.match(css, /^\.grid\{/m, 'the grid primitive');
|
|
275
|
+
assert.match(css, /@media \(max-width:900px\)/, 'the mid breakpoint');
|
|
276
|
+
// and the page must not re-declare them locally
|
|
277
|
+
const styles = [...HTML.matchAll(/<style>([\s\S]*?)<\/style>/g)].map((m) => m[1]).join('\n');
|
|
278
|
+
assert.doesNotMatch(styles, /^\.wrap\{/m, '.wrap belongs to the shared layer');
|
|
279
|
+
assert.doesNotMatch(styles, /^\.grid\{/m, '.grid belongs to the shared layer');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// ── the assertion tests/platform_boot.mjs will make once the apex flips ────
|
|
283
|
+
|
|
284
|
+
test('the page satisfies the parked platform_boot assertions', () => {
|
|
285
|
+
assert.match(HTML, /id="who"/);
|
|
286
|
+
assert.match(HTML, /Enter the Hall/);
|
|
287
|
+
// an ORG-level action that lives only on the gate — asserted absent there
|
|
288
|
+
assert.doesNotMatch(HTML, /Create a project/);
|
|
289
|
+
});
|