@bongos/core 1.25.5 → 1.25.6

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.
Files changed (35) hide show
  1. package/.bongos-core.json +49 -39
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +2 -0
  4. package/clients/bongos-client/index.cjs +2 -0
  5. package/clients/bongos-client/index.d.ts +4 -1
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/docs/api/openapi.json +56 -2
  8. package/docs/api-reference.md +4 -3
  9. package/docs/copy-inventory.md +12 -12
  10. package/docs/copy-registry.json +13 -13
  11. package/docs/module-api-changelog.md +2 -0
  12. package/docs/page-readings.json +49 -52
  13. package/modules/hall-ui/public/shell-game.js +34 -3
  14. package/modules/hall-ui/public/welcome-boot.js +27 -11
  15. package/modules/hall-ui/records/start-screen.md +10 -0
  16. package/modules/onboarding/genesis-home.js +35 -1
  17. package/modules/onboarding/routes/onboarding.js +7 -2
  18. package/modules/provisioning/catalog-bridge.js +3 -0
  19. package/modules/provisioning/paid-shape-gate.js +3 -1
  20. package/modules/provisioning/provisioning.js +3 -3
  21. package/modules/provisioning/routes/provisioning.js +7 -4
  22. package/modules/provisioning/test-project.js +65 -0
  23. package/package-lock.json +2 -2
  24. package/package.json +1 -1
  25. package/release-notes.json +10 -0
  26. package/scripts/bongos/government-walk.js +2 -2
  27. package/src/branding.js +7 -0
  28. package/src/module-api.js +1 -1
  29. package/tests/currency_label.mjs +3 -1
  30. package/tests/hall_start_screen.mjs +62 -8
  31. package/tests/provision_settings_apply.mjs +2 -1
  32. package/tests/provisioning_paid_shape_gate.mjs +16 -0
  33. package/tests/provisioning_settings_env.mjs +2 -2
  34. package/tests/test_project.mjs +122 -0
  35. package/tests/wizard_demo.mjs +25 -4
@@ -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
+ });
@@ -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
- assert.equal((await post('/provisioning/instances', { slug: 'born-test', hosting_shape: 'byo-host', genesis: true })).status, 201);
177
- assert.deepEqual(order, ['birth:41', 'enqueue:provision'], 'the genesis toggle still births a test project, with no demo box');
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', genesis: true })).status, 201,
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
  });