@bongos/core 1.20.59 → 1.20.61
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 +151 -91
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +10 -0
- package/clients/bongos-client/index.cjs +10 -0
- package/clients/bongos-client/index.d.ts +14 -1
- package/clients/bongos-client/index.mjs +10 -0
- package/docs/adr/0358-a-hall-wears-a-look-and-its-night-takes-a-tint.md +52 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +324 -3
- package/docs/api-reference.md +8 -3
- package/docs/copy-inventory.md +343 -333
- package/docs/copy-registry.json +527 -433
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +4 -1
- package/docs/page-readings.json +758 -707
- package/modules/autonomy/db.js +13 -6
- package/modules/autonomy/migrations/autonomy_004_runner_identity.sql +30 -0
- package/modules/autonomy/routes/autonomy.js +16 -0
- package/modules/autonomy/runner-health.js +66 -5
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/gate.js +24 -8
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +5 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +8 -0
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/provisioning/look.js +169 -0
- package/modules/provisioning/migrations/provisioning_034_project_logo.sql +35 -0
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/provisioning.js +4 -3
- package/modules/provisioning/routes/look.js +204 -0
- package/modules/provisioning/routes/provisioning.js +5 -59
- package/modules/provisioning/settings-push.js +76 -0
- package/modules/public-landing/public/assets/cosmos.css +1 -1
- package/modules/public-landing/public/projects.html +499 -28
- package/modules/public-landing/public/projects.probes.json +2 -2
- package/modules/public-landing/public/projects.states.json +6 -3
- package/modules/ui-design/kit/serve.js +50 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/autobongos-run.js +43 -2
- package/scripts/gds/fitness.js +4 -0
- package/src/bongos/routes.js +4 -1
- package/src/bongos/serve-internal.js +6 -0
- package/src/branding.js +26 -1
- package/src/hall-look.js +99 -0
- package/src/module-api.js +1 -1
- package/src/night-tint.js +26 -0
- package/tests/autonomy_runner_identity.mjs +215 -0
- package/tests/currency_label.mjs +7 -4
- package/tests/hall_tokens.mjs +10 -1
- package/tests/hall_tweak_editor.mjs +1 -1
- package/tests/look_night_tint.mjs +169 -0
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/projects_hub.mjs +5 -4
- package/tests/projects_hub_app_status.mjs +1 -1
- package/tests/projects_hub_app_step.mjs +8 -7
- package/tests/projects_hub_look.mjs +255 -0
- package/tests/projects_hub_module_picker.mjs +2 -2
- package/tests/provision_settings_apply.mjs +12 -4
- package/tests/provisioning_look.mjs +376 -0
- package/tests/provisioning_settings.mjs +10 -7
- package/tests/provisioning_settings_apply.mjs +4 -4
- package/tests/provisioning_settings_env.mjs +4 -0
- package/tests/wizard_draft_resume.mjs +2 -2
- package/tests/wizard_front_door.mjs +2 -2
- package/tests/wizard_intent_resume.mjs +16 -15
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// tests/autonomy_runner_identity.mjs — the runner card says which PC, which Claude
|
|
2
|
+
// account, which code, and whether the runner is live or restarting (task 1004407).
|
|
3
|
+
//
|
|
4
|
+
// Owner ruling, 2026-10-01: showing the code version is good, and showing when it
|
|
5
|
+
// is restarting versus live is better, along with which always-on PC and which
|
|
6
|
+
// Claude account it runs on. These pin each of those from the runner that reports
|
|
7
|
+
// them, through the route and the SQL, to the verdict the hall renders. The rule
|
|
8
|
+
// that holds across all of it: an unknown is shown as nothing, never as a guess,
|
|
9
|
+
// and a secret is never what gets sent.
|
|
10
|
+
//
|
|
11
|
+
// DB-free, no network. Run: node --test tests/autonomy_runner_identity.mjs
|
|
12
|
+
|
|
13
|
+
import { test } from 'node:test';
|
|
14
|
+
import assert from 'node:assert/strict';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
const req = createRequire(import.meta.url);
|
|
22
|
+
const express = req('express');
|
|
23
|
+
const { attachFail } = req(path.join(ROOT, 'src/bongos/middleware/error-envelope.js'));
|
|
24
|
+
const health = req(path.join(ROOT, 'modules/autonomy/runner-health.js'));
|
|
25
|
+
const runner = req(path.join(ROOT, 'scripts/gds/autobongos-run.js'));
|
|
26
|
+
|
|
27
|
+
const NOW = Date.parse('2026-10-01T12:00:00.000Z');
|
|
28
|
+
const agoS = (s) => new Date(NOW - s * 1000).toISOString();
|
|
29
|
+
const A = 'a'.repeat(40);
|
|
30
|
+
const B = 'b'.repeat(40);
|
|
31
|
+
const hb = (over = {}) => ({ host: 'gmk-always-on', mode: 'full', last_event: 'loop', last_seen_at: agoS(30), ...over });
|
|
32
|
+
|
|
33
|
+
// ── live or restarting ───────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
test('a runner that just exited to pick up new code reads RESTARTING, not dead', () => {
|
|
36
|
+
const d = health.describeRunner(hb({ last_event: 'upgrade_exit', last_seen_at: agoS(90) }), { nowMs: NOW });
|
|
37
|
+
assert.equal(d.state, 'restarting');
|
|
38
|
+
assert.equal(d.ok, true, 'a restart is not a fault');
|
|
39
|
+
assert.match(d.detail, /restarting on gmk-always-on to pick up new code/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('a restart that never came back is dead, and says it was a restart', () => {
|
|
43
|
+
const d = health.describeRunner(hb({ last_event: 'upgrade_exit', last_seen_at: agoS(health.RESTART_S + 60) }), { nowMs: NOW });
|
|
44
|
+
assert.equal(d.state, 'dead');
|
|
45
|
+
assert.equal(d.ok, false);
|
|
46
|
+
assert.match(d.detail, /exited on gmk-always-on .* to restart on new code and has not come back/);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('the restart window ends exactly at RESTART_S', () => {
|
|
50
|
+
assert.equal(health.describeRunner(hb({ last_event: 'upgrade_exit', last_seen_at: agoS(health.RESTART_S) }), { nowMs: NOW }).state, 'restarting');
|
|
51
|
+
assert.equal(health.describeRunner(hb({ last_event: 'upgrade_exit', last_seen_at: agoS(health.RESTART_S + 1) }), { nowMs: NOW }).state, 'dead');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('a live runner says what it last did, in words', () => {
|
|
55
|
+
assert.match(health.describeRunner(hb({ last_event: 'code_refreshed' }), { nowMs: NOW }).detail, /updated its code in place on gmk-always-on/);
|
|
56
|
+
assert.match(health.describeRunner(hb({ last_event: 'waiting' }), { nowMs: NOW }).detail, /waiting for work/);
|
|
57
|
+
assert.match(health.describeRunner(hb({ last_event: 'loop' }), { nowMs: NOW }).detail, /^running on/, 'an event with no words keeps the old wording');
|
|
58
|
+
assert.match(health.describeRunner(hb({ working_task_id: '4242', last_event: 'worked' }), { nowMs: NOW }).detail, /working task 4242/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ── which code ───────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
test('the code version is shown short, and flagged when it is behind main', () => {
|
|
64
|
+
const d = health.describeRunner(hb({ head: A, main_head: B }), { nowMs: NOW });
|
|
65
|
+
assert.equal(d.code.short, 'aaaaaaaa');
|
|
66
|
+
assert.equal(d.code.behind, true);
|
|
67
|
+
assert.equal(d.code.mainShort, 'bbbbbbbb');
|
|
68
|
+
assert.equal(health.describeRunner(hb({ head: A, main_head: A }), { nowMs: NOW }).code.behind, false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('an unknown main is never drawn as "out of date"', () => {
|
|
72
|
+
const c = health.describeRunner(hb({ head: A }), { nowMs: NOW }).code;
|
|
73
|
+
assert.equal(c.behind, null);
|
|
74
|
+
assert.equal(c.mainShort, null);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('a checkout refreshed in place is current by what is on DISK', () => {
|
|
78
|
+
// Workers and child processes run what is on disk (task 1004406), so a
|
|
79
|
+
// refreshed runner whose own process is older is still up to date.
|
|
80
|
+
const c = health.describeRunner(hb({ head: A, disk_head: B, main_head: B }), { nowMs: NOW }).code;
|
|
81
|
+
assert.equal(c.behind, false);
|
|
82
|
+
assert.equal(c.diskShort, 'bbbbbbbb');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('no head, or a head that is not a commit, shows no code line at all', () => {
|
|
86
|
+
for (const head of [null, undefined, '', 'abc', 'Z'.repeat(40), A.toUpperCase()]) {
|
|
87
|
+
assert.equal(health.describeRunner(hb({ head }), { nowMs: NOW }).code, null, String(head));
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ── which PC and which account ───────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
test('the PC and the Claude account ride on every state, dead included', () => {
|
|
94
|
+
for (const [over, state] of [[{}, 'alive'], [{ last_seen_at: agoS(health.LATE_S + 1) }, 'dead'], [{ last_event: 'upgrade_exit' }, 'restarting']]) {
|
|
95
|
+
const d = health.describeRunner(hb({ claude_account: 'me@example.com', head: A, ...over }), { nowMs: NOW });
|
|
96
|
+
assert.equal(d.state, state);
|
|
97
|
+
assert.equal(d.host, 'gmk-always-on');
|
|
98
|
+
assert.equal(d.account, 'me@example.com', `${state} must still say whose account it spent`);
|
|
99
|
+
assert.ok(d.code, `${state} must still say which code`);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('the account label names a key or a token by its variable, never its value', () => {
|
|
104
|
+
const secret = 'sk-ant-SECRET-VALUE';
|
|
105
|
+
const viaKey = runner.claudeAccountLabel({ env: { ANTHROPIC_API_KEY: secret }, readFile: () => { throw new Error('unread'); } });
|
|
106
|
+
assert.equal(viaKey, 'an API key (ANTHROPIC_API_KEY)');
|
|
107
|
+
const viaToken = runner.claudeAccountLabel({ env: { CLAUDE_CODE_OAUTH_TOKEN: secret }, readFile: () => { throw new Error('unread'); } });
|
|
108
|
+
assert.equal(viaToken, 'a Claude token (CLAUDE_CODE_OAUTH_TOKEN)');
|
|
109
|
+
assert.ok(!viaKey.includes(secret) && !viaToken.includes(secret));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('otherwise the label is the login Claude Code records, with its organisation', () => {
|
|
113
|
+
const seen = [];
|
|
114
|
+
const file = (o) => (p) => { seen.push(p); return JSON.stringify({ oauthAccount: o, primaryApiKey: 'never-read' }); };
|
|
115
|
+
assert.equal(runner.claudeAccountLabel({ env: { CLAUDE_CONFIG_DIR: '/cfg' }, readFile: file({ emailAddress: 'me@example.com', organizationName: 'Acme' }) }), 'me@example.com · Acme');
|
|
116
|
+
assert.equal(path.normalize(seen[0]), path.normalize('/cfg/.claude.json'), 'CLAUDE_CONFIG_DIR is where Claude Code keeps it when set');
|
|
117
|
+
assert.equal(runner.claudeAccountLabel({ env: {}, readFile: file({ emailAddress: 'me@example.com' }) }), 'me@example.com');
|
|
118
|
+
assert.equal(runner.claudeAccountLabel({ env: {}, readFile: file({}) }), null, 'no login reads as unknown');
|
|
119
|
+
assert.equal(runner.claudeAccountLabel({ env: {}, readFile: () => 'not json' }), null);
|
|
120
|
+
assert.equal(runner.claudeAccountLabel({ env: {}, readFile: () => { throw new Error('ENOENT'); } }), null);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ── the runner sends it ──────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
test('every server check-in carries the code and the account, and the newest main seen', async () => {
|
|
126
|
+
const bodies = [];
|
|
127
|
+
const api = { autonomy: { postAutonomyHeartbeat: async ({ body }) => { bodies.push(body); return { ok: true }; } } };
|
|
128
|
+
const state = runner.newRunnerState();
|
|
129
|
+
// codeDrifted is what learns main's tip; it must keep it for the heartbeat.
|
|
130
|
+
await runner.codeDrifted({ loadedHead: async () => A, run: async () => ({ ok: true, stdout: `${B}\trefs/heads/main\n` }), state });
|
|
131
|
+
assert.equal(state.mainHead, B);
|
|
132
|
+
await runner.loadedHead({ run: async () => ({ ok: true, stdout: `${A}\n` }) });
|
|
133
|
+
await runner.postHeartbeat({ last_event: 'loop' }, { api, state, claudeAccountLabel: () => 'me@example.com' });
|
|
134
|
+
assert.equal(bodies[0].head, A);
|
|
135
|
+
assert.equal(bodies[0].main_head, B);
|
|
136
|
+
assert.equal(bodies[0].claude_account, 'me@example.com');
|
|
137
|
+
assert.equal(bodies[0].last_event, 'loop');
|
|
138
|
+
assert.equal('disk_head' in bodies[0], false, 'no refresh in place, so no disk_head');
|
|
139
|
+
|
|
140
|
+
state.diskHead = B;
|
|
141
|
+
await runner.postHeartbeat({}, { api, state, claudeAccountLabel: () => null });
|
|
142
|
+
assert.equal(bodies[1].disk_head, B);
|
|
143
|
+
assert.equal('claude_account' in bodies[1], false, 'an unknown account is left out, not sent empty');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ── the route and the SQL ────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
function autonomyRouter() {
|
|
149
|
+
const api = req(path.join(ROOT, 'src/module-api.js'));
|
|
150
|
+
const saved = { b: api.requireBuilder, p: api.requirePermission };
|
|
151
|
+
api.requireBuilder = (r, _s, next) => { r.builder = { id: 3, rank: 'archon' }; next(); };
|
|
152
|
+
api.requirePermission = () => (_r, _s, next) => next();
|
|
153
|
+
const routePath = req.resolve(path.join(ROOT, 'modules/autonomy/routes/autonomy.js'));
|
|
154
|
+
delete req.cache[routePath];
|
|
155
|
+
try { return req(routePath)(); } finally { api.requireBuilder = saved.b; api.requirePermission = saved.p; }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
test('the route stores a commit only if it is one, and the account label as sent', async () => {
|
|
159
|
+
const db = req(path.join(ROOT, 'modules/autonomy/db.js'));
|
|
160
|
+
const saved = db.recordRunnerHeartbeat;
|
|
161
|
+
const seen = [];
|
|
162
|
+
db.recordRunnerHeartbeat = async (a) => { seen.push(a); return {}; };
|
|
163
|
+
const app = express(); app.use(express.json()); app.use(attachFail); app.use(autonomyRouter());
|
|
164
|
+
const server = app.listen(0); await new Promise((r) => server.once('listening', r));
|
|
165
|
+
const post = (body) => fetch(`http://127.0.0.1:${server.address().port}/autonomy/heartbeat`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
|
|
166
|
+
try {
|
|
167
|
+
let r = await post({ host: 'gmk-always-on', head: A, disk_head: B, main_head: B, claude_account: ' me@example.com ' });
|
|
168
|
+
assert.equal(r.status, 200);
|
|
169
|
+
assert.deepEqual([seen[0].head, seen[0].diskHead, seen[0].mainHead, seen[0].claudeAccount], [A, B, B, 'me@example.com']);
|
|
170
|
+
r = await post({ host: 'gmk-always-on', head: 'not-a-commit', main_head: A.toUpperCase(), claude_account: '' });
|
|
171
|
+
assert.equal(r.status, 200, 'a bad version never costs the runner its check-in');
|
|
172
|
+
assert.deepEqual([seen[1].head, seen[1].mainHead, seen[1].claudeAccount], [null, null, null]);
|
|
173
|
+
r = await post({ host: 'gmk-always-on', claude_account: 'x'.repeat(201) });
|
|
174
|
+
assert.equal(r.status, 400, 'the label is capped, so a key pasted into it by mistake is refused, not stored');
|
|
175
|
+
} finally {
|
|
176
|
+
server.close();
|
|
177
|
+
db.recordRunnerHeartbeat = saved;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('the heartbeat SQL overwrites the identity every beat and reads it back', async () => {
|
|
182
|
+
const api = req(path.join(ROOT, 'src/module-api.js'));
|
|
183
|
+
const db = req(path.join(ROOT, 'modules/autonomy/db.js'));
|
|
184
|
+
const saved = api.pool.query;
|
|
185
|
+
const calls = [];
|
|
186
|
+
api.pool.query = async (sql, params) => { calls.push({ sql, params }); return { rows: [{ head: A }] }; };
|
|
187
|
+
try {
|
|
188
|
+
await db.recordRunnerHeartbeat({ builderId: 3, host: 'gmk-always-on', head: A, diskHead: B, mainHead: B, claudeAccount: 'me@example.com' });
|
|
189
|
+
for (const col of ['head', 'disk_head', 'main_head', 'claude_account']) {
|
|
190
|
+
assert.match(calls[0].sql, new RegExp(`\\b${col} = EXCLUDED\\.${col}\\b`), `${col} is overwritten, never kept from the last process`);
|
|
191
|
+
}
|
|
192
|
+
assert.deepEqual(calls[0].params.slice(9), [A, B, B, 'me@example.com']);
|
|
193
|
+
await db.readRunnerHeartbeats(3);
|
|
194
|
+
assert.match(calls[1].sql, /head, disk_head, main_head, claude_account/);
|
|
195
|
+
} finally {
|
|
196
|
+
api.pool.query = saved;
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('the migration adds the four columns, additively', () => {
|
|
201
|
+
const sql = fs.readFileSync(path.join(ROOT, 'modules/autonomy/migrations/autonomy_004_runner_identity.sql'), 'utf8')
|
|
202
|
+
.split('\n').filter((l) => !l.trim().startsWith('--')).join('\n');
|
|
203
|
+
for (const col of ['head', 'disk_head', 'main_head', 'claude_account']) {
|
|
204
|
+
assert.match(sql, new RegExp(`ADD COLUMN IF NOT EXISTS ${col} text`));
|
|
205
|
+
}
|
|
206
|
+
assert.doesNotMatch(sql, /\bDROP\b/i);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('the gate card draws the PC, the account, the code and the restarting label', () => {
|
|
210
|
+
const src = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/gate.js'), 'utf8');
|
|
211
|
+
assert.match(src, /restarting: 'Restarting'/);
|
|
212
|
+
assert.match(src, /PC <strong>\$\{escapeHtml\(h\.host\)\}<\/strong>/);
|
|
213
|
+
assert.match(src, /Claude account \$\{escapeHtml\(h\.account\)\}/);
|
|
214
|
+
assert.match(src, /behind main/);
|
|
215
|
+
});
|
package/tests/currency_label.mjs
CHANGED
|
@@ -59,11 +59,14 @@ test('clientBranding exposes project policy — the settings read-back (task 100
|
|
|
59
59
|
// is published for a reason of its own: it is the one key that can REFUSE a
|
|
60
60
|
// module, so an owner looking at a module that will not turn on needs the
|
|
61
61
|
// instance's own answer rather than the platform's record of what they saved.
|
|
62
|
+
// `look` and `nightTint` joined in task 1004421 (ADR 0358) — the look the hall wears
|
|
63
|
+
// and whether its night takes a tint. Public because the hall paints them in the
|
|
64
|
+
// browser; their fallbacks ('own', 'on') are what an unconfigured hall shows today.
|
|
62
65
|
assert.deepEqual(clientBranding({}).project,
|
|
63
|
-
{ platformVisibility: 'public', joinability: 'apply', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none' });
|
|
66
|
+
{ platformVisibility: 'public', joinability: 'apply', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none', look: 'own', nightTint: 'on' });
|
|
64
67
|
assert.deepEqual(
|
|
65
|
-
clientBranding({ project: { platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off', spendPayer: 'builder' } }).project,
|
|
66
|
-
{ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off', spendPayer: 'builder' },
|
|
68
|
+
clientBranding({ project: { platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off', spendPayer: 'builder', look: 'grove', nightTint: 'off' } }).project,
|
|
69
|
+
{ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off', spendPayer: 'builder', look: 'grove', nightTint: 'off' },
|
|
67
70
|
'what the instance actually resolved (env override included) is what it publishes');
|
|
68
71
|
});
|
|
69
72
|
|
|
@@ -86,7 +89,7 @@ test('clientBranding is a SMALL allow-list — no server-only field leaks to the
|
|
|
86
89
|
// public for the same read-back reason as its neighbours. Its companion stamp
|
|
87
90
|
// (artistGateSince) deliberately does NOT: that is platform bookkeeping the server
|
|
88
91
|
// reads, and no browser surface has a question it answers.
|
|
89
|
-
assert.deepEqual(Object.keys(projected.project).sort(), ['artistGate', 'joinGrant', 'joinability', 'platformVisibility', 'spendPayer', 'visibility']);
|
|
92
|
+
assert.deepEqual(Object.keys(projected.project).sort(), ['artistGate', 'joinGrant', 'joinability', 'look', 'nightTint', 'platformVisibility', 'spendPayer', 'visibility']);
|
|
90
93
|
for (const leak of ['domains', 'repo', 'db', 'envPrefix', 'llmProjectName']) {
|
|
91
94
|
assert.equal(projected[leak], undefined, `${leak} must not reach the client`);
|
|
92
95
|
}
|
package/tests/hall_tokens.mjs
CHANGED
|
@@ -41,6 +41,7 @@ import assert from 'node:assert/strict';
|
|
|
41
41
|
import fs from 'node:fs';
|
|
42
42
|
import path from 'node:path';
|
|
43
43
|
import { fileURLToPath } from 'node:url';
|
|
44
|
+
import { createRequire } from 'node:module';
|
|
44
45
|
|
|
45
46
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
46
47
|
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
@@ -102,9 +103,17 @@ const PACK = Object.fromEntries(
|
|
|
102
103
|
Object.entries(packUi.colors).map(([k, v]) => [camelToVar(k), v]),
|
|
103
104
|
);
|
|
104
105
|
|
|
106
|
+
// UI_DESIGN_TINT=1 measures the NIGHT TINT a hall wears with a look (ADR 0358, task
|
|
107
|
+
// 1004421): themeMarkup() lays src/night-tint.js's four grounds over the dark tier,
|
|
108
|
+
// after the sheet, so they win. tests/look_night_tint.mjs runs this file once per look
|
|
109
|
+
// with it set, and once per custom accent round the hue wheel.
|
|
110
|
+
const NIGHT_TINT = process.env.UI_DESIGN_TINT === '1'
|
|
111
|
+
? createRequire(import.meta.url)('../src/night-tint.js').NIGHT_TINT
|
|
112
|
+
: {};
|
|
113
|
+
|
|
105
114
|
const RAW = {
|
|
106
115
|
light: { ...contractTier, ...PACK },
|
|
107
|
-
dark: { ...contractTier, ...PACK, ...darkExplicit },
|
|
116
|
+
dark: { ...contractTier, ...PACK, ...darkExplicit, ...NIGHT_TINT },
|
|
108
117
|
};
|
|
109
118
|
|
|
110
119
|
// ── color resolution ─────────────────────────────────────────────────────────
|
|
@@ -635,7 +635,7 @@ try {
|
|
|
635
635
|
assert.match(HTML, /<body data-page="tweak-editor"[^>]*data-fullscreen-switch/);
|
|
636
636
|
assert.equal((HTML.match(/<h1\b/g) || []).length, 1);
|
|
637
637
|
assert.ok(HTML.indexOf('tweak-editor-lib.js') < HTML.indexOf('tweak-editor.js?'), 'the lib loads first');
|
|
638
|
-
assert.match(HTML, /shell\.js\?v=2026-
|
|
638
|
+
assert.match(HTML, /shell\.js\?v=2026-10-01-look/);
|
|
639
639
|
assert.equal(/fonts\.googleapis\.com\/css/.test(HTML), false);
|
|
640
640
|
for (const id of ['tw-bar', 'tw-counter', 'tw-open', 'tw-open-more', 'tw-divider', 'tw-openwrap', 'tw-open-menu', 'tw-open-tab', 'tw-open-split', 'tw-frame', 'tw-frame-view', 'tw-split-close', 'tw-download', 'tw-upload', 'tw-file', 'tw-submit', 'tw-paper', 'tw-doc', 'tw-notice', 'tw-done', 'tw-take', 'tw-pick', 'tw-picks', 'tw-refused', 'tw-refused-title', 'tw-refused-list', 'tw-refused-next', 'tw-refused-download', 'tw-refused-close']) {
|
|
641
641
|
assert.ok(new RegExp(`id="${id}"`).test(HTML), `#${id} is in the markup`);
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// tests/look_night_tint.mjs — the look a hall wears, its night tint and its logo, hall
|
|
2
|
+
// side (task 1004421, BV2.PS06; ADR 0358).
|
|
3
|
+
//
|
|
4
|
+
// The load-bearing assertions:
|
|
5
|
+
// • THE BLEND RULE: a hall that never chose a look is exactly today's hall — the
|
|
6
|
+
// same theme, the same markup, pure black at night — whatever its tint setting;
|
|
7
|
+
// • a pushed look name becomes that look's fifteen on the instance, read from the
|
|
8
|
+
// style library the core ships; an unknown name or a malformed accent leaves the
|
|
9
|
+
// hall on its own pack rather than half-applying;
|
|
10
|
+
// • the night tint is emitted only with a look, as both dark twins, after the sheet;
|
|
11
|
+
// • CONTRAST: the hall's own token suite (tests/hall_tokens.mjs) passes under every
|
|
12
|
+
// library look with the tint on, and under custom accents round the hue wheel
|
|
13
|
+
// after the platform's nudge — light, dark and tinted dark;
|
|
14
|
+
// • a logo address reaches the hall mark and the favicon only when it is a plain
|
|
15
|
+
// https URL.
|
|
16
|
+
//
|
|
17
|
+
// Run: node --test tests/look_night_tint.mjs
|
|
18
|
+
|
|
19
|
+
import { test } from 'node:test';
|
|
20
|
+
import assert from 'node:assert/strict';
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import os from 'node:os';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { spawnSync } from 'node:child_process';
|
|
25
|
+
import { createRequire } from 'node:module';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
const require = createRequire(import.meta.url);
|
|
29
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
30
|
+
const { resolveBranding, clientBranding } = require('../src/branding.js');
|
|
31
|
+
const { themeMarkup } = require('../src/bongos/serve-internal.js');
|
|
32
|
+
const { NIGHT_TINT, nightTintCss } = require('../src/night-tint.js');
|
|
33
|
+
const { lookAdjustAccent, LIBRARY_LOOKS } = require('../modules/provisioning/look.js');
|
|
34
|
+
|
|
35
|
+
const neutral = JSON.parse(fs.readFileSync(path.join(ROOT, 'config', 'branding.neutral.json'), 'utf8'));
|
|
36
|
+
const brand = (env = {}) => clientBranding(resolveBranding({ neutral, env }));
|
|
37
|
+
const packOf = (name) => JSON.parse(fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'styles', name, 'pack.json'), 'utf8')).theme.ui;
|
|
38
|
+
|
|
39
|
+
test('the blend rule: no look means today\'s hall, whatever the tint setting says', () => {
|
|
40
|
+
const today = brand();
|
|
41
|
+
for (const env of [{ BONGOS_LOOK: 'own' }, { BONGOS_NIGHT_TINT: 'on' }, { BONGOS_LOOK: 'own', BONGOS_NIGHT_TINT: 'on' }]) {
|
|
42
|
+
const b = brand(env);
|
|
43
|
+
assert.deepEqual(b.theme, today.theme, JSON.stringify(env));
|
|
44
|
+
assert.equal(themeMarkup(b.theme), themeMarkup(today.theme));
|
|
45
|
+
}
|
|
46
|
+
assert.equal(today.theme.nightTint, false);
|
|
47
|
+
assert.equal(today.theme.look, '');
|
|
48
|
+
assert.ok(!themeMarkup(today.theme).includes('data-theme'), 'no dark block is injected for a hall without a look');
|
|
49
|
+
assert.deepEqual(today.theme.colors, neutral.theme.ui.colors);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('a library look becomes its fifteen, and the manifest reads back what was pushed', () => {
|
|
53
|
+
for (const name of LIBRARY_LOOKS) {
|
|
54
|
+
const b = brand({ BONGOS_LOOK: name });
|
|
55
|
+
const ui = packOf(name);
|
|
56
|
+
assert.deepEqual(b.theme.colors, ui.colors, name);
|
|
57
|
+
assert.deepEqual(b.theme.fonts, { display: ui.fonts.display, body: ui.fonts.body, links: ui.fonts.links }, name);
|
|
58
|
+
assert.equal(b.theme.look, name);
|
|
59
|
+
assert.equal(b.project.look, name, 'the settings read-back (settingsConsumed) sees the pushed name');
|
|
60
|
+
assert.equal(b.theme.nightTint, true, 'the tint is on by default with a look');
|
|
61
|
+
assert.match(b.theme.favicon, new RegExp(encodeURIComponent(ui.colors.accent)), 'the tab icon is drawn in the look\'s accent');
|
|
62
|
+
}
|
|
63
|
+
assert.equal(brand({ BONGOS_LOOK: 'grove', BONGOS_NIGHT_TINT: 'off' }).theme.nightTint, false, 'the founder can switch back to pure black');
|
|
64
|
+
assert.equal(brand({ BONGOS_LOOK: 'grove', BONGOS_NIGHT_TINT: 'off' }).project.nightTint, 'off');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('a custom look is the chrome world with one accent; an unusable accent or an unknown look leaves the hall alone', () => {
|
|
68
|
+
const b = brand({ BONGOS_LOOK: 'custom', BONGOS_LOOK_ACCENT: '#B48CF2' });
|
|
69
|
+
assert.deepEqual(b.theme.colors, { ...packOf('chrome-world').colors, accent: '#b48cf2' });
|
|
70
|
+
assert.equal(b.theme.look, 'custom');
|
|
71
|
+
const today = brand();
|
|
72
|
+
for (const env of [{ BONGOS_LOOK: 'custom' }, { BONGOS_LOOK: 'custom', BONGOS_LOOK_ACCENT: 'red;}body{x' }, { BONGOS_LOOK: 'neon' }, { BONGOS_LOOK: '../../etc' }]) {
|
|
73
|
+
assert.deepEqual(brand(env).theme, today.theme, JSON.stringify(env));
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('the night tint is emitted with a look, as both dark twins, and nowhere else', () => {
|
|
78
|
+
const on = themeMarkup(brand({ BONGOS_LOOK: 'expedition' }).theme);
|
|
79
|
+
assert.ok(on.includes(nightTintCss()), 'the tint block is in the markup');
|
|
80
|
+
assert.ok(on.indexOf(':root{') < on.indexOf(nightTintCss()), 'after the palette, so the dark grounds win on order');
|
|
81
|
+
assert.equal(nightTintCss().split('--bg:').length - 1, 2, 'written twice: the [data-theme] switch and the media twin');
|
|
82
|
+
assert.match(nightTintCss(), /html\[data-theme="dark"\]\{/);
|
|
83
|
+
assert.match(nightTintCss(), /@media \(prefers-color-scheme: dark\)\{html:not\(\[data-theme\]\)\{/);
|
|
84
|
+
for (const v of Object.values(NIGHT_TINT)) assert.match(v, /^color-mix\(in srgb, var\(--accent\) \d+%, #[0-9a-f]{6}\)$/, 'derived from the look\'s own accent, never a literal colour');
|
|
85
|
+
assert.ok(!themeMarkup(brand({ BONGOS_LOOK: 'expedition', BONGOS_NIGHT_TINT: 'off' }).theme).includes('data-theme'));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('a logo reaches the hall only as a plain https address, and becomes its favicon', () => {
|
|
89
|
+
const url = 'https://platform.test/api/bongos/provisioning/logos/' + 'a'.repeat(64) + '.png';
|
|
90
|
+
const b = brand({ BONGOS_LOGO_URL: url });
|
|
91
|
+
assert.equal(b.theme.logo, url);
|
|
92
|
+
assert.equal(b.theme.favicon, url);
|
|
93
|
+
assert.equal(b.theme.nightTint, false, 'a logo alone tints nothing');
|
|
94
|
+
assert.deepEqual(b.theme.colors, neutral.theme.ui.colors, 'a logo alone changes no colour');
|
|
95
|
+
for (const bad of ['http://x.test/a.png', 'javascript:alert(1)', 'https://x.test/a.png"><script>', 'https://x.test/a b.png', 'https://x.test/a.png)']) {
|
|
96
|
+
assert.equal(brand({ BONGOS_LOGO_URL: bad }).theme.logo, '', bad);
|
|
97
|
+
}
|
|
98
|
+
const shell = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'shell.js'), 'utf8');
|
|
99
|
+
assert.match(shell, /brand\.theme && brand\.theme\.logo\s*\?\s*'<img class="brand-mark__logo" src="' \+ esc\(brand\.theme\.logo\)/, 'the hall mark shows the logo, escaped, in the dot\'s place');
|
|
100
|
+
assert.match(fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'style.css'), 'utf8'), /\.brand-mark__logo \{/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ── contrast: the hall's own suite, under each look, with the tint on ──────────────
|
|
104
|
+
const childEnv = (extra) => {
|
|
105
|
+
const env = { ...process.env, ...extra };
|
|
106
|
+
delete env.NODE_TEST_CONTEXT; // a nested node --test must run its own tests, not report to ours
|
|
107
|
+
return env;
|
|
108
|
+
};
|
|
109
|
+
function hallSuite(extra) {
|
|
110
|
+
const r = spawnSync(process.execPath, ['--test', '--test-reporter=tap', 'tests/hall_tokens.mjs'], {
|
|
111
|
+
cwd: ROOT, encoding: 'utf8', env: childEnv({ UI_DESIGN_TINT: '1', ...extra }), timeout: 120000,
|
|
112
|
+
});
|
|
113
|
+
const fails = (r.stdout || '').split('\n').filter((l) => /= \d+\.\d+:1/.test(l)).map((l) => l.trim());
|
|
114
|
+
return { status: r.status, fails };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
test('the hall clears AA under every library look with the night tint on', () => {
|
|
118
|
+
for (const name of LIBRARY_LOOKS) {
|
|
119
|
+
const r = hallSuite({ UI_DESIGN_PACK: path.join(ROOT, 'modules', 'ui-design', 'styles', name, 'pack.json') });
|
|
120
|
+
assert.equal(r.status, 0, `${name}, tinted night:\n${r.fails.join('\n')}`);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('the hall clears AA under custom accents round the hue wheel, once the platform has nudged them', () => {
|
|
125
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'look-tint-'));
|
|
126
|
+
const chrome = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'styles', 'chrome-world', 'pack.json'), 'utf8'));
|
|
127
|
+
const hsl = (h, s, l) => {
|
|
128
|
+
const a = s * Math.min(l, 1 - l), k = (n) => (n + h / 30) % 12;
|
|
129
|
+
const f = (n) => Math.round(255 * (l - a * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1))));
|
|
130
|
+
return '#' + [f(0), f(8), f(4)].map((x) => x.toString(16).padStart(2, '0')).join('');
|
|
131
|
+
};
|
|
132
|
+
// Saturated hues at three lightnesses, plus the greys — the corners of the space.
|
|
133
|
+
const raw = [];
|
|
134
|
+
for (let h = 0; h < 360; h += 45) for (const l of [0.25, 0.5, 0.8]) raw.push(hsl(h, 1, l));
|
|
135
|
+
raw.push('#000000', '#808080', '#ffffff');
|
|
136
|
+
const failures = [];
|
|
137
|
+
try {
|
|
138
|
+
for (const hex of raw) {
|
|
139
|
+
const kept = lookAdjustAccent(hex).hex;
|
|
140
|
+
const pack = structuredClone(chrome);
|
|
141
|
+
pack.theme.ui.colors.accent = kept;
|
|
142
|
+
const file = path.join(tmp, `${hex.slice(1)}.json`);
|
|
143
|
+
fs.writeFileSync(file, JSON.stringify(pack));
|
|
144
|
+
for (const tint of ['0', '1']) {
|
|
145
|
+
const r = hallSuite({ UI_DESIGN_PACK: file, UI_DESIGN_TINT: tint });
|
|
146
|
+
if (r.status !== 0) failures.push(`${hex} → ${kept} (${tint === '1' ? 'tinted' : 'black'} night): ${r.fails.join('; ') || 'suite failed'}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} finally { fs.rmSync(tmp, { recursive: true, force: true }); }
|
|
150
|
+
assert.deepEqual(failures, []);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('UI_DESIGN_TINT is honoured, not decorative: a tint that is too strong fails the hall suite', () => {
|
|
154
|
+
// Proves the knob reaches the resolver: the same suite, with the card tinted far past
|
|
155
|
+
// the shipped share, must go red on the pair the 8% draft already failed.
|
|
156
|
+
const src = fs.readFileSync(path.join(ROOT, 'tests', 'hall_tokens.mjs'), 'utf8');
|
|
157
|
+
assert.match(src, /process\.env\.UI_DESIGN_TINT === '1'/);
|
|
158
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'look-tint-strong-'));
|
|
159
|
+
try {
|
|
160
|
+
const strong = path.join(tmp, 'night-tint.js');
|
|
161
|
+
fs.writeFileSync(strong, fs.readFileSync(path.join(ROOT, 'src', 'night-tint.js'), 'utf8').replace(/var\(--accent\) 5%, #101012/, 'var(--accent) 30%, #101012'));
|
|
162
|
+
const probe = path.join(tmp, 'hall_tokens.mjs');
|
|
163
|
+
fs.writeFileSync(probe, src.replace("'../src/night-tint.js'", JSON.stringify(strong)).replace(
|
|
164
|
+
"path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')", JSON.stringify(ROOT)));
|
|
165
|
+
const r = spawnSync(process.execPath, ['--test', '--test-reporter=tap', probe], { cwd: ROOT, encoding: 'utf8', env: childEnv({ UI_DESIGN_TINT: '1' }), timeout: 120000 });
|
|
166
|
+
assert.notEqual(r.status, 0, 'a 30% card tint must fail the hall suite');
|
|
167
|
+
assert.match(r.stdout, /dark --[\w-]+ on --[\w-]+ = \d/);
|
|
168
|
+
} finally { fs.rmSync(tmp, { recursive: true, force: true }); }
|
|
169
|
+
});
|
|
@@ -87,7 +87,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
|
|
|
87
87
|
// the file and left the query alone, so a returning browser kept the cached copy —
|
|
88
88
|
// a fix that reaches nobody looks identical to a fix that does not work.
|
|
89
89
|
|
|
90
|
-
const SHELL_SHA = '
|
|
90
|
+
const SHELL_SHA = '51a13ea563542113';
|
|
91
91
|
|
|
92
92
|
test('shell.js has not changed without its cache-bust being bumped', () => {
|
|
93
93
|
const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -715,8 +715,9 @@ test('a renumbering of the wizard invalidates old drafts — the key carries the
|
|
|
715
715
|
const panels = (SCRIPT.match(/var panels = \{([^}]*)\}/) || [, ''])[1];
|
|
716
716
|
const count = [...panels.matchAll(/\d+:\s*'panel-/g)].length;
|
|
717
717
|
// task 1004417 put the front door's two screens (terms, fork) in front: the sixth renumbering
|
|
718
|
-
|
|
719
|
-
assert.equal(
|
|
718
|
+
// task 1004421 put the Look step between App and Review: the seventh
|
|
719
|
+
assert.equal(count, 13, 'gate + the front door\'s two + nine numbered steps + done — bump DRAFT_KEY when this changes');
|
|
720
|
+
assert.equal(key, 'cb-create-draft-v7', 'and the key moved with the Look step');
|
|
720
721
|
});
|
|
721
722
|
|
|
722
723
|
test('every row lays the same five parts — the ledger has no ragged row', () => {
|
|
@@ -2137,7 +2138,7 @@ test('the wizard takes the world by scope: no card ground, the six-step strip as
|
|
|
2137
2138
|
assert.match(block, /#view-new \.card\{background:none;border-radius:0;box-shadow:none;/, 'no card ground');
|
|
2138
2139
|
// eight tracks since task 1004351 added the App step (seven since 1002339); the count is the
|
|
2139
2140
|
// rail's own, so it moves with the panels map rather than being a magic number
|
|
2140
|
-
assert.match(block, /#view-new \.steps\{display:grid;grid-template-columns:repeat\(
|
|
2141
|
+
assert.match(block, /#view-new \.steps\{display:grid;grid-template-columns:repeat\(9,1fr\);/, 'the strip lays a track per numbered step');
|
|
2141
2142
|
assert.match(block, /#view-new \.steps li::before\{content:"";position:absolute;top:-4px;left:0;right:auto;width:7px;height:7px;/, 'the connector becomes the 7px dot');
|
|
2142
2143
|
assert.match(block, /#view-new \.steps li\.now::before\{background:var\(--accent\);border-color:var\(--accent\);outline:4px solid/, 'the lit step carries the accent and an outline halo');
|
|
2143
2144
|
assert.match(block, /#view-new \.panel h1\{font-weight:300;[^}]*text-transform:lowercase;/, 'the step title in the display voice, lowercase');
|
|
@@ -2184,7 +2185,7 @@ test('the wizard\'s own structures wear the world: hairline fields, cells of one
|
|
|
2184
2185
|
assert.match(block, /\.addrBig code\{display:block;font-family:var\(--font-display\);/, 'the address stands in the display voice');
|
|
2185
2186
|
assert.doesNotMatch(block, /linear-gradient/, 'no tinted plates');
|
|
2186
2187
|
assert.doesNotMatch(block, /box-shadow:(?!none)/, 'no box shadow in the wizard block');
|
|
2187
|
-
assert.match(HUB, /<div class="nav">\s*<button type="button" class="btn ghost" data-back="
|
|
2188
|
+
assert.match(HUB, /<div class="nav">\s*<button type="button" class="btn ghost" data-back="10">← Back<\/button>\s*<button type="button" class="btn big" id="doCreate">Create my project<\/button>/, 'the review\'s doors sit on one row, back then the primary');
|
|
2188
2189
|
assert.match(HUB, /<h2 class="stepsHead" id="doneStepsHead">Your steps — the parts only you can play<\/h2>/, 'the steps heading is the label face by class, not an inline style (the id is where a skipped invite step hands focus on, task 1002346)');
|
|
2189
2190
|
// what is enqueued is unchanged (ADR 0111): the filing body the control plane reads
|
|
2190
2191
|
assert.match(SCRIPT, /body: JSON\.stringify\(\{\s*slug: state\.slug,/, 'the filing still carries the slug first');
|
|
@@ -240,7 +240,7 @@ test('a repaint never lands on top of an open Retry — from this watch or a fre
|
|
|
240
240
|
});
|
|
241
241
|
|
|
242
242
|
test('both surfaces use it: the done panel and the project page', () => {
|
|
243
|
-
assert.match(HUB, /if \(box\) watchApp\(instanceId, box, function \(\) \{ return state\.step ===
|
|
243
|
+
assert.match(HUB, /if \(box\) watchApp\(instanceId, box, function \(\) \{ return state\.step === 12; \}, 5000\);/);
|
|
244
244
|
assert.match(HUB, /<div id="doneApp" hidden><\/div>/);
|
|
245
245
|
assert.match(HUB, /<div class="card mSec" id="mAppCard" hidden>\n\s*<h2>Where its app runs<\/h2>\n\s*<div id="mApp"><\/div>/);
|
|
246
246
|
assert.match(HUB, /loadAppStatus\(inst\.id \|\| asked, inst\);/);
|
|
@@ -142,16 +142,17 @@ test('the panel is real markup: Render and Decide later, Decide later the defaul
|
|
|
142
142
|
assert.doesNotMatch(PANEL, /Coming soon/, 'and is no longer marked soon');
|
|
143
143
|
assert.match(PANEL, /<input type="radio" name="appPlatform" value="later" checked>/, 'Decide later is the default');
|
|
144
144
|
assert.match(PANEL, /We host your Builders' hall/, 'the step says the hall is ours to host (ADR 0345 D1), in the page\'s one spelling of it');
|
|
145
|
-
assert.match(PANEL, /<form data-next="10">/, 'Next leads to the review');
|
|
145
|
+
assert.match(PANEL, /<form data-next="10">/, 'Next leads to the Look step (task 1004421), then the review');
|
|
146
146
|
assert.match(PANEL, /data-back="8"/, 'Back returns to the address step');
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
-
test('the step sits between Address and
|
|
150
|
-
assert.match(HUB, /8: 'panel-address', 9: 'panel-app', 10: 'panel-
|
|
149
|
+
test('the step sits between Address and the Look step in the panels map and on the rail', () => {
|
|
150
|
+
assert.match(HUB, /8: 'panel-address', 9: 'panel-app', 10: 'panel-look', 11: 'panel-review', 12: 'panel-done'/);
|
|
151
151
|
assert.match(HUB, /<li data-step="9"><span class="snum">7<\/span><span class="slbl">App<\/span><\/li>/);
|
|
152
|
-
assert.match(HUB, /<li data-step="10"><span class="snum">8<\/span><span class="slbl">
|
|
153
|
-
assert.match(HUB,
|
|
154
|
-
assert.match(HUB, /
|
|
152
|
+
assert.match(HUB, /<li data-step="10"><span class="snum">8<\/span><span class="slbl">Look<\/span><\/li>/);
|
|
153
|
+
assert.match(HUB, /<li data-step="11"><span class="snum">9<\/span><span class="slbl">Create<\/span><\/li>/);
|
|
154
|
+
assert.match(HUB, /data-back="10">← Back<\/button>\n\s*<button type="button" class="btn big" id="doCreate">/, "the review's Back returns to the Look step");
|
|
155
|
+
assert.match(HUB, /if \(step === 9\) \{ syncAppStep\(\); syncRenderSetup\(\); \}\n\s*if \(step === 10\) renderLook\(\);\n\s*if \(step === 11\) renderReview\(\);/, 'entering the step repaints it, Render setup included');
|
|
155
156
|
assert.match(HUB, /if \(step === 9\) return validateRender\(\);/, 'Next on the step runs the Render check');
|
|
156
157
|
});
|
|
157
158
|
|
|
@@ -455,5 +456,5 @@ test('the review names exactly what was chosen, prices included', () => {
|
|
|
455
456
|
});
|
|
456
457
|
|
|
457
458
|
test('Create hands the app to Render straight after the project is filed', () => {
|
|
458
|
-
assert.match(HUB, /show\(
|
|
459
|
+
assert.match(HUB, /show\(12, true\);\n\s*sendRenderSetup\(data\.instance && data\.instance\.id\);/);
|
|
459
460
|
});
|