@bongos/core 1.24.16 → 1.24.18
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 +53 -33
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +7 -1
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +119 -4
- package/docs/api-reference.md +4 -2
- package/docs/architecture.md +15 -0
- package/docs/copy-inventory.md +4 -6
- package/docs/copy-registry.json +5 -23
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +8 -8
- package/migrations/core_290_builder_payouts.sql +86 -0
- package/modules/ai-studio/keys.js +4 -0
- package/modules/ai-studio/routes/ai-studio.js +1 -1
- package/modules/economy/module.json +1 -1
- package/modules/economy/routes/project-account.js +10 -1
- package/modules/hall-ui/public/ai-studio-lib.js +16 -10
- package/modules/hall-ui/public/ai-studio.js +8 -8
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/bongos/run-unit-tests.js +4 -0
- package/src/bongos/builder-payouts.js +264 -0
- package/src/bongos/routes/payout-approvals.js +41 -1
- package/src/module-api.js +1 -1
- package/tests/ai_studio_generate.mjs +2 -0
- package/tests/builder_payouts.mjs +295 -0
- package/tests/builder_payouts_db.mjs +109 -0
- package/tests/hall_ai_studio.mjs +15 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// tests/builder_payouts_db.mjs — task 1003183 (goal 1000092 M04). Real DB.
|
|
2
|
+
//
|
|
3
|
+
// Applies the REAL economy_002 and core_290 migrations and proves what Postgres itself
|
|
4
|
+
// refuses: a second debt for one builder and task, a second debt on one approval, a second
|
|
5
|
+
// `sent` attempt for one debt, a sent attempt with no transfer reference, a failed one with
|
|
6
|
+
// no reason, and any UPDATE or DELETE. Then drives the real payForTask and listPayouts over
|
|
7
|
+
// the real tables: held with no processor, paid once, the movement booked, a re-press a no-op.
|
|
8
|
+
//
|
|
9
|
+
// payout_approvals is a minimal stand-in here (its own triggers are core_283's, proven in
|
|
10
|
+
// tests/payout_approvals_db.mjs); the authority is stubbed, as the gates are core's.
|
|
11
|
+
//
|
|
12
|
+
// Self-skips without a reachable Postgres.
|
|
13
|
+
// Run: DATABASE_URL=... node tests/builder_payouts_db.mjs
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
import { strict as assert } from 'node:assert';
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const { Pool } = require('pg');
|
|
21
|
+
const P = require('../src/bongos/builder-payouts.js');
|
|
22
|
+
const ACCOUNT = require('../modules/economy/project-account.js');
|
|
23
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const mig = (...p) => readFileSync(join(here, '..', ...p), 'utf8');
|
|
25
|
+
|
|
26
|
+
const pgPool = new Pool({ connectionString: process.env.DATABASE_URL || undefined, max: 1 });
|
|
27
|
+
let client;
|
|
28
|
+
try { client = await pgPool.connect(); } catch (e) {
|
|
29
|
+
console.log(`builder_payouts_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
|
|
30
|
+
await pgPool.end().catch(() => {});
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
const SCHEMA = `t1003183_${process.pid}`;
|
|
34
|
+
let pass = 0;
|
|
35
|
+
const ok = (c, m) => { assert.ok(c, m); pass++; console.log(` ok - ${m}`); };
|
|
36
|
+
const rejects = async (sql, params) => {
|
|
37
|
+
await client.query('SAVEPOINT s');
|
|
38
|
+
let err = null;
|
|
39
|
+
try { await client.query(sql, params); } catch (e) { err = e; }
|
|
40
|
+
await client.query('ROLLBACK TO SAVEPOINT s');
|
|
41
|
+
return err;
|
|
42
|
+
};
|
|
43
|
+
const code = (e) => e && e.code;
|
|
44
|
+
const OWED = 'INSERT INTO builder_payouts_owed (builder_id, task_id, approval_id, amount_cents, currency, recorded_by) VALUES ($1,$2,$3,$4,$5,$6) RETURNING id';
|
|
45
|
+
const TRY = 'INSERT INTO builder_payout_attempts (owed_id, outcome, processor_ref, reason) VALUES ($1,$2,$3,$4)';
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
await client.query(`CREATE SCHEMA ${SCHEMA}`);
|
|
49
|
+
await client.query(`SET search_path TO ${SCHEMA}`);
|
|
50
|
+
await client.query('CREATE TABLE builders (id bigserial PRIMARY KEY, display_name text)');
|
|
51
|
+
await client.query('CREATE TABLE tasks (id bigserial PRIMARY KEY, title text)');
|
|
52
|
+
await client.query('CREATE TABLE schema_migrations (version text PRIMARY KEY)');
|
|
53
|
+
await client.query(`CREATE TABLE payout_approvals (id bigserial PRIMARY KEY, kind text NOT NULL, recipient_id bigint,
|
|
54
|
+
amount_cents bigint, currency text)`);
|
|
55
|
+
await client.query("INSERT INTO builders (display_name) VALUES ('Ada'), ('Owner')");
|
|
56
|
+
await client.query("INSERT INTO tasks (title) VALUES ('Build the thing'), ('Build another')");
|
|
57
|
+
await client.query(`INSERT INTO payout_approvals (kind, recipient_id, amount_cents, currency)
|
|
58
|
+
VALUES ('payout_approved', 1, 12500, 'USD'), ('payout_approved', 1, 300, 'USD'), ('payout_approved', 1, 400, 'USD')`);
|
|
59
|
+
await client.query(mig('modules', 'economy', 'migrations', 'economy_002_project_account.sql'));
|
|
60
|
+
await client.query(mig('migrations', 'core_290_builder_payouts.sql'));
|
|
61
|
+
await client.query('BEGIN');
|
|
62
|
+
|
|
63
|
+
// ── what the database refuses ──
|
|
64
|
+
const owedId = (await client.query(OWED, [1, 2, 3, 400, 'USD', 2])).rows[0].id;
|
|
65
|
+
ok(code(await rejects(OWED, [1, 2, 2, 300, 'USD', 2])) === '23505', 'a second debt for one builder and task is refused');
|
|
66
|
+
ok(code(await rejects(OWED, [1, 1, 3, 400, 'USD', 2])) === '23505', 'a second debt on one approval is refused');
|
|
67
|
+
ok(code(await rejects(OWED, [1, 1, 2, 0, 'USD', 2])) === '23514', 'a debt of zero is refused');
|
|
68
|
+
ok(code(await rejects(OWED, [1, 1, 2, 300, 'usd', 2])) === '23514', 'a currency that is not a three-letter code is refused');
|
|
69
|
+
ok(code(await rejects(TRY, [owedId, 'sent', null, null])) === '23514', 'a sent attempt with no transfer reference is refused');
|
|
70
|
+
ok(code(await rejects(TRY, [owedId, 'failed', null, null])) === '23514', 'a failed attempt with no reason is refused');
|
|
71
|
+
ok(code(await rejects(TRY, [owedId, 'paid', 'tr_x', null])) === '23514', 'an unknown outcome is refused');
|
|
72
|
+
await client.query(TRY, [owedId, 'sent', 'tr_a', null]);
|
|
73
|
+
ok(code(await rejects(TRY, [owedId, 'sent', 'tr_b', null])) === '23505', 'a second sent attempt for one debt is refused');
|
|
74
|
+
ok(code(await rejects('UPDATE builder_payouts_owed SET amount_cents = 1')) === 'P0001', 'a debt is never edited');
|
|
75
|
+
ok(code(await rejects('DELETE FROM builder_payouts_owed')) === 'P0001', 'a debt is never deleted');
|
|
76
|
+
ok(code(await rejects("UPDATE builder_payout_attempts SET outcome = 'failed', reason = 'x'")) === 'P0001', 'an attempt is never edited');
|
|
77
|
+
ok(code(await rejects('DELETE FROM builder_payout_attempts')) === 'P0001', 'an attempt is never deleted');
|
|
78
|
+
|
|
79
|
+
// ── the real writer over the real tables ──
|
|
80
|
+
const db = { query: (s, p) => client.query(s, p) };
|
|
81
|
+
const allow = { checkApproval: async () => ({ ok: true, pay_to: 'self' }) };
|
|
82
|
+
const calls = [];
|
|
83
|
+
const processor = { async transfer(req) { calls.push(req); return { ref: 'tr_real_1' }; } };
|
|
84
|
+
|
|
85
|
+
const held = await P.payForTask({ approvalId: 1, taskId: 1, actorId: 2 }, { pool: db, account: ACCOUNT, authority: allow });
|
|
86
|
+
ok(held.status === 'held' && held.reason === 'no_processor', 'with no processor the debt is recorded and held');
|
|
87
|
+
const paid = await P.payForTask({ approvalId: 1, taskId: 1, actorId: 2 }, { pool: db, account: ACCOUNT, authority: allow, processor });
|
|
88
|
+
ok(paid.status === 'paid' && paid.processor_ref === 'tr_real_1', 'the same debt is then paid');
|
|
89
|
+
const again = await P.payForTask({ approvalId: 1, taskId: 1, actorId: 2 }, { pool: db, account: ACCOUNT, authority: allow, processor });
|
|
90
|
+
ok(again.status === 'paid' && again.already === true && calls.length === 1, 'pressing again sends nothing');
|
|
91
|
+
const mv = await client.query("SELECT amount_micro, builder_id, task_id FROM economy_project_account_movements WHERE kind = 'payout'");
|
|
92
|
+
ok(mv.rows.length === 1 && Number(mv.rows[0].amount_micro) === 125_000_000 && String(mv.rows[0].task_id) === '1',
|
|
93
|
+
'one payout movement, in micro-dollars, naming the task');
|
|
94
|
+
|
|
95
|
+
const list = await P.listPayouts(db, {});
|
|
96
|
+
const row = list.payouts.find((x) => x.approval_id === '1');
|
|
97
|
+
ok(list.total === 2 && row.status === 'paid' && row.processor_ref === 'tr_real_1' && row.builder_name === 'Ada',
|
|
98
|
+
'the owner list reads it as paid, with the person and the transfer');
|
|
99
|
+
const other = list.payouts.find((x) => x.approval_id === '3');
|
|
100
|
+
ok(other.status === 'paid', 'a debt with a sent attempt reads paid');
|
|
101
|
+
|
|
102
|
+
await client.query('ROLLBACK');
|
|
103
|
+
console.log(`builder_payouts_db.mjs: ${pass} passed`);
|
|
104
|
+
} finally {
|
|
105
|
+
await client.query('ROLLBACK').catch(() => {});
|
|
106
|
+
await client.query(`DROP SCHEMA IF EXISTS ${SCHEMA} CASCADE`).catch(() => {});
|
|
107
|
+
client.release();
|
|
108
|
+
await pgPool.end();
|
|
109
|
+
}
|
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
|
+
});
|