@bongos/core 1.19.113 → 1.19.115
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 +26 -31
- package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +1 -1
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -1
- package/docs/api/openapi.json +1 -1
- package/modules/provisioning/onboard-plan.js +2 -2
- package/modules/provisioning/routes/provisioning.js +3 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +31 -3
- package/public-landing/projects.html +1871 -50
- package/scripts/gds/audit-authorship.sh +0 -4
- package/scripts/gds/onboard.js +1 -1
- package/src/module-api.js +5 -1
- package/src/platform-server.js +13 -5
- package/tests/cosmos_token_contrast.mjs +10 -7
- package/tests/module-scope-map.mjs +1 -1
- package/tests/onboard.mjs +20 -16
- package/tests/platform_boot.mjs +21 -8
- package/tests/projects_hub.mjs +148 -11
- package/tests/public_landing_projects.mjs +15 -10
- package/public-landing/create.html +0 -1505
|
@@ -202,10 +202,6 @@ ALLOWLIST=(
|
|
|
202
202
|
'tests/auth_anthropic_identity.mjs'
|
|
203
203
|
'modules/dev-box/app/test/unit.mjs'
|
|
204
204
|
|
|
205
|
-
# The public landing page links the open-source repo by URL — repo-hosting
|
|
206
|
-
# identity (the provision-staging.sh class), not a builder identity.
|
|
207
|
-
'public-landing/create.html'
|
|
208
|
-
|
|
209
205
|
# The mirror-redaction floor: PRIOR_INSTANCE_FLOOR carries the prior
|
|
210
206
|
# instance's owner literal precisely so packaging can SCRUB it from every
|
|
211
207
|
# published core — the anti-leak spec, not a runtime hardcode (task 1002640).
|
package/scripts/gds/onboard.js
CHANGED
|
@@ -23,7 +23,7 @@ const oauth = require(path.join(__dirname, 'oauth-secret.js')); // validateC
|
|
|
23
23
|
// ── PURE core (exported for tests) ───────────────────────────────────────────
|
|
24
24
|
|
|
25
25
|
// The canonical step sequence now lives in ONE place — the provisioning module — so the
|
|
26
|
-
// CLI (here)
|
|
26
|
+
// CLI (here) and the hub's ?view=new wizard can't drift (task 2085; goal
|
|
27
27
|
// 35 / criterion 165: "the canonical step sequence is the source of truth; skill/CLI/hall
|
|
28
28
|
// are forms over it"). The web forms read it over GET /provisioning/onboard-plan; the CLI
|
|
29
29
|
// requires it directly. Re-exported below so `bongos onboard` + tests keep the same API.
|
package/src/module-api.js
CHANGED
|
@@ -653,8 +653,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
653
653
|
// landed since 1.19.111 with no explicit bump. run 31814245230. (task 1002620)
|
|
654
654
|
// 1.19.113 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
655
655
|
// landed since 1.19.112 with no explicit bump. run 31816239246. (task 1002620)
|
|
656
|
+
// 1.19.114 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
657
|
+
// landed since 1.19.113 with no explicit bump. run 31816599678. (task 1002620)
|
|
658
|
+
// 1.19.115 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
659
|
+
// landed since 1.19.114 with no explicit bump. run 31825824596. (task 1002620)
|
|
656
660
|
// ---------------------------------------------------------------------------
|
|
657
|
-
const CORE_VERSION = '1.19.
|
|
661
|
+
const CORE_VERSION = '1.19.115'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
658
662
|
|
|
659
663
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
660
664
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/src/platform-server.js
CHANGED
|
@@ -184,13 +184,21 @@ function buildPlatformApp() {
|
|
|
184
184
|
if (!html) return next();
|
|
185
185
|
res.type('html').send(html);
|
|
186
186
|
});
|
|
187
|
-
// The
|
|
188
|
-
//
|
|
187
|
+
// The create wizard was absorbed into the hub's ?view=new view (task 1002763;
|
|
188
|
+
// direction §4 — "/create redirects to ?view=new"). This route only carries
|
|
189
|
+
// visitors — and any ?name= seed the old links still send — into it. 302, not
|
|
190
|
+
// 301: /create is an OAuth return target in the wild, and a browser-cached
|
|
191
|
+
// permanent redirect would outlive any future change of shape.
|
|
189
192
|
app.get('/create', (req, res, next) => {
|
|
190
193
|
if (!isApexHost(req)) return next();
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
+
// rebuilt, not spliced: an inbound ?view= must not ride along (the hub
|
|
195
|
+
// reads the FIRST view param — a duplicate would bet the route on that
|
|
196
|
+
// parser detail), and an empty query must not leave a bare trailing '&'
|
|
197
|
+
const qi = req.url.indexOf('?');
|
|
198
|
+
const rest = new URLSearchParams(qi >= 0 ? req.url.slice(qi + 1) : '');
|
|
199
|
+
rest.delete('view');
|
|
200
|
+
const tail = rest.toString();
|
|
201
|
+
res.redirect(302, '/projects?view=new' + (tail ? '&' + tail : ''));
|
|
194
202
|
});
|
|
195
203
|
// THE PROJECT HUB (task 1002743) — one surface holding every project-shaped
|
|
196
204
|
// action: the map of what exists (the orb sky the gate used to own, plus the
|
|
@@ -144,8 +144,9 @@ const PAIRS = [
|
|
|
144
144
|
['.btn.ghost:hover label', 'var(--accent-deep)', CARD, 4.5],
|
|
145
145
|
['.btn.danger label', 'var(--err)', CARD, 4.5],
|
|
146
146
|
// Inline links: cosmos.css recolors its own to --sun-lit in dark (:179-262), but
|
|
147
|
-
//
|
|
148
|
-
//
|
|
147
|
+
// the ?view=new wizard styles (absorbed from create.html, task 1002763) carry
|
|
148
|
+
// eleven --accent-deep consumers and zero dark rules of their own, so there the
|
|
149
|
+
// token itself must carry dark.
|
|
149
150
|
['inline link on a card', 'var(--accent-deep)', CARD, 4.5],
|
|
150
151
|
['inline link on the page ground', 'var(--accent-deep)', 'var(--bg)', 4.5],
|
|
151
152
|
['code chip on --bg-deep', 'var(--accent-deep)', 'var(--bg-deep)', 4.5],
|
|
@@ -172,11 +173,13 @@ const PAIRS = [
|
|
|
172
173
|
['.projMsg.ok', 'var(--ok)', CARD, 4.5],
|
|
173
174
|
['.projMsg.err / .confirmRow', 'var(--err)', CARD, 4.5],
|
|
174
175
|
// chrome: wordmark + footer sit on the page ground, not on a card
|
|
175
|
-
//
|
|
176
|
-
// darkest tints, so cosmos.css's pairs
|
|
177
|
-
|
|
178
|
-
['
|
|
179
|
-
['
|
|
176
|
+
// wizard grounds (projects.html ?view=new, absorbed from create.html in task
|
|
177
|
+
// 1002763) — its panels carry their own darkest tints, so cosmos.css's pairs
|
|
178
|
+
// alone do not cover them
|
|
179
|
+
['?view=new .ghrow .priv 11px', 'var(--ink-soft)', 'color-mix(in srgb,var(--ink) 8%,var(--bg-deep))', 4.5],
|
|
180
|
+
['--on-ok on an --ok fill', 'var(--on-ok)', 'var(--ok)', 4.5],
|
|
181
|
+
['?view=new input ::placeholder', 'var(--ink-faint)', CARD, 4.5],
|
|
182
|
+
['?view=new .ghlist .hint on deep', 'var(--ink-faint)', 'var(--bg-deep)', 4.5],
|
|
180
183
|
['--chrome-accent wordmark', 'var(--chrome-accent)', 'var(--page-bg)', 4.5],
|
|
181
184
|
['--chrome-ink', 'var(--chrome-ink)', 'var(--page-bg)', 4.5],
|
|
182
185
|
['--foot-ink footer copy', 'var(--foot-ink)', 'var(--page-bg)', 4.5],
|
|
@@ -127,7 +127,7 @@ test('platform-identity joined the roster (task 1002322) — the catalog territo
|
|
|
127
127
|
'modules/platform-identity/routes/projects.js', // the feed + invite routes
|
|
128
128
|
'modules/status-ui/public/explore.js', // orb render-type surface
|
|
129
129
|
'modules/hall-ui/public/my-projects.js', // owner settings / applications queue
|
|
130
|
-
'public-landing/
|
|
130
|
+
'public-landing/projects.html', // the creation step, ?view=new since task 1002763
|
|
131
131
|
'modules/provisioning/', // module-bundle selection at provision time
|
|
132
132
|
]) {
|
|
133
133
|
assert.equal(covers(touch), true, `wall does not cover ${touch}`);
|
package/tests/onboard.mjs
CHANGED
|
@@ -86,18 +86,19 @@ test('every render surface carries the terminal hall step (task 1002703)', () =>
|
|
|
86
86
|
const cli = read('scripts/gds/onboard.js');
|
|
87
87
|
assert.match(cli, /builders hall/i, 'the CLI walkthrough ends at the hall');
|
|
88
88
|
assert.match(cli, /bongos dev/, 'the CLI offers the local-hall one-liner when no domain');
|
|
89
|
-
const
|
|
90
|
-
const enterHall =
|
|
91
|
-
assert.ok(enterHall.length >= 1, '
|
|
92
|
-
assert.match(enterHall.join(''), /\/builders/, '
|
|
93
|
-
assert.match(enterHall.join(''), /bongos dev/, '
|
|
89
|
+
const hubHtml = read('public-landing/projects.html');
|
|
90
|
+
const enterHall = hubHtml.match(/data-onboard-step="enter-hall"[^]*?<\/li>/g) || [];
|
|
91
|
+
assert.ok(enterHall.length >= 1, 'the ?view=new wizard tags the enter-hall step');
|
|
92
|
+
assert.match(enterHall.join(''), /\/builders/, 'the wizard points at the hall address');
|
|
93
|
+
assert.match(enterHall.join(''), /bongos dev/, 'the wizard offers the local hall when no domain');
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
test('the
|
|
96
|
+
test('the wizard done panel is plan-driven, manifest-driven, and owner-honest (task 1002694)', () => {
|
|
97
97
|
// Gaps 2/3/4/14 of the project-creation audit (docs/audits/project-creation-gap-list.md).
|
|
98
|
+
// The wizard lives inside the hub page since task 1002763.
|
|
98
99
|
const { readFileSync } = require('node:fs');
|
|
99
100
|
const path = require('node:path');
|
|
100
|
-
const src = readFileSync(path.join(import.meta.dirname, '..', 'public-landing/
|
|
101
|
+
const src = readFileSync(path.join(import.meta.dirname, '..', 'public-landing/projects.html'), 'utf8');
|
|
101
102
|
// gap 2 (idea 1000675): the checklist renders FROM the canonical plan endpoint
|
|
102
103
|
assert.match(src, /provisioning\/onboard-plan/, 'the done panel fetches the canonical plan');
|
|
103
104
|
// gap 3 (idea 1000676): the ADR 0133 one-click manifest flow has a UI caller again
|
|
@@ -105,12 +106,14 @@ test('the /create done panel is plan-driven, manifest-driven, and owner-honest (
|
|
|
105
106
|
assert.match(src, /github-app\/status/, 'and polls its progress to reflect live state');
|
|
106
107
|
// gap 4: the operator-only control-plane command never reaches a new owner
|
|
107
108
|
assert.doesNotMatch(src, /oauth-secret place/, 'owners are never handed the control-plane placement command');
|
|
108
|
-
// gap 14 (idea 1000684): repo visibility is stated, with the private path named
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
// gap 14 (idea 1000684): repo visibility is stated, with the private path named.
|
|
110
|
+
// Landmarks unique to the wizard — bare /public/ and /adopt/i match hub copy
|
|
111
|
+
// that has nothing to do with it, which would leave this guard vacuous.
|
|
112
|
+
assert.match(src, /public stays public, private stays private/, 'visibility is stated');
|
|
113
|
+
assert.match(src, /id="c-adopt"/, 'the private path (adopt your own private repo) is named');
|
|
111
114
|
});
|
|
112
115
|
|
|
113
|
-
test('the
|
|
116
|
+
test('the wizard gate trust link points at a live artifact, not the dead private-repo URL (task 1002705)', () => {
|
|
114
117
|
// Gap 1 of the project-creation audit: the gate linked a nonexistent private repo
|
|
115
118
|
// (404 even authenticated — ADR 0167 names it). No public code artifact exists yet
|
|
116
119
|
// (the ADR 0099 mirror is dormant until ~2026-11, and both npm scopes 404 anonymously
|
|
@@ -118,7 +121,7 @@ test('the /create gate trust link points at a live artifact, not the dead privat
|
|
|
118
121
|
// "open source" is the license itself.
|
|
119
122
|
const { readFileSync } = require('node:fs');
|
|
120
123
|
const path = require('node:path');
|
|
121
|
-
const src = readFileSync(path.join(import.meta.dirname, '..', 'public-landing/
|
|
124
|
+
const src = readFileSync(path.join(import.meta.dirname, '..', 'public-landing/projects.html'), 'utf8');
|
|
122
125
|
assert.doesNotMatch(src, /Claude-OTB/, 'the dead repo link is gone');
|
|
123
126
|
assert.match(src, /https:\/\/www\.gnu\.org\/licenses\/agpl-3\.0/, 'the trust link resolves (the AGPL-3.0 license text)');
|
|
124
127
|
});
|
|
@@ -168,19 +171,20 @@ test('onboardPlan is the SAME function the provisioning module owns (single sour
|
|
|
168
171
|
assert.equal(ob.onboardPlan, canonical, 'scripts/gds/onboard.js must re-export the module’s onboardPlan');
|
|
169
172
|
});
|
|
170
173
|
|
|
171
|
-
test('the web form (
|
|
174
|
+
test('the web form (?view=new) declares EXACTLY the canonical human steps — drift guard', () => {
|
|
172
175
|
// goal 35 / criterion 165: "the canonical step sequence is the source of truth;
|
|
173
176
|
// skill/CLI/web are forms over it." The web form tags its human-step <li>s with
|
|
174
177
|
// data-onboard-step="<key>"; this asserts that set equals the plan's human-step keys,
|
|
175
178
|
// so adding/removing/renaming a human step in the single source fails CI until every
|
|
176
179
|
// form is updated (and vice-versa) — the sequence can no longer drift between forms.
|
|
177
|
-
// (The hall new-project wizard was removed in task 1002675
|
|
178
|
-
//
|
|
180
|
+
// (The hall new-project wizard was removed in task 1002675, and the apex /create
|
|
181
|
+
// page was absorbed into the hub's ?view=new view in task 1002763; the skill +
|
|
182
|
+
// CLI + the hub wizard remain the canonical forms.)
|
|
179
183
|
const { readFileSync } = require('node:fs');
|
|
180
184
|
const path = require('node:path');
|
|
181
185
|
const humanKeys = ob.onboardPlan().filter((s) => s.kind === 'human').map((s) => s.key).sort();
|
|
182
186
|
const surfaces = {
|
|
183
|
-
'/
|
|
187
|
+
'/projects?view=new wizard': 'public-landing/projects.html',
|
|
184
188
|
};
|
|
185
189
|
for (const [name, rel] of Object.entries(surfaces)) {
|
|
186
190
|
const src = readFileSync(path.join(import.meta.dirname, '..', rel), 'utf8');
|
package/tests/platform_boot.mjs
CHANGED
|
@@ -121,16 +121,29 @@ test('the apex host serves the landing page — not the game — on a game-less
|
|
|
121
121
|
assert.doesNotMatch(res.body, /Create a project/);
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
-
test('
|
|
125
|
-
// The
|
|
126
|
-
//
|
|
124
|
+
test('/create redirects into the hub wizard, seed intact (task 1002763)', async () => {
|
|
125
|
+
// The wizard was absorbed into /projects?view=new (direction §4); /create
|
|
126
|
+
// survives only as a 302 so old links and OAuth returns keep working. The
|
|
127
|
+
// query string rides along — /create?name= was the seeding entry point.
|
|
127
128
|
const res = await request('/create', 'game.test.local');
|
|
128
|
-
assert.equal(res.status,
|
|
129
|
-
assert.
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
assert.equal(res.status, 302);
|
|
130
|
+
assert.equal(res.headers.location, '/projects?view=new');
|
|
131
|
+
// the query is rebuilt (URLSearchParams re-encodes space as '+', which the
|
|
132
|
+
// hub's URLSearchParams reading decodes right back), and an inbound view=
|
|
133
|
+
// never rides along to fight the one this route asserts
|
|
134
|
+
const seeded = await request('/create?name=Sky%20Farm&view=mine', 'game.test.local');
|
|
135
|
+
assert.equal(seeded.status, 302);
|
|
136
|
+
assert.equal(seeded.headers.location, '/projects?view=new&name=Sky+Farm');
|
|
137
|
+
const empty = await request('/create?', 'game.test.local');
|
|
138
|
+
assert.equal(empty.headers.location, '/projects?view=new');
|
|
139
|
+
// the branded wizard itself now ships inside the hub page
|
|
140
|
+
const hub = await request('/projects', 'game.test.local');
|
|
141
|
+
assert.equal(hub.status, 200);
|
|
142
|
+
assert.match(hub.headers['content-type'] || '', /text\/html/);
|
|
143
|
+
assert.match(hub.body, /window\.__BRANDING__=/);
|
|
144
|
+
assert.match(hub.body, /Create a project/);
|
|
132
145
|
// the wizard files the request against the provisioning API, nothing else
|
|
133
|
-
assert.match(
|
|
146
|
+
assert.match(hub.body, /provisioning\/instances/);
|
|
134
147
|
});
|
|
135
148
|
|
|
136
149
|
test('R51: an API-shaped 404 still gets a self-describing JSON body', async () => {
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -212,17 +212,85 @@ test('the gate is retired — no route, no files, no links into it', () => {
|
|
|
212
212
|
assert.doesNotMatch(HUB, /href="\/gate/);
|
|
213
213
|
});
|
|
214
214
|
|
|
215
|
-
test('/create
|
|
216
|
-
// Task 1002743
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
assert.
|
|
225
|
-
|
|
215
|
+
test('/create is absorbed — the wizard lives here and the route only redirects (task 1002763)', () => {
|
|
216
|
+
// Task 1002743 stopped at step 1: the hub named a project and navigated to
|
|
217
|
+
// /create?name=. This task carries the absorption the owner amended into it
|
|
218
|
+
// (direction §4: "/create redirects to ?view=new"): the wizard's six panels
|
|
219
|
+
// are real markup inside the new view (C7), create.html is deleted — the
|
|
220
|
+
// gate precedent above: express.static would keep serving the raw file —
|
|
221
|
+
// and the name handoff became an in-page step advance.
|
|
222
|
+
assert.equal(fs.existsSync(path.join(ROOT, 'public-landing', 'create.html')), false,
|
|
223
|
+
'create.html must be gone: express.static would keep serving it raw and unbranded');
|
|
224
|
+
assert.doesNotMatch(SCRIPT, /location\.assign\('\/create'/,
|
|
225
|
+
'naming a project advances the wizard in place — no navigation');
|
|
226
|
+
assert.doesNotMatch(HUB, /href="\/create"/, 'no link may point at the retired route');
|
|
227
|
+
assert.match(HUB, /href="\?view=new"/, 'in-page links carry the view query instead');
|
|
228
|
+
const server = fs.readFileSync(path.join(ROOT, 'src', 'platform-server.js'), 'utf8');
|
|
229
|
+
assert.match(server, /res\.redirect\(302, '\/projects\?view=new'/,
|
|
230
|
+
'/create must 302 into the hub (302, not 301: the URL is an OAuth return target in the wild)');
|
|
231
|
+
for (const id of ['panel-gate', 'panel-name', 'panel-home', 'panel-address', 'panel-review', 'panel-done']) {
|
|
232
|
+
assert.match(HUB, new RegExp(`id="${id}"`), `the wizard's ${id} ships as real markup (C7)`);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('the absorbed wizard sheds its two URL landmines — /gate/ and the whole-query strip', () => {
|
|
237
|
+
// create.html carried a document-level Escape that navigated to the retired
|
|
238
|
+
// /gate/ (a 404 the href-only guard above cannot see: it was a
|
|
239
|
+
// location.assign, not an href) and a replaceState that rewrote the URL to
|
|
240
|
+
// location.pathname — which on this page would destroy ?view=new on load.
|
|
241
|
+
assert.doesNotMatch(SCRIPT, /['"]\/gate\//,
|
|
242
|
+
'no code path may navigate into the retired gate');
|
|
243
|
+
assert.doesNotMatch(SCRIPT, /history\.replaceState\(null, '', location\.pathname\)/,
|
|
244
|
+
"the hub's '?view=' + v rewrite is the only URL writer on this page");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('no id is declared twice — the wizard merge had four collisions to clear', () => {
|
|
248
|
+
// modeToggle, who, signin, gateAlert all existed on BOTH pages; a duplicate
|
|
249
|
+
// id makes the second node unreachable by getElementById and the first
|
|
250
|
+
// silently wins. Scan the real markup only (JS builds ids like `dom-<iid>`
|
|
251
|
+
// from fragments that would false-positive here).
|
|
252
|
+
const afterHead = HUB.slice(HUB.indexOf('</script>') + '</script>'.length);
|
|
253
|
+
const markup = afterHead.slice(0, afterHead.indexOf('<script>'));
|
|
254
|
+
assert.ok(markup.includes('id="view-new"'), 'the markup slice must span the views');
|
|
255
|
+
const ids = [...markup.matchAll(/\sid="([^"]+)"/g)].map((m) => m[1]);
|
|
256
|
+
const seen = new Set();
|
|
257
|
+
const dupes = ids.filter((id) => (seen.has(id) ? true : (seen.add(id), false)));
|
|
258
|
+
assert.deepEqual(dupes, [], `duplicate ids in the hub markup: ${dupes}`);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('the new view balances its own tree — the map-only walkers never covered it', () => {
|
|
262
|
+
// div-balance (:535) counts view-map only, and every openAncestorsAt needle
|
|
263
|
+
// lives there too; a lopsided div in the wizard block would be caught by
|
|
264
|
+
// nothing else while silently reparenting the footer.
|
|
265
|
+
const start = HUB.indexOf('<section id="view-new"');
|
|
266
|
+
const end = HUB.indexOf('<div class="foot"');
|
|
267
|
+
assert.ok(start !== -1 && end > start, 'need the view-new span');
|
|
268
|
+
const wiz = HUB.slice(start, end);
|
|
269
|
+
assert.equal((wiz.match(/<div\b/g) || []).length, (wiz.match(/<\/div>/g) || []).length,
|
|
270
|
+
'every div opened in #view-new closes in #view-new');
|
|
271
|
+
assert.equal((wiz.match(/<section\b/g) || []).length, (wiz.match(/<\/section>/g) || []).length,
|
|
272
|
+
'every section opened in #view-new closes in #view-new');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('C8 — the wizard polls stop dead off the new view, like the sky off the map', () => {
|
|
276
|
+
// create.html's progress watch (5s) and manifest poll (4s/8s) had NO view
|
|
277
|
+
// guard — merged unguarded they would poll a hidden view for up to 33
|
|
278
|
+
// minutes. Both ticks must consult the same gate the sky consults.
|
|
279
|
+
assert.match(SCRIPT, /function wizPollActive\(\)/, 'the wizard needs its own stage guard');
|
|
280
|
+
assert.match(SCRIPT, /!document\.hidden && currentView === 'new'/,
|
|
281
|
+
'the guard is hidden-tab aware and view-scoped');
|
|
282
|
+
const gatedTicks = (SCRIPT.match(/if \(!wizPollActive\(\)\)/g) || []).length;
|
|
283
|
+
assert.ok(gatedTicks >= 2, `both wizard ticks (progress + manifest) must check the guard, found ${gatedTicks}`);
|
|
284
|
+
assert.match(SCRIPT, /if \(v === 'new'\) wizard\.enter\(focus\); else wizard\.leave\(\);/,
|
|
285
|
+
'setView hands the view to the wizard exactly like it starts/stops the sky');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('the wizard never steals focus on boot — focus moves only on deliberate steps', () => {
|
|
289
|
+
// create.html's show() unconditionally focused each panel's h1, and its boot
|
|
290
|
+
// always called show() — merged as-is, every hub load would rip focus into a
|
|
291
|
+
// hidden view. The hub rule (setView's `focus` flag) now reaches show() too.
|
|
292
|
+
assert.match(SCRIPT, /function show\(step, moveFocus\)/, 'show() must carry the focus flag');
|
|
293
|
+
assert.match(SCRIPT, /if \(moveFocus\)/, 'and only focus when it is set');
|
|
226
294
|
});
|
|
227
295
|
|
|
228
296
|
/* ── The platform's own entry (task 1002760) ────────────────────────────────
|
|
@@ -702,3 +770,72 @@ test('the chip-freshness tick spends no requests — it re-renders what it has (
|
|
|
702
770
|
assert.match(fn, /document\.hidden/, 'and does nothing in a hidden tab');
|
|
703
771
|
assert.doesNotMatch(fn, /fetch\(/, 'zero requests — the re-fetch belongs to entry and tab return only');
|
|
704
772
|
});
|
|
773
|
+
|
|
774
|
+
/* ── The living sky (task 1002762) ──────────────────────────────────────────
|
|
775
|
+
The retired gate's n-body cluster + pan/zoom/hover/spotlight, rewritten for
|
|
776
|
+
the hub: one camera layer, physics in ~110 guarded lines, and the mock's
|
|
777
|
+
static field as the reduced-motion and no-JS state. These pin the C8/C9
|
|
778
|
+
contract and the structural topology the camera depends on. ── */
|
|
779
|
+
|
|
780
|
+
test('the sky camera is one layer inside the field, outside the sentinels', () => {
|
|
781
|
+
const stack = openAncestorsAt(HUB, 'id="skyCam"');
|
|
782
|
+
assert.ok(stack.includes('view-map') && stack.includes('orbField'),
|
|
783
|
+
'the camera lives in the orb field. open: ' + stack.join(' > '));
|
|
784
|
+
assert.ok(!stack.includes('sub-table-body'), 'and never inside the table');
|
|
785
|
+
const selfStack = openAncestorsAt(HUB, 'id="selfOrb"');
|
|
786
|
+
assert.ok(selfStack.includes('skyCam'), 'the platform orb pans and zooms with everyone else');
|
|
787
|
+
const camAt = HUB.indexOf('id="skyCam"');
|
|
788
|
+
const selfAt = HUB.indexOf('id="selfOrb"');
|
|
789
|
+
const sentinelAt = HUB.lastIndexOf('<!--apex-only:start-->', selfAt);
|
|
790
|
+
assert.ok(camAt !== -1 && camAt < sentinelAt && sentinelAt < selfAt,
|
|
791
|
+
'the camera opens BEFORE the orb sentinel — stripApexOnly() must never eat its open tag');
|
|
792
|
+
const hoverStack = openAncestorsAt(HUB, 'id="hoverCard"');
|
|
793
|
+
assert.ok(hoverStack.includes('orbField') && !hoverStack.includes('skyCam'),
|
|
794
|
+
'the hover card sits outside the camera so it never scales or pans');
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
test('the sim stops dead when the sky is not on stage (C8, C9)', () => {
|
|
798
|
+
const active = (SCRIPT.match(/function skyActive\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
799
|
+
assert.ok(active, 'skyActive() must exist');
|
|
800
|
+
for (const guard of ['REDUCED', 'document.hidden', "currentView === 'map'", 'sub-sky-body']) {
|
|
801
|
+
assert.ok(active.includes(guard), `skyActive must check ${guard}`);
|
|
802
|
+
}
|
|
803
|
+
const tick = (SCRIPT.match(/function skyTick\(now\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
804
|
+
assert.ok(tick, 'skyTick() must exist');
|
|
805
|
+
assert.match(tick, /if \(!skyActive\(\)\) return/, 'a tick on a hidden sky ends the loop, not just the work');
|
|
806
|
+
assert.doesNotMatch(tick, /fetch\(/, 'the sky spends no requests (C6+C8)');
|
|
807
|
+
const step = (SCRIPT.match(/function physStep\(dt\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
808
|
+
assert.ok(step, 'physStep() must exist');
|
|
809
|
+
assert.doesNotMatch(step, /fetch\(/);
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
test('a drag is a pan and a click is a click — the two never blur', () => {
|
|
813
|
+
assert.match(SCRIPT, /var SKY_DEADZONE = 5/, 'the 5px Euclidean deadzone from the gate');
|
|
814
|
+
// mutation-found gap: a bare /skyPanned/ match was satisfied by the flag's
|
|
815
|
+
// declaration alone — pin the CONSUME site and the deferred clear instead
|
|
816
|
+
assert.match(SCRIPT, /if \(skyPanned\) \{ ev\.preventDefault\(\); return; \}/,
|
|
817
|
+
'the delegated click handler consumes the pan flag AND still cancels the link');
|
|
818
|
+
assert.match(SCRIPT, /setTimeout\(function \(\) \{ skyPanned = false; \}, 0\)/,
|
|
819
|
+
'the flag clears a tick after the pan ends, so a keyboard Enter is never eaten');
|
|
820
|
+
assert.match(SCRIPT, /pointercancel/, 'an interrupted touch must not strand the grab state');
|
|
821
|
+
assert.match(SCRIPT, /panId/, 'one pointer owns the drag — a second finger cannot hijack it');
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
test('anchors are never read from a hidden field', () => {
|
|
825
|
+
// review CRITICAL: offsetLeft/Top and clientWidth all read 0 under
|
|
826
|
+
// [hidden]{display:none} — anchoring to zeros paints every orb at
|
|
827
|
+
// slot + physics position when the sky returns. The rebuild must defer.
|
|
828
|
+
const fn = (SCRIPT.match(/function rebuildSkyBodies\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
829
|
+
assert.ok(fn, 'rebuildSkyBodies() must exist');
|
|
830
|
+
assert.match(fn, /clientWidth\) \{ skyNeedsRebuild = true; return; \}/,
|
|
831
|
+
'an unlaid-out field defers the rebuild instead of anchoring to zeros');
|
|
832
|
+
assert.match(SCRIPT, /if \(skyNeedsRebuild\) rebuildSkyBodies\(\);/,
|
|
833
|
+
'the deferred rebuild is consumed when the sky comes back on stage');
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
test('zoom heads for the cursor and survives line-mode wheels', () => {
|
|
837
|
+
assert.match(SCRIPT, /deltaMode/, 'Firefox line-mode deltas must be normalized');
|
|
838
|
+
assert.match(SCRIPT, /Math\.exp\(/, 'exponential zoom — symmetric in and out');
|
|
839
|
+
assert.ok(COSMOS.includes('transform-origin:0 0'),
|
|
840
|
+
'the camera algebra assumes the layer scales about its origin');
|
|
841
|
+
});
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
// intent_conflict, not_teardownable, bad_domain, domain_taken,
|
|
15
15
|
// domain_already_set, domain_not_attachable are all SAID, never swallowed)
|
|
16
16
|
// • an instance with no domain gets an HONEST no-address state, not a blank
|
|
17
|
-
// •
|
|
18
|
-
// has it" stop (the hall
|
|
17
|
+
// • the wizard's poll give-up points at the projects view — the false "the
|
|
18
|
+
// hall has it" stop (the hall card was removed in task 1002675) is gone.
|
|
19
|
+
// (create.html itself was absorbed into ?view=new in task 1002763.)
|
|
19
20
|
//
|
|
20
21
|
// Run: node --test tests/public_landing_projects.mjs
|
|
21
22
|
|
|
@@ -106,17 +107,20 @@ test('a domainless instance gets an honest no-address state', async () => {
|
|
|
106
107
|
assert.match(body, /[Nn]o address yet/);
|
|
107
108
|
});
|
|
108
109
|
|
|
109
|
-
test('the empty state points a new owner at
|
|
110
|
+
test('the empty state points a new owner at the wizard view — never at a hall', async () => {
|
|
110
111
|
const { body } = await request('/projects', 'game.test.local');
|
|
111
|
-
|
|
112
|
+
// /create was absorbed into ?view=new (task 1002763); the CTA moves in-page
|
|
113
|
+
assert.match(body, /href="\?view=new"/);
|
|
114
|
+
assert.doesNotMatch(body, /href="\/create"/, 'no link may point at the retired route');
|
|
112
115
|
// the hall my-projects card was REMOVED (task 1002675; hall my-projects is
|
|
113
116
|
// federation-only) — this page must not send an owner there
|
|
114
117
|
assert.doesNotMatch(body, /Enter the Hall/i);
|
|
115
118
|
});
|
|
116
119
|
|
|
117
|
-
test('
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
test('the watch give-up hands the owner their projects view — "the hall has it" is dead', async () => {
|
|
121
|
+
// the wizard (and its 33-minute watch cap) lives on /projects now
|
|
122
|
+
const { body } = await request('/projects', 'game.test.local');
|
|
123
|
+
assert.match(body, /your projects page/, 'the give-up path must hand the owner the projects view');
|
|
120
124
|
assert.doesNotMatch(body, /the hall has it/, 'the false poll-stop copy must be gone');
|
|
121
125
|
});
|
|
122
126
|
|
|
@@ -151,7 +155,8 @@ test('the stamp is derived from CONTENT, so it changes only when the file does',
|
|
|
151
155
|
});
|
|
152
156
|
|
|
153
157
|
test('every landing surface is stamped, not just the one that broke', async () => {
|
|
154
|
-
|
|
158
|
+
// /create dropped from the loop: it 302s since task 1002763 (no body to stamp)
|
|
159
|
+
for (const p of ['/', '/projects']) {
|
|
155
160
|
const res = await request(p);
|
|
156
161
|
if (res.status !== 200) continue;
|
|
157
162
|
assert.doesNotMatch(res.body, /(href|src)="\/assets\/[^"?]+"/,
|
|
@@ -163,8 +168,8 @@ test('routes and API paths are never mistaken for assets', async () => {
|
|
|
163
168
|
// stamping resolves the URL against public-landing/ — a route names no file,
|
|
164
169
|
// so it must come back untouched rather than acquiring a meaningless ?v=
|
|
165
170
|
const res = await request('/projects');
|
|
166
|
-
assert.match(res.body, /href="\/
|
|
167
|
-
assert.doesNotMatch(res.body, /href="
|
|
171
|
+
assert.match(res.body, /href="\/"/, "'/' is a route, not an asset");
|
|
172
|
+
assert.doesNotMatch(res.body, /href="\/\?v=/);
|
|
168
173
|
assert.doesNotMatch(res.body, /href="\/projects\?v=/);
|
|
169
174
|
assert.doesNotMatch(res.body, /\/api\/bongos\/[^"]*\?v=[0-9a-f]{8}/,
|
|
170
175
|
'API paths must never be stamped');
|