@bongos/core 1.25.5 → 1.25.7
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 +69 -49
- 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 +4 -1
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +56 -2
- package/docs/api-reference.md +4 -3
- package/docs/copy-inventory.md +20 -19
- package/docs/copy-registry.json +32 -23
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +49 -52
- package/modules/hall-ui/public/shell-game.js +132 -5
- package/modules/hall-ui/public/style.css +59 -0
- package/modules/hall-ui/public/visor.css +11 -0
- package/modules/hall-ui/public/visor.js +14 -5
- package/modules/hall-ui/public/welcome-boot.js +27 -11
- package/modules/hall-ui/records/game-menu.md +22 -0
- package/modules/hall-ui/records/game-mode.md +2 -2
- package/modules/hall-ui/records/options-menu.md +1 -1
- package/modules/hall-ui/records/start-screen.md +12 -2
- package/modules/hall-ui/records/visor.md +4 -0
- package/modules/onboarding/genesis-home.js +35 -1
- package/modules/onboarding/routes/onboarding.js +7 -2
- package/modules/provisioning/catalog-bridge.js +3 -0
- package/modules/provisioning/paid-shape-gate.js +3 -1
- package/modules/provisioning/provisioning.js +3 -3
- package/modules/provisioning/routes/provisioning.js +7 -4
- package/modules/provisioning/test-project.js +65 -0
- package/modules/ui-design/public/brand.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +25 -0
- package/scripts/bongos/government-walk.js +2 -2
- package/src/branding.js +7 -0
- package/src/module-api.js +1 -1
- package/tests/currency_label.mjs +3 -1
- package/tests/hall_game_menu.mjs +195 -0
- package/tests/hall_options_menu.mjs +4 -1
- package/tests/hall_start_screen.mjs +62 -8
- package/tests/hall_visor.mjs +40 -5
- package/tests/provision_settings_apply.mjs +2 -1
- package/tests/provisioning_paid_shape_gate.mjs +16 -0
- package/tests/provisioning_settings_env.mjs +2 -2
- package/tests/test_project.mjs +122 -0
- package/tests/wizard_demo.mjs +25 -4
package/tests/hall_visor.mjs
CHANGED
|
@@ -105,8 +105,8 @@ test('no key ever means two things: every character over every real scene, and O
|
|
|
105
105
|
assert.notEqual(r.options.key, 'esc', `${c}: Options is not on Escape; Escape is Close`);
|
|
106
106
|
assert.ok(r.options.key, `${c}: Options has a key of its own`);
|
|
107
107
|
}
|
|
108
|
-
assert.equal(V.resolveKeys('artist').options.key, '
|
|
109
|
-
assert.equal(V.resolveKeys('patron').options.key, '
|
|
108
|
+
assert.equal(V.resolveKeys('artist').options.key, 'm', 'M is the game menu where nothing else holds it');
|
|
109
|
+
assert.equal(V.resolveKeys('patron').options.key, 'm', 'no character holds M, so the game menu keeps it for all three');
|
|
110
110
|
});
|
|
111
111
|
|
|
112
112
|
test('a clash moves the later claimant to a free letter of its own name, and its key follows', () => {
|
|
@@ -205,6 +205,25 @@ test('source: the minimal visor spends the Helm tokens and types in no colour, s
|
|
|
205
205
|
assert.doesNotMatch(glass, /background|box-shadow|backdrop-filter|filter:/, 'the glass itself paints nothing: no edge band, no vignette, no veil');
|
|
206
206
|
assert.doesNotMatch(css, /--night-glass|linear-gradient|radial-gradient\((?!circle, currentColor)|--type-halo/, 'no band of the night anywhere on the glass, and no dark glow round its type');
|
|
207
207
|
assert.match(css, /\.visor__place, \.visor__who, \.visor__hint \{ background: var\(--night-plate\); clip-path: var\(--cut-path-sm\); \}/, 'the place, each hint and your credits stand on a small cut plate of the night');
|
|
208
|
+
// smoked glass (task 1005252): where the browser can blur, each plate is a pane that softens the scene
|
|
209
|
+
// behind it, from the Helm tokens; asking for less transparency puts the night plate back
|
|
210
|
+
const PANES = '.visor__place, .visor__who, .visor__hint, .visor__progress-words {';
|
|
211
|
+
const block = (head) => { const at = css.indexOf(head); assert.ok(at >= 0, head); const open = css.indexOf(PANES, at); assert.ok(open > at, head + ' holds the four plates'); return css.slice(open, css.indexOf('}', open)); };
|
|
212
|
+
const smoke = block('@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {');
|
|
213
|
+
assert.ok(smoke.includes('background: var(--smoke-sheen), var(--smoke);'), 'a light smoke with a sheen, from tokens');
|
|
214
|
+
assert.ok(smoke.includes('box-shadow: var(--smoke-edge);'), 'a lit top edge makes it a pane');
|
|
215
|
+
assert.ok(smoke.includes(' backdrop-filter: var(--smoke-blur);'), 'it blurs the scene behind it');
|
|
216
|
+
const STYLE = fs.readFileSync(new URL('../modules/hall-ui/public/style.css', import.meta.url), 'utf8');
|
|
217
|
+
const plainAt = STYLE.indexOf('@media (prefers-reduced-transparency: reduce) {');
|
|
218
|
+
const plain = STYLE.slice(plainAt, STYLE.indexOf('}', plainAt));
|
|
219
|
+
assert.ok(plainAt >= 0 && plain.includes('html .visor__place, html .visor__who, html .visor__hint, html .visor__progress-words {'), 'less transparency is answered in style.css, lifted over visor.css');
|
|
220
|
+
assert.ok(plain.includes('background: var(--night-plate);') && plain.includes(' backdrop-filter: none;'), 'less transparency: the night plate again');
|
|
221
|
+
const smokeTokens = STYLE.match(/--smoke[a-z-]*:[^;]*;/g) || [];
|
|
222
|
+
assert.equal(smokeTokens.length, 4, 'four smoke tokens: the tint, the blur, the sheen, the edge');
|
|
223
|
+
assert.doesNotMatch(smokeTokens.join(' '), /rgba?\(|#[0-9a-f]{3,8}\b/i, 'every smoke colour derives from a Helm token, no literal');
|
|
224
|
+
assert.ok(STYLE.includes('html[data-game="on"] .visor__who, html[data-game="on"] .visor__hint { background: var(--night-plate); box-shadow: none; -webkit-backdrop-filter: none; backdrop-filter: none; }'), 'a phone keeps the night plate: no live blur on the weakest GPU');
|
|
225
|
+
assert.ok(STYLE.indexOf('backdrop-filter: none; }') > STYLE.indexOf('tasks 1005148 and 1005195, the visor'), 'and that rule sits in the shared phone block');
|
|
226
|
+
assert.ok(css.indexOf('@supports ((backdrop-filter') > css.indexOf('.visor__progress-words {'), 'the smoke comes after every plate rule, so it wins the cascade');
|
|
208
227
|
assert.match(css, /\.visor__line--under \{ stroke: var\(--bg\); stroke-opacity: 1/, 'every line is drawn over a solid stroke of the night, so it reads on cloud and on night');
|
|
209
228
|
assert.match(css, /\.visor__key \{[^}]*background: var\(--bg\)/, 'a key sits on a chip of the night');
|
|
210
229
|
assert.doesNotMatch(css, /data-hour|data-time|data-sky/, 'no per-hour style');
|
|
@@ -365,12 +384,26 @@ test('a lead scene action is the first key hint, and its click and its key run i
|
|
|
365
384
|
await ctx.close();
|
|
366
385
|
});
|
|
367
386
|
|
|
387
|
+
// task 1005251: the options window keeps its aria-modal box after it closes. Only a dialog that
|
|
388
|
+
// is SHOWN holds the visor's keys; before this, one visit to Options left every key dead.
|
|
389
|
+
test('a closed dialog left in the page does not hold the keys; an open one does', { skip }, async () => {
|
|
390
|
+
const { ctx, page } = await open();
|
|
391
|
+
await page.evaluate(() => { const d = document.createElement('div'); d.id = 'gone'; d.setAttribute('aria-modal', 'true'); d.hidden = true; document.body.appendChild(d); });
|
|
392
|
+
await page.keyboard.press('a');
|
|
393
|
+
assert.equal(await page.evaluate(() => OTBVisor.isOpen()), true, 'a hidden modal box: A still opens the sheet');
|
|
394
|
+
await page.keyboard.press('Escape');
|
|
395
|
+
await page.evaluate(() => { document.getElementById('gone').hidden = false; document.getElementById('gone').textContent = 'up'; });
|
|
396
|
+
await page.keyboard.press('a');
|
|
397
|
+
assert.equal(await page.evaluate(() => OTBVisor.isOpen()), false, 'a modal that is up keeps the keyboard');
|
|
398
|
+
await ctx.close();
|
|
399
|
+
});
|
|
400
|
+
|
|
368
401
|
test('the sheet lists the character\'s actions, then this place, and runs a scene action', { skip }, async () => {
|
|
369
402
|
const { ctx, page } = await open();
|
|
370
403
|
await page.click('[data-visor-sheet]');
|
|
371
404
|
const rows = await page.$$eval('.visor__sheet .visor__row', (r) => r.map((x) => [...x.children].map((c) => c.textContent).join(' ')));
|
|
372
405
|
// task 1005200: Claim a task and Blockers open classic pages, so they are off the glass.
|
|
373
|
-
assert.deepEqual(rows, ['Goal sky G', 'Your thinking T', 'Back to centre 0', 'Look around drag', '
|
|
406
|
+
assert.deepEqual(rows, ['Goal sky G', 'Your thinking T', 'Back to centre 0', 'Look around drag', 'Game menu M']);
|
|
374
407
|
assert.equal(await page.$eval('.visor__sheet [data-visor-close]', (b) => b.textContent), 'Esc', 'Escape is the Close of the sheet, and only that');
|
|
375
408
|
await page.click('[data-visor-scene="0"]');
|
|
376
409
|
assert.deepEqual(await page.evaluate(() => ({ centred: window.centred, open: OTBVisor.isOpen() })), { centred: true, open: false });
|
|
@@ -481,7 +514,7 @@ test('the glass is the kit\'s: the cut accent button, ice hints with no box, the
|
|
|
481
514
|
return {
|
|
482
515
|
mainBg: cs('.visor__main').backgroundColor, accent: probe('var(--accent)'), mainClip: cs('.visor__main').clipPath,
|
|
483
516
|
mainCase: cs('.visor__main').textTransform, hintBg: cs('.visor__hint').backgroundColor, hintBorder: cs('.visor__hint').borderTopWidth,
|
|
484
|
-
plate: probe('var(--
|
|
517
|
+
plate: probe('var(--smoke)'), blur: cs('.visor__place').backdropFilter, edge: cs('.visor__hint').boxShadow, placeBg: cs('.visor__place').backgroundColor, whoBg: cs('.visor__who').backgroundColor, hintClip: cs('.visor__hint').clipPath,
|
|
485
518
|
glassBg: cs('#visor').backgroundImage + ' ' + cs('#visor').backgroundColor,
|
|
486
519
|
hintColor: cs('.visor__hint').color, ice: probe('var(--ink-soft)'), num: cs('.visor__num').color, unit: cs('.visor__unit').color, white: probe('var(--ink)'),
|
|
487
520
|
placeShadow: cs('.visor__place').textShadow, radius: cs('.visor__sprite').borderTopLeftRadius, spriteClip: cs('.visor__sprite').clipPath,
|
|
@@ -492,7 +525,9 @@ test('the glass is the kit\'s: the cut accent button, ice hints with no box, the
|
|
|
492
525
|
assert.match(g.mainClip, /^polygon\(/, 'cut corner');
|
|
493
526
|
assert.equal(g.mainCase, 'uppercase', 'the name voice');
|
|
494
527
|
assert.equal(g.glassBg, 'none rgba(0, 0, 0, 0)', 'the glass darkens nothing: the scene shows to every edge');
|
|
495
|
-
assert.equal(g.hintBg, g.plate, 'a hint stands on a
|
|
528
|
+
assert.equal(g.hintBg, g.plate, 'a hint stands on a pane of smoked glass (task 1005252)');
|
|
529
|
+
assert.match(g.blur, /blur\(14px\)/, 'the pane blurs the scene behind it');
|
|
530
|
+
assert.match(g.edge, /inset/, 'and has a lit edge');
|
|
496
531
|
assert.equal(g.placeBg, g.plate, 'so does the place');
|
|
497
532
|
assert.equal(g.whoBg, g.plate, 'and your credits');
|
|
498
533
|
assert.match(g.hintClip, /^polygon\(/, 'the plate is cut');
|
|
@@ -80,6 +80,7 @@ ${NEUTRAL_PREFIX}_FOUNDING_GENESIS=
|
|
|
80
80
|
${NEUTRAL_PREFIX}_DEMO_PEOPLE=
|
|
81
81
|
${NEUTRAL_PREFIX}_DEMO_HOURS=
|
|
82
82
|
${NEUTRAL_PREFIX}_SHIP_MODE=
|
|
83
|
+
${NEUTRAL_PREFIX}_TEST_PROJECT=
|
|
83
84
|
${NEUTRAL_PREFIX}_HUB_UPGRADE_URL=
|
|
84
85
|
`;
|
|
85
86
|
|
|
@@ -151,7 +152,7 @@ t('settingsEnvVarsFor: the runner pushes the SAME pair webEnvBody writes at stan
|
|
|
151
152
|
// the birth marker and a demo's box (task 1004506), empty on an un-stamped row
|
|
152
153
|
[`${NEUTRAL_PREFIX}_FOUNDING_BIRTH`]: '', [`${NEUTRAL_PREFIX}_FOUNDING_GENESIS`]: '', [`${NEUTRAL_PREFIX}_DEMO_PEOPLE`]: '', [`${NEUTRAL_PREFIX}_DEMO_HOURS`]: '',
|
|
153
154
|
// the wizard's ship mode (task 1004629), empty when none was picked
|
|
154
|
-
[`${NEUTRAL_PREFIX}_SHIP_MODE`]: '',
|
|
155
|
+
[`${NEUTRAL_PREFIX}_SHIP_MODE`]: '', [`${NEUTRAL_PREFIX}_TEST_PROJECT`]: '',
|
|
155
156
|
// where the hall's Update button goes (task 1004608), empty when no hub origin resolves
|
|
156
157
|
[`${NEUTRAL_PREFIX}_HUB_UPGRADE_URL`]: '',
|
|
157
158
|
});
|
|
@@ -243,6 +243,22 @@ await ta('cloud-host: ADMITTED past the limit for a builder holding provisioning
|
|
|
243
243
|
assert.equal(lastCreateArgs && lastCreateArgs.hostingShape, 'cloud-host');
|
|
244
244
|
});
|
|
245
245
|
|
|
246
|
+
await ta('cloud-host: a TEST project holds no free place, so it does not make the next one a second (task 1005036)', async () => {
|
|
247
|
+
// Only an archon makes a test project, and it is for trying features: counting it would
|
|
248
|
+
// charge a builder's one free place for a project nobody meant to keep.
|
|
249
|
+
held = [];
|
|
250
|
+
owned = [{ ...LIVE_HOSTED, slug: 'sandbox', settings: { test_project: 'on' } }];
|
|
251
|
+
lastCreateArgs = null;
|
|
252
|
+
const res = await post('/provisioning/instances', { slug: 'demo', hosting_shape: 'cloud-host', no_address: true });
|
|
253
|
+
assert.equal(res.status, 201, 'a test project alone leaves the free place free: ' + res.raw);
|
|
254
|
+
held = [];
|
|
255
|
+
owned = [LIVE_HOSTED, { ...LIVE_HOSTED, slug: 'sandbox', settings: { test_project: 'on' } }];
|
|
256
|
+
lastCreateArgs = null;
|
|
257
|
+
const second = await post('/provisioning/instances', { slug: 'demo', hosting_shape: 'cloud-host', no_address: true });
|
|
258
|
+
owned = [];
|
|
259
|
+
assert.equal(second.status, 403, 'a real project plus a test one still counts the real one');
|
|
260
|
+
});
|
|
261
|
+
|
|
246
262
|
// ── control-plane is not requestable AT ALL (task 1003370) ────────────────────────
|
|
247
263
|
|
|
248
264
|
await ta('control-plane: refused for an unprivileged builder, and the refusal is not a permission', async () => {
|
|
@@ -128,7 +128,7 @@ t('settingsEnvVars is the single source both legs derive from — same keys, sam
|
|
|
128
128
|
// route never stamped, which is every row made before it shipped (spec D8).
|
|
129
129
|
CLOUDBONGOS_FOUNDING_BIRTH: '', CLOUDBONGOS_FOUNDING_GENESIS: '', CLOUDBONGOS_DEMO_PEOPLE: '', CLOUDBONGOS_DEMO_HOURS: '',
|
|
130
130
|
// The wizard's ship mode (task 1004629): empty when none was picked.
|
|
131
|
-
CLOUDBONGOS_SHIP_MODE: '',
|
|
131
|
+
CLOUDBONGOS_SHIP_MODE: '', CLOUDBONGOS_TEST_PROJECT: '',
|
|
132
132
|
});
|
|
133
133
|
// varsOf splits on '=', so an empty value reads as '' on both sides — the point
|
|
134
134
|
// of the comparison is that the standup lines carry exactly the same key set.
|
|
@@ -137,7 +137,7 @@ t('settingsEnvVars is the single source both legs derive from — same keys, sam
|
|
|
137
137
|
X_PLATFORM_VISIBILITY: 'public', X_JOINABILITY: 'apply', X_VISIBILITY: 'public', X_JOIN_GRANT: 'full',
|
|
138
138
|
X_ARTIST_GATE: 'strict', X_ARTIST_GATE_SINCE: '', X_SPEND_PAYER: 'none',
|
|
139
139
|
X_LOOK: 'own', X_NIGHT_TINT: 'on', X_LOOK_ACCENT: '', X_LOGO_URL: '',
|
|
140
|
-
X_FOUNDING_BIRTH: '', X_FOUNDING_GENESIS: '', X_DEMO_PEOPLE: '', X_DEMO_HOURS: '', X_SHIP_MODE: '',
|
|
140
|
+
X_FOUNDING_BIRTH: '', X_FOUNDING_GENESIS: '', X_DEMO_PEOPLE: '', X_DEMO_HOURS: '', X_SHIP_MODE: '', X_TEST_PROJECT: '',
|
|
141
141
|
}, 'defaults, explicitly');
|
|
142
142
|
});
|
|
143
143
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// tests/test_project.mjs — A TEST PROJECT: one made to try features that are not live yet
|
|
2
|
+
// (task 1005036, idea 1001299; owner, 2026-11-01).
|
|
3
|
+
//
|
|
4
|
+
// Done-when: only an archon can ask for one; it is stamped once on the row just made; it is
|
|
5
|
+
// kept off the public map; it reaches the instance as project.test; and the not-yet-live
|
|
6
|
+
// genesis switch opens on a test project and nowhere else. The free-place count is pinned in
|
|
7
|
+
// tests/provisioning_paid_shape_gate.mjs and the create route in tests/wizard_demo.mjs.
|
|
8
|
+
//
|
|
9
|
+
// Run: node --test tests/test_project.mjs
|
|
10
|
+
|
|
11
|
+
import { strict as assert } from 'node:assert';
|
|
12
|
+
import { test } from 'node:test';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import { execFileSync } from 'node:child_process';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
const tp = require('../modules/provisioning/test-project.js');
|
|
21
|
+
const provisioning = require('../modules/provisioning/provisioning.js');
|
|
22
|
+
const bridge = require('../modules/provisioning/catalog-bridge.js');
|
|
23
|
+
const gh = require('../modules/onboarding/genesis-home.js');
|
|
24
|
+
const fm = require('../modules/onboarding/founding-mode.js');
|
|
25
|
+
|
|
26
|
+
const ARCHON = { id: 1, rank: 'archon' };
|
|
27
|
+
const METIC = { id: 2, rank: 'metic' };
|
|
28
|
+
const ROW = (settings) => ({ id: 41, slug: 'sandbox', domain: 'sandbox.example.test', status: 'active', hosting_shape: 'cloud-host', settings });
|
|
29
|
+
|
|
30
|
+
test('only an archon can ask for a test project, and genesis means nothing without one', () => {
|
|
31
|
+
assert.deepEqual(tp.parseTestAsk({ test: true }, ARCHON), { ok: true, test: true, genesis: false });
|
|
32
|
+
assert.deepEqual(tp.parseTestAsk({ test: true, genesis: true }, ARCHON), { ok: true, test: true, genesis: true });
|
|
33
|
+
assert.deepEqual(tp.parseTestAsk({}, METIC), { ok: true, test: false, genesis: false }, 'a request that asks for nothing is fine for anyone');
|
|
34
|
+
const refused = tp.parseTestAsk({ test: true }, METIC);
|
|
35
|
+
assert.equal(refused.ok, false);
|
|
36
|
+
assert.equal(refused.status, 403);
|
|
37
|
+
assert.equal(refused.code, 'test_project_forbidden');
|
|
38
|
+
assert.equal(tp.parseTestAsk({ test: true }, undefined).ok, false, 'no builder, no test project');
|
|
39
|
+
assert.equal(tp.parseTestAsk({ test: 'yes' }, METIC).ok, true, 'only a real boolean true asks');
|
|
40
|
+
const alone = tp.parseTestAsk({ genesis: true }, ARCHON);
|
|
41
|
+
assert.equal(alone.ok, false);
|
|
42
|
+
assert.equal(alone.code, 'genesis_needs_test');
|
|
43
|
+
assert.equal(alone.status, 400);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('the mark: stamped on the row it is given, once; read only when it says on', async () => {
|
|
47
|
+
const seen = [];
|
|
48
|
+
await tp.stampTestProject({ query: async (sql, params) => { seen.push({ sql, params }); return { rows: [] }; } }, 41);
|
|
49
|
+
assert.equal(seen.length, 1);
|
|
50
|
+
assert.match(seen[0].sql, /WHERE id = \$1 AND NOT \(COALESCE\(settings, '\{\}'::jsonb\) \? \$2::text\)/, 'write-once');
|
|
51
|
+
assert.deepEqual(seen[0].params, [41, 'test_project']);
|
|
52
|
+
assert.equal(tp.isTestProject(ROW({ test_project: 'on' })), true);
|
|
53
|
+
for (const bad of [undefined, null, {}, { test_project: 'yes' }, { test_project: true }, 'on']) {
|
|
54
|
+
assert.equal(tp.isTestProject(ROW(bad)), false, JSON.stringify(bad));
|
|
55
|
+
}
|
|
56
|
+
assert.equal(tp.isTestProject(null), false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('the owner view says it, and it reaches web.env as TEST_PROJECT, empty on every other project', () => {
|
|
60
|
+
assert.equal(provisioning.publicInstance(ROW({ test_project: 'on' })).is_test, true);
|
|
61
|
+
assert.equal(provisioning.publicInstance(ROW({})).is_test, false);
|
|
62
|
+
assert.equal(provisioning.settingsEnvVars(ROW({ test_project: 'on' }), 'X').X_TEST_PROJECT, 'on');
|
|
63
|
+
assert.equal(provisioning.settingsEnvVars(ROW({}), 'X').X_TEST_PROJECT, '', 'a line that came and went would leave a stale one behind');
|
|
64
|
+
assert.equal(provisioning.settingsEnvVars(ROW({ test_project: 'maybe' }), 'X').X_TEST_PROJECT, '');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('a test project is never on the public map; the same row without the mark is', () => {
|
|
68
|
+
assert.equal(bridge.catalogEntryForInstance(ROW({ test_project: 'on' }), { ownerGithubId: 99 }), null);
|
|
69
|
+
const plain = bridge.catalogEntryForInstance(ROW({}), { ownerGithubId: 99 });
|
|
70
|
+
assert.ok(plain && plain.clientId === 'sandbox', 'an ordinary project still projects');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('the instance reads TEST_PROJECT into project.test, and publishes it as a boolean beside project, not in it', () => {
|
|
74
|
+
const read = (env) => JSON.parse(execFileSync(process.execPath, ['-e',
|
|
75
|
+
"const b=require('./src/branding.js');const r=(b.branding||b)();console.log(JSON.stringify({p:r.project&&r.project.test,c:b.clientBranding(r).testProject}))"],
|
|
76
|
+
{ cwd: ROOT, encoding: 'utf8', env: { ...process.env, ...env } }));
|
|
77
|
+
assert.deepEqual(read({}), { c: false }, 'no env, not a test project');
|
|
78
|
+
assert.deepEqual(read({ CLOUDBONGOS_TEST_PROJECT: 'on' }), { p: 'on', c: true });
|
|
79
|
+
assert.deepEqual(read({ CLOUDBONGOS_TEST_PROJECT: '' }), { c: false }, 'the empty line every other project carries');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ── the genesis switch, on the instance ────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
function world({ test = true, settings = {} } = {}) {
|
|
85
|
+
const store = new Map(Object.entries(settings));
|
|
86
|
+
const deps = {
|
|
87
|
+
branding: { project: { test: test ? 'on' : '' } },
|
|
88
|
+
getSetting: async (k) => (store.has(k) ? { key: k, value: store.get(k) } : null),
|
|
89
|
+
setSetting: async (k, v) => { store.set(k, String(v)); return { key: k, value: String(v) }; },
|
|
90
|
+
lifecycle: { seedGenesis: async () => ({ seeded: 0 }), genesisTasks: async () => ({}) },
|
|
91
|
+
government: null,
|
|
92
|
+
now: new Date('2026-11-02T09:00:00Z'),
|
|
93
|
+
};
|
|
94
|
+
return { store, deps };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
test('genesis cannot be switched on from a project that is not a test project', async () => {
|
|
98
|
+
const w = world({ test: false });
|
|
99
|
+
assert.deepEqual(await gh.setEnabled(true, 1, w.deps), { ok: false, reason: 'not_a_test_project' });
|
|
100
|
+
assert.deepEqual(await gh.ownerStart(1, w.deps), { ok: false, reason: 'not_a_test_project' });
|
|
101
|
+
assert.equal(w.store.size, 0, 'nothing is written');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('on a test project the switch starts genesis, pauses it, and brings it back', async () => {
|
|
105
|
+
const w = world();
|
|
106
|
+
const on = await gh.setEnabled(true, 1, w.deps);
|
|
107
|
+
assert.equal(on.ok, true);
|
|
108
|
+
assert.equal(on.mode.active, true);
|
|
109
|
+
assert.equal(w.store.get(fm.KEYS.ENABLED), 'true');
|
|
110
|
+
const off = await gh.setEnabled(false, 1, w.deps);
|
|
111
|
+
assert.equal(off.ok, true);
|
|
112
|
+
assert.equal(off.mode, null, 'paused: the hall opens full');
|
|
113
|
+
assert.equal(w.store.get(fm.KEYS.ACTIVE), 'true', 'the founding record is kept');
|
|
114
|
+
const again = await gh.setEnabled(true, 1, w.deps);
|
|
115
|
+
assert.equal(again.mode.active, true, 'it comes back where it was');
|
|
116
|
+
assert.equal((await gh.ownerStart(1, world().deps)).ok, true, 'the owner\'s own start works on a test project');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('a genesis that came alive cannot be switched back on', async () => {
|
|
120
|
+
const w = world({ settings: { [fm.KEYS.STARTED_AT]: '2026-11-01T00:00:00Z', [fm.KEYS.ENDED_AT]: '2026-11-01T05:00:00Z' } });
|
|
121
|
+
assert.deepEqual(await gh.setEnabled(true, 1, w.deps), { ok: false, reason: 'already_alive' });
|
|
122
|
+
});
|
package/tests/wizard_demo.mjs
CHANGED
|
@@ -117,6 +117,9 @@ provisioning.getInstanceByDomain = async () => null;
|
|
|
117
117
|
const order = [];
|
|
118
118
|
const birth = require('../modules/provisioning/birth.js');
|
|
119
119
|
birth.stampBirth = async (_db, id) => { order.push(`birth:${id}`); };
|
|
120
|
+
const testProject = require('../modules/provisioning/test-project.js');
|
|
121
|
+
testProject.stampTestProject = async (_db, id) => { order.push(`test:${id}`); };
|
|
122
|
+
let rank = 'metic'; // who is asking: a test project is an archon's alone (task 1005036)
|
|
120
123
|
provisioning.createInstance = async (_db, args) => {
|
|
121
124
|
created = args;
|
|
122
125
|
const fresh = args.slug !== 'old-one';
|
|
@@ -126,7 +129,7 @@ provisioning.enqueueIntent = async (_db, _id, action) => { order.push(`enqueue:$
|
|
|
126
129
|
provisioning.getOpenIntent = async () => null;
|
|
127
130
|
provisioning.recordEvent = async () => {};
|
|
128
131
|
demo.startDemoProject = async (_db, _p, id, d) => { started = { id, d }; order.push(`demo:${id}`); return null; };
|
|
129
|
-
api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, github_login: 'owner' }; next(); };
|
|
132
|
+
api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, github_login: 'owner', rank }; next(); };
|
|
130
133
|
let stored = null;
|
|
131
134
|
provisioning.getInstanceById = async (_db, id) => (id === 41 ? { id: 41, owner_builder_id: 7, demo: stored } : null);
|
|
132
135
|
api.pool.query = async (sql, params) => { if (/UPDATE provisioning_instances SET demo/.test(sql)) stored = JSON.parse(params[1]); return { rows: [] }; };
|
|
@@ -173,8 +176,26 @@ test('a new project is stamped born, once, before its provision intent; an exist
|
|
|
173
176
|
assert.equal((await post('/provisioning/instances', { slug: 'born-real', hosting_shape: 'byo-host' })).status, 201);
|
|
174
177
|
assert.deepEqual(order, ['enqueue:provision'], 'a real project opens as the full hall: genesis is off by default (owner, 2026-11-01)');
|
|
175
178
|
order.length = 0;
|
|
176
|
-
|
|
177
|
-
|
|
179
|
+
// A test project (task 1005036): an archon's alone, marked once on the row, and the only way to be born with genesis.
|
|
180
|
+
const asked = await post('/provisioning/instances', { slug: 'sneaky', hosting_shape: 'byo-host', test: true });
|
|
181
|
+
assert.equal(asked.status, 403, 'a non-archon cannot make one');
|
|
182
|
+
assert.equal(asked.body.error.code, 'test_project_forbidden');
|
|
183
|
+
assert.deepEqual(order, [], 'and nothing is made, marked or queued');
|
|
184
|
+
const bare = await post('/provisioning/instances', { slug: 'bare-genesis', hosting_shape: 'byo-host', genesis: true });
|
|
185
|
+
assert.equal(bare.status, 400, 'genesis alone is no longer a way in');
|
|
186
|
+
assert.equal(bare.body.error.code, 'genesis_needs_test');
|
|
187
|
+
assert.deepEqual(order, []);
|
|
188
|
+
rank = 'archon';
|
|
189
|
+
try {
|
|
190
|
+
assert.equal((await post('/provisioning/instances', { slug: 'sandbox-a', hosting_shape: 'byo-host', test: true })).status, 201);
|
|
191
|
+
assert.deepEqual(order, ['test:41', 'enqueue:provision'], 'a test project is marked, not born into genesis');
|
|
192
|
+
order.length = 0;
|
|
193
|
+
assert.equal((await post('/provisioning/instances', { slug: 'sandbox-g', hosting_shape: 'byo-host', test: true, genesis: true })).status, 201);
|
|
194
|
+
assert.deepEqual(order, ['birth:41', 'test:41', 'enqueue:provision'], 'a test project that asks for genesis is born into it, marked, before the runner can read it');
|
|
195
|
+
order.length = 0;
|
|
196
|
+
assert.equal((await post('/provisioning/instances', { slug: 'archon-real', hosting_shape: 'byo-host' })).status, 201);
|
|
197
|
+
assert.deepEqual(order, ['enqueue:provision'], 'an ordinary project made by an archon is still an ordinary project');
|
|
198
|
+
} finally { rank = 'metic'; }
|
|
178
199
|
order.length = 0;
|
|
179
200
|
const again = await post('/provisioning/instances', { slug: 'old-one', hosting_shape: 'byo-host' });
|
|
180
201
|
assert.ok(again.status < 300, JSON.stringify(again.body));
|
|
@@ -182,7 +203,7 @@ test('a new project is stamped born, once, before its provision intent; an exist
|
|
|
182
203
|
const keep = birth.stampBirth;
|
|
183
204
|
birth.stampBirth = async () => { throw new Error('db hiccup'); };
|
|
184
205
|
try {
|
|
185
|
-
assert.equal((await post('/provisioning/instances', { slug: 'unlucky', hosting_shape: 'byo-host',
|
|
206
|
+
assert.equal((await post('/provisioning/instances', { slug: 'unlucky', hosting_shape: 'byo-host', demo: { people: 2, hours_each: 3 } })).status, 201,
|
|
186
207
|
'a missed marker costs the project its genesis, never its creation');
|
|
187
208
|
} finally { birth.stampBirth = keep; }
|
|
188
209
|
});
|