@bongos/core 1.24.18 → 1.24.20

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.18",
3
+ "version": "1.24.20",
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",
@@ -10993,5 +10993,21 @@
10993
10993
  "id": "1003183",
10994
10994
  "text": "Builders can now be paid for a task, and never twice. Each payment is written down as owed before anything is sent, so a failed payment still leaves a record. No money moves yet: the Stripe connection comes next."
10995
10995
  }
10996
+ ],
10997
+ "1.24.19": [
10998
+ {
10999
+ "id": "1005030",
11000
+ "text": "A mockup of the Builders leaderboard now shows the circuit-trace divider from the owner's Studio idea between the column headings and the table. The live page is unchanged."
11001
+ }
11002
+ ],
11003
+ "1.24.20": [
11004
+ {
11005
+ "id": "1005031",
11006
+ "text": "The Studio now shows what a picture will cost and asks before it starts, shows the final cost, and writes its answers in readable headings and lists."
11007
+ },
11008
+ {
11009
+ "id": "1005032",
11010
+ "text": "The Builders page now has the circuit-trace divider from the owner's Studio idea between the column headings and the first row, in light and dark and on phones."
11011
+ }
10996
11012
  ]
10997
11013
  }
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.18'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.20'; // 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');
@@ -53,13 +53,37 @@ test('the receipt: our key splits the provider and our markup; an own key charge
53
53
  const ours = lib.receipt(done, 1.08);
54
54
  assert.deepEqual(ours.rows, [['Paid with', 'your balance'], ['OpenRouter', '$0.0390'], ['Our 8%', '$0.0031'], ['You paid', '$0.0421']]);
55
55
  const gemini = lib.receipt(dated(fixture('ai-studio__requests__own')).request, 1.08);
56
- assert.deepEqual(gemini.rows, [['Paid with', 'your own Gemini key'], ['Cost', 'not reported']]);
56
+ assert.deepEqual(gemini.rows, [['Paid with', 'your own Gemini key'], ['Cost', 'about $0.0640']]);
57
+ assert.match(gemini.note, /estimate/);
57
58
  assert.match(gemini.note, /We charged nothing/);
58
59
  const openrouter = lib.receipt({ status: 'done', key_source: 'own', provider_cost_micro: 39000 }, 1.08);
59
60
  assert.deepEqual(openrouter.rows, [['Paid with', 'your own OpenRouter key'], ['Cost', '$0.0390']]);
60
61
  assert.deepEqual(lib.receipt(dated(fixture('ai-studio__requests__failed')).request, 1.08).rows, [], 'a failed run has no receipt');
61
62
  });
62
63
 
64
+ test('the estimate is the median of finished runs, capped at the ceiling; the cost line reads plainly', () => {
65
+ assert.deepEqual(lib.estimate([], 500000), { typical: null, max: 500000, basis: 0 });
66
+ const runs = [{ status: 'done', key_source: 'ours', price_micro: 40000 }, { status: 'done', key_source: 'ours', price_micro: 20000 }, { status: 'done', key_source: 'ours', price_micro: 90000 }, { status: 'failed', key_source: 'ours', price_micro: 999999 }, { status: 'done', key_source: 'own', provider_cost_micro: 30000, price_micro: 0 }];
67
+ assert.deepEqual(lib.estimate(runs, 500000, true), { typical: 40000, max: 500000, basis: 3 });
68
+ assert.deepEqual(lib.estimate(runs, 500000, false), { typical: 30000, max: 500000, basis: 1 }, 'an own-key guess uses the provider cost, no markup');
69
+ assert.equal(lib.estimate([{ status: 'done', key_source: 'ours', price_micro: 900000 }], 500000, true).typical, 500000);
70
+ assert.equal(lib.costHeadline({ status: 'done', key_source: 'ours', price_micro: 41000 }), 'This cost $0.0410, taken from your balance.');
71
+ assert.match(lib.costHeadline({ status: 'done', key_source: 'own', provider_cost_micro: null }), /about \$0\.0640 on your Gemini key/);
72
+ assert.equal(lib.geminiEstimate(3), 142000, 'a reply plus each picture');
73
+ assert.equal(lib.costHeadline({ status: 'failed', key_source: 'ours', price_micro: 1 }), '');
74
+ });
75
+
76
+ test('the reply reads as headings, a list and folded code, never raw markup', () => {
77
+ const fence = '`'.repeat(3);
78
+ const md = ['### What works', '* **Feel:** the `rule` line', '* Second', '--', `${fence}xml`, '<svg a="1"/>', fence, 'Done <b>x</b>'].join('\n');
79
+ const html = lib.replyHtml(md, escapeHtml);
80
+ assert.ok(html.includes('<h3 class="ais-rh">What works</h3>'), html);
81
+ assert.ok(html.includes('<ul><li><b>Feel:</b> the <code>rule</code> line</li><li>Second</li></ul>'), html);
82
+ assert.ok(html.includes('<summary>Code from the answer</summary><pre>&lt;svg a=&quot;1&quot;/&gt;</pre></details>'), html);
83
+ assert.ok(html.includes('<p>Done &lt;b&gt;x&lt;/b&gt;</p>'), html);
84
+ assert.ok(!html.includes('###') && !html.includes(fence) && !html.includes('<p>--'), html);
85
+ });
86
+
63
87
  test('the month: added, spent, money back and own-key requests, from the ledger', () => {
64
88
  const bal = dated(fixture('ai-studio__balance'));
65
89
  const sum = lib.monthSummary(bal.ledger, new Date());
@@ -368,6 +392,9 @@ test('page: step 3 sends the request on the card pressed, with copies of the car
368
392
  assert.match(p.page.innerHTML, /\$12\.40 left/);
369
393
  const ours = p.page.querySelectorAll('[data-pay]').find((b) => b.getAttribute('data-pay') === 'ours');
370
394
  await ours.fire('click');
395
+ assert.match(p.page.querySelector('#ais-confirm').innerHTML, /to make this/, 'the cost is shown before anything is sent');
396
+ assert.equal(p.calls.filter((c) => c.method === 'POST').length, 0, 'nothing is sent until the artist says yes');
397
+ await p.page.querySelector('#ais-confirm').querySelector('#ais-confirm-go').fire('click');
371
398
  await settle();
372
399
  const uploads = p.calls.filter((c) => c.method === 'POST' && c.url.endsWith('/ai-studio/uploads'));
373
400
  assert.equal(uploads.length, 3, 'a file goes with one request only, so each carried file is uploaded again');
@@ -470,6 +497,19 @@ test('walls: the studio home shows the AI door only where the module is on', ()
470
497
  assert.match(js, /loadAi\(\)\]/);
471
498
  });
472
499
 
500
+ test('page: Not yet closes the confirm box and sends nothing', async () => {
501
+ const p = bootPage('?step=make&from=1001');
502
+ await settle();
503
+ const ours = p.page.querySelectorAll('[data-pay]').find((b) => b.getAttribute('data-pay') === 'ours');
504
+ await ours.fire('click');
505
+ const box = p.page.querySelector('#ais-confirm');
506
+ assert.ok(box.innerHTML.includes('It will never cost more than $0.50.'), box.innerHTML);
507
+ await box.querySelector('#ais-confirm-cancel').fire('click');
508
+ assert.equal(box.innerHTML, '');
509
+ assert.equal(box.hidden, true);
510
+ assert.equal(p.calls.filter((c) => c.method === 'POST').length, 0);
511
+ });
512
+
473
513
  test('page: with both own keys saved, step 3 offers one card each and sends the one pressed', async () => {
474
514
  const both = { keys: [{ provider: 'openrouter', set: true, last4: '4f2a' }, { provider: 'gemini', set: true, last4: '9c1d' }] };
475
515
  assert.deepEqual(lib.ownKeys(both).map((k) => k.provider), ['openrouter', 'gemini']);
@@ -480,6 +520,9 @@ test('page: with both own keys saved, step 3 offers one card each and sends the
480
520
  assert.match(p.page.innerHTML, /My Gemini key/);
481
521
  const gem = p.page.querySelectorAll('[data-pay]').find((b) => b.getAttribute('data-pay') === 'gemini');
482
522
  await gem.fire('click');
523
+ assert.match(p.page.querySelector('#ais-confirm').innerHTML, /to make this/, 'the cost is shown before anything is sent');
524
+ assert.equal(p.calls.filter((c) => c.method === 'POST').length, 0, 'nothing is sent until the artist says yes');
525
+ await p.page.querySelector('#ais-confirm').querySelector('#ais-confirm-go').fire('click');
483
526
  await settle();
484
527
  const sent = p.calls.find((c) => c.method === 'POST' && c.url.endsWith('/ai-studio/requests'));
485
528
  assert.equal(sent.body.pay_with, 'gemini');
@@ -302,3 +302,13 @@ test('the states files exercise the redesign\'s new states', () => {
302
302
  const sessions = JSON.parse(read('sessions.states.json'));
303
303
  assert.deepEqual(sessions.states.ledger.expect.visible, ['#stats-scroll', '#sessions-scroll']);
304
304
  });
305
+
306
+ test('the roster draws the circuit divider in CSS, on the first row\'s cell and not the row', () => {
307
+ const css = read('roster.css');
308
+ const rule = css.slice(css.indexOf('the circuit divider'));
309
+ assert.match(rule, /tr:first-child:not\(:has\(\.ledger__empty\)\) td:first-child::before/, 'the traces hang on the first cell');
310
+ assert.match(rule, /td:first-child::after/, 'and so do the nodes');
311
+ assert.doesNotMatch(rule, /:has\(\.ledger__empty\)\)::(before|after)/, 'never on the tr itself: a tr pseudo becomes a table cell and shifts the row');
312
+ assert.match(rule, /background: var\(--ink-faint\)/, 'traces spend the faint ink token');
313
+ assert.match(rule, /background: var\(--accent\)/, 'nodes spend the accent token');
314
+ });