@bongos/core 1.24.15 → 1.24.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.15",
3
+ "version": "1.24.16",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -10975,5 +10975,11 @@
10975
10975
  "id": "1004906",
10976
10976
  "text": "The payment processor is chosen: Stripe Connect. Each project's money will sit in its own Stripe account, so we never hold anyone's money. J.P. Morgan Payments is recorded as the backup. Nothing is bought or opened yet, and th"
10977
10977
  }
10978
+ ],
10979
+ "1.24.16": [
10980
+ {
10981
+ "id": "1005028",
10982
+ "text": "Settings now has a Your age section where you record your age group, which switches on AI Studio for adults."
10983
+ }
10978
10984
  ]
10979
10985
  }
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.24.15'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.16'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -0,0 +1,110 @@
1
+ // tests/settings_age.mjs — the "Your age" panel on /builders/settings (task 1005028).
2
+ // WHY. AI Studio and payouts read the latest age record, and the only writer is a
3
+ // route that needs a person signed in on the website. This page is the one way to
4
+ // write it, so the panel must read the band, offer exactly the server's four bands,
5
+ // and save through that route — a panel that drifted from the server's tiers would
6
+ // offer a choice the server refuses.
7
+ import { test } from 'node:test';
8
+ import assert from 'node:assert/strict';
9
+ import { readFileSync } from 'node:fs';
10
+ import { createRequire } from 'node:module';
11
+ import vm from 'node:vm';
12
+
13
+ const require = createRequire(import.meta.url);
14
+ const read = (f) => readFileSync(new URL(`../modules/hall-ui/public/${f}`, import.meta.url), 'utf8');
15
+ const gates = require('../src/bongos/payout-gates.js');
16
+
17
+ const html = read('settings.html');
18
+ const js = read('settings-age.js');
19
+
20
+ test('the panel offers exactly the age bands the server accepts', () => {
21
+ const offered = [...html.matchAll(/name="age-tier" value="([^"]+)"/g)].map((m) => m[1]).sort();
22
+ assert.deepEqual(offered, [...gates.AGE_TIERS].sort());
23
+ });
24
+
25
+ test('the section joins a rail group, starts hidden, and its script is loaded and self-boots', () => {
26
+ const section = html.match(/<section class="scroll" id="age-scroll"[^>]*>/);
27
+ assert.ok(section, 'the panel has a section');
28
+ assert.match(section[0], /data-group="account"/);
29
+ assert.match(section[0], /\bhidden\b/);
30
+ assert.match(html, /<script src="\/builders\/settings-age\.js/);
31
+ assert.match(js, /\$\('#age-scroll'\)\.hidden = false/);
32
+ assert.match(read('settings.js'), /'age': 'age-scroll'/, 'the deep link /settings#age resolves');
33
+ });
34
+
35
+ // Run the real script against a fake page and a fake client.
36
+ async function run({ tier, saveStatus = 201, loadStatus = 200 }) {
37
+ const calls = [];
38
+ const el = () => ({ hidden: true, textContent: '', disabled: false, listeners: {}, addEventListener(e, f) { this.listeners[e] = f; } });
39
+ const nodes = { '#age-current': el(), '#age-status': el(), '#age-save': el(), '#age-scroll': el() };
40
+ let checked = null;
41
+ const radios = {};
42
+ const sandbox = {
43
+ window: {
44
+ OTB: { $: (q) => nodes[q], toast: (m) => calls.push(['toast', m]) },
45
+ BongosClient: {
46
+ createClient: () => ({
47
+ request: async (method, path, opts) => {
48
+ calls.push([method, path, opts && opts.body]);
49
+ if (method === 'GET') return loadStatus < 400 ? { ok: true, status: 200, data: { age_tier: tier } } : { ok: false, status: loadStatus, data: { error: { message: 'The server said so.' } } };
50
+ return saveStatus < 400 ? { ok: true, status: saveStatus, data: {} } : { ok: false, status: saveStatus, data: { error: { message: 'no' } } };
51
+ },
52
+ }),
53
+ },
54
+ location: { assign() {} },
55
+ },
56
+ document: {
57
+ querySelector: (q) => {
58
+ if (q === 'input[name="age-tier"]:checked') return checked;
59
+ const m = q.match(/value="([^"]+)"/);
60
+ return m ? (radios[m[1]] = radios[m[1]] || { value: m[1], checked: false }) : null;
61
+ },
62
+ },
63
+ };
64
+ vm.runInNewContext(js, sandbox);
65
+ await new Promise((r) => setTimeout(r, 0));
66
+ return { calls, nodes, radios, choose: (v) => { checked = { value: v }; } };
67
+ }
68
+
69
+ test('nothing on record says so, and shows the panel', async () => {
70
+ const r = await run({ tier: null });
71
+ assert.match(r.nodes['#age-current'].textContent, /Nothing on record/);
72
+ assert.equal(r.nodes['#age-scroll'].hidden, false);
73
+ });
74
+
75
+ test('the band on record is shown and preselected', async () => {
76
+ const r = await run({ tier: 'adult' });
77
+ assert.match(r.nodes['#age-current'].textContent, /18 or older/);
78
+ assert.equal(r.radios.adult.checked, true);
79
+ });
80
+
81
+ test('saving posts the chosen band to the server route, and refuses to post with nothing chosen', async () => {
82
+ const r = await run({ tier: null });
83
+ await r.nodes['#age-save'].listeners.click();
84
+ assert.equal(r.calls.filter((c) => c[0] === 'POST').length, 0, 'no choice, no request');
85
+ r.choose('adult');
86
+ await r.nodes['#age-save'].listeners.click();
87
+ // JSON, because an object born inside the vm fails strict deepEqual on its prototype.
88
+ assert.equal(JSON.stringify(r.calls.find((c) => c[0] === 'POST')), JSON.stringify(['POST', '/api/bongos/me/payout-age', { tier: 'adult' }]));
89
+ assert.match(r.nodes['#age-current'].textContent, /18 or older/);
90
+ });
91
+
92
+ test('a refused save keeps what was on record and tells the person', async () => {
93
+ const r = await run({ tier: null, saveStatus: 403 });
94
+ r.choose('adult');
95
+ await r.nodes['#age-save'].listeners.click();
96
+ assert.match(r.nodes['#age-current'].textContent, /Nothing on record/);
97
+ assert.ok(r.calls.some((c) => c[0] === 'toast' && /Could not save/.test(c[1])));
98
+ });
99
+
100
+ test('a failed read says what the server said and leaves the panel hidden', async () => {
101
+ const r = await run({ tier: null, loadStatus: 500 });
102
+ assert.equal(r.nodes['#age-scroll'].hidden, true);
103
+ assert.ok(r.calls.some((c) => c[0] === 'toast' && /The server said so./.test(c[1])), 'the envelope message, not a bare HTTP code');
104
+ });
105
+
106
+ test('a band the page does not know is never put into a selector', async () => {
107
+ const r = await run({ tier: 'x"],body[x="' });
108
+ assert.match(r.nodes['#age-current'].textContent, /Nothing on record/);
109
+ assert.equal(Object.keys(r.radios).length, 0);
110
+ });