@bongos/core 1.24.15 → 1.24.17
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 +37 -27
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/api/openapi.json +3 -1
- package/docs/copy-inventory.md +97 -87
- package/docs/copy-registry.json +198 -108
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +1 -0
- package/docs/page-readings.json +102 -92
- package/modules/ai-studio/keys.js +4 -0
- package/modules/ai-studio/routes/ai-studio.js +1 -1
- package/modules/hall-ui/public/ai-studio-lib.js +16 -10
- package/modules/hall-ui/public/ai-studio.js +8 -8
- package/modules/hall-ui/public/panel.css +5 -0
- package/modules/hall-ui/public/settings-age.js +65 -0
- package/modules/hall-ui/public/settings.html +26 -0
- package/modules/hall-ui/public/settings.js +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/module-api.js +1 -1
- package/tests/ai_studio_generate.mjs +2 -0
- package/tests/hall_ai_studio.mjs +15 -0
- package/tests/settings_age.mjs +110 -0
|
@@ -331,12 +331,12 @@
|
|
|
331
331
|
const files = readyFiles();
|
|
332
332
|
const c = lib.payChoices(state.keys, state.balance);
|
|
333
333
|
const thumbs = files.slice(0, 3).map((f) => thumb(f)).join('');
|
|
334
|
-
const ownCard = c.
|
|
335
|
-
<button type="button" class="st-action ais-card ais-card--own" data-pay="
|
|
336
|
-
<span class="st-action__label">My own key</span>
|
|
337
|
-
<span class="ais-card__line">Free here. ${esc(
|
|
338
|
-
<span class="ais-card__foot">${esc(
|
|
339
|
-
</button>`
|
|
334
|
+
const ownCard = c.owns.map((k) => `
|
|
335
|
+
<button type="button" class="st-action ais-card ais-card--own" data-pay="${esc(k.provider)}">
|
|
336
|
+
<span class="st-action__label">${c.owns.length > 1 ? `My ${esc(k.name)} key` : 'My own key'}</span>
|
|
337
|
+
<span class="ais-card__line">Free here. ${esc(k.billedBy)} bills you for it directly.</span>
|
|
338
|
+
<span class="ais-card__foot">${c.owns.length > 1 ? '' : `${esc(k.name)} `}key ending ${esc(k.last4 || '')}</span>
|
|
339
|
+
</button>`).join('');
|
|
340
340
|
const balanceCard = c.payments ? `
|
|
341
341
|
<button type="button" class="st-action ais-card ais-card--ours" data-pay="ours" ${c.short ? 'disabled' : ''}>
|
|
342
342
|
<span class="st-action__label">My balance</span>
|
|
@@ -346,8 +346,8 @@
|
|
|
346
346
|
let foot = 'You only pay for what it really cost. If it fails, nothing is taken.';
|
|
347
347
|
let extra = '';
|
|
348
348
|
if (c.payments && c.short) extra = `<p class="ais-faint">Your balance is under ${esc(c.setAside)}. <a href="/builders/ai-studio?view=balance">Add money</a> to pay this way.</p>`;
|
|
349
|
-
if (!c.
|
|
350
|
-
if (!c.
|
|
349
|
+
if (!c.owns.length) extra += `<p class="ais-faint">Have your own OpenRouter or Gemini key? <a href="${SETTINGS_KEYS}">Add it in Settings</a> and make things for free here.</p>`;
|
|
350
|
+
if (!c.owns.length && !c.payments) foot = 'Paying from a balance is not set up here yet, so add your own key in Settings to make something.';
|
|
351
351
|
page.innerHTML = `${stepHead('make')}
|
|
352
352
|
<h1 class="ais-ask" data-head tabindex="-1">How do you want to pay?</h1>
|
|
353
353
|
<div class="ais-row ais-row--summary">${thumbs}<span class="ais-grow ais-soft">${esc(lib.quoteLine(state.draft.prompt))}</span><a class="ais-link" href="/builders/ai-studio?step=describe" data-nav="step=describe">change</a></div>
|
|
@@ -341,3 +341,8 @@ button.craft-save { display: inline-flex; align-items: center; justify-content:
|
|
|
341
341
|
.prow--pick { flex-direction: row; align-items: flex-start; }
|
|
342
342
|
.pinput, .display-name-input, .artkey-input, .join input, .join-note { max-width: none; }
|
|
343
343
|
}
|
|
344
|
+
|
|
345
|
+
/* ---- Your age (task 1005028): four radio choices, one per line ---------------- */
|
|
346
|
+
.age-choices { border: 0; padding: 0; margin: 0 0 var(--space-3, 14px); display: grid; gap: 2px; }
|
|
347
|
+
.age-choices label { display: flex; align-items: center; gap: 10px; min-height: 32px; cursor: pointer; }
|
|
348
|
+
.age-choices input { width: 18px; height: 18px; margin: 0; }
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// modules/hall-ui/public/settings-age.js — the "Your age" panel on /builders/settings
|
|
2
|
+
// (task 1005028). Shows the age band on record and records a new one. AI Studio and
|
|
3
|
+
// payouts read the latest record; the server only accepts it from a person signed in
|
|
4
|
+
// on the website (POST /me/payout-age), so this page is the one way to write it.
|
|
5
|
+
(() => {
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const API = '/api/bongos';
|
|
9
|
+
const { $ } = window.OTB;
|
|
10
|
+
const toast = (msg) => window.OTB.toast(msg);
|
|
11
|
+
const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
12
|
+
|
|
13
|
+
const LABELS = { adult: '18 or older', '16_17': '16 to 17', '13_15': '13 to 15', under_13: 'Under 13' };
|
|
14
|
+
|
|
15
|
+
// The server's error envelope is { error: { message } }.
|
|
16
|
+
const why = (res) => (res.data && res.data.error && res.data.error.message) || `HTTP ${res.status}`;
|
|
17
|
+
|
|
18
|
+
const currentText = (tier) => (LABELS[tier]
|
|
19
|
+
? `On record: ${LABELS[tier]}.`
|
|
20
|
+
: 'Nothing on record yet, so AI Studio stays closed to you.');
|
|
21
|
+
|
|
22
|
+
async function load() {
|
|
23
|
+
const res = await api.request('GET', `${API}/me/payouts`);
|
|
24
|
+
if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); return; }
|
|
25
|
+
if (!res.ok) throw new Error(why(res));
|
|
26
|
+
const tier = res.data && res.data.age_tier;
|
|
27
|
+
$('#age-current').textContent = currentText(tier);
|
|
28
|
+
// Only a known band is looked up, so nothing from the response reaches a selector.
|
|
29
|
+
const pick = LABELS[tier] ? document.querySelector(`input[name="age-tier"][value="${tier}"]`) : null;
|
|
30
|
+
if (pick) pick.checked = true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function save() {
|
|
34
|
+
const chosen = document.querySelector('input[name="age-tier"]:checked');
|
|
35
|
+
const status = $('#age-status');
|
|
36
|
+
if (!chosen) { status.textContent = 'Pick the group you are in first.'; return; }
|
|
37
|
+
const btn = $('#age-save');
|
|
38
|
+
btn.disabled = true;
|
|
39
|
+
status.textContent = 'Saving…';
|
|
40
|
+
try {
|
|
41
|
+
const res = await api.request('POST', `${API}/me/payout-age`, { body: { tier: chosen.value } });
|
|
42
|
+
if (!res.ok) throw new Error(why(res));
|
|
43
|
+
status.textContent = '';
|
|
44
|
+
$('#age-current').textContent = currentText(chosen.value);
|
|
45
|
+
toast('Age saved.');
|
|
46
|
+
} catch (err) {
|
|
47
|
+
status.textContent = '';
|
|
48
|
+
toast(`Could not save your age: ${err.message}`);
|
|
49
|
+
} finally {
|
|
50
|
+
btn.disabled = false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function setup() {
|
|
55
|
+
try {
|
|
56
|
+
await load();
|
|
57
|
+
$('#age-scroll').hidden = false;
|
|
58
|
+
$('#age-save').addEventListener('click', save);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
toast(`Could not load your age: ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setup();
|
|
65
|
+
})();
|
|
@@ -91,6 +91,31 @@
|
|
|
91
91
|
<p class="section-note">Saves when you press Save; shows everywhere at once.</p>
|
|
92
92
|
</section>
|
|
93
93
|
|
|
94
|
+
<!-- YOUR AGE — the band on record, and where you record it (task 1005028).
|
|
95
|
+
AI Studio and payouts both read the latest payout_age_records row, and
|
|
96
|
+
the only writer is POST /me/payout-age, which needs a person signed in
|
|
97
|
+
on the website. Rendered by settings-age.js; only the band is kept. -->
|
|
98
|
+
<section class="scroll" id="age-scroll" aria-labelledby="age-h" data-group="account" hidden>
|
|
99
|
+
<h2 id="age-h" class="scroll__h">Your age</h2>
|
|
100
|
+
<p class="scroll__lede">
|
|
101
|
+
AI Studio and getting paid both depend on your age. Pick the group you
|
|
102
|
+
are in. Only the group is kept, never your birthday.
|
|
103
|
+
</p>
|
|
104
|
+
<p class="settings-card__hint" id="age-current" aria-live="polite"></p>
|
|
105
|
+
<fieldset class="age-choices" id="age-choices">
|
|
106
|
+
<legend class="field-label">I am</legend>
|
|
107
|
+
<label><input type="radio" name="age-tier" value="adult"> 18 or older</label>
|
|
108
|
+
<label><input type="radio" name="age-tier" value="16_17"> 16 to 17</label>
|
|
109
|
+
<label><input type="radio" name="age-tier" value="13_15"> 13 to 15</label>
|
|
110
|
+
<label><input type="radio" name="age-tier" value="under_13"> Under 13</label>
|
|
111
|
+
</fieldset>
|
|
112
|
+
<div class="display-name-row">
|
|
113
|
+
<button type="button" class="craft-save" id="age-save">Save</button>
|
|
114
|
+
</div>
|
|
115
|
+
<p class="settings-status" id="age-status" role="status" aria-live="polite"></p>
|
|
116
|
+
<p class="section-note">AI Studio is for adults only, because the AI companies behind it only serve adults.</p>
|
|
117
|
+
</section>
|
|
118
|
+
|
|
94
119
|
<!-- PROFILE AND PRIVACY — one link out to the hub's account page
|
|
95
120
|
(task 1004259, ADR 0337 D4.3). Handle, bio, links, public/private,
|
|
96
121
|
recruiters and the hide-my-numbers switch belong to the platform
|
|
@@ -501,6 +526,7 @@
|
|
|
501
526
|
settings.js, which sits close to the 1500-line size ratchet. -->
|
|
502
527
|
<script src="/builders/settings-sessions.js?v=2026-08-29-sessions"></script>
|
|
503
528
|
<script src="/builders/settings-passkeys.js?v=2026-10-15-passkeys"></script>
|
|
529
|
+
<script src="/builders/settings-age.js?v=2026-10-30-age"></script>
|
|
504
530
|
<script src="/builders/settings-ai-keys.js?v=2026-10-28-ai-keys"></script>
|
|
505
531
|
<!-- task 1002514: the interaction-profile panel ("How Claude talks to you"), on
|
|
506
532
|
the same precedent and for the same reason — settings.js is against the
|
|
@@ -1107,6 +1107,7 @@
|
|
|
1107
1107
|
const PANEL_ANCHORS = {
|
|
1108
1108
|
'connections': 'connections-scroll',
|
|
1109
1109
|
'display-name': 'display-name-scroll',
|
|
1110
|
+
'age': 'age-scroll',
|
|
1110
1111
|
// task 1004259: the two privacy panels became one link-out row; their old
|
|
1111
1112
|
// anchors still land on it (the hall profile links /settings#cross-project).
|
|
1112
1113
|
'profile-privacy': 'account-hub-scroll',
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.17",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.24.
|
|
9
|
+
"version": "1.24.17",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.17",
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -10975,5 +10975,17 @@
|
|
|
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
|
+
}
|
|
10984
|
+
],
|
|
10985
|
+
"1.24.17": [
|
|
10986
|
+
{
|
|
10987
|
+
"id": "1005029",
|
|
10988
|
+
"text": "Artists with both an OpenRouter and a Gemini key saved can now choose which one runs their AI request."
|
|
10989
|
+
}
|
|
10978
10990
|
]
|
|
10979
10991
|
}
|
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.
|
|
84
|
+
const CORE_VERSION = '1.24.17'; // 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');
|
|
@@ -246,6 +246,8 @@ test('which key runs: theirs whenever they have one, unless they ask for ours',
|
|
|
246
246
|
assert.deepEqual(K.chooseKey(['openrouter'], 'ours'), { source: 'ours' });
|
|
247
247
|
assert.deepEqual(K.chooseKey([], 'own'), { error: 'no_own_key' });
|
|
248
248
|
assert.deepEqual(K.chooseKey(['openrouter'], 'free'), { error: 'bad_pay_with' });
|
|
249
|
+
assert.deepEqual(K.chooseKey(['gemini', 'openrouter'], 'gemini'), { source: 'own', provider: 'gemini' }, 'a named key wins over the default');
|
|
250
|
+
assert.deepEqual(K.chooseKey(['openrouter'], 'gemini'), { error: 'no_own_key' });
|
|
249
251
|
});
|
|
250
252
|
|
|
251
253
|
test('on their own key no money moves: one own_key row, price 0, cost noted', async () => {
|
package/tests/hall_ai_studio.mjs
CHANGED
|
@@ -469,3 +469,18 @@ test('walls: the studio home shows the AI door only where the module is on', ()
|
|
|
469
469
|
assert.match(js, /mods\.enabled\['ai-studio'\] !== true\) return;/, 'off by default: a missing flag hides the door');
|
|
470
470
|
assert.match(js, /loadAi\(\)\]/);
|
|
471
471
|
});
|
|
472
|
+
|
|
473
|
+
test('page: with both own keys saved, step 3 offers one card each and sends the one pressed', async () => {
|
|
474
|
+
const both = { keys: [{ provider: 'openrouter', set: true, last4: '4f2a' }, { provider: 'gemini', set: true, last4: '9c1d' }] };
|
|
475
|
+
assert.deepEqual(lib.ownKeys(both).map((k) => k.provider), ['openrouter', 'gemini']);
|
|
476
|
+
const p = bootPage('?step=make&from=1001', { routes: { 'GET /api/bongos/ai-studio/keys': both } });
|
|
477
|
+
await settle();
|
|
478
|
+
const pays = p.page.querySelectorAll('[data-pay]').map((b) => b.getAttribute('data-pay'));
|
|
479
|
+
assert.deepEqual(pays, ['openrouter', 'gemini', 'ours']);
|
|
480
|
+
assert.match(p.page.innerHTML, /My Gemini key/);
|
|
481
|
+
const gem = p.page.querySelectorAll('[data-pay]').find((b) => b.getAttribute('data-pay') === 'gemini');
|
|
482
|
+
await gem.fire('click');
|
|
483
|
+
await settle();
|
|
484
|
+
const sent = p.calls.find((c) => c.method === 'POST' && c.url.endsWith('/ai-studio/requests'));
|
|
485
|
+
assert.equal(sent.body.pay_with, 'gemini');
|
|
486
|
+
});
|
|
@@ -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
|
+
});
|