@bongos/core 1.19.387 → 1.19.388
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 +130 -50
- package/config/branding.neutral.json +19 -19
- package/config/design-tokens.neutral.json +31 -36
- package/docs/adr/0196-chrome-world-token-contract-and-the-landing-sheets.md +31 -0
- package/docs/adr/README.md +1 -0
- package/docs/branding-contract.md +1 -1
- package/docs/copy-inventory.md +78 -53
- package/docs/copy-registry.json +389 -120
- package/docs/file-map.md +14 -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/dmg-background.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
- package/modules/dev-box/app/build-resources/icon.png +0 -0
- package/modules/dev-box/app/renderer/index.html +1 -1
- package/modules/dev-box/app/renderer/styles.css +3 -1
- package/modules/dev-box/app/renderer/theme.generated.css +13 -13
- package/modules/dev-box/app/scripts/gen-brand.js +13 -13
- 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/modules/provisioning/routes/provisioning.js +4 -4
- package/modules/status-ui/public/explore.html +12 -6
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +82 -64
- package/public-landing/assets/world/og-card.jpg +0 -0
- package/public-landing/assets/world/r2-bongos-chrome-a-cut.webp +0 -0
- package/public-landing/assets/world/r2-bongos-chrome-a-cut.webp.json +12 -0
- package/public-landing/assets/world/r2-bongos-chrome-b.jpg +0 -0
- package/public-landing/assets/world/r2-bongos-chrome-b.jpg.json +12 -0
- package/public-landing/assets/world/r2-chrome-blob-one-cut.webp +0 -0
- package/public-landing/assets/world/r2-chrome-blob-one-cut.webp.json +13 -0
- package/public-landing/assets/world/r2-chrome-blob-one.jpg +0 -0
- package/public-landing/assets/world/r2-chrome-blob-one.jpg.json +12 -0
- package/public-landing/assets/world/r2-chrome-blobs-set-cut.webp +0 -0
- package/public-landing/assets/world/r2-chrome-blobs-set-cut.webp.json +13 -0
- package/public-landing/assets/world/r2-chrome-blobs-set.jpg +0 -0
- package/public-landing/assets/world/r2-chrome-blobs-set.jpg.json +12 -0
- package/public-landing/assets/world.css +241 -0
- package/public-landing/contact.html +1 -1
- package/public-landing/index.html +1139 -580
- package/public-landing/privacy.html +1 -1
- package/public-landing/projects.html +2 -2
- package/public-landing/terms.html +1 -1
- package/src/bongos/platform-visibility-gate.js +8 -7
- package/src/bongos/stealth-gate.js +9 -8
- package/src/module-api.js +3 -1
- package/src/ui/adapters/claude-design/index.js +2 -2
- package/tests/cloud_bongos_pack.mjs +22 -18
- package/tests/cosmos_token_contrast.mjs +23 -8
- package/tests/landing_legal_pages.mjs +8 -5
- package/tests/landing_page.mjs +295 -166
- package/tests/platform_boot.mjs +3 -1
- package/tests/projects_hub.mjs +9 -5
- package/tests/projects_hub_pre_uat.mjs +3 -1
- package/tests/world_tokens.mjs +351 -0
package/tests/landing_page.mjs
CHANGED
|
@@ -1,21 +1,32 @@
|
|
|
1
|
-
// tests/landing_page.mjs — the apex landing page
|
|
1
|
+
// tests/landing_page.mjs — the apex landing page in the chrome world
|
|
2
|
+
// (task 1003298, goal 1000080; rewritten from the task 1002748 original).
|
|
2
3
|
//
|
|
3
|
-
// Contract: docs/design/landing-page-direction.md +
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// Contract: docs/design/landing-page-direction-v2.md + the approved composition
|
|
5
|
+
// docs/design/mocks/landing/v3.html, built on public-landing/assets/world.css.
|
|
6
|
+
// The composition itself is graded by eye and by browser probes; what THIS file
|
|
7
|
+
// locks are the properties that fail SILENTLY — nothing about the page looks
|
|
8
|
+
// wrong in a browser when any of them breaks:
|
|
6
9
|
//
|
|
7
10
|
// * the apex conditional — public-landing/ ships wholesale to every instance,
|
|
8
|
-
// so the platform
|
|
11
|
+
// so the platform's claims AND the platform's numbers must be REMOVED (not
|
|
12
|
+
// hidden) off the hub, while the portable spine still reads as a landing;
|
|
9
13
|
// * the server-stamped head — social tags are read before JS runs, so they
|
|
10
14
|
// cannot be client-filled, and they must not be baked to one instance;
|
|
11
15
|
// * the branding cascade — a page-local :root silently outranks the pack's
|
|
12
|
-
// injected theme and defeats re-branding (direction C1)
|
|
13
|
-
//
|
|
16
|
+
// injected theme and defeats re-branding (direction C1), and a dark rule
|
|
17
|
+
// written once only works for half of users (C2);
|
|
18
|
+
// * the motion kill switch — the hero breathes by default, so reduced motion
|
|
19
|
+
// is an accessibility constraint nobody sees fail; it holds through a
|
|
20
|
+
// cross-FILE dependency (world.css carries ONE universal switch rather than
|
|
21
|
+
// every page repeating it), which is the right shape but an invisible one;
|
|
22
|
+
// * the live-data contract — every figure and row on the page is baked real
|
|
23
|
+
// and then refreshed from five public feeds, with third-party strings in
|
|
24
|
+
// the payload; the no-JS state, the XSS discipline and the definitions of
|
|
25
|
+
// the figures are all invisible to a passing render.
|
|
14
26
|
//
|
|
15
|
-
// Deliberately NOT asserted here: that GET / returns the page.
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// This file tests the artifact that flip will serve.
|
|
27
|
+
// Deliberately NOT asserted here: that GET / returns the page (tests/platform_boot.mjs
|
|
28
|
+
// owns that), and nothing about geometry or colour — the world's token layer is
|
|
29
|
+
// measured by tests/world_tokens.mjs.
|
|
19
30
|
|
|
20
31
|
import { test } from 'node:test';
|
|
21
32
|
import assert from 'node:assert/strict';
|
|
@@ -27,17 +38,33 @@ import { createRequire } from 'node:module';
|
|
|
27
38
|
const require = createRequire(import.meta.url);
|
|
28
39
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
40
|
const HTML = fs.readFileSync(path.join(ROOT, 'public-landing', 'index.html'), 'utf8');
|
|
41
|
+
const WORLD = fs.readFileSync(path.join(ROOT, 'public-landing', 'assets', 'world.css'), 'utf8');
|
|
30
42
|
|
|
31
43
|
const { stripApexOnly } = require('../src/platform-server.js');
|
|
32
44
|
const { applyBrandTokens } = require('../src/bongos/serve-internal.js');
|
|
33
45
|
|
|
34
|
-
//
|
|
46
|
+
// The page's one <style> element, and the same minus its comments. Several
|
|
47
|
+
// assertions below rule out shapes the comments deliberately QUOTE, so they read
|
|
48
|
+
// the stripped copy; the rest read the source so a value inside a comment still
|
|
49
|
+
// counts as documentation, not as a declaration.
|
|
50
|
+
const PAGE_STYLES = [...HTML.matchAll(/<style>([\s\S]*?)<\/style>/g)].map((m) => m[1]).join('\n');
|
|
51
|
+
const PAGE_CSS = PAGE_STYLES.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
52
|
+
// The page script minus its comments — these assertions are about what the code
|
|
53
|
+
// DOES, and the comments quote the very shapes being ruled out.
|
|
54
|
+
const CODE = HTML.split('<script>').pop().replace(/\/\*[\s\S]*?\*\//g, '');
|
|
55
|
+
// The pre-paint mode script, which is the FIRST <script> in <head>.
|
|
56
|
+
const PREPAINT = HTML.split('<script>')[1].split('</script>')[0];
|
|
57
|
+
|
|
58
|
+
// ── (1) the apex conditional ───────────────────────────────────────────────
|
|
35
59
|
|
|
36
60
|
test('the apex-only sentinels are balanced and each one wraps content', () => {
|
|
37
61
|
const starts = HTML.match(/<!--apex-only:start-->/g) || [];
|
|
38
62
|
const ends = HTML.match(/<!--apex-only:end-->/g) || [];
|
|
39
63
|
assert.equal(starts.length, ends.length, 'every apex-only:start needs its end');
|
|
40
|
-
|
|
64
|
+
// four sections plus every baked apex FIGURE (task 1003298: the page bakes real
|
|
65
|
+
// numbers as its no-JS state, and a child must show an empty figure, not the
|
|
66
|
+
// platform's count, until its own feed answers)
|
|
67
|
+
assert.ok(starts.length >= 4, 'the direction marks at least four apex-only regions');
|
|
41
68
|
// no start after an unclosed start — a nested pair would make the non-greedy
|
|
42
69
|
// strip close on the wrong sentinel and leak the platform's claims.
|
|
43
70
|
let depth = 0;
|
|
@@ -55,24 +82,60 @@ test('the hub keeps every apex-only section', () => {
|
|
|
55
82
|
test('a child instance is served none of the platform-only claims', () => {
|
|
56
83
|
const child = stripApexOnly(HTML, false);
|
|
57
84
|
assert.doesNotMatch(child, /<!--apex-only:(start|end)-->/, 'no sentinel survives the strip');
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
85
|
+
for (const claim of [
|
|
86
|
+
['Verro Labs', 'a child instance is not built by Verro Labs'],
|
|
87
|
+
['Built on Cloud Bongos', 'a child has no catalog of other instances'],
|
|
88
|
+
['Bongos Managed', "the commercial offer is the platform's, not a child's"],
|
|
89
|
+
['capture-resistant commons', "the commons claim is the platform's, not a child's"],
|
|
90
|
+
]) {
|
|
91
|
+
assert.ok(!child.includes(claim[0]), claim[1]);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("a child instance is served none of the platform's NUMBERS either", () => {
|
|
96
|
+
// The v3 composition puts real ledger values in the markup as the no-JS state.
|
|
97
|
+
// A claim is obvious when it leaks; a number is not — a fresh child instance
|
|
98
|
+
// would silently advertise the hub's 2,107 shipped tasks and its V1 progress
|
|
99
|
+
// line until its own feeds answered. Both are apex-only for exactly that reason.
|
|
100
|
+
// the MARKUP only: the page script names every one of these hooks in a
|
|
101
|
+
// querySelector, so scanning the whole file would match its own selectors.
|
|
102
|
+
const child = stripApexOnly(HTML, false)
|
|
103
|
+
.split('<body>')[1].split('<script>')[0]
|
|
104
|
+
.replace(/<!--[\s\S]*?-->/g, '');
|
|
105
|
+
assert.deepEqual(
|
|
106
|
+
[...child.matchAll(/\d,\d{3}/g)].map((m) => m[0]), [],
|
|
107
|
+
'a thousands-separated figure survived the strip',
|
|
108
|
+
);
|
|
109
|
+
for (const [what, re, expected] of [
|
|
110
|
+
['the three hero figures', /data-fig="[a-z]+"[^>]*>([\s\S]*?)<\/span>/g, 3],
|
|
111
|
+
['the ledger line', /data-ledger="line"[^>]*>([\s\S]*?)<\/p>/g, 1],
|
|
112
|
+
['the ledger meter', /data-ledger="meter"[^>]*>([\s\S]*?)<\/div>/g, 1],
|
|
113
|
+
]) {
|
|
114
|
+
const found = [...child.matchAll(re)];
|
|
115
|
+
assert.equal(found.length, expected, `${what}: hook missing from the child page`);
|
|
116
|
+
for (const m of found) {
|
|
117
|
+
assert.equal(m[1].trim(), '', `${what} still carries a baked apex value: ${m[1].trim()}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const rows = child.match(/data-rows[^>]*>([\s\S]*?)<\/ol>/);
|
|
121
|
+
assert.ok(rows, 'the ledger list survives on a child instance');
|
|
122
|
+
assert.equal(rows[1].trim(), '', 'the baked ledger rows are the apex\'s own shipped tasks');
|
|
65
123
|
});
|
|
66
124
|
|
|
67
125
|
test('a child instance keeps the whole portable spine', () => {
|
|
68
126
|
const child = stripApexOnly(HTML, false);
|
|
69
127
|
for (const portable of [
|
|
70
|
-
'Building Digitally,
|
|
71
|
-
'
|
|
72
|
-
'A
|
|
73
|
-
'
|
|
74
|
-
'
|
|
75
|
-
'
|
|
128
|
+
'Building Digitally,', // the tagline, first half
|
|
129
|
+
'as simple as Bongos.', // the tagline, second half
|
|
130
|
+
'A project as a codebase', // the lifecycle heading
|
|
131
|
+
'A laddered beat progresses all tasks', // its subtitle
|
|
132
|
+
'Start a project', 'Find a project', // the closing pair
|
|
133
|
+
'Built by agents, on Cloud', // the portable half of the footer
|
|
134
|
+
'id="who"', // the identity slot
|
|
135
|
+
'The six most recent, straight from the ledger.', // the ledger's own sub-line
|
|
136
|
+
// the live hooks: a child's OWN feeds fill these, so the shells must survive
|
|
137
|
+
'data-fig="shipped"', 'data-fig="value"', 'data-fig="builders"',
|
|
138
|
+
'data-ledger="line"', 'data-ledger="meter"', 'data-rows',
|
|
76
139
|
]) {
|
|
77
140
|
assert.ok(child.includes(portable), `a child instance lost "${portable}"`);
|
|
78
141
|
}
|
|
@@ -80,23 +143,25 @@ test('a child instance keeps the whole portable spine', () => {
|
|
|
80
143
|
|
|
81
144
|
test('the footer keeps its attribution half and loses only the commons half', () => {
|
|
82
145
|
const child = stripApexOnly(HTML, false);
|
|
83
|
-
const foot = child.match(/<div class="
|
|
146
|
+
const foot = child.match(/<div class="footrow">([\s\S]*?)<\/div>/);
|
|
84
147
|
assert.ok(foot, 'the footer survives on a child instance');
|
|
85
148
|
assert.doesNotMatch(foot[1], /non-profit/);
|
|
86
149
|
assert.match(foot[1], /Built by agents/);
|
|
87
150
|
});
|
|
88
151
|
|
|
89
|
-
// ── the server-stamped head
|
|
152
|
+
// ── (2) the server-stamped head ────────────────────────────────────────────
|
|
90
153
|
|
|
91
154
|
const BRAND = {
|
|
92
155
|
identity: { productName: 'Spike Harbor', worldName: 'Spike Harbor' },
|
|
93
156
|
copy: { tagline: 'A different beat entirely.' },
|
|
94
157
|
};
|
|
158
|
+
const stamp = (html = HTML, origin = 'https://spike-harbor.example', hall = 'https://builders.spike-harbor.example') =>
|
|
159
|
+
applyBrandTokens(html, BRAND, origin, hall);
|
|
95
160
|
|
|
96
161
|
test('every head tag a crawler reads is stamped, and no token survives', () => {
|
|
97
|
-
const out =
|
|
162
|
+
const out = stamp();
|
|
98
163
|
assert.doesNotMatch(out, /\{\{[a-zA-Z]+\}\}/, 'an unstamped {{token}} would render literally');
|
|
99
|
-
assert.match(out, /<title>Spike Harbor
|
|
164
|
+
assert.match(out, /<title>Spike Harbor: A different beat entirely\.<\/title>/);
|
|
100
165
|
assert.match(out, /<link rel="canonical" href="https:\/\/spike-harbor\.example\/">/);
|
|
101
166
|
for (const prop of ['og:type', 'og:site_name', 'og:url', 'og:title', 'og:description', 'og:image']) {
|
|
102
167
|
assert.match(out, new RegExp(`property="${prop}"`), `missing ${prop}`);
|
|
@@ -108,12 +173,30 @@ test('every head tag a crawler reads is stamped, and no token survives', () => {
|
|
|
108
173
|
assert.doesNotMatch(out.split('</head>')[0], /cloudbongos\.com/);
|
|
109
174
|
});
|
|
110
175
|
|
|
176
|
+
test('the social card is the world card, stamped onto THIS instance', () => {
|
|
177
|
+
// og:image is the one head tag whose value is a PATH, so it is the one that
|
|
178
|
+
// silently rots: the retired figure buddha.png outlived two redesigns as the
|
|
179
|
+
// page's social image. It must be the card captured from the built page, and
|
|
180
|
+
// it must be origin-stamped or every child's share preview loads from whoever
|
|
181
|
+
// authored the file.
|
|
182
|
+
const out = stamp();
|
|
183
|
+
assert.doesNotMatch(HTML, /buddha\.png/, 'the retired hero figure is not this page\'s social card');
|
|
184
|
+
for (const attr of ['og:image', 'twitter:image']) {
|
|
185
|
+
const m = out.match(new RegExp(`(?:property|name)="${attr}" content="([^"]*)"`));
|
|
186
|
+
assert.ok(m, `missing ${attr}`);
|
|
187
|
+
assert.equal(m[1], 'https://spike-harbor.example/assets/world/og-card.jpg');
|
|
188
|
+
}
|
|
189
|
+
assert.ok(
|
|
190
|
+
fs.existsSync(path.join(ROOT, 'public-landing', 'assets', 'world', 'og-card.jpg')),
|
|
191
|
+
'the og card the head names must exist under public-landing/',
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
111
195
|
test('every cross-host link is stamped to THIS instance, never the platform', () => {
|
|
112
|
-
const out =
|
|
196
|
+
const out = stamp();
|
|
113
197
|
const halls = [...out.matchAll(/data-link-hall href="([^"]*)"/g)].map((m) => m[1]);
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
assert.ok(halls.length >= 1, 'the header Hall entry points at the hall');
|
|
198
|
+
// the header nav pill and the footer entry
|
|
199
|
+
assert.ok(halls.length >= 2, 'the header and the footer both point at the hall');
|
|
117
200
|
for (const href of halls) assert.equal(href, 'https://builders.spike-harbor.example');
|
|
118
201
|
// C6: this is the bug the stamping fixes — clientBranding() omits domains, so a
|
|
119
202
|
// client fill can never re-point these and every child sent visitors to the platform.
|
|
@@ -121,27 +204,10 @@ test('every cross-host link is stamped to THIS instance, never the platform', ()
|
|
|
121
204
|
});
|
|
122
205
|
|
|
123
206
|
test('a pack with no hall of its own falls back to its own apex, not the platform', () => {
|
|
124
|
-
const out =
|
|
207
|
+
const out = stamp(HTML, 'https://spike-harbor.example', '');
|
|
125
208
|
assert.match(out, /data-link-hall href="https:\/\/spike-harbor\.example"/);
|
|
126
209
|
});
|
|
127
210
|
|
|
128
|
-
test('no repo path is baked into the page', () => {
|
|
129
|
-
// task 1002934 removed the "What you own" section and with it the page's only
|
|
130
|
-
// {{sourceRepoUrl}} link (owner-accepted). What must still hold: no HARDCODED
|
|
131
|
-
// owner/name — a baked one would point every instance at whoever authored
|
|
132
|
-
// this file, and the authorship audit fails it.
|
|
133
|
-
assert.doesNotMatch(HTML, /github\.com\/[a-z0-9-]+\/[a-z0-9-]+/i, 'no baked repo path');
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test('an unstamped link is removed, not left dangling', () => {
|
|
137
|
-
// the page keeps its empty-href scrubber so ANY future unstamped token link
|
|
138
|
-
// (the header hall entry today) is dropped rather than rendered dead.
|
|
139
|
-
const out = applyBrandTokens(HTML, BRAND, 'https://o.example', '');
|
|
140
|
-
assert.match(CODE, /a\[href=""\]/, 'the page drops empty-href links');
|
|
141
|
-
assert.match(CODE, /\.remove\(\)/);
|
|
142
|
-
assert.ok(out.length > 0);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
211
|
test('a pack with NO domains drops the hall pill server-side rather than stamping href="" (task 1003242)', () => {
|
|
146
212
|
// the hub has no client scrubber, so an empty token must never reach it as a dead link
|
|
147
213
|
const HUB = fs.readFileSync(path.join(ROOT, 'public-landing', 'projects.html'), 'utf8');
|
|
@@ -152,64 +218,91 @@ test('a pack with NO domains drops the hall pill server-side rather than stampin
|
|
|
152
218
|
assert.doesNotMatch(none, /\{\{[a-zA-Z]+\}\}/, 'every other token still stamps');
|
|
153
219
|
const apexOnly = applyBrandTokens(HUB, BRAND, 'https://spike-harbor.example', '');
|
|
154
220
|
assert.match(apexOnly, /data-link-hall href="https:\/\/spike-harbor\.example"/, 'an apex alone keeps the pill, pointed at the apex');
|
|
155
|
-
const both = applyBrandTokens(HUB, BRAND, 'https://spike-harbor.example', 'https://builders.spike-harbor.example');
|
|
156
|
-
assert.match(both, /data-link-hall href="https:\/\/builders\.spike-harbor\.example"/);
|
|
157
221
|
// the landing page too — its client-side scrubber is no longer the only guard
|
|
158
222
|
assert.doesNotMatch(applyBrandTokens(HTML, BRAND, '', ''), /<a\b[^>]*href=""/);
|
|
159
223
|
// a token INSIDE a longer href is a relative path once stamped empty, not a dead link — left alone
|
|
160
224
|
assert.equal(applyBrandTokens('<a href="{{publicOrigin}}/docs">Docs</a>', BRAND, '', ''), '<a href="/docs">Docs</a>');
|
|
161
225
|
});
|
|
162
226
|
|
|
163
|
-
|
|
227
|
+
test('an unstamped link is removed, not left dangling', () => {
|
|
228
|
+
// the page keeps its empty-href scrubber so ANY future unstamped token link
|
|
229
|
+
// is dropped client-side rather than rendered dead.
|
|
230
|
+
assert.match(CODE, /a\[href=""\]/, 'the page drops empty-href links');
|
|
231
|
+
assert.match(CODE, /\.remove\(\)/);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('no repo path is baked into the page', () => {
|
|
235
|
+
// A hardcoded owner/name would point every instance at whoever authored this
|
|
236
|
+
// file, and the authorship audit fails it. The AGPL link is to gnu.org, which
|
|
237
|
+
// is the licence, not a repo.
|
|
238
|
+
assert.doesNotMatch(HTML, /github\.com\/[a-z0-9-]+\/[a-z0-9-]+/i, 'no baked repo path');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ── (3) the branding cascade + portability ─────────────────────────────────
|
|
164
242
|
|
|
165
243
|
test('the page declares no :root — a page-local one defeats re-branding (C1)', () => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
assert.doesNotMatch(
|
|
171
|
-
assert.doesNotMatch(
|
|
244
|
+
assert.ok(PAGE_STYLES.length > 0, 'the page ships a local stylesheet');
|
|
245
|
+
// :root[data-mode="dark"] body{…} and :root[data-mode="dark"] .sun{…} are
|
|
246
|
+
// DESCENDANT rules, not :root declaration blocks — they scope page-local tokens
|
|
247
|
+
// and cannot outrank the pack's own :root.
|
|
248
|
+
assert.doesNotMatch(PAGE_CSS, /:root\s*\{/, 'a bare :root{ } block outranks the injected pack');
|
|
249
|
+
assert.doesNotMatch(PAGE_CSS, /:root\[data-mode="dark"\]\s*\{/, 'same, for the dark tier');
|
|
172
250
|
});
|
|
173
251
|
|
|
174
252
|
test('every dark-mode rule is written twice — the block and its media twin (C2)', () => {
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
const media = (styles.match(/:root:not\(\[data-mode="light"\]\)/g) || []).length;
|
|
253
|
+
const explicit = (PAGE_CSS.match(/:root\[data-mode="dark"\]/g) || []).length;
|
|
254
|
+
const media = (PAGE_CSS.match(/:root:not\(\[data-mode="light"\]\)/g) || []).length;
|
|
178
255
|
assert.equal(explicit, media, 'a dark rule with no prefers-color-scheme twin only half-works');
|
|
179
|
-
|
|
256
|
+
// the shipped page writes four: the mode-toggle icon swap (.sun and .moon are
|
|
257
|
+
// one selector list each), the wide plate/geometry token block, and the two
|
|
258
|
+
// narrow ones. The floor is the count observed at the build, so deleting a
|
|
259
|
+
// twin fails even when the pair stays balanced.
|
|
260
|
+
assert.ok(explicit >= 4, `expected at least 4 dark rules, found ${explicit}`);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('the page links world.css root-absolute, and does not fork the hub sheet', () => {
|
|
264
|
+
// root-absolute so landingPage()'s ?v= content stamp applies: the relative
|
|
265
|
+
// assets/cosmos.css link this page used to carry was never stamped, and a
|
|
266
|
+
// four-hour browser cache then styled new markup with an old sheet (task 1002766).
|
|
267
|
+
assert.match(HTML, /<link rel="stylesheet" href="\/assets\/world\.css">/);
|
|
268
|
+
// cosmos.css is the HUB's component layer (/projects + the legal pages). The
|
|
269
|
+
// landing lives in the world's token layer alone; linking both would give the
|
|
270
|
+
// page two competing derived tiers.
|
|
271
|
+
assert.doesNotMatch(HTML, /cosmos\.css/, 'the landing does not link the hub sheet');
|
|
180
272
|
});
|
|
181
273
|
|
|
182
|
-
test('
|
|
183
|
-
|
|
184
|
-
const skin = styles.match(/\.bongos\{[^}]*\}/);
|
|
185
|
-
assert.ok(skin, '.bongos carries its own skin tokens');
|
|
186
|
-
assert.match(skin[0], /--skin-hi:/);
|
|
187
|
-
assert.doesNotMatch(styles, /--skin-[a-z]+:\s*var\(--bg-card\)/);
|
|
274
|
+
test('nothing about the platform is hardcoded — the file is portable (C5)', () => {
|
|
275
|
+
assert.doesNotMatch(HTML, /cloudbongos\.com/);
|
|
188
276
|
});
|
|
189
277
|
|
|
190
|
-
test('the page
|
|
191
|
-
|
|
278
|
+
test('the page reads without JavaScript: the spine is markup, not script-built', () => {
|
|
279
|
+
const body = HTML.split('<body>')[1].split('<script>')[0];
|
|
280
|
+
for (const copy of [
|
|
281
|
+
'Building Digitally,', // the hero, first half
|
|
282
|
+
'as simple as Bongos.', // the hero, second half
|
|
283
|
+
'A project as a codebase', // the lifecycle heading
|
|
284
|
+
'Start a project', 'Find a project', // the closing pair
|
|
285
|
+
'The six most recent, straight from the ledger.', // the ledger's baked sub-line
|
|
286
|
+
'Skip to content',
|
|
287
|
+
]) {
|
|
288
|
+
assert.ok(body.includes(copy), `"${copy}" is only reachable with scripting on`);
|
|
289
|
+
}
|
|
290
|
+
// the sign-in door is markup; the script only UPGRADES it to the signed-in chip
|
|
291
|
+
assert.match(body, /class="pill signin" href="\/api\/bongos\/auth\/web\/start\?return=\/">Sign in</);
|
|
192
292
|
});
|
|
193
293
|
|
|
194
|
-
// ── the
|
|
294
|
+
// ── (4) the motion kill switch ─────────────────────────────────────────────
|
|
195
295
|
// The hero animates by default, so this is an accessibility constraint that
|
|
196
296
|
// fails SILENTLY — nothing about the page looks wrong to a developer who does
|
|
197
|
-
// not have the OS setting on. It holds via a cross-FILE dependency (
|
|
198
|
-
// carries one global
|
|
199
|
-
//
|
|
200
|
-
// Verified in a real browser with reducedMotion emulation: every hero element
|
|
201
|
-
// computes animation-name:none, and the ripples additionally display:none.
|
|
202
|
-
|
|
203
|
-
const COSMOS = fs.readFileSync(path.join(ROOT, 'public-landing', 'assets', 'cosmos.css'), 'utf8');
|
|
204
|
-
const PAGE_STYLES = [...HTML.matchAll(/<style>([\s\S]*?)<\/style>/g)].map((m) => m[1]).join('\n');
|
|
205
|
-
// declarations only — the comments quote the very shapes these tests rule out
|
|
206
|
-
const PAGE_CSS = PAGE_STYLES.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
297
|
+
// not have the OS setting on. It holds via a cross-FILE dependency (world.css
|
|
298
|
+
// carries one global switch rather than every page repeating it), which is the
|
|
299
|
+
// right shape but an invisible one, so both halves are locked here.
|
|
207
300
|
|
|
208
|
-
test('
|
|
209
|
-
const block =
|
|
210
|
-
assert.ok(block, '
|
|
301
|
+
test('world.css still carries the global motion kill switch the hero relies on', () => {
|
|
302
|
+
const block = WORLD.match(/@media\s*\(prefers-reduced-motion:\s*reduce\)\s*\{([\s\S]*?)\n\}/);
|
|
303
|
+
assert.ok(block, 'world.css must keep a prefers-reduced-motion block');
|
|
211
304
|
assert.match(block[1], /\*[^{]*\{[^}]*animation:\s*none\s*!important/,
|
|
212
|
-
'the universal selector + !important is what stops the
|
|
305
|
+
'the universal selector + !important is what stops the sculpture; without it the blob keeps breathing');
|
|
213
306
|
});
|
|
214
307
|
|
|
215
308
|
test('nothing on the page marks an animation !important and escapes that switch', () => {
|
|
@@ -218,109 +311,145 @@ test('nothing on the page marks an animation !important and escapes that switch'
|
|
|
218
311
|
'an !important animation here would outrank the global kill switch');
|
|
219
312
|
});
|
|
220
313
|
|
|
221
|
-
test('the
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
314
|
+
test('the signature animations are plain declarations, so the switch reaches them', () => {
|
|
315
|
+
for (const name of [
|
|
316
|
+
'breathe', 'sheen', 'orbit', // the breath
|
|
317
|
+
'arrive-form', 'arrive-orb', 'arrive-bead', 'arrive-mark', // the authored moment
|
|
318
|
+
]) {
|
|
319
|
+
assert.match(PAGE_CSS, new RegExp(`@keyframes\\s+${name}\\s*\\{`), `@keyframes ${name} is gone`);
|
|
320
|
+
assert.match(PAGE_CSS, new RegExp(`animation:[^;}]*\\b${name}\\b`), `nothing runs ${name}`);
|
|
226
321
|
}
|
|
227
322
|
});
|
|
228
323
|
|
|
229
|
-
test('the
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
assert.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// assistant is what varies. Cycling the first slot produced "type claude in
|
|
245
|
-
// any AI", which nobody does and which inverts the claim the line makes.
|
|
246
|
-
assert.doesNotMatch(HTML, /type <span class="typed"/, 'the typed span must not hold the typed-word slot');
|
|
247
|
-
const words = CODE.match(/words\s*=\s*\[([^\]]*)\]/);
|
|
248
|
-
assert.ok(words, 'the eyebrow word list must be findable');
|
|
249
|
-
assert.doesNotMatch(words[1], /'bongos'/, 'bongos is fixed copy, never a cycled word');
|
|
250
|
-
for (const ai of ['Claude', 'ChatGPT', 'Gemini']) {
|
|
251
|
-
assert.match(words[1], new RegExp(`'${ai}'`), `${ai} must be one of the cycled assistants`);
|
|
324
|
+
test('the breath stays slow and the arrival stays brief', () => {
|
|
325
|
+
// The signature is meant to be almost subliminal — a period a reader notices
|
|
326
|
+
// is a distraction, and a fast one is a moving-content accessibility problem
|
|
327
|
+
// (WCAG 2.2.2 territory). The floors are well under the shipped values, so
|
|
328
|
+
// this catches a period being retuned by an order of magnitude, not a nudge.
|
|
329
|
+
const period = (name, unit) => {
|
|
330
|
+
const m = PAGE_CSS.match(new RegExp(`\\b${name}\\s+([\\d.]+)${unit}\\b`));
|
|
331
|
+
assert.ok(m, `${name}'s period is no longer findable — update this test`);
|
|
332
|
+
return Number(m[1]);
|
|
333
|
+
};
|
|
334
|
+
assert.ok(period('breathe', 's') >= 30, 'the blob breathes at 32s; under 30s it reads as a pulse');
|
|
335
|
+
assert.ok(period('sheen', 's') >= 30, 'the hue drift is 40s; under 30s the iridescence shimmers');
|
|
336
|
+
assert.ok(period('orbit', 's') >= 60, 'the bead walks its circle in 60s');
|
|
337
|
+
for (const arrive of ['arrive-form', 'arrive-orb', 'arrive-bead', 'arrive-mark']) {
|
|
338
|
+
assert.ok(period(arrive, 'ms') <= 800, `${arrive} holds the first paint too long`);
|
|
252
339
|
}
|
|
253
340
|
});
|
|
254
341
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
'
|
|
266
|
-
'
|
|
267
|
-
'A project as a codebase', // S4 heading
|
|
268
|
-
'Explore Bongo Projects', // the hero CTA
|
|
269
|
-
'Start a Project', // the closing pair
|
|
270
|
-
'Find a Project',
|
|
342
|
+
// ── the live-data contract ─────────────────────────────────────────────────
|
|
343
|
+
// Five public feeds fill the page after it has already rendered a real, baked
|
|
344
|
+
// state. Everything here is invisible to a passing render: a feed renamed, an
|
|
345
|
+
// innerHTML crept in with third-party titles in it, a figure quietly redefined.
|
|
346
|
+
|
|
347
|
+
test('the script reads the five public feeds the head comment documents', () => {
|
|
348
|
+
for (const feed of [
|
|
349
|
+
'/public/progress', // the shipped figure + the ledger line
|
|
350
|
+
'/public/cost-summary', // the value figure
|
|
351
|
+
'/public/leaderboard', // the builders figure
|
|
352
|
+
'/public/recent-shipped?limit=6', // the ledger rows
|
|
353
|
+
'/projects/featured', // living proof
|
|
271
354
|
]) {
|
|
272
|
-
assert.ok(
|
|
355
|
+
assert.ok(CODE.includes(feed), `the page no longer reads ${feed}`);
|
|
273
356
|
}
|
|
274
357
|
});
|
|
275
358
|
|
|
276
|
-
test('
|
|
277
|
-
|
|
278
|
-
// and
|
|
279
|
-
assert.
|
|
280
|
-
assert.
|
|
359
|
+
test('feed values are never interpolated as HTML, and hrefs are scheme-checked', () => {
|
|
360
|
+
// every string in these payloads is third-party: task titles, builder names,
|
|
361
|
+
// project names and origins, all written by someone other than this page.
|
|
362
|
+
assert.doesNotMatch(CODE, /innerHTML/, 'the feeds carry third-party strings');
|
|
363
|
+
assert.doesNotMatch(CODE, /document\.write/);
|
|
364
|
+
assert.match(CODE, /createElement\(/, 'the DOM is built, not concatenated');
|
|
365
|
+
assert.match(CODE, /textContent\s*=/);
|
|
366
|
+
assert.match(CODE, /u\.protocol === 'https:' \|\| u\.protocol === 'http:'/);
|
|
367
|
+
assert.match(CODE, /rel = 'noopener'/, 'an outbound project link opens without handing over the opener');
|
|
281
368
|
});
|
|
282
369
|
|
|
283
|
-
|
|
284
|
-
//
|
|
285
|
-
|
|
370
|
+
test('a live task title is normalised to the front door\'s own punctuation', () => {
|
|
371
|
+
// Task titles are write-once quoted data and the ledger hands the page
|
|
372
|
+
// punctuation the taste pre-flight bans (an em dash, a trailing parenthetical
|
|
373
|
+
// like "(owner decision 2026-08-24)"). The rule is applied at RENDER time, so
|
|
374
|
+
// it is invisible until a title with a dash ships.
|
|
375
|
+
const rules = CODE.match(/\.replace\([^\n]*/g) || [];
|
|
376
|
+
const has = (frag) => rules.some((r) => r.includes(frag));
|
|
377
|
+
assert.ok(has('[—–]'), 'em and en dashes become a comma');
|
|
378
|
+
assert.ok(has("', '"), 'and a comma is what they become');
|
|
379
|
+
assert.ok(has('\\([^()]*\\)\\s*$'), 'a trailing parenthetical is dropped');
|
|
380
|
+
assert.ok(has('/,\\s*,/g'), 'the comma a dash leaves beside one collapses');
|
|
381
|
+
});
|
|
286
382
|
|
|
287
|
-
test('
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
assert.
|
|
292
|
-
assert.
|
|
383
|
+
test('each figure is defined the way the head comment says it is', () => {
|
|
384
|
+
// A figure whose definition drifts still renders a plausible number, so the
|
|
385
|
+
// page would lie without ever looking wrong. These are the three definitions.
|
|
386
|
+
assert.match(CODE, /total \+= Number\(r\.shipped_count\)/, 'shipped = the SUM over every progress row');
|
|
387
|
+
assert.match(CODE, /setFig\('shipped'/);
|
|
388
|
+
assert.match(CODE, /data\.total_value_credits/, 'value = total_value_credits from the cost summary');
|
|
389
|
+
assert.match(CODE, /setFig\('value'/);
|
|
390
|
+
assert.match(CODE, /Number\(r\.total_credits\) > 0/, 'builders = leaderboard entries HOLDING credits');
|
|
391
|
+
assert.match(CODE, /setFig\('builders'/);
|
|
392
|
+
// the ledger line + meter come off the first `building` row
|
|
393
|
+
assert.match(CODE, /r\.status === 'building'/);
|
|
394
|
+
assert.match(CODE, /' shipped on '/);
|
|
395
|
+
// numbers are grouped, so the live value matches the baked one's shape
|
|
396
|
+
assert.match(CODE, /toLocaleString\('en-US'\)/);
|
|
293
397
|
});
|
|
294
398
|
|
|
295
|
-
test('
|
|
296
|
-
|
|
297
|
-
|
|
399
|
+
test('a failed or empty feed leaves the baked state standing', () => {
|
|
400
|
+
// The page has no empty state to fall back to — the baked values ARE the
|
|
401
|
+
// fallback, so every handler must bail rather than blank its target.
|
|
402
|
+
assert.match(CODE, /if \(!rows\.length\) return;/);
|
|
403
|
+
assert.match(CODE, /if \(!tasks\.length \|\| !list\) return;/);
|
|
404
|
+
assert.match(CODE, /if \(!list\.length \|\| !host\) return;/);
|
|
405
|
+
// and a non-2xx or a dropped connection resolves to null rather than throwing
|
|
406
|
+
assert.match(CODE, /res\.ok \? res\.json\(\) : null/);
|
|
407
|
+
assert.match(CODE, /\.catch\(function \(\) \{ return null; \}\)/);
|
|
298
408
|
});
|
|
299
409
|
|
|
300
|
-
|
|
410
|
+
test('sign out POSTs, and expires both cookies in both Domain forms', () => {
|
|
411
|
+
// The logout endpoint is POST + cookie-gated, so a plain-anchor GET would 404
|
|
412
|
+
// and leave the session live (the hall bug of task 1002245). And a Max-Age=0
|
|
413
|
+
// only matches the cookie whose Domain attribute it names, so the host-only
|
|
414
|
+
// and apex-scoped forms must BOTH be expired (#726).
|
|
415
|
+
assert.match(CODE, /fetch\(API \+ '\/auth\/logout', \{ method: 'POST', credentials: 'same-origin' \}\)/);
|
|
416
|
+
assert.match(CODE, /\['gds_session', 'pms_session'\]/);
|
|
417
|
+
assert.match(CODE, /name \+ '=; Max-Age=0; path=\/';/);
|
|
418
|
+
assert.match(CODE, /name \+ '=; Max-Age=0; path=\/; Domain=\.' \+ apex/);
|
|
419
|
+
// it runs whether the POST lands or not
|
|
420
|
+
assert.match(CODE, /\.then\(finish, finish\)/);
|
|
421
|
+
});
|
|
301
422
|
|
|
302
|
-
test('the
|
|
303
|
-
|
|
423
|
+
test('the tagline is one server-stamped string, split for the composition only', () => {
|
|
424
|
+
// The h1 is {{tagline}}. The composition wants it on two lines, so the neutral
|
|
425
|
+
// halves are BAKED as two spans (the page reads correctly standalone and on
|
|
426
|
+
// the apex) — and a host whose tagline is not those halves must get it back
|
|
427
|
+
// whole rather than a headline cut in the wrong place.
|
|
428
|
+
assert.match(HTML, /<h1 data-brand-tagline>/);
|
|
429
|
+
assert.match(HTML, /<span class="l1">Building Digitally,<\/span><span class="l2">as simple as Bongos\.<\/span>/);
|
|
430
|
+
assert.match(CODE, /window\.__BRANDING__/);
|
|
431
|
+
assert.match(CODE, /brand\.copy && brand\.copy\.tagline/);
|
|
432
|
+
assert.match(CODE, /line !== baked\.join\(' '\)/, 'the rejoin is what decides a host tagline differs');
|
|
433
|
+
assert.match(PAGE_CSS, /h1 \.whole\{[^}]*white-space:normal/, 'the whole-tagline fallback must wrap');
|
|
304
434
|
});
|
|
305
435
|
|
|
306
|
-
test('the
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
assert.match(
|
|
311
|
-
assert.match(
|
|
312
|
-
assert.match(
|
|
313
|
-
//
|
|
314
|
-
|
|
315
|
-
assert.
|
|
316
|
-
assert.doesNotMatch(styles, /^\.grid\{/m, '.grid belongs to the shared layer');
|
|
436
|
+
test('the mode is resolved before first paint, and ?mode= wins', () => {
|
|
437
|
+
// A stored dark choice applied by the deferred script flashes the light page
|
|
438
|
+
// first; and ?mode= is how a reviewer walks both modes on one URL, which the
|
|
439
|
+
// render probes depend on.
|
|
440
|
+
assert.match(PREPAINT, /URLSearchParams\(location\.search\)\.get\('mode'\)/);
|
|
441
|
+
assert.match(PREPAINT, /localStorage\.getItem\('cb-color-mode'\)/);
|
|
442
|
+
assert.match(PREPAINT, /setAttribute\('data-mode'/);
|
|
443
|
+
// it is in <head>, ahead of the stylesheet link and the body
|
|
444
|
+
assert.ok(HTML.indexOf('cb-color-mode') < HTML.indexOf('<body>'));
|
|
445
|
+
assert.match(CODE, /localStorage\.setItem\('cb-color-mode', next\)/, 'the toggle persists the choice');
|
|
317
446
|
});
|
|
318
447
|
|
|
319
|
-
// ── the
|
|
448
|
+
// ── the assertions tests/platform_boot.mjs will make once the apex flips ───
|
|
320
449
|
|
|
321
450
|
test('the page satisfies the parked platform_boot assertions', () => {
|
|
322
451
|
assert.match(HTML, /id="who"/);
|
|
323
|
-
assert.match(HTML, /Find a
|
|
452
|
+
assert.match(HTML, /Find a project/); // the landing marker
|
|
324
453
|
// an ORG-level action that lives only on the gate — asserted absent there
|
|
325
454
|
assert.doesNotMatch(HTML, /Create a project/);
|
|
326
455
|
});
|