@bongos/core 1.20.60 → 1.20.62
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 +95 -40
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +6 -2
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0357-the-update-rule-set-on-deploy-is-the-one-the-sweep-follows.md +2 -0
- package/docs/api/openapi.json +112 -3
- package/docs/api-reference.md +5 -4
- package/docs/copy-inventory.md +377 -328
- package/docs/copy-registry.json +894 -421
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +4 -1
- package/docs/page-readings.json +546 -483
- package/docs/recipes/upgrading-the-core.md +30 -1
- 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/gate.js +24 -8
- package/modules/provisioning/demo.js +176 -0
- package/modules/provisioning/migrations/provisioning_035_demo.sql +26 -0
- package/modules/provisioning/module.json +5 -3
- package/modules/provisioning/pollers/demo-archive.js +61 -0
- package/modules/provisioning/provisioning.js +2 -2
- package/modules/provisioning/routes/demo.js +54 -0
- package/modules/provisioning/routes/provisioning.js +7 -4
- package/modules/public-landing/public/projects-demo.states.json +53 -0
- package/modules/public-landing/public/projects.html +300 -23
- package/modules/public-landing/public/projects.probes.json +3 -3
- package/modules/public-landing/public/projects.states.json +2 -1
- package/modules/ui-design/kit/fixtures/provisioning-instances-demo.json +114 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +20 -0
- package/scripts/gds/autobongos-run.js +43 -2
- package/scripts/gds/update-channel.js +11 -2
- package/scripts/gds/update-sweep.js +717 -0
- package/src/module-api.js +1 -1
- package/tests/autonomy_runner_identity.mjs +215 -0
- package/tests/update_channel_db.mjs +13 -6
- package/tests/update_subscription_engine.mjs +5 -5
- package/tests/update_sweep_home.mjs +100 -0
- package/tests/wizard_demo.mjs +264 -0
- package/tests/wizard_front_door.mjs +16 -20
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// tests/wizard_demo.mjs — the demo route: training wheels, a soft time box, and "keep
|
|
2
|
+
// your planet?" (task 1004420 / BV2.PS09, goal 1000121; spec D3 + D9).
|
|
3
|
+
//
|
|
4
|
+
// Done-when: a demo can be created, run and carried over. D3: the time box is SOFT — a
|
|
5
|
+
// countdown, then the question; work is never frozen. D9: a demo is a normal project
|
|
6
|
+
// marked "demo"; one nobody keeps goes offline after 30 days through the existing
|
|
7
|
+
// take-offline path, with the owner told first.
|
|
8
|
+
//
|
|
9
|
+
// Three layers, all RUN: modules/provisioning/demo.js (the marker, the defaults, the
|
|
10
|
+
// view, the answer, the sweep); the create route and the keep route over a mounted
|
|
11
|
+
// router; and the hub's demo controller sliced out of projects.html and driven in a vm.
|
|
12
|
+
|
|
13
|
+
import { strict as assert } from 'node:assert';
|
|
14
|
+
import { test, after } from 'node:test';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import vm from 'node:vm';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
+
const LANDING = path.join(ROOT, 'modules', 'public-landing', 'public');
|
|
24
|
+
const HUB = fs.readFileSync(path.join(LANDING, 'projects.html'), 'utf8').replace(/\r\n/g, '\n');
|
|
25
|
+
const demo = require('../modules/provisioning/demo.js');
|
|
26
|
+
const DAY = 86400000;
|
|
27
|
+
|
|
28
|
+
// ── the module ───────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
test('a demo asks for people and hours each, inside their limits, and nothing else', () => {
|
|
31
|
+
assert.deepEqual(demo.parseDemo(undefined), { ok: true, demo: null }, 'no demo field: an ordinary project');
|
|
32
|
+
assert.deepEqual(demo.parseDemo({ people: 3, hours_each: 4 }), { ok: true, demo: { people: 3, hours_each: 4 } });
|
|
33
|
+
assert.equal(demo.parseDemo({ people: 0, hours_each: 4 }).ok, false);
|
|
34
|
+
assert.equal(demo.parseDemo({ people: 13, hours_each: 4 }).ok, false);
|
|
35
|
+
assert.equal(demo.parseDemo({ people: 3, hours_each: 41 }).ok, false);
|
|
36
|
+
assert.equal(demo.parseDemo({ people: 3, hours_each: 4, ends_at: 'never' }).ok, false, 'a caller cannot name its own end');
|
|
37
|
+
assert.equal(demo.parseDemo([3, 4]).ok, false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('the first read says what fits in the builder-hours', () => {
|
|
41
|
+
assert.equal(demo.fitCheck(1, 4).verdict, 'tight');
|
|
42
|
+
assert.equal(demo.fitCheck(3, 4).verdict, 'small');
|
|
43
|
+
assert.equal(demo.fitCheck(3, 4).total, 12);
|
|
44
|
+
assert.equal(demo.fitCheck(5, 4).verdict, 'plenty');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('the demo picks no rewards and all managed, and an owner\'s own answer wins', () => {
|
|
48
|
+
assert.deepEqual(demo.withDemoDetail(undefined), { reward_intents: [], framing: 'managed' });
|
|
49
|
+
assert.deepEqual(demo.withDemoDetail({ framing: 'hybrid', team_shape: 'small-team' }), { reward_intents: [], framing: 'hybrid', team_shape: 'small-team' });
|
|
50
|
+
assert.deepEqual({ ...demo.DEMO_SETTINGS }, { visibility: 'private', joinability: 'invite_only' }, 'private to the team');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('the time box is soft: it only counts down and then asks (D3)', () => {
|
|
54
|
+
const start = new Date('2026-10-01T09:00:00Z');
|
|
55
|
+
const row = { demo: demo.demoRecord({ people: 3, hours_each: 4 }, start) };
|
|
56
|
+
assert.equal(row.demo.ends_at, '2026-10-01T13:00:00.000Z');
|
|
57
|
+
const mid = demo.demoView(row, new Date('2026-10-01T11:30:00Z'));
|
|
58
|
+
assert.equal(mid.remaining_seconds, 5400);
|
|
59
|
+
assert.equal(mid.time_up, false);
|
|
60
|
+
assert.equal(mid.builder_hours, 12);
|
|
61
|
+
const late = demo.demoView(row, new Date('2026-10-01T15:00:00Z'));
|
|
62
|
+
assert.equal(late.remaining_seconds, 0);
|
|
63
|
+
assert.equal(late.time_up, true, 'it asks; nothing in the view stops anything');
|
|
64
|
+
assert.equal(late.offline_on, '2026-10-31T09:00:00.000Z', '30 days after it started (D9)');
|
|
65
|
+
assert.equal(late.offline_soon, false);
|
|
66
|
+
assert.equal(demo.demoView(row, new Date('2026-10-29T09:00:00Z')).offline_soon, true, 'the owner is told from three days before');
|
|
67
|
+
assert.equal(demo.demoView({ demo: null }), null, 'an ordinary project has no demo view');
|
|
68
|
+
assert.equal(demo.demoView({ demo: { ...row.demo, outcome: 'carried' } }), null, 'a carried-over demo is a real project');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
function fakeDb(rows = []) {
|
|
72
|
+
const writes = [];
|
|
73
|
+
return { writes, query: async (sql, params) => { if (/^\s*UPDATE/.test(sql)) writes.push(params); return { rows }; } };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
test('keep your planet? is answered once', async () => {
|
|
77
|
+
const db = fakeDb();
|
|
78
|
+
const inst = { id: 9, demo: demo.demoRecord({ people: 2, hours_each: 3 }) };
|
|
79
|
+
const r = await demo.settleDemo(db, inst, 'carried');
|
|
80
|
+
assert.equal(r.ok, true);
|
|
81
|
+
assert.equal(JSON.parse(db.writes[0][1]).outcome, 'carried');
|
|
82
|
+
assert.equal((await demo.settleDemo(db, { ...inst, demo: r.demo }, 'kept')).reason, 'already_settled');
|
|
83
|
+
assert.equal((await demo.settleDemo(db, { id: 1, demo: null }, 'kept')).reason, 'not_a_demo');
|
|
84
|
+
assert.equal((await demo.settleDemo(db, inst, 'frozen')).reason, 'bad_outcome');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('the sweep warns three days before and takes a demo nobody carried over offline at 30 days', async () => {
|
|
88
|
+
const started = new Date('2026-09-01T00:00:00Z');
|
|
89
|
+
const rows = [
|
|
90
|
+
{ id: 1, owner_builder_id: 7, status: 'active', demo: demo.demoRecord({ people: 1, hours_each: 2 }, started) },
|
|
91
|
+
{ id: 2, owner_builder_id: 7, status: 'active', demo: { ...demo.demoRecord({ people: 1, hours_each: 2 }, new Date('2026-09-04T00:00:00Z')), outcome: 'kept' } },
|
|
92
|
+
{ id: 3, owner_builder_id: 7, status: 'active', demo: demo.demoRecord({ people: 1, hours_each: 2 }, new Date('2026-09-20T00:00:00Z')) },
|
|
93
|
+
];
|
|
94
|
+
const torn = [];
|
|
95
|
+
const db = fakeDb(rows);
|
|
96
|
+
const out = await demo.sweepDemos(db, {
|
|
97
|
+
enqueueTeardown: async (_db, id) => { torn.push(id); },
|
|
98
|
+
recordEvent: async () => {},
|
|
99
|
+
now: new Date('2026-10-02T00:00:00Z'),
|
|
100
|
+
});
|
|
101
|
+
assert.deepEqual(torn, [1], 'only the one past 30 days goes offline, through the teardown path');
|
|
102
|
+
assert.equal(out.offline, 1);
|
|
103
|
+
assert.equal(out.warned, 1, 'the kept demo two days out is warned; the young one is left alone');
|
|
104
|
+
assert.match(fs.readFileSync(path.join(ROOT, 'modules', 'provisioning', 'demo.js'), 'utf8'), /COALESCE\(demo->>'outcome', ''\) <> 'carried'/, 'a carried-over demo is never swept');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ── the routes ───────────────────────────────────────────────────────────────
|
|
108
|
+
const api = require('../src/module-api.js');
|
|
109
|
+
const provisioning = require('../modules/provisioning/provisioning.js');
|
|
110
|
+
let created = null;
|
|
111
|
+
let started = null;
|
|
112
|
+
provisioning.getInstanceBySlug = async () => null;
|
|
113
|
+
provisioning.getInstanceByDomain = async () => null;
|
|
114
|
+
provisioning.createInstance = async (_db, args) => {
|
|
115
|
+
created = args;
|
|
116
|
+
return { instance: { id: 41, owner_builder_id: 7, slug: args.slug, status: 'requested', hosting_shape: 'byo-host', detail: args.detail || {} }, created: true };
|
|
117
|
+
};
|
|
118
|
+
provisioning.enqueueIntent = async (_db, _id, action) => ({ intent: { id: 9, action, state: 'pending' }, created: true });
|
|
119
|
+
provisioning.getOpenIntent = async () => null;
|
|
120
|
+
provisioning.recordEvent = async () => {};
|
|
121
|
+
demo.startDemoProject = async (_db, _p, id, d) => { started = { id, d }; return null; };
|
|
122
|
+
api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, github_login: 'owner' }; next(); };
|
|
123
|
+
let stored = null;
|
|
124
|
+
provisioning.getInstanceById = async (_db, id) => (id === 41 ? { id: 41, owner_builder_id: 7, demo: stored } : null);
|
|
125
|
+
api.pool.query = async (sql, params) => { if (/UPDATE provisioning_instances SET demo/.test(sql)) stored = JSON.parse(params[1]); return { rows: [] }; };
|
|
126
|
+
|
|
127
|
+
const express = require('express');
|
|
128
|
+
const app = express();
|
|
129
|
+
app.use(express.json());
|
|
130
|
+
app.use((_req, res, next) => {
|
|
131
|
+
res.fail = (code, opts) => {
|
|
132
|
+
const status = typeof opts === 'number' ? opts : (opts && opts.status) || 400;
|
|
133
|
+
res.status(status).json({ error: { code, message: opts && opts.message } });
|
|
134
|
+
};
|
|
135
|
+
next();
|
|
136
|
+
});
|
|
137
|
+
app.use('/api/bongos', require('../modules/provisioning/routes/provisioning.js')());
|
|
138
|
+
app.use('/api/bongos', require('../modules/provisioning/routes/demo.js')());
|
|
139
|
+
const server = app.listen(0);
|
|
140
|
+
after(() => server.close());
|
|
141
|
+
async function post(p, body) {
|
|
142
|
+
await new Promise((r) => (server.listening ? r() : server.once('listening', r)));
|
|
143
|
+
const res = await fetch(`http://127.0.0.1:${server.address().port}/api/bongos${p}`, {
|
|
144
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
|
|
145
|
+
});
|
|
146
|
+
return { status: res.status, body: await res.json() };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
test('the create files a normal project marked demo, with the demo\'s picked answers under the owner\'s', async () => {
|
|
150
|
+
const r = await post('/provisioning/instances', { slug: 'reading-list', hosting_shape: 'byo-host', demo: { people: 3, hours_each: 4 }, detail: { team_shape: 'small-team' } });
|
|
151
|
+
assert.equal(r.status, 201, JSON.stringify(r.body));
|
|
152
|
+
assert.deepEqual(created.detail, { reward_intents: [], framing: 'managed', team_shape: 'small-team' });
|
|
153
|
+
assert.deepEqual(started, { id: 41, d: { people: 3, hours_each: 4 } }, 'the row is marked and made private');
|
|
154
|
+
started = null;
|
|
155
|
+
const plain = await post('/provisioning/instances', { slug: 'real-one', hosting_shape: 'byo-host' });
|
|
156
|
+
assert.equal(plain.status, 201);
|
|
157
|
+
assert.equal(started, null, 'an ordinary project is never marked');
|
|
158
|
+
assert.equal((await post('/provisioning/instances', { slug: 'bad-demo', hosting_shape: 'byo-host', demo: { people: 99, hours_each: 4 } })).status, 400);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('keep your planet? over the route: the owner answers once', async () => {
|
|
162
|
+
stored = demo.demoRecord({ people: 3, hours_each: 4 }, new Date(Date.now() - 5 * 3600000));
|
|
163
|
+
const r = await post('/provisioning/instances/41/demo', { outcome: 'kept' });
|
|
164
|
+
assert.equal(r.status, 200, JSON.stringify(r.body));
|
|
165
|
+
assert.equal(stored.outcome, 'kept');
|
|
166
|
+
assert.equal(r.body.demo.outcome, 'kept');
|
|
167
|
+
assert.equal((await post('/provisioning/instances/41/demo', { outcome: 'carried' })).status, 409, 'answered once');
|
|
168
|
+
assert.equal((await post('/provisioning/instances/99/demo', { outcome: 'carried' })).status, 404, 'not yours, not found');
|
|
169
|
+
assert.equal((await post('/provisioning/instances/41/demo', { outcome: 'frozen' })).status, 400);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('the module declares the route, the poller and its migration', () => {
|
|
173
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules', 'provisioning', 'module.json'), 'utf8'));
|
|
174
|
+
assert.ok(manifest.contributes.routes.includes('demo'));
|
|
175
|
+
assert.ok(manifest.contributes.pollers.includes('demo-archive'));
|
|
176
|
+
assert.match(fs.readFileSync(path.join(ROOT, 'modules', 'provisioning', 'migrations', 'provisioning_035_demo.sql'), 'utf8'), /ADD COLUMN IF NOT EXISTS demo jsonb/);
|
|
177
|
+
const poller = require('../modules/provisioning/pollers/demo-archive.js');
|
|
178
|
+
assert.equal(typeof poller.start, 'function');
|
|
179
|
+
assert.equal(poller.INTERVAL_MS, 3600000);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ── the hub, RUN ─────────────────────────────────────────────────────────────
|
|
183
|
+
const SLICE = HUB.slice(HUB.indexOf(' /* ── the demo route (task 1004420, BV2.PS09) ──'), HUB.indexOf(' /* ── field wiring ── */'));
|
|
184
|
+
|
|
185
|
+
test('the hub carries the server\'s limits and fit sentences exactly', () => {
|
|
186
|
+
assert.ok(SLICE.length > 1000, 'the demo controller was found');
|
|
187
|
+
const box = {};
|
|
188
|
+
vm.createContext(box);
|
|
189
|
+
vm.runInContext(SLICE.slice(SLICE.indexOf(' var DEMO_LIMITS'), SLICE.indexOf(' function demoHandle')).replace('var demoDraft = state.demo', 'var state = {}; var demoDraft = state.demo'), box);
|
|
190
|
+
assert.deepEqual(JSON.parse(JSON.stringify(box.DEMO_LIMITS)), { people: [...demo.DEMO_LIMITS.people], hours: [...demo.DEMO_LIMITS.hours_each] });
|
|
191
|
+
for (const [p, h] of [[1, 2], [3, 4], [5, 4], [12, 40]]) {
|
|
192
|
+
assert.deepEqual(JSON.parse(JSON.stringify(box.demoFit(p, h))), demo.fitCheck(p, h), `${p} × ${h}`);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
class El {
|
|
197
|
+
constructor(id) {
|
|
198
|
+
this.id = id; this.hidden = false; this.value = ''; this.textContent = ''; this.attrs = {}; this.listeners = {};
|
|
199
|
+
const self = this; this.classList = { toggle() {} };
|
|
200
|
+
this._label = { hidden: false }; this.closest = () => self._label;
|
|
201
|
+
}
|
|
202
|
+
setAttribute(k, v) { this.attrs[k] = String(v); }
|
|
203
|
+
getAttribute(k) { return this.attrs[k] ?? null; }
|
|
204
|
+
addEventListener(t, fn) { this.listeners[t] = fn; }
|
|
205
|
+
focus() {}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function bootHub() {
|
|
209
|
+
const els = {};
|
|
210
|
+
const $ = (id) => (els[id] = els[id] || new El(id));
|
|
211
|
+
const steppers = [['people', '1'], ['people', '-1'], ['hours', '1'], ['hours', '-1']].map(([k, by]) => {
|
|
212
|
+
const b = new El(`step-${k}${by}`); b.attrs['data-demo-step'] = k; b.attrs['data-by'] = by; return b;
|
|
213
|
+
});
|
|
214
|
+
const shown = [];
|
|
215
|
+
const state = { demo: null, description: '', teamShape: null };
|
|
216
|
+
const sandbox = { $, state, saveDraft() {}, show: (n) => shown.push(n), root: { querySelectorAll: () => steppers } };
|
|
217
|
+
vm.createContext(sandbox);
|
|
218
|
+
vm.runInContext(SLICE, sandbox);
|
|
219
|
+
const click = (id) => $(id).listeners.click.call($(id));
|
|
220
|
+
return { $, state, shown, steppers, click, sandbox };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
test('the door opens the setup, the steppers stay in their limits, and the first read follows them', () => {
|
|
224
|
+
const h = bootHub();
|
|
225
|
+
h.click('wizDemo');
|
|
226
|
+
assert.equal(h.$('forkIntro').hidden, true);
|
|
227
|
+
assert.equal(h.$('demoSetup').hidden, false);
|
|
228
|
+
assert.equal(h.$('demoRing').hidden, false, 'the time-box ring on the planet');
|
|
229
|
+
assert.equal(h.$('wizDemo').attrs['aria-expanded'], 'true');
|
|
230
|
+
for (let i = 0; i < 20; i += 1) h.steppers[0].listeners.click.call(h.steppers[0]);
|
|
231
|
+
assert.equal(h.$('demoPeople').textContent, '12', 'people stop at twelve');
|
|
232
|
+
for (let i = 0; i < 20; i += 1) h.steppers[3].listeners.click.call(h.steppers[3]);
|
|
233
|
+
assert.equal(h.$('demoHours').textContent, '1', 'hours stop at one');
|
|
234
|
+
assert.equal(h.$('demoFitSum').textContent, '12 people × 1 hour = 12 builder-hours.');
|
|
235
|
+
assert.equal(h.$('demoFitVerdict').textContent, demo.fitCheck(12, 1).sentence);
|
|
236
|
+
h.click('demoBack1');
|
|
237
|
+
assert.equal(h.$('forkIntro').hidden, false, 'Back returns to the two doors');
|
|
238
|
+
assert.equal(h.$('wizDemo').attrs['aria-expanded'], 'false');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('Name my demo planet carries the demo into the ordinary wizard: the idea, the team, the invites', () => {
|
|
242
|
+
const h = bootHub();
|
|
243
|
+
h.click('wizDemo');
|
|
244
|
+
h.$('demoIdea').value = 'A shared reading list.';
|
|
245
|
+
h.$('demoIdea').listeners.input.call(h.$('demoIdea'));
|
|
246
|
+
h.click('demoNext1');
|
|
247
|
+
assert.equal(h.$('demoReady').hidden, false);
|
|
248
|
+
h.$('demoInvite1').value = '@avery';
|
|
249
|
+
h.$('demoInvite2').value = 'not a handle!';
|
|
250
|
+
h.click('demoStart');
|
|
251
|
+
assert.deepEqual(JSON.parse(JSON.stringify(h.state.demo)), { people: 3, hours_each: 4, idea: 'A shared reading list.', invites: ['avery'] });
|
|
252
|
+
assert.equal(h.state.description, 'A shared reading list.', 'the idea is the description');
|
|
253
|
+
assert.equal(h.state.teamShape, 'small-team');
|
|
254
|
+
assert.deepEqual(h.shown, [3], 'on to the name step');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('the create sends the demo, the review names it, and the project page asks the question', () => {
|
|
258
|
+
assert.match(HUB, /demo: state\.demo \? \{ people: state\.demo\.people, hours_each: state\.demo\.hours_each \} : undefined,/);
|
|
259
|
+
assert.match(HUB, /if \(state\.demo\) \{\n\s*rows\.unshift\(\['Demo',/);
|
|
260
|
+
assert.match(HUB, /<p class="demoKeepH">Keep your planet\?<\/p>/);
|
|
261
|
+
assert.match(HUB, /data-demo-settle="carried">Carry it over</);
|
|
262
|
+
assert.match(HUB, /data-demo-settle="kept">Keep it as a demo</);
|
|
263
|
+
assert.match(HUB, /\$\('mDemoKeep'\)\.hidden = !\(d\.time_up && !d\.outcome\);/, 'the question waits for the time box (D3)');
|
|
264
|
+
});
|
|
@@ -113,24 +113,26 @@ test('Proceed is refused until the box is ticked, and the refusal says why in th
|
|
|
113
113
|
assert.equal(given.ok, true, 'a tick goes on');
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
-
test('the fork
|
|
116
|
+
test('the fork refuses nothing — its doors are the answer, and Begin clears a demo started earlier', () => {
|
|
117
117
|
assert.equal(runValidate(2, false).ok, true, 'the fork asks nothing a validator could refuse');
|
|
118
|
-
|
|
118
|
+
// task 1004420: "Begin planet creation" is a real project, so a demo the founder
|
|
119
|
+
// set up and then walked back from must not ride the create
|
|
120
|
+
assert.match(validateSlice, /if \(step === 2\) \{ state\.demo = null; return true; \}/);
|
|
119
121
|
});
|
|
120
122
|
|
|
121
123
|
// ── the wiring, executed ───────────────────────────────────────────────────────
|
|
122
124
|
const WIRE_START = " $('termsAck').checked = !!state.termsAck;";
|
|
123
|
-
const WIRE_END = ' /* ──
|
|
125
|
+
const WIRE_END = ' /* ── the demo route (task 1004420, BV2.PS09) ──';
|
|
124
126
|
const wireSlice = SCRIPT.includes(WIRE_START) ? SCRIPT.slice(SCRIPT.indexOf(WIRE_START), SCRIPT.indexOf(WIRE_END)) : null;
|
|
125
127
|
|
|
126
128
|
function runWiring(initialAck) {
|
|
127
129
|
const handlers = {};
|
|
128
130
|
const mk = (id) => ({
|
|
129
|
-
id, checked: false, hidden:
|
|
131
|
+
id, checked: false, hidden: false, textContent: '', className: '', attrs: {},
|
|
130
132
|
addEventListener(type, fn) { handlers[id + ':' + type] = fn; },
|
|
131
133
|
setAttribute(k, v) { this.attrs[k] = v; },
|
|
132
134
|
});
|
|
133
|
-
const els = { termsAck: mk('termsAck'),
|
|
135
|
+
const els = { termsAck: mk('termsAck'), 'h-terms': mk('h-terms') };
|
|
134
136
|
let saves = 0;
|
|
135
137
|
const sandbox = { $: (id) => els[id], state: { termsAck: initialAck }, saveDraft: () => { saves += 1; } };
|
|
136
138
|
sandbox.globalThis = sandbox;
|
|
@@ -141,7 +143,7 @@ function runWiring(initialAck) {
|
|
|
141
143
|
|
|
142
144
|
test('the front door\'s wiring is still findable', () => {
|
|
143
145
|
assert.ok(wireSlice && wireSlice.length > 100, 'the wiring block was not found in projects.html');
|
|
144
|
-
assert.match(wireSlice, /\$\('
|
|
146
|
+
assert.match(wireSlice, /\$\('termsAck'\)\.addEventListener\('change'/, 'the tick is part of the slice');
|
|
145
147
|
});
|
|
146
148
|
|
|
147
149
|
test('the tick mirrors into the draft both ways, and a resumed draft paints its own answer', () => {
|
|
@@ -156,17 +158,10 @@ test('the tick mirrors into the draft both ways, and a resumed draft paints its
|
|
|
156
158
|
assert.equal(saves(), 2, 'each change is saved');
|
|
157
159
|
});
|
|
158
160
|
|
|
159
|
-
test('the demo door
|
|
160
|
-
|
|
161
|
-
assert.
|
|
162
|
-
|
|
163
|
-
assert.equal(els.demoSoon.hidden, false, 'the placeholder opens');
|
|
164
|
-
assert.equal(els.wizDemo.attrs['aria-expanded'], 'true');
|
|
165
|
-
handlers['wizDemo:click'].call(els.wizDemo);
|
|
166
|
-
assert.equal(els.demoSoon.hidden, true, 'and closes again');
|
|
167
|
-
assert.equal(els.wizDemo.attrs['aria-expanded'], 'false');
|
|
168
|
-
assert.match(FORK, /id="wizDemo" aria-controls="demoSoon" aria-expanded="false"/, 'the door names what it opens');
|
|
169
|
-
assert.match(FORK, /<p class="demoSoon" id="demoSoon" hidden>The demo isn’t open yet\./, 'baked markup, never built by script');
|
|
161
|
+
test('the demo door names what it opens: the demo route\'s setup screen (task 1004420; run in tests/wizard_demo.mjs)', () => {
|
|
162
|
+
assert.match(FORK, /id="wizDemo" aria-controls="demoSetup" aria-expanded="false"/, 'the door names what it opens');
|
|
163
|
+
assert.match(FORK, /<div id="demoSetup" class="demoView" hidden>/, 'baked markup, hidden at rest, never built by script');
|
|
164
|
+
assert.doesNotMatch(FORK, /demoSoon|isn’t open yet/, 'the placeholder that said the demo was not open is gone');
|
|
170
165
|
});
|
|
171
166
|
|
|
172
167
|
// ── screen 2 · the fork ─────────────────────────────────────────────────────────
|
|
@@ -186,7 +181,7 @@ test('screen 2 is the door pill: two equal doors in one object, the accent only
|
|
|
186
181
|
});
|
|
187
182
|
|
|
188
183
|
test('the seed stands on the world\'s own field plate — nothing drawn but hairlines', () => {
|
|
189
|
-
assert.match(FORK, /<div class="forkStage" aria-hidden="true">/, 'decorative, so hidden from a screen reader');
|
|
184
|
+
assert.match(FORK, /<div class="forkStage" id="forkStage" aria-hidden="true">/, 'decorative, so hidden from a screen reader');
|
|
190
185
|
assert.match(FORK, /<img class="forkSeed" src="\/assets\/world\/r2-chrome-blob-one-cut\.webp" alt="">/, 'a real plate, root-absolute so it is stamped');
|
|
191
186
|
assert.ok(fs.existsSync(path.join(LANDING, 'assets', 'world', 'r2-chrome-blob-one-cut.webp')), 'and it exists on disk');
|
|
192
187
|
assert.match(STYLE, /\.forkStage\{[^}]*background:var\(--sky-bg\) var\(--field-plate\) center\/cover no-repeat;/, 'the field is the plate token, not a new image');
|
|
@@ -197,9 +192,10 @@ test('the seed stands on the world\'s own field plate — nothing drawn but hair
|
|
|
197
192
|
assert.doesNotMatch(block, /#[0-9a-f]{3,8}\b|box-shadow:(?!none)|linear-gradient/i, 'every colour a token, no shadow, no gradient');
|
|
198
193
|
});
|
|
199
194
|
|
|
200
|
-
test('the two routes are described honestly for today: the demo
|
|
195
|
+
test('the two routes are described honestly for today: the demo is open (task 1004420), creation promises no questions that do not exist yet', () => {
|
|
201
196
|
const routes = (FORK.match(/<div class="forkRoutes">[\s\S]*$/) || [''])[0];
|
|
202
|
-
assert.match(routes, /Route 1 · demo
|
|
197
|
+
assert.match(routes, /Route 1 · demo<\/p>/);
|
|
198
|
+
assert.doesNotMatch(routes, /coming soon/, 'the demo is built, so the route no longer says it is coming');
|
|
203
199
|
assert.match(routes, /Route 2 · planet creation/);
|
|
204
200
|
assert.doesNotMatch(routes, /seven questions/i, 'the seven planet questions are PS07\'s — the copy must not promise them before they ship');
|
|
205
201
|
assert.doesNotMatch(FORK, /\bBongos\b(?! pair)/, 'no product name baked where a child instance would ship it');
|