@bongos/core 1.24.44 → 1.24.46
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 +179 -144
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +8 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md +15 -0
- package/docs/api/openapi.json +152 -3
- package/docs/api-reference.md +4 -2
- package/docs/copy-inventory.md +1111 -1111
- package/docs/copy-registry.json +1547 -1547
- package/docs/module-api-changelog.md +4 -0
- package/docs/onboarding/diagrams/assertions.json +2 -2
- package/docs/page-inventory.json +11 -0
- package/docs/page-readings.json +823 -823
- package/migrations/core_295_runner_logins.sql +50 -0
- package/modules/government/session-scopes.json +5 -0
- package/modules/hall-ui/public/ai-studio.html +1 -1
- package/modules/hall-ui/public/ai-studio.states.json +2 -0
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/atlas.states.json +2 -0
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/board-room.states.json +2 -0
- package/modules/hall-ui/public/city.html +1 -1
- package/modules/hall-ui/public/city.states.json +2 -0
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/collab.states.json +2 -0
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/commands.states.json +2 -0
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/copy-desk.states.json +2 -0
- package/modules/hall-ui/public/craft-mode.js +4 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/deploy.states.json +2 -0
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goal-map.states.json +2 -0
- package/modules/hall-ui/public/goals.css +1 -0
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/goals.states.json +2 -0
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/government.states.json +2 -0
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/idea.states.json +2 -0
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/ideas.states.json +2 -0
- package/modules/hall-ui/public/index-founding.states.json +2 -0
- package/modules/hall-ui/public/index-genesis-decide.states.json +2 -0
- package/modules/hall-ui/public/index-genesis-demo.states.json +2 -0
- package/modules/hall-ui/public/index-genesis.states.json +2 -0
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/index.states.json +5 -0
- package/modules/hall-ui/public/market-watch-item.html +1 -1
- package/modules/hall-ui/public/market-watch-item.states.json +2 -0
- package/modules/hall-ui/public/market-watch-rules.html +1 -1
- package/modules/hall-ui/public/market-watch-rules.states.json +2 -0
- package/modules/hall-ui/public/market-watch.html +1 -1
- package/modules/hall-ui/public/market-watch.states.json +2 -0
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/modules.states.json +2 -0
- package/modules/hall-ui/public/money.html +1 -1
- package/modules/hall-ui/public/money.states.json +2 -0
- package/modules/hall-ui/public/my-agents.html +1 -1
- package/modules/hall-ui/public/my-agents.states.json +2 -0
- package/modules/hall-ui/public/notes-panel.css +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/primer.states.json +2 -0
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/profile.states.json +2 -0
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/roster.states.json +2 -0
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/settings.states.json +2 -0
- package/modules/hall-ui/public/shell.js +21 -1
- package/modules/hall-ui/public/street-card.states.json +2 -0
- package/modules/hall-ui/public/street.html +1 -1
- package/modules/hall-ui/public/street.states.json +2 -0
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/studio.states.json +2 -0
- package/modules/hall-ui/public/style.css +27 -24
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/task.states.json +2 -0
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/thinking.states.json +2 -0
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/tweak-editor.states.json +2 -0
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/watch.states.json +2 -0
- package/modules/hall-ui/public/welcome-boot.js +6 -2
- package/modules/hall-ui/public/work-founding.states.json +2 -0
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/public/work.states.json +3 -1
- package/modules/hall-ui/records/hall-navigation.md +5 -0
- package/modules/lifecycle/db-claims.js +6 -3
- package/modules/lifecycle/routes/claims.js +10 -0
- package/modules/lifecycle/routes/tasks.js +1 -1
- package/modules/lifecycle/runner-self-guard.js +19 -10
- package/modules/platform-identity/platform-identity.js +1 -1
- package/modules/platform-identity/tests/platform-identity.mjs +13 -2
- package/modules/provisioning/core-upgrade-authz.js +8 -0
- package/modules/public-landing/public/account.html +1 -0
- package/modules/public-landing/public/community.html +1 -0
- package/modules/public-landing/public/connections.html +1 -0
- package/modules/public-landing/public/contact.html +1 -0
- package/modules/public-landing/public/guild.html +1 -0
- package/modules/public-landing/public/index.html +1 -0
- package/modules/public-landing/public/invitations.html +1 -0
- package/modules/public-landing/public/privacy.html +1 -0
- package/modules/public-landing/public/projects.html +1 -0
- package/modules/public-landing/public/terms.html +1 -0
- package/modules/raise/views/gate.html +1 -0
- package/modules/raise/views/nda.html +1 -0
- package/modules/raise/views/page.html +1 -0
- package/modules/status-ui/public/index.html +1 -0
- package/modules/ui-design/kit/README.md +1 -1
- package/modules/ui-design/kit/fixtures/versions.json +1 -0
- package/modules/ui-design/kit/lib.js +48 -3
- package/modules/ui-design/kit/probe.js +1 -0
- package/modules/ui-design/kit/render.js +2 -0
- package/modules/ui-design/kit/serve.js +1 -0
- package/modules/ui-design/module.json +1 -1
- package/modules/ui-design/public/tokens.css +125 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +20 -0
- package/scripts/bongos/fitness.js +1 -1
- package/scripts/bongos/gen-hall-sitemap.js +3 -1
- package/scripts/bongos/run-unit-tests.js +3 -0
- package/src/bongos/auth.js +25 -4
- package/src/bongos/route-rank-check.js +5 -0
- package/src/bongos/routes/auth.js +48 -0
- package/src/bongos/runner-login.js +151 -0
- package/src/bongos/serve-internal.js +6 -1
- package/src/module-api.js +1 -1
- package/tests/autobongos_runner_login.mjs +296 -0
- package/tests/autobongos_runner_login_db.mjs +105 -0
- package/tests/craft_mode_nav.mjs +51 -3
- package/tests/design_tokens_shared.mjs +183 -0
- package/tests/government_session_scope.mjs +2 -1
- package/tests/hall_reading_world.mjs +5 -1
- package/tests/hall_tokens.mjs +22 -3
- package/tests/nav_permission_atoms.mjs +2 -1
- package/tests/ui_design_kit.mjs +30 -0
- package/tests/welcome_boot_browser.mjs +4 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// tests/autobongos_runner_login_db.mjs — an Autobongos runner's one-time code against
|
|
2
|
+
// a REAL Postgres (task 1004547, goal 1000139).
|
|
3
|
+
//
|
|
4
|
+
// tests/autobongos_runner_login.mjs proves the login DB-free, but its fake pool
|
|
5
|
+
// re-implements the WHERE that burns a code, so it cannot catch that WHERE being
|
|
6
|
+
// wrong. This file runs the real migration (core_295) and the real exchangeCode SQL:
|
|
7
|
+
// a code works once even when two PCs race it, an expired code mints nothing, and
|
|
8
|
+
// the session it mints carries source 'runner', which the widened CHECK admits.
|
|
9
|
+
//
|
|
10
|
+
// It SELF-SKIPS (exit 0) with no reachable Postgres, and refuses any database that
|
|
11
|
+
// already has a `builders` table — it creates its own throwaway tables. Listed in
|
|
12
|
+
// run-unit-tests.js's INTEGRATION set.
|
|
13
|
+
//
|
|
14
|
+
// Run: createdb runner_login_scratch && DATABASE_URL=postgres:///runner_login_scratch node tests/autobongos_runner_login_db.mjs
|
|
15
|
+
|
|
16
|
+
import pg from 'pg';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import url from 'node:url';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import { strict as assert } from 'node:assert';
|
|
22
|
+
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL || undefined });
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
await pool.query('SELECT 1');
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.log(`autobongos_runner_login_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}). Set DATABASE_URL to run.`);
|
|
30
|
+
await pool.end().catch(() => {});
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
const { rows: realRows } = await pool.query(`SELECT to_regclass('public.builders') IS NOT NULL AS has_builders`);
|
|
34
|
+
if (realRows[0].has_builders) {
|
|
35
|
+
console.log('autobongos_runner_login_db.mjs SKIPPED — this database has a `builders` table, so it is not a scratch DB.');
|
|
36
|
+
await pool.end().catch(() => {});
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let passed = 0, failed = 0;
|
|
41
|
+
async function t(name, fn) {
|
|
42
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
43
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The minimum of the three core tables the migration alters, as 003_pms.sql made them.
|
|
47
|
+
await pool.query(`
|
|
48
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (version text PRIMARY KEY);
|
|
49
|
+
CREATE TABLE builders (id bigserial PRIMARY KEY, github_login text, status text NOT NULL DEFAULT 'active');
|
|
50
|
+
CREATE TABLE builder_sessions (
|
|
51
|
+
id bigserial PRIMARY KEY, builder_id bigint REFERENCES builders(id), token_hash text,
|
|
52
|
+
source text NOT NULL CHECK (source IN ('cli', 'web')), user_agent text, expires_at timestamptz);
|
|
53
|
+
CREATE TABLE claims (id bigserial PRIMARY KEY, task_id bigint);
|
|
54
|
+
`);
|
|
55
|
+
const HERE = path.dirname(url.fileURLToPath(import.meta.url));
|
|
56
|
+
await pool.query(fs.readFileSync(path.join(HERE, '..', 'migrations', 'core_295_runner_logins.sql'), 'utf8'));
|
|
57
|
+
// Re-running is a no-op (every step is IF NOT EXISTS / DROP IF EXISTS).
|
|
58
|
+
await pool.query(fs.readFileSync(path.join(HERE, '..', 'migrations', 'core_295_runner_logins.sql'), 'utf8'));
|
|
59
|
+
|
|
60
|
+
const runnerLogin = require('../src/bongos/runner-login.js');
|
|
61
|
+
const { rows: [owner] } = await pool.query(`INSERT INTO builders (github_login) VALUES ('owner') RETURNING id`);
|
|
62
|
+
const { rows: [gone] } = await pool.query(`INSERT INTO builders (github_login, status) VALUES ('gone', 'inactive') RETURNING id`);
|
|
63
|
+
|
|
64
|
+
await t('a code trades for a runner session exactly once', async () => {
|
|
65
|
+
const { code } = await runnerLogin.issueCode({ builderId: owner.id, label: 'pc' }, pool);
|
|
66
|
+
const first = await runnerLogin.exchangeCode({ code }, pool);
|
|
67
|
+
assert.equal(String(first.builder_id), String(owner.id));
|
|
68
|
+
const { rows } = await pool.query('SELECT source, expires_at FROM builder_sessions WHERE id = $1', [first.session_id]);
|
|
69
|
+
assert.equal(rows[0].source, 'runner');
|
|
70
|
+
assert.equal(rows[0].expires_at, null, 'a runner login does not lapse; it is revoked');
|
|
71
|
+
assert.deepEqual(await runnerLogin.exchangeCode({ code }, pool), { refused: 'code_invalid' });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await t('two PCs racing one code: one wins, one session', async () => {
|
|
75
|
+
const { code } = await runnerLogin.issueCode({ builderId: owner.id }, pool);
|
|
76
|
+
const before = (await pool.query('SELECT count(*)::int AS n FROM builder_sessions')).rows[0].n;
|
|
77
|
+
const results = await Promise.all([runnerLogin.exchangeCode({ code }, pool), runnerLogin.exchangeCode({ code }, pool)]);
|
|
78
|
+
assert.equal(results.filter((r) => r.token).length, 1);
|
|
79
|
+
assert.equal(results.filter((r) => r.refused === 'code_invalid').length, 1);
|
|
80
|
+
const after = (await pool.query('SELECT count(*)::int AS n FROM builder_sessions')).rows[0].n;
|
|
81
|
+
assert.equal(after - before, 1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await t('an expired code mints nothing', async () => {
|
|
85
|
+
const { code } = await runnerLogin.issueCode({ builderId: owner.id, now: Date.now() - 16 * 60 * 1000 }, pool);
|
|
86
|
+
assert.deepEqual(await runnerLogin.exchangeCode({ code }, pool), { refused: 'code_invalid' });
|
|
87
|
+
const { rows } = await pool.query('SELECT used_at FROM runner_enrollment_codes WHERE code_hash = $1', [require('../src/bongos/db-kernel.js').sessionTokenHash(code)]);
|
|
88
|
+
assert.equal(rows[0].used_at, null, 'an expired code is not even marked used');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await t('an offboarded builder\'s code mints nothing and stays unused', async () => {
|
|
92
|
+
const { code } = await runnerLogin.issueCode({ builderId: gone.id }, pool);
|
|
93
|
+
assert.deepEqual(await runnerLogin.exchangeCode({ code }, pool), { refused: 'builder_inactive' });
|
|
94
|
+
const { rows } = await pool.query(`SELECT count(*)::int AS n FROM builder_sessions WHERE builder_id = $1`, [gone.id]);
|
|
95
|
+
assert.equal(rows[0].n, 0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await t('every claim reads not-autonomous by default; the mark is a real column', async () => {
|
|
99
|
+
const { rows } = await pool.query(`INSERT INTO claims (task_id) VALUES (1) RETURNING autonomous`);
|
|
100
|
+
assert.equal(rows[0].autonomous, false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await pool.end();
|
|
104
|
+
console.log(`\nautobongos_runner_login_db.mjs: ${passed} passed, ${failed} failed`);
|
|
105
|
+
process.exit(failed ? 1 : 0);
|
package/tests/craft_mode_nav.mjs
CHANGED
|
@@ -63,7 +63,7 @@ function parseSidebar(html) {
|
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
function boot({ session = {}, perms = { 'board.vote.cast': true } } = {}) {
|
|
66
|
+
function boot({ session = {}, local = {}, perms = { 'board.vote.cast': true } } = {}) {
|
|
67
67
|
const root = makeEl('HTML');
|
|
68
68
|
const topbar = makeEl('DIV');
|
|
69
69
|
const sidebar = makeEl('NAV');
|
|
@@ -95,7 +95,7 @@ function boot({ session = {}, perms = { 'board.vote.cast': true } } = {}) {
|
|
|
95
95
|
__BRANDING__: {}, __MODULES__: { enabled: { provisioning: true } },
|
|
96
96
|
BongosClient: { createClient: () => ({ request: () => new Promise(() => {}) }) },
|
|
97
97
|
matchMedia: () => ({ matches: false, addEventListener() {} }),
|
|
98
|
-
localStorage: mk(
|
|
98
|
+
localStorage: mk(local), sessionStorage: mk(session), location,
|
|
99
99
|
clearTimeout() {}, setTimeout() { return 0; }, requestAnimationFrame() {}, scrollTo() {},
|
|
100
100
|
addEventListener() {}, dispatchEvent() { return true; },
|
|
101
101
|
};
|
|
@@ -116,6 +116,7 @@ function boot({ session = {}, perms = { 'board.vote.cast': true } } = {}) {
|
|
|
116
116
|
return {
|
|
117
117
|
craft, root,
|
|
118
118
|
signIn: (rank = 'archon') => shell.applyAccess({ authed: true, rank, permissions: perms, builder: { id: 7, rank } }),
|
|
119
|
+
signOut: () => shell.applyAccess({ authed: false }),
|
|
119
120
|
// The tabs a reader can see: the gate let them through AND the mode kept them.
|
|
120
121
|
showing: () => parsed().flatMap((g) => g.items).filter((a) => !a.hidden && !a.hasAttribute('data-craft-off')).map((a) => a.id.slice(4)),
|
|
121
122
|
headShowing: (group) => {
|
|
@@ -150,7 +151,20 @@ test('every tab a mode names is a real NAV item, and every NAV item lives in som
|
|
|
150
151
|
assert.ok(NAV_IDS.length >= 24, `read ${NAV_IDS.length} NAV ids from shell.js`);
|
|
151
152
|
const named = new Set(CRAFTS.flatMap((m) => craft.tabsFor(m)));
|
|
152
153
|
for (const id of named) assert.ok(NAV_IDS.includes(id), `${id} is a NAV id`);
|
|
153
|
-
|
|
154
|
+
// Home is the one exception: in a mode the craft plate IS the Home button
|
|
155
|
+
// (task 1005065), so no mode lists the Home tab beside it (task 1005070).
|
|
156
|
+
for (const id of NAV_IDS) {
|
|
157
|
+
if (id === 'home') continue;
|
|
158
|
+
assert.ok(named.has(id), `${id} is reachable from some mode's nav`);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('ONE HOME: no mode shows the Home tab, because the plate is Home in every mode (task 1005070)', () => {
|
|
163
|
+
const { craft } = boot();
|
|
164
|
+
for (const m of CRAFTS) assert.ok(!craft.tabsFor(m).includes('home'), `${m} does not list the Home tab beside the plate`);
|
|
165
|
+
const none = boot();
|
|
166
|
+
none.signIn();
|
|
167
|
+
assert.ok(none.showing().includes('home'), 'with no mode there is no plate, so the Home tab shows');
|
|
154
168
|
});
|
|
155
169
|
|
|
156
170
|
test('Government shows only in Governing, and Governing is the shortest list', () => {
|
|
@@ -326,3 +340,37 @@ test('the dropdown opens on its own when the page you are on is inside it', () =
|
|
|
326
340
|
assert.equal(bootTree({ mode: 'artistry' }).fold(), null, 'no other mode folds anything');
|
|
327
341
|
assert.equal(bootTree().fold(), null, 'nor does no mode');
|
|
328
342
|
});
|
|
343
|
+
|
|
344
|
+
// ── the remembered nav (task 1005070) ──────────────────────────────────────
|
|
345
|
+
// Every hall page is a full load and /me is the slowest read the owner makes, so
|
|
346
|
+
// the rail used to boot empty and pop its tabs in once /me answered. The shell
|
|
347
|
+
// now keeps the last settled access and paints from it; /me (whose client never
|
|
348
|
+
// answers in this harness) still settles the rest.
|
|
349
|
+
const NAV_KEY = 'hall-nav-access';
|
|
350
|
+
|
|
351
|
+
test('THE REMEMBERED NAV: a signed-in settle is kept, and the next page paints it before /me answers', () => {
|
|
352
|
+
const local = {};
|
|
353
|
+
const first = boot({ local });
|
|
354
|
+
first.signIn();
|
|
355
|
+
const settled = first.showing();
|
|
356
|
+
const kept = JSON.parse(local[NAV_KEY]);
|
|
357
|
+
assert.equal(kept.rank, 'archon');
|
|
358
|
+
assert.equal(kept.builder.id, 7);
|
|
359
|
+
assert.ok(!('needs' in kept), 'never the needs roster, so a badge is never stale');
|
|
360
|
+
const next = boot({ local });
|
|
361
|
+
assert.deepEqual(next.showing(), settled, 'the next page paints the same tabs with /me still out');
|
|
362
|
+
const cold = boot();
|
|
363
|
+
assert.deepEqual(cold.showing(), ['home'], 'with nothing kept the rail waits for /me, as before');
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('THE REMEMBERED NAV is forgotten on a signed-out settle, and an old one is not painted', () => {
|
|
367
|
+
const local = {};
|
|
368
|
+
const p = boot({ local });
|
|
369
|
+
p.signIn();
|
|
370
|
+
assert.ok(local[NAV_KEY]);
|
|
371
|
+
p.signOut();
|
|
372
|
+
assert.ok(!(NAV_KEY in local), 'a sign-out or a 401 drops it');
|
|
373
|
+
const stale = boot({ local: { [NAV_KEY]: JSON.stringify({ at: Date.now() - 2 * 24 * 60 * 60 * 1000, rank: 'archon', builder: { id: 7 } }) } });
|
|
374
|
+
assert.deepEqual(stale.showing(), ['home'], 'a day-old memory is not painted');
|
|
375
|
+
assert.match(SHELL, /function doLogout\(\) \{\n\s+forgetAccess\(\);/, 'the sign-out link forgets it before it leaves');
|
|
376
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// tests/design_tokens_shared.mjs — the ONE shared token sheet every Cloud Bongos
|
|
2
|
+
// surface loads (task 1005081, goal 1000138 "One Cloud Bongos design system").
|
|
3
|
+
//
|
|
4
|
+
// modules/ui-design/public/tokens.css holds the named scales — type sizes, spacing,
|
|
5
|
+
// corners, shadows, motion, stacking layers — namespaced --ds-*, and the hall's own
|
|
6
|
+
// scale names became aliases onto it. What this pins, DB-free and browser-free:
|
|
7
|
+
// * the sheet exists and defines every scale;
|
|
8
|
+
// * it never redefines the fifteen pack tokens (the pack must keep winning) and
|
|
9
|
+
// carries no colour literal except black/white with alpha (the Fifteen Rule);
|
|
10
|
+
// * its dark rule is written twice, byte-identical (the dark twins);
|
|
11
|
+
// * every surface loads it — the hall through style.css's @import (all 33 hall
|
|
12
|
+
// pages link style.css), the landing + hub pages, the status page and the three
|
|
13
|
+
// raise views through a <link> placed BEFORE their own sheet, so a surface's own
|
|
14
|
+
// values still win on source order;
|
|
15
|
+
// * the hall's old token names still resolve — each alias names a --ds-* token the
|
|
16
|
+
// sheet defines;
|
|
17
|
+
// * the one URL is served on every host, status. included (serve-internal.js's
|
|
18
|
+
// module-asset route used to step aside on a generic surface host).
|
|
19
|
+
//
|
|
20
|
+
// Run: node --test tests/design_tokens_shared.mjs
|
|
21
|
+
|
|
22
|
+
import { test } from 'node:test';
|
|
23
|
+
import assert from 'node:assert/strict';
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import http from 'node:http';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { createRequire } from 'node:module';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
|
|
30
|
+
const require = createRequire(import.meta.url);
|
|
31
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
32
|
+
const URL_PATH = '/modules/ui-design/public/tokens.css';
|
|
33
|
+
const SHEET = fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'public', 'tokens.css'), 'utf8');
|
|
34
|
+
const decomment = (s) => s.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
35
|
+
const CODE = decomment(SHEET);
|
|
36
|
+
const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), 'utf8');
|
|
37
|
+
|
|
38
|
+
const blockAfter = (src, marker) => {
|
|
39
|
+
const at = src.indexOf(marker);
|
|
40
|
+
assert.notEqual(at, -1, `missing ${marker}`);
|
|
41
|
+
const open = src.indexOf('{', at);
|
|
42
|
+
return src.slice(open + 1, src.indexOf('}', open));
|
|
43
|
+
};
|
|
44
|
+
const decls = (body) => Object.fromEntries([...body.matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g)].map((m) => [m[1], m[2].trim()]));
|
|
45
|
+
const ROOT_TOKENS = decls(blockAfter(CODE, ':root {'));
|
|
46
|
+
|
|
47
|
+
test('the shared sheet defines every named scale', () => {
|
|
48
|
+
const scales = {
|
|
49
|
+
'type sizes': ['--ds-text-3xs', '--ds-text-2xs', '--ds-text-xs', '--ds-text-sm', '--ds-text-md', '--ds-text-lg', '--ds-text-xl', '--ds-text-2xl', '--ds-text-3xl', '--ds-text-4xl', '--ds-text-5xl', '--ds-text-title', '--ds-text-headline', '--ds-text-figure', '--ds-text-display'],
|
|
50
|
+
spacing: ['--ds-space-hair', '--ds-space-xs', '--ds-space-sm', '--ds-space-md', '--ds-space-lg', '--ds-space-xl'],
|
|
51
|
+
corners: ['--ds-radius-pill', '--ds-radius-panel', '--ds-radius-tile'],
|
|
52
|
+
shadows: ['--ds-shadow-pop'],
|
|
53
|
+
motion: ['--ds-dur-fast', '--ds-dur-base', '--ds-dur-slow', '--ds-ease', '--ds-ease-arrive'],
|
|
54
|
+
'stacking layers': ['--ds-z-progress', '--ds-z-topbar', '--ds-z-sidebar', '--ds-z-scrim', '--ds-z-drawer', '--ds-z-modal', '--ds-z-diagram-full'],
|
|
55
|
+
};
|
|
56
|
+
for (const [scale, names] of Object.entries(scales)) {
|
|
57
|
+
for (const n of names) assert.ok(ROOT_TOKENS[n], `${scale}: ${n} is defined on :root`);
|
|
58
|
+
}
|
|
59
|
+
// DESIGN.md's values, so loading the sheet moves nothing
|
|
60
|
+
assert.equal(ROOT_TOKENS['--ds-radius-pill'], '999px');
|
|
61
|
+
assert.equal(ROOT_TOKENS['--ds-radius-panel'], '16px');
|
|
62
|
+
assert.equal(ROOT_TOKENS['--ds-radius-tile'], '10px');
|
|
63
|
+
assert.deepEqual(['hair', 'xs', 'sm', 'md', 'lg', 'xl'].map((k) => ROOT_TOKENS[`--ds-space-${k}`]), ['6px', '10px', '14px', '22px', '34px', '52px']);
|
|
64
|
+
assert.equal(ROOT_TOKENS['--ds-ease'].replace(/\s+/g, ''), 'cubic-bezier(.2,.7,.3,1)', 'the one state easing');
|
|
65
|
+
assert.equal(ROOT_TOKENS['--ds-ease-arrive'].replace(/\s+/g, ''), 'cubic-bezier(.16,1,.3,1)', 'the entrance ease-out');
|
|
66
|
+
// the type sizes climb
|
|
67
|
+
const px = Object.entries(ROOT_TOKENS).filter(([k, v]) => k.startsWith('--ds-text-') && /^[\d.]+px$/.test(v)).map(([, v]) => parseFloat(v));
|
|
68
|
+
assert.deepEqual(px, [...px].sort((a, b) => a - b), 'the fixed type steps are declared small to large');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('every name is --ds-* — no shared name can collide with a surface\'s own', () => {
|
|
72
|
+
// cosmos.css spends --line as a COLOUR and raise.css sets --radius to 10px: an
|
|
73
|
+
// un-namespaced shared token would silently change whichever surface lacked it
|
|
74
|
+
const names = [...CODE.matchAll(/(--[\w-]+)\s*:/g)].map((m) => m[1]);
|
|
75
|
+
assert.ok(names.length > 40);
|
|
76
|
+
assert.deepEqual(names.filter((n) => !n.startsWith('--ds-')), []);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('it never redefines the fifteen pack tokens and holds no colour literal but black/white alpha', () => {
|
|
80
|
+
const pack = JSON.parse(read('config', 'branding.neutral.json')).theme.ui;
|
|
81
|
+
const fifteen = [...Object.keys(pack.colors).map((k) => '--' + k.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase())), '--font-display', '--font-body'];
|
|
82
|
+
assert.equal(fifteen.length, 15);
|
|
83
|
+
for (const t of fifteen) assert.doesNotMatch(CODE, new RegExp(`${t}\\s*:`), `${t} belongs to the pack`);
|
|
84
|
+
assert.doesNotMatch(CODE, /#[0-9a-f]{3,8}\b/i, 'no hex colour');
|
|
85
|
+
for (const m of CODE.matchAll(/rgba?\(([^)]*)\)/g)) {
|
|
86
|
+
const [r, g, b] = m[1].split(',').map((x) => parseFloat(x));
|
|
87
|
+
assert.ok((r === 0 && g === 0 && b === 0) || (r === 255 && g === 255 && b === 255), `only black/white with alpha: ${m[0]}`);
|
|
88
|
+
}
|
|
89
|
+
assert.doesNotMatch(CODE, /\b(hsla?|oklch|lab|lch)\(/, 'no other colour function');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('the dark rule is written twice, byte-identical', () => {
|
|
93
|
+
const sw = blockAfter(CODE, 'html[data-theme="dark"], :root[data-mode="dark"] {');
|
|
94
|
+
const media = CODE.slice(CODE.indexOf('@media (prefers-color-scheme: dark)'));
|
|
95
|
+
const twin = blockAfter(media, 'html:not([data-theme]):not([data-mode="light"]) {');
|
|
96
|
+
assert.ok(sw.trim().length > 0);
|
|
97
|
+
assert.equal(sw.replace(/\s+/g, ' ').trim(), twin.replace(/\s+/g, ' ').trim());
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('the reduced-motion switch zeroes the motion scale', () => {
|
|
101
|
+
const rm = blockAfter(CODE.slice(CODE.indexOf('@media (prefers-reduced-motion: reduce)')), ':root {');
|
|
102
|
+
for (const n of Object.keys(ROOT_TOKENS).filter((k) => k.startsWith('--ds-dur-') || k === '--ds-stagger')) {
|
|
103
|
+
assert.match(rm, new RegExp(`${n}\\s*:\\s*0ms`), `${n} is zeroed under reduced motion`);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('the hall loads it: style.css @imports it FIRST, and every hall page links style.css', () => {
|
|
108
|
+
const style = read('modules', 'hall-ui', 'public', 'style.css');
|
|
109
|
+
const imports = [...decomment(style).matchAll(/@import url\("([^"]+)"\)/g)].map((m) => m[1]);
|
|
110
|
+
assert.equal(imports[0], URL_PATH, 'the shared sheet is the first import, so the hall\'s own rules follow it');
|
|
111
|
+
const dir = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
112
|
+
const pages = fs.readdirSync(dir).filter((f) => f.endsWith('.html'));
|
|
113
|
+
assert.ok(pages.length > 20);
|
|
114
|
+
for (const p of pages) assert.match(fs.readFileSync(path.join(dir, p), 'utf8'), /<link rel="stylesheet" href="\/builders\/style\.css/, `${p} links style.css`);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('the landing, hub, status and raise pages link it before their own sheet', () => {
|
|
118
|
+
const tag = `<link rel="stylesheet" href="${URL_PATH}">`;
|
|
119
|
+
const pages = [
|
|
120
|
+
...fs.readdirSync(path.join(ROOT, 'modules', 'public-landing', 'public')).filter((f) => f.endsWith('.html')).map((f) => ['modules', 'public-landing', 'public', f]),
|
|
121
|
+
['modules', 'status-ui', 'public', 'index.html'],
|
|
122
|
+
['modules', 'raise', 'views', 'page.html'], ['modules', 'raise', 'views', 'gate.html'], ['modules', 'raise', 'views', 'nda.html'],
|
|
123
|
+
];
|
|
124
|
+
for (const p of pages) {
|
|
125
|
+
const html = read(...p);
|
|
126
|
+
const own = html.search(/<link rel="stylesheet" href="[^"]*(world|cosmos|style|raise)\.css/);
|
|
127
|
+
if (own === -1) continue; // a page with no surface sheet of its own (none today, but not this test's business)
|
|
128
|
+
const at = html.indexOf(tag);
|
|
129
|
+
assert.ok(at > -1, `${p.join('/')} links the shared sheet`);
|
|
130
|
+
assert.ok(at < own, `${p.join('/')} links it BEFORE its own sheet, so the surface's values still win`);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('the hall\'s old token names still resolve — each alias names a defined --ds-* token', () => {
|
|
135
|
+
const style = decomment(read('modules', 'hall-ui', 'public', 'style.css'));
|
|
136
|
+
const want = ['--line', '--radius', '--radius-sm', '--radius-pill', '--sp-hair', '--sp-xs', '--sp-sm', '--sp-md', '--sp-lg', '--sp-xl',
|
|
137
|
+
'--shadow-pop', '--z-progress', '--z-topbar', '--z-sidebar', '--z-scrim', '--z-drawer', '--z-modal', '--z-diagram-full', '--label-size'];
|
|
138
|
+
for (const n of want) {
|
|
139
|
+
const m = style.match(new RegExp(`${n}\\s*:\\s*var\\((--ds-[\\w-]+)\\)`));
|
|
140
|
+
assert.ok(m, `${n} is an alias onto the shared sheet`);
|
|
141
|
+
assert.ok(ROOT_TOKENS[m[1]], `${n} → ${m[1]} is defined in tokens.css`);
|
|
142
|
+
}
|
|
143
|
+
// the four-step --space-* scale still rides the --sp-* aliases
|
|
144
|
+
for (const n of ['--space-1', '--space-2', '--space-3', '--space-4']) assert.match(style, new RegExp(`${n}\\s*:\\s*var\\(--sp-`));
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ── the one URL, on every host ────────────────────────────────────────────────
|
|
148
|
+
// node:http, not fetch: fetch silently drops a Host override (tests/module_asset_subdomain.mjs).
|
|
149
|
+
function get(port, p, host) {
|
|
150
|
+
return new Promise((resolve, reject) => {
|
|
151
|
+
const req = http.request({ host: '127.0.0.1', port, path: p, headers: { Host: host } }, (res) => {
|
|
152
|
+
let body = '';
|
|
153
|
+
res.setEncoding('utf8');
|
|
154
|
+
res.on('data', (c) => { body += c; });
|
|
155
|
+
res.on('end', () => resolve({ status: res.statusCode, body, type: res.headers['content-type'] || '' }));
|
|
156
|
+
});
|
|
157
|
+
req.on('error', reject);
|
|
158
|
+
req.end();
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
test('the sheet is served at one path on the apex, the hall host and the status host', async () => {
|
|
163
|
+
const express = require('express');
|
|
164
|
+
const { mountInternalSurfaces } = require('../src/bongos/serve-internal.js');
|
|
165
|
+
const app = express();
|
|
166
|
+
mountInternalSurfaces(app);
|
|
167
|
+
const server = await new Promise((resolve) => { const s = app.listen(0, () => resolve(s)); });
|
|
168
|
+
try {
|
|
169
|
+
const port = server.address().port;
|
|
170
|
+
for (const host of ['example.com', 'builders.example.com', 'status.example.com']) {
|
|
171
|
+
const r = await get(port, URL_PATH, host);
|
|
172
|
+
assert.equal(r.status, 200, `${host} serves ${URL_PATH}`);
|
|
173
|
+
assert.match(r.type, /text\/css/, `${host} serves it as CSS`);
|
|
174
|
+
assert.match(r.body, /--ds-radius-panel:/, `${host} serves the shared sheet's bytes`);
|
|
175
|
+
}
|
|
176
|
+
// the status host still answers its OWN files first
|
|
177
|
+
const own = await get(port, '/style.css', 'status.example.com');
|
|
178
|
+
assert.equal(own.status, 200);
|
|
179
|
+
assert.doesNotMatch(own.body, /--ds-radius-panel:/);
|
|
180
|
+
} finally {
|
|
181
|
+
await new Promise((r) => server.close(r));
|
|
182
|
+
}
|
|
183
|
+
});
|
|
@@ -41,7 +41,8 @@ const scopes = require(path.join(GOV, 'session-scopes.js'));
|
|
|
41
41
|
const { attachFail } = require(path.join(ROOT, 'src', 'bongos', 'middleware', 'error-envelope.js'));
|
|
42
42
|
|
|
43
43
|
// Hand-written: the sources this instance accepts.
|
|
44
|
-
|
|
44
|
+
// 'runner' is an Autobongos PC's own login (task 1004547, src/bongos/runner-login.js).
|
|
45
|
+
const EXPECTED_SOURCES = ['cli', 'web', 'runner', null];
|
|
45
46
|
|
|
46
47
|
function fakeRes() {
|
|
47
48
|
const res = {
|
|
@@ -278,7 +278,11 @@ test('the viewer spends the named z rung, not a hand-written 9999', () => {
|
|
|
278
278
|
assert.ok(full, '.dgv.is-fullscreen has a rule');
|
|
279
279
|
assert.match(full, /z-index:\s*var\(--z-diagram-full\)/);
|
|
280
280
|
assert.doesNotMatch(decomment(viewerCss), /z-index:\s*\d/, 'no bare number is left in the sheet');
|
|
281
|
-
|
|
281
|
+
// task 1005081: the ladder's numbers live in the shared token sheet; the hall's
|
|
282
|
+
// rung is the alias onto it
|
|
283
|
+
assert.match(decomment(styleCss), /--z-diagram-full:\s*var\(--ds-z-diagram-full\)/, 'and the rung is named in the shell ladder');
|
|
284
|
+
const shared = fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'public', 'tokens.css'), 'utf8');
|
|
285
|
+
assert.match(decomment(shared), /--ds-z-diagram-full:\s*\d+/, 'and the shared sheet gives it a number');
|
|
282
286
|
});
|
|
283
287
|
|
|
284
288
|
test('the fullscreen scroll lock is on the root, where the shell already locks', () => {
|
package/tests/hall_tokens.mjs
CHANGED
|
@@ -82,6 +82,21 @@ const contractTier = declarations(bodyAt(src.indexOf('{', bareRoots[0].index)));
|
|
|
82
82
|
const darkExplicit = declarations(blockAfter('html[data-theme="dark"] {'));
|
|
83
83
|
const darkMedia = declarations(blockAfter('html:not([data-theme]) {'));
|
|
84
84
|
|
|
85
|
+
// THE SHARED TOKEN SHEET (task 1005081). style.css @imports
|
|
86
|
+
// modules/ui-design/public/tokens.css first, so its scales (--ds-*) sit UNDER the
|
|
87
|
+
// hall's own declarations, and the hall's scale names (--radius, --sp-*, --z-*,
|
|
88
|
+
// --shadow-pop, --line, --label-size) are aliases onto it. Its :root and its dark
|
|
89
|
+
// switch rule are read here so the resolver sees what the browser sees.
|
|
90
|
+
const SHARED_SRC = fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'public', 'tokens.css'), 'utf8');
|
|
91
|
+
const sharedBlock = (marker) => {
|
|
92
|
+
const at = SHARED_SRC.indexOf(marker);
|
|
93
|
+
assert.notEqual(at, -1, `tokens.css no longer contains ${marker} — update this test`);
|
|
94
|
+
const open = SHARED_SRC.indexOf('{', at);
|
|
95
|
+
return declarations(SHARED_SRC.slice(open + 1, SHARED_SRC.indexOf('}', open)));
|
|
96
|
+
};
|
|
97
|
+
const sharedRoot = sharedBlock(':root {');
|
|
98
|
+
const sharedDark = sharedBlock('html[data-theme="dark"]');
|
|
99
|
+
|
|
85
100
|
// THE CASCADE THAT ACTUALLY SHIPS. injectBrandingInto splices the pack's theme
|
|
86
101
|
// <style> at the END of <head> — after style.css's <link> — as a plain :root.
|
|
87
102
|
// Equal specificity, later in document order, so for the tokens the pack emits
|
|
@@ -112,8 +127,8 @@ const NIGHT_TINT = process.env.UI_DESIGN_TINT === '1'
|
|
|
112
127
|
: {};
|
|
113
128
|
|
|
114
129
|
const RAW = {
|
|
115
|
-
light: { ...contractTier, ...PACK },
|
|
116
|
-
dark: { ...contractTier, ...PACK, ...darkExplicit, ...NIGHT_TINT },
|
|
130
|
+
light: { ...sharedRoot, ...contractTier, ...PACK },
|
|
131
|
+
dark: { ...sharedRoot, ...contractTier, ...PACK, ...sharedDark, ...darkExplicit, ...NIGHT_TINT },
|
|
117
132
|
};
|
|
118
133
|
|
|
119
134
|
// ── color resolution ─────────────────────────────────────────────────────────
|
|
@@ -319,7 +334,10 @@ test('--line is a width, not a colour', () => {
|
|
|
319
334
|
// DESIGN.md warns about this exact name: --line is a 1px WIDTH here and a
|
|
320
335
|
// hairline COLOUR in the hub's sheet. Page sheets that treated it as a colour
|
|
321
336
|
// were repointed at --rule / --rule-soft in task 1003303.
|
|
322
|
-
|
|
337
|
+
// an alias onto the shared sheet since task 1005081 — follow it one step
|
|
338
|
+
const alias = /^var\((--[\w-]+)\)$/.exec(contractTier['--line']);
|
|
339
|
+
const line = alias ? sharedRoot[alias[1]] : contractTier['--line'];
|
|
340
|
+
assert.match(String(line), /^\d+px$/, '--line must resolve to a length');
|
|
323
341
|
const sheets = fs.readdirSync(HALL).filter((f) => f.endsWith('.css'));
|
|
324
342
|
const misuse = sheets.filter((f) => /var\(--line\s*,/.test(fs.readFileSync(path.join(HALL, f), 'utf8')));
|
|
325
343
|
assert.deepEqual(misuse, [], 'a colour fallback on --line means the sheet thinks it is a colour');
|
|
@@ -346,6 +364,7 @@ test('every token a hall stylesheet references is defined', () => {
|
|
|
346
364
|
const sheets = fs.readdirSync(HALL).filter((f) => f.endsWith('.css'));
|
|
347
365
|
const defined = new Set([
|
|
348
366
|
...Object.keys(contractTier), ...Object.keys(darkExplicit), ...Object.keys(PACK),
|
|
367
|
+
...Object.keys(sharedRoot), // the shared token sheet the hall @imports (task 1005081)
|
|
349
368
|
]);
|
|
350
369
|
// A sheet may define its own private tokens (atlas.css's --ab-* family), and
|
|
351
370
|
// a page script may set one inline on the element (roadmap.js writes
|
|
@@ -100,7 +100,8 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
|
|
|
100
100
|
// task 1005076: the setup flow's holdForSetup;
|
|
101
101
|
// task 1005050: Accomplishments left it (folded into Profile; its hashes redirect);
|
|
102
102
|
// task 1005051: Roadmap left it (folded into Goals; /roadmap redirects).
|
|
103
|
-
|
|
103
|
+
// task 1005070: the remembered nav (the rail paints before /me).
|
|
104
|
+
const SHELL_SHA = 'dbbe6f2eb49a65c3';
|
|
104
105
|
|
|
105
106
|
test('shell.js has not changed without its cache-bust being bumped', () => {
|
|
106
107
|
const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
|
package/tests/ui_design_kit.mjs
CHANGED
|
@@ -125,6 +125,36 @@ test('the page argument survives Git Bash path conversion of a bare slash', () =
|
|
|
125
125
|
assert.equal(lib.pageName('/'), 'index'); assert.equal(lib.pageName('/builders/ranks'), 'ranks'); assert.equal(lib.pageName('/projects?view=mine'), 'projects');
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
+
test('a page at the surface root reads index.states.json (task 1005080)', () => {
|
|
129
|
+
const hall = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
130
|
+
assert.equal(lib.sidecarPath(hall, '/builders/', 'states'), path.join(hall, 'index.states.json'), 'the hall Home has no last segment of its own');
|
|
131
|
+
assert.equal(lib.sidecarPath(hall, '/builders/work', 'states'), path.join(hall, 'work.states.json'), 'a named page keeps its own file');
|
|
132
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'kit-root-'));
|
|
133
|
+
assert.equal(lib.sidecarPath(tmp, '/builders/', 'states'), path.join(tmp, 'builders.states.json'), 'no index file: the old name stands, so the miss is reported by name');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('storage: a states or probes file may seed browser storage, and a bad shape is refused (task 1005080)', () => {
|
|
137
|
+
const ok = { page: '/x', storage: { session: { a: '1' }, local: { b: 'x' } }, states: { s: { storage: { session: { c: '2' } } } } };
|
|
138
|
+
assert.deepEqual(lib.validateStatesFile(ok).errors, []);
|
|
139
|
+
assert.ok(lib.validateStatesFile({ page: '/x', storage: { cookie: {} }, states: { s: {} } }).errors.some((e) => /storage\.cookie/.test(e)), 'only session and local');
|
|
140
|
+
assert.ok(lib.validateStatesFile({ page: '/x', storage: { session: { a: 1 } }, states: { s: {} } }).errors.some((e) => /storage\.session/.test(e)), 'values are strings');
|
|
141
|
+
assert.ok(lib.validateStatesFile({ page: '/x', states: { s: { storage: [] } } }).errors.some((e) => /states\.s\.storage/.test(e)), 'a state may carry its own');
|
|
142
|
+
assert.ok(lib.validateProbesFile({ page: '/x', storage: 'yes', probes: [{ name: 'p', expect: { visible: ['a'] } }] }).errors.some((e) => /^storage/.test(e)), 'a probes file too');
|
|
143
|
+
assert.deepEqual(lib.mergeStorage({ session: { a: '1', b: '1' } }, undefined, { session: { b: '2' }, local: { c: '3' } }), { session: { a: '1', b: '2' }, local: { c: '3' } }, 'a state layers over the file');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('every hall sidecar renders the page a returning builder sees, not the first-visit mode picker (task 1005080)', () => {
|
|
147
|
+
const hall = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
148
|
+
const files = fs.readdirSync(hall).filter((n) => /\.(states|probes)\.json$/.test(n));
|
|
149
|
+
assert.ok(files.length > 30);
|
|
150
|
+
for (const n of files) {
|
|
151
|
+
const json = JSON.parse(fs.readFileSync(path.join(hall, n), 'utf8'));
|
|
152
|
+
assert.equal(json.storage && json.storage.session && json.storage.session['hall-craft-asked'], '1', `${n}: seeds the picker as already asked`);
|
|
153
|
+
}
|
|
154
|
+
// the key is craft-mode.js's own; a rename there must break this pin, not silently bring the picker back
|
|
155
|
+
assert.match(fs.readFileSync(path.join(hall, 'craft-mode.js'), 'utf8'), /var ASKED_KEY = 'hall-craft-asked';/);
|
|
156
|
+
});
|
|
157
|
+
|
|
128
158
|
// ── the mock checker: platform rules vs the instance's taste ──────────────────
|
|
129
159
|
const GOOD_MOCK = `<!doctype html><html><head><style>
|
|
130
160
|
body{--pad:8px;color:var(--ink);background:var(--bg)}
|
|
@@ -89,6 +89,8 @@ test('first visit of the day: the welcome, not the craft picker; Skip brings the
|
|
|
89
89
|
await signIn(page);
|
|
90
90
|
assert.deepEqual(await state(page), { welcome: true, picker: false, craft: null, asked: '1' },
|
|
91
91
|
'the welcome shows, the picker is marked asked and does not stack on it');
|
|
92
|
+
assert.equal(await page.evaluate(() => document.querySelector('.wb__banner').tagName), 'DIV',
|
|
93
|
+
'over a signed-in hall the shell keeps the one <header>; the banner is a plain block (task 1005080)');
|
|
92
94
|
// The painting loads and its motion is laid over it.
|
|
93
95
|
await page.waitForFunction(() => { const i = document.querySelector('#welcome-boot .wb__img'); return i && i.complete && i.naturalWidth === 1024; });
|
|
94
96
|
assert.equal(await page.isVisible('[data-wb-skip]'), true);
|
|
@@ -135,9 +137,11 @@ test('a visitor who is not signed in gets the Welcome on Home with a sign-in but
|
|
|
135
137
|
roles: !!document.querySelector('[data-wb-roles]'),
|
|
136
138
|
keys: Object.keys(localStorage).filter((k) => k.startsWith('hall-welcome-day') || k === 'hall-welcome-signin'),
|
|
137
139
|
picker: !!document.getElementById('craft-picker'),
|
|
140
|
+
banner: document.querySelector('.wb__banner').tagName,
|
|
138
141
|
}));
|
|
139
142
|
assert.match(s.hi, /^Welcome to /);
|
|
140
143
|
assert.deepEqual([s.signin, s.roles, s.keys, s.picker], [true, false, [], false]);
|
|
144
|
+
assert.equal(s.banner, 'HEADER', 'for a visitor the welcome is the page, so its banner is the one <header> (task 1005080)');
|
|
141
145
|
await ctx.close();
|
|
142
146
|
});
|
|
143
147
|
|