@bongos/core 1.19.90 → 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 +30 -25
- package/config/branding.neutral.json +6 -6
- package/modules/dev-box/app/assets/tray/win-alert.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 +9 -9
- package/modules/dev-box/app/scripts/gen-brand.js +10 -10
- 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 +112 -64
- 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 +3 -1
- package/src/platform-server.js +26 -2
- package/tests/cloud_bongos_pack.mjs +11 -7
- package/tests/landing_page.mjs +289 -0
|
@@ -57,17 +57,23 @@ const { branding, clientBranding } = require('../src/branding');
|
|
|
57
57
|
// with src/bongos/serve-internal.js applyBrandTokens. `brand` is the client-safe pack
|
|
58
58
|
// and `publicOrigin` the resolved origin — both injected, never read ambiently, so the
|
|
59
59
|
// transform stays pure and testable.
|
|
60
|
-
function brandTokens(html, brand = {}, publicOrigin = '') {
|
|
60
|
+
function brandTokens(html, brand = {}, publicOrigin = '', buildersOrigin = '') {
|
|
61
61
|
const world = (brand.identity && brand.identity.worldName) || '';
|
|
62
62
|
const product = (brand.identity && brand.identity.productName) || '';
|
|
63
|
+
const tagline = (brand.copy && brand.copy.tagline) || '';
|
|
63
64
|
const cur = (brand.currency && brand.currency.label) || 'credits';
|
|
64
65
|
const Cur = cur ? cur.charAt(0).toUpperCase() + cur.slice(1) : cur;
|
|
66
|
+
const esc = (s) => String(s == null ? '' : s)
|
|
67
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
68
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
65
69
|
return html
|
|
66
|
-
.split('{{worldName}}').join(world)
|
|
67
|
-
.split('{{productName}}').join(product)
|
|
68
|
-
.split('{{Currency}}').join(Cur)
|
|
69
|
-
.split('{{currency}}').join(cur)
|
|
70
|
-
.split('{{
|
|
70
|
+
.split('{{worldName}}').join(esc(world))
|
|
71
|
+
.split('{{productName}}').join(esc(product))
|
|
72
|
+
.split('{{Currency}}').join(esc(Cur))
|
|
73
|
+
.split('{{currency}}').join(esc(cur))
|
|
74
|
+
.split('{{tagline}}').join(esc(tagline))
|
|
75
|
+
.split('{{publicOrigin}}').join(esc(publicOrigin || ''))
|
|
76
|
+
.split('{{buildersOrigin}}').join(esc(buildersOrigin || publicOrigin || ''));
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
// ---- the three off-box index.html injections -------------------------------
|
|
@@ -153,21 +153,45 @@ function injectBrandingGlobal(html, brand = clientBranding(), mods = clientModul
|
|
|
153
153
|
// stays brand-free (the fitness identity gate). Runs BEFORE the __BRANDING__
|
|
154
154
|
// script is inserted; the injected JSON holds resolved values (never tokens), so
|
|
155
155
|
// order is immaterial. Tokens: {{worldName}} {{productName}} {{currency}}
|
|
156
|
-
// (lowercase label) {{Currency}} (Capitalized) {{
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
// `
|
|
160
|
-
|
|
156
|
+
// (lowercase label) {{Currency}} (Capitalized) {{tagline}} (the pack's one-line
|
|
157
|
+
// pitch), plus the two ORIGINS — {{publicOrigin}} (this instance's apex) and
|
|
158
|
+
// {{buildersOrigin}} (its hall). Both origins are sourced from the FULL pack's
|
|
159
|
+
// `domains`, which clientBranding deliberately omits, so they are the only way a
|
|
160
|
+
// served page can carry a correct cross-host href or canonical/og: URL: a client
|
|
161
|
+
// fill cannot reach them, and a baked one would make every instance advertise
|
|
162
|
+
// whichever instance happened to author the file (task 1002748, direction C5/C6).
|
|
163
|
+
// Pure + exported for the test; both origins are injectable so it drives with zero I/O.
|
|
164
|
+
function applyBrandTokens(html, brand = clientBranding(), origin = safePublicOrigin(), hallOrigin = safeBuildersOrigin(), sourceRepo = safeSourceRepoUrl()) {
|
|
161
165
|
const world = (brand.identity && brand.identity.worldName) || '';
|
|
162
166
|
const product = (brand.identity && brand.identity.productName) || '';
|
|
167
|
+
const tagline = (brand.copy && brand.copy.tagline) || '';
|
|
163
168
|
const cur = (brand.currency && brand.currency.label) || 'credits';
|
|
164
169
|
const Cur = cur ? cur.charAt(0).toUpperCase() + cur.slice(1) : cur;
|
|
165
170
|
return String(html == null ? '' : html)
|
|
166
|
-
.split('{{worldName}}').join(world)
|
|
167
|
-
.split('{{productName}}').join(product)
|
|
168
|
-
.split('{{Currency}}').join(Cur)
|
|
169
|
-
.split('{{currency}}').join(cur)
|
|
170
|
-
.split('{{
|
|
171
|
+
.split('{{worldName}}').join(esc(world))
|
|
172
|
+
.split('{{productName}}').join(esc(product))
|
|
173
|
+
.split('{{Currency}}').join(esc(Cur))
|
|
174
|
+
.split('{{currency}}').join(esc(cur))
|
|
175
|
+
.split('{{tagline}}').join(esc(tagline))
|
|
176
|
+
.split('{{publicOrigin}}').join(esc(origin || ''))
|
|
177
|
+
// a pack that names no hall of its own resolves to its OWN apex — never to
|
|
178
|
+
// whichever instance authored the file
|
|
179
|
+
.split('{{buildersOrigin}}').join(esc(hallOrigin || origin || ''))
|
|
180
|
+
.split('{{sourceRepoUrl}}').join(esc(sourceRepo || ''));
|
|
181
|
+
}
|
|
182
|
+
// Every token lands in HTML — as element text, or inside a double-quoted
|
|
183
|
+
// attribute (href/content). Escaping is defence in depth, not a live fix: the
|
|
184
|
+
// only source is config/branding.json, which is committed, code-reviewed and
|
|
185
|
+
// Archon-only, never request-supplied. But a pack string carrying a quote or a
|
|
186
|
+
// '<' would otherwise break out of its attribute or close <title> early, and
|
|
187
|
+
// the values now reach four times as many places as when this was written.
|
|
188
|
+
// No token is ever emitted into a <script> or a CSS context (checked across
|
|
189
|
+
// public-landing/, hall-ui/, status-ui/) — that would need different escaping.
|
|
190
|
+
// Ampersand first, or the other replacements get double-escaped.
|
|
191
|
+
function esc(s) {
|
|
192
|
+
return String(s == null ? '' : s)
|
|
193
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
194
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
171
195
|
}
|
|
172
196
|
// The instance apex origin from the full branding pack (domains is server-only,
|
|
173
197
|
// so it never rides clientBranding). Best-effort so a branding hiccup can't crash
|
|
@@ -175,6 +199,25 @@ function applyBrandTokens(html, brand = clientBranding(), origin = safePublicOri
|
|
|
175
199
|
function safePublicOrigin() {
|
|
176
200
|
try { return branding().domains.publicOrigin || ''; } catch { return ''; }
|
|
177
201
|
}
|
|
202
|
+
// The instance's OWN builders' hall origin, same server-only source. Empty is fine:
|
|
203
|
+
// applyBrandTokens falls back to the apex, so a pack that names no hall still gets
|
|
204
|
+
// a same-instance link rather than a dangling href.
|
|
205
|
+
function safeBuildersOrigin() {
|
|
206
|
+
try { return branding().domains.buildersOrigin || ''; } catch { return ''; }
|
|
207
|
+
}
|
|
208
|
+
// The instance's own source repo, from the pack's `repo: {owner, name}` binding —
|
|
209
|
+
// another server-only field (the branding contract keeps repo off clientBranding
|
|
210
|
+
// alongside domains). A page that wants to link "read the source" must take it
|
|
211
|
+
// from here: the authorship audit fails any hardcoded owner/name, and rightly so,
|
|
212
|
+
// since a baked one would point every instance at whoever authored the file.
|
|
213
|
+
// '' when the pack names no repo — the page drops the link rather than emit a
|
|
214
|
+
// dangling href.
|
|
215
|
+
function safeSourceRepoUrl() {
|
|
216
|
+
try {
|
|
217
|
+
const repo = branding().repo;
|
|
218
|
+
return repo && repo.owner && repo.name ? `https://github.com/${repo.owner}/${repo.name}` : '';
|
|
219
|
+
} catch { return ''; }
|
|
220
|
+
}
|
|
178
221
|
// Instance product/world name for the API-discovery index (task 1918). Best-effort:
|
|
179
222
|
// a branding hiccup must not break the JSON response.
|
|
180
223
|
function safeProductName() {
|
package/src/module-api.js
CHANGED
|
@@ -607,8 +607,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
607
607
|
// landed since 1.19.88 with no explicit bump. run 31657467485. (task 1002620)
|
|
608
608
|
// 1.19.90 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
609
609
|
// landed since 1.19.89 with no explicit bump. run 31717551308. (task 1002620)
|
|
610
|
+
// 1.19.91 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
611
|
+
// landed since 1.19.90 with no explicit bump. run 31722841411. (task 1002620)
|
|
610
612
|
// ---------------------------------------------------------------------------
|
|
611
|
-
const CORE_VERSION = '1.19.
|
|
613
|
+
const CORE_VERSION = '1.19.91'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
612
614
|
|
|
613
615
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
614
616
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/src/platform-server.js
CHANGED
|
@@ -65,12 +65,36 @@ function landingPage(file) {
|
|
|
65
65
|
if (!_landingPages.has(file)) {
|
|
66
66
|
let html = '';
|
|
67
67
|
try {
|
|
68
|
-
html = injectBrandingGlobal(fs.readFileSync(path.join(LANDING_DIR, file), 'utf8'));
|
|
68
|
+
html = injectBrandingGlobal(stripApexOnly(fs.readFileSync(path.join(LANDING_DIR, file), 'utf8')));
|
|
69
69
|
} catch { /* absent → '' */ }
|
|
70
70
|
_landingPages.set(file, html);
|
|
71
71
|
}
|
|
72
72
|
return _landingPages.get(file);
|
|
73
73
|
}
|
|
74
|
+
|
|
75
|
+
// THE APEX CONDITIONAL (task 1002748). public-landing/ ships wholesale to every
|
|
76
|
+
// instance, so one file has to render two shapes: the platform's own front door,
|
|
77
|
+
// and a child project's. Sections that are true only of the platform — the
|
|
78
|
+
// catalog of OTHER instances, the "capture-resistant commons" claim, the door
|
|
79
|
+
// into the catalog — are wrapped in <!--apex-only:start--> … <!--apex-only:end-->
|
|
80
|
+
// and REMOVED here on any instance that is not the hub.
|
|
81
|
+
//
|
|
82
|
+
// Removed, not CSS-hidden: a child instance must not ship the platform's claims
|
|
83
|
+
// to a crawler, and hiding is not the same as not saying it.
|
|
84
|
+
//
|
|
85
|
+
// Comment sentinels rather than an attribute selector because this is a string
|
|
86
|
+
// transform, not a DOM one — matching a balanced close tag with a regex is the
|
|
87
|
+
// fragile shape, and a sentinel pair has no such failure mode.
|
|
88
|
+
//
|
|
89
|
+
// `platform-identity` IS the hub flag, not a proxy for it: its manifest reads
|
|
90
|
+
// "Enabled ONLY on the hub instance; ordinary hosted + self-hosted instances
|
|
91
|
+
// leave it off", and it is the module that owns the very /projects catalog these
|
|
92
|
+
// sections point at. So this asks the instance what it IS, and nothing anywhere
|
|
93
|
+
// has to know the platform's hostname (direction C5).
|
|
94
|
+
const APEX_ONLY = /<!--apex-only:start-->[\s\S]*?<!--apex-only:end-->/g;
|
|
95
|
+
function stripApexOnly(html, isHub = isModuleEnabled('platform-identity')) {
|
|
96
|
+
return isHub ? html : String(html).replace(APEX_ONLY, '');
|
|
97
|
+
}
|
|
74
98
|
function landingHtml() { return landingPage('index.html'); }
|
|
75
99
|
// The interactive landing gate (public-landing/gate/, ported from amazonprimea
|
|
76
100
|
// in task 1002407). When an instance ships it, it is the apex front door
|
|
@@ -232,4 +256,4 @@ if (require.main === module) {
|
|
|
232
256
|
});
|
|
233
257
|
}
|
|
234
258
|
|
|
235
|
-
module.exports = { buildPlatformApp, startBackgroundServices };
|
|
259
|
+
module.exports = { buildPlatformApp, startBackgroundServices, stripApexOnly };
|
|
@@ -34,14 +34,16 @@ test('R74: the vanilla pack carries the Cloud Bongos LOOK (sunrise palette, Balo
|
|
|
34
34
|
const c = clientBranding(vanilla());
|
|
35
35
|
// fired down from the original #f97316 so white-on-accent clears WCAG AA
|
|
36
36
|
// (2.80:1 -> 4.61:1, task 1002749) — same ember hue, readable
|
|
37
|
-
assert.equal(c.theme.colors.accent, '#ce4806', '
|
|
38
|
-
|
|
37
|
+
assert.equal(c.theme.colors.accent, '#ce4806', 'ember orange accent');
|
|
38
|
+
// ember marigold (task 1002748): the base is PIGMENTED, not near-white. The
|
|
39
|
+
// old #fff7ed sat within a few points of the cards and the page read as fog.
|
|
40
|
+
assert.equal(c.theme.colors.bg, '#f9d9a8', 'marigold base');
|
|
39
41
|
assert.match(c.theme.fonts.display, /Baloo 2/);
|
|
40
42
|
assert.match(c.theme.fonts.body, /Quicksand/);
|
|
41
43
|
assert.ok(c.theme.fonts.links.some((l) => /Baloo\+2/.test(l) && /Quicksand/.test(l) && /Fira\+Code/.test(l)), 'loads Baloo 2 + Quicksand + Fira Code');
|
|
42
44
|
assert.match(c.theme.favicon, /circle/, 'bongo-pair favicon (circles), not the Greek Α');
|
|
43
45
|
assert.doesNotMatch(c.theme.favicon, /font-family='serif'>Α/);
|
|
44
|
-
assert.
|
|
46
|
+
assert.equal(c.theme.backdrop, '#f9d9a8', 'the marigold ground holds all the way down');
|
|
45
47
|
assert.equal(c.copy.tagline, 'Building software is laying down a beat.');
|
|
46
48
|
assert.equal(c.rankLabels.archon, 'Owner', 'plain rank labels, not "Archon"');
|
|
47
49
|
});
|
|
@@ -50,9 +52,9 @@ test('R74: serving the hall on a vanilla boot injects the Cloud Bongos sunrise t
|
|
|
50
52
|
const c = clientBranding(vanilla());
|
|
51
53
|
const html = '<head><link rel="stylesheet" href="/builders/style.css"><title>x</title></head><body></body>';
|
|
52
54
|
const out = injectBrandingGlobal(html, c);
|
|
53
|
-
// palette override +
|
|
55
|
+
// palette override + the marigold body backdrop + bongo favicon + the type links
|
|
54
56
|
assert.match(out, /:root\{[^}]*--accent:#ce4806;/);
|
|
55
|
-
assert.match(out, /body\{background
|
|
57
|
+
assert.match(out, /body\{background:#f9d9a8;\}/);
|
|
56
58
|
assert.match(out, /<link rel="icon" href="data:image\/svg\+xml[^"]*circle/);
|
|
57
59
|
assert.match(out, /fonts\.googleapis\.com\/css2\?family=Baloo\+2/);
|
|
58
60
|
// and it lands AFTER the stylesheet so the Cloud Bongos look wins
|
|
@@ -63,8 +65,10 @@ test('R74: themeMarkup paints the backdrop only when the pack supplies one', ()
|
|
|
63
65
|
// OTB-shaped pack (no backdrop) → no body{background} rule.
|
|
64
66
|
const otb = themeMarkup({ colors: { bg: '#ece0c2' }, fonts: { links: [] }, favicon: '', backdrop: '' });
|
|
65
67
|
assert.doesNotMatch(otb, /body\{background/);
|
|
66
|
-
// Cloud Bongos pack → backdrop present.
|
|
67
|
-
|
|
68
|
+
// Cloud Bongos pack → backdrop present. Asserted by PRESENCE, not by shape:
|
|
69
|
+
// this test's subject is whether a backdrop paints at all, so it must not
|
|
70
|
+
// re-break every time the palette legitimately changes.
|
|
71
|
+
assert.match(themeMarkup(clientBranding(vanilla()).theme), /body\{background:.+\}/);
|
|
68
72
|
});
|
|
69
73
|
|
|
70
74
|
test('customer-0: GDS_BRANDING_FILE selects the cloudbongos.com instance pack (task 1374)', () => {
|
|
@@ -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
|
+
});
|