@bongos/core 1.24.19 → 1.24.21
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 +46 -41
- package/docs/copy-inventory.md +28 -22
- package/docs/copy-registry.json +87 -23
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +698 -698
- package/modules/ai-studio/studio.js +1 -1
- package/modules/autonomy/governor-docket.js +3 -0
- package/modules/government/docket.js +51 -0
- package/modules/government/treasury.js +2 -0
- package/modules/hall-ui/public/ai-studio-lib.js +74 -9
- package/modules/hall-ui/public/ai-studio.css +12 -0
- package/modules/hall-ui/public/ai-studio.js +37 -1
- package/modules/hall-ui/public/city-decide.js +5 -2
- package/modules/hall-ui/public/city.css +1 -0
- package/modules/hall-ui/public/gov-living.js +7 -3
- package/modules/hall-ui/public/roster.css +43 -0
- package/modules/hall-ui/public/street-deck.css +3 -0
- package/modules/hall-ui/public/street-deck.js +5 -1
- package/modules/hall-ui/records/builders-roster.md +9 -0
- package/modules/ideas/governor-docket.js +3 -0
- package/modules/lifecycle/governor-docket.js +26 -0
- package/modules/onboarding/routes/access-requests.js +2 -0
- package/modules/security/routes/reports.js +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/src/bongos/module-rechecks.js +3 -0
- package/src/bongos/routes/core-update.js +2 -0
- package/src/module-api.js +1 -1
- package/tests/city_card_context.mjs +139 -0
- package/tests/government_treasury.mjs +1 -1
- package/tests/governor_docket.mjs +3 -1
- package/tests/hall_ai_studio.mjs +44 -1
- package/tests/hall_gov_real_site.mjs +1 -1
- package/tests/hall_roster_world.mjs +10 -0
- package/tests/hall_street_depth.mjs +7 -1
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.21",
|
|
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.21",
|
|
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.21",
|
|
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
|
@@ -10999,5 +10999,21 @@
|
|
|
10999
10999
|
"id": "1005030",
|
|
11000
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
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
|
+
}
|
|
11012
|
+
],
|
|
11013
|
+
"1.24.21": [
|
|
11014
|
+
{
|
|
11015
|
+
"id": "1005013",
|
|
11016
|
+
"text": "Every card in the city now says what it is. Under each question sits a short plain explanation: what kind of thing it is, which goal, task, person or module it belongs to, and what answering does. A Review card used to show a"
|
|
11017
|
+
}
|
|
11002
11018
|
]
|
|
11003
11019
|
}
|
|
@@ -77,6 +77,9 @@ function moduleRecheckMatter(r, mayRerun) {
|
|
|
77
77
|
proposer: null,
|
|
78
78
|
tally: null,
|
|
79
79
|
href: '/modules',
|
|
80
|
+
context: failed
|
|
81
|
+
? `${r.module_key} is a module (an add-on to this project). Version ${r.version} failed its automatic security check, so it is held back until someone runs the check again.`
|
|
82
|
+
: `${r.module_key} is a module (an add-on to this project). Version ${r.version} is being checked again for safety; nothing is needed unless the check fails.`,
|
|
80
83
|
// "<key>@<version>": the reassess route's two path parts (task 1004566)
|
|
81
84
|
ref: { type: 'module_version', id: `${r.module_key}@${r.version}` },
|
|
82
85
|
// the one act a governor takes on it, under the reassess route's own atom,
|
|
@@ -143,6 +143,8 @@ function coreUpdateDocketMatters(update) {
|
|
|
143
143
|
tally: null,
|
|
144
144
|
href: '/settings#software-update',
|
|
145
145
|
ref: { type: 'core_version', id: String(update.latest) },
|
|
146
|
+
question: `Update Bongos to ${update.latest}?`,
|
|
147
|
+
context: `Bongos is the software this project runs on. This project runs ${update.running}; ${update.latest} is out. Taking it moves the project to the new version on its next restart.`,
|
|
146
148
|
}];
|
|
147
149
|
}
|
|
148
150
|
// The running core, or null when it cannot be named. Read lazily and fail-soft,
|
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.21'; // 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,139 @@
|
|
|
1
|
+
// tests/city_card_context.mjs — EVERY CITY CARD SAYS WHAT IT IS (task 1005013).
|
|
2
|
+
//
|
|
3
|
+
// The owner, 2026-10-29: the cards "exist without context". A Review card showed a
|
|
4
|
+
// done-when criterion's raw wording with no question; a security card "Security
|
|
5
|
+
// report: <title>". What this pins:
|
|
6
|
+
// * the docket passes a matter's own `context` and `question` through, bounded,
|
|
7
|
+
// and gives every source the design names a plain sentence of its own when a
|
|
8
|
+
// matter brought none — no card is ever only a bare title;
|
|
9
|
+
// * every source's mapper writes context naming what it belongs to (the goal,
|
|
10
|
+
// the task, the person, the module) — the Review card names its goal, its
|
|
11
|
+
// shipped tasks and what the sign-off closes, and asks a real question;
|
|
12
|
+
// * the hall carries both to the card, and the one card drawing (drawAsk: deck,
|
|
13
|
+
// street, interior, city) shows the context under the question.
|
|
14
|
+
//
|
|
15
|
+
// Run: node --test tests/city_card_context.mjs
|
|
16
|
+
|
|
17
|
+
import { test } from 'node:test';
|
|
18
|
+
import assert from 'node:assert/strict';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const docket = require('../modules/government/docket.js');
|
|
23
|
+
const lifecycleDocket = require('../modules/lifecycle/governor-docket.js');
|
|
24
|
+
const ideasDocket = require('../modules/ideas/governor-docket.js');
|
|
25
|
+
const reportsRoute = require('../modules/security/routes/reports.js');
|
|
26
|
+
const accessRoute = require('../modules/onboarding/routes/access-requests.js');
|
|
27
|
+
const coreUpdate = require('../src/bongos/routes/core-update.js');
|
|
28
|
+
const moduleRechecks = require('../src/bongos/module-rechecks.js');
|
|
29
|
+
const treasury = require('../modules/government/treasury.js');
|
|
30
|
+
const living = require('../modules/hall-ui/public/gov-living.js');
|
|
31
|
+
const deck = require('../modules/hall-ui/public/street-deck.js');
|
|
32
|
+
|
|
33
|
+
const at = { source: 'criteria_uat', building: 'review' };
|
|
34
|
+
|
|
35
|
+
test('every source the design names has its own plain sentence, used when a matter brings none', () => {
|
|
36
|
+
for (const s of docket.EXPECTED_SOURCES) {
|
|
37
|
+
const words = docket.SOURCE_CONTEXT[s.source];
|
|
38
|
+
assert.ok(typeof words === 'string' && words.length > 40, `${s.source} has a sentence`);
|
|
39
|
+
assert.equal(docket.normalizeMatter({ id: 1, title: 't' }, { source: s.source, building: s.building }).context, words);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('a matter\'s own context and question win, bounded and on one line; junk is dropped', () => {
|
|
44
|
+
const m = docket.normalizeMatter({ id: 1, title: 't', context: ' Its own\nwords. ', question: ' Does it work? ' }, at);
|
|
45
|
+
assert.equal(m.context, 'Its own words.');
|
|
46
|
+
assert.equal(m.question, 'Does it work?');
|
|
47
|
+
const long = docket.normalizeMatter({ id: 1, title: 't', context: 'word '.repeat(400), question: 'q '.repeat(200) }, at);
|
|
48
|
+
assert.ok(long.context.length <= 600 && long.context.endsWith('…'));
|
|
49
|
+
assert.ok(long.question.length <= 160);
|
|
50
|
+
for (const bad of [null, '', ' ', 42, { x: 1 }]) {
|
|
51
|
+
const n = docket.normalizeMatter({ id: 1, title: 't', context: bad, question: bad }, at);
|
|
52
|
+
assert.equal(n.context, docket.SOURCE_CONTEXT.criteria_uat, 'falls back to the source sentence');
|
|
53
|
+
assert.ok(!('question' in n));
|
|
54
|
+
}
|
|
55
|
+
assert.equal(docket.normalizeMatter({ id: 1, title: 't' }, { source: 'nobody_named', building: 'review' }).context, undefined, 'an unknown source invents nothing');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('the Review card asks a real question and names the goal, the shipped work and what the sign-off closes', () => {
|
|
59
|
+
const c = { id: 9, criterion_md: 'Builders can sign in with **Google**', goal_id: 7, goal_title: 'Google sign-in', shipped_total: 3, backend_only: false };
|
|
60
|
+
const m = lifecycleDocket.uatMatter(c, {});
|
|
61
|
+
assert.equal(m.question, 'Does this work on the live site now?');
|
|
62
|
+
assert.match(m.context, /The promise: "Builders can sign in with Google"/);
|
|
63
|
+
assert.match(m.context, /done-when promises of the goal "Google sign-in"/);
|
|
64
|
+
assert.match(m.context, /All 3 tasks built for it have shipped/);
|
|
65
|
+
assert.match(m.context, /record a short video/);
|
|
66
|
+
assert.match(m.context, /the goal is achieved once all its promises are closed/);
|
|
67
|
+
assert.equal(m.title, c.criterion_md, 'the title stays the promise itself (desks and the street list read it)');
|
|
68
|
+
const back = lifecycleDocket.uatMatter({ ...c, backend_only: true, shipped_total: 1, goal_title: null }, {});
|
|
69
|
+
assert.match(back.question, /Check it and sign it off/);
|
|
70
|
+
assert.match(back.context, /nothing to see on screen/);
|
|
71
|
+
assert.match(back.context, /The one task built for it has shipped/);
|
|
72
|
+
assert.match(back.context, /of this version/);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('every other source names what it belongs to', () => {
|
|
76
|
+
const lc = lifecycleDocket;
|
|
77
|
+
assert.match(lc.overrideMatter({ id: 1, task_id: 4, task_title: 'Fix login', requester_login: 'kai' }, true).context, /"Fix login".*failed/);
|
|
78
|
+
assert.match(lc.removalMatter({ proposal_task_id: 2, title: 'x', goal_title: 'Docs' }).context, /goal "Docs"/);
|
|
79
|
+
assert.match(lc.joinRequestMatter({ id: 3, goal_id: 5, goal_title: 'Maps', display_name: 'Ana' }, false).context, /Ana asks to join the team of the goal "Maps"/);
|
|
80
|
+
assert.match(lc.joinRequestMatter({ id: 3, goal_id: 5, goal_title: 'Maps' }, true).context, /^You asked to join/);
|
|
81
|
+
assert.match(ideasDocket.blockerDocketMatter({ id: 1, title: 'x', blocking_task_ids: [1, 2] }, true).context, /2 tasks are stopped/);
|
|
82
|
+
const r = reportsRoute.redteamDocketMatter({ id: 2, title: 'No limit on tries', status: 'open', target: '/login', severity: 'high' });
|
|
83
|
+
assert.equal(r.question, 'Is this security flaw real?');
|
|
84
|
+
assert.match(r.context, /flaw in this project itself \(in \/login\), rated high\. "No limit on tries"/);
|
|
85
|
+
const a = accessRoute.accessRequestDocketMatter({ id: 1, github_login: 'kai', display_name: 'Kai', vouched: true });
|
|
86
|
+
assert.equal(a.question, 'Let Kai join this project?');
|
|
87
|
+
assert.match(a.context, /GitHub: kai/);
|
|
88
|
+
const [u] = coreUpdate.coreUpdateDocketMatters({ reading: 'ok', behind: true, latest: '1.25.0', running: '1.24.1' });
|
|
89
|
+
assert.match(u.context, /runs 1\.24\.1; 1\.25\.0 is out/);
|
|
90
|
+
assert.match(moduleRechecks.moduleRecheckMatter({ version_id: 7, module_key: 'image-tools', version: '1.2.0', security_failed: true }, true).context, /image-tools is a module.*failed its automatic security check/);
|
|
91
|
+
const b = treasury.borrowMatter({ id: 1, goal_id: 3, spent_usd: 20, budget_usd: 20, amount_usd: 10, filed_at: null }, { title: 'Maps' });
|
|
92
|
+
assert.match(b.context, /Maps has spent \$20/);
|
|
93
|
+
assert.match(b.question, /^Lend Maps/);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('board sittings say what is being decided and by what rule', () => {
|
|
97
|
+
const item = (subject_type, pass_rule) => ({ id: 1, subject_type, subject_id: 1, subject: { title: 'An idea', author_id: 9 }, constitution: { pass_rule }, votes: [], closed_at: null });
|
|
98
|
+
assert.match(docket.boardMatter(item('full_idea', 'first_ratifier'), { viewerId: 1, members: [] }).context, /becomes part of the plan.*One yes decides it\./);
|
|
99
|
+
assert.match(docket.boardMatter(item('constitutional_amendment', 'majority'), { viewerId: 1, members: [] }).context, /change to the charter.*more than half/);
|
|
100
|
+
assert.match(docket.boardMatter(item('genesis_stage', 'consent'), { viewerId: 1, members: [] }).context, /founding stage.*unless someone objects/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// a DOM just big enough for drawAsk
|
|
104
|
+
function fakeDoc() {
|
|
105
|
+
const make = (tag) => ({
|
|
106
|
+
tag, className: '', textContent: '', children: [], attrs: {},
|
|
107
|
+
setAttribute(k, v) { this.attrs[k] = v; }, appendChild(c) { this.children.push(c); return c; }, addEventListener() {},
|
|
108
|
+
});
|
|
109
|
+
return { createElement: make, createTextNode: (t) => ({ tag: '#text', textContent: t, children: [] }) };
|
|
110
|
+
}
|
|
111
|
+
function find(node, cls) {
|
|
112
|
+
if (node.className && String(node.className).split(' ').includes(cls)) return node;
|
|
113
|
+
for (const c of node.children || []) { const hit = find(c, cls); if (hit) return hit; }
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
test('the hall carries context and question to the card, and the card draws the context under its question', () => {
|
|
118
|
+
const matter = docket.normalizeMatter(lifecycleDocket.uatMatter({ id: 9, criterion_md: 'Builders can sign in with Google', goal_title: 'Google sign-in', shipped_total: 3 }, {}), at);
|
|
119
|
+
const model = living.livingModel({ docket: { matters: [matter] }, now: Date.now() });
|
|
120
|
+
const ask = model.city.asks.find((x) => x.id === matter.id);
|
|
121
|
+
assert.ok(ask, 'the matter is an ask');
|
|
122
|
+
assert.equal(ask.raw.question, 'Does this work on the live site now?');
|
|
123
|
+
assert.equal(ask.raw.context, matter.context);
|
|
124
|
+
const cv = deck.cardView(ask, {});
|
|
125
|
+
assert.equal(cv.question, 'Does this work on the live site now?');
|
|
126
|
+
assert.equal(cv.context, matter.context);
|
|
127
|
+
const card = deck.drawAsk(fakeDoc(), cv, {}, {});
|
|
128
|
+
const q = find(card, 'ask__q');
|
|
129
|
+
const ctx = find(card, 'ask__ctx');
|
|
130
|
+
assert.ok(q && ctx, 'question and context are both drawn');
|
|
131
|
+
assert.equal(ctx.textContent, matter.context);
|
|
132
|
+
assert.ok(card.children.indexOf(ctx) === card.children.indexOf(q) + 1, 'the context sits right under the question');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('a blocker read straight from GET /blockers carries the docket\'s blocker sentence', () => {
|
|
136
|
+
const model = living.livingModel({ blockers: [{ id: 4, title: 'Pick a host', status: 'open', created_at: '2026-10-01T00:00:00Z' }], now: Date.now() });
|
|
137
|
+
const ask = model.city.asks.find((x) => x.id === 'blockers:4');
|
|
138
|
+
assert.equal(ask.raw.context, docket.SOURCE_CONTEXT.blockers, 'the two copies of the sentence agree');
|
|
139
|
+
});
|
|
@@ -67,7 +67,7 @@ function project({ pots = [], borrows = [], cap = null, spend = {}, lastMonth =
|
|
|
67
67
|
return { rows: [{ month: params[0], total_usd: thisMonth }, ...(st.lastMonth ? [{ month: params[1], total_usd: st.lastMonth }] : [])] };
|
|
68
68
|
}
|
|
69
69
|
if (/^SELECT t\.goal_id, COALESCE\(SUM\(cl\.amount_usd\), 0\) AS total_usd FROM cost_log cl JOIN tasks t/.test(s)) {
|
|
70
|
-
assert.
|
|
70
|
+
assert.match(params[0], /^\d{4}-\d{2}$/); // the month asked for: NOW's in the direct cases (pinned by t.month below), the real clock's over the route
|
|
71
71
|
return { rows: Object.entries(st.spend).map(([goal_id, total_usd]) => ({ goal_id, total_usd })) };
|
|
72
72
|
}
|
|
73
73
|
if (/^SELECT goal_id, budget_usd, set_by, set_at FROM government_pots/.test(s)) return { rows: st.pots };
|
|
@@ -98,7 +98,9 @@ test('normalizeMatter: the one shape, prefixed id, ISO dates', () => {
|
|
|
98
98
|
assert.deepEqual(m.proposer, { id: '3', login: 'theo', name: null });
|
|
99
99
|
assert.deepEqual(m.tally, { for: 2, against: 1, open: 2, needs: 'x' });
|
|
100
100
|
assert.deepEqual(m.ref, { type: 'a', id: '5' });
|
|
101
|
-
|
|
101
|
+
// `context` is the source's own sentence when the matter brought none (task 1005013)
|
|
102
|
+
assert.deepEqual(Object.keys(m).sort(), ['building', 'context', 'deadline', 'href', 'id', 'opened_at', 'proposer', 'ref', 'rule', 'short', 'source', 'state', 'tally', 'title', 'where'].sort());
|
|
103
|
+
assert.equal(m.context, docket.SOURCE_CONTEXT.access_requests);
|
|
102
104
|
});
|
|
103
105
|
|
|
104
106
|
test('normalizeMatter carries a matter\'s detail for the card\'s "more" only as bounded text (task 1004566)', () => {
|
package/tests/hall_ai_studio.mjs
CHANGED
|
@@ -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', '
|
|
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><svg a="1"/></pre></details>'), html);
|
|
83
|
+
assert.ok(html.includes('<p>Done <b>x</b></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');
|
|
@@ -425,7 +425,7 @@ test('the decision deck on a real site: the docket\'s own asks in their own word
|
|
|
425
425
|
noPrototype(p, 'deck');
|
|
426
426
|
const deck = p.byId.get('streetDeck');
|
|
427
427
|
assert.equal(deck.hidden, false, 'the deck opens');
|
|
428
|
-
const docketTitles = fx('government__docket').matters.
|
|
428
|
+
const docketTitles = fx('government__docket').matters.flatMap((m) => [m.title, m.question]).filter(Boolean).concat(fx('blockers').blockers.map((b) => b.title));
|
|
429
429
|
assert.ok(docketTitles.some((t) => deck.textContent.includes(t)), 'a real ask, as its source words it');
|
|
430
430
|
// the format's refusal is the prototype's: a real ask goes to its floor's desk, where its real acts are
|
|
431
431
|
assert.doesNotMatch(deck.textContent, /sent back|until its agent writes it|prototype data|pass it on/, 'no refusal, no key, no seats to pass to');
|
|
@@ -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
|
+
});
|
|
@@ -52,6 +52,12 @@ require(path.join(PUB, 'street-desk.js')); // the line rule's fit, on the global
|
|
|
52
52
|
const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8');
|
|
53
53
|
const fx = (name) => JSON.parse(fs.readFileSync(path.join(FX, name + '.json'), 'utf8'));
|
|
54
54
|
const NOW = Date.parse('2026-10-09T14:00:00.000Z');
|
|
55
|
+
// The page reads the clock itself (street-depth.js, GovFeeds.model(raw, Date.now())), and the fixtures are
|
|
56
|
+
// October's: pin the page's clock to NOW so a later real month does not read them as stale and hide a tile.
|
|
57
|
+
class PinnedDate extends Date {
|
|
58
|
+
constructor(...a) { super(...(a.length ? a : [NOW])); }
|
|
59
|
+
static now() { return NOW; }
|
|
60
|
+
}
|
|
55
61
|
const clone = (o) => JSON.parse(JSON.stringify(o));
|
|
56
62
|
|
|
57
63
|
function feeds(over = {}) {
|
|
@@ -475,7 +481,7 @@ async function bootPage({ withLiving = true } = {}) {
|
|
|
475
481
|
const window = { innerWidth: 1440, innerHeight: 900, location: { hash: '' }, localStorage: { getItem: () => 'dark', setItem() {} }, matchMedia: () => ({ matches: true, addEventListener() {} }), addEventListener() {} };
|
|
476
482
|
class MutationObserver { observe() {} }
|
|
477
483
|
window.fetch = fetch;
|
|
478
|
-
const ctx = vm.createContext({ window, document, fetch, Promise, Math, String, Number, Array, Object, JSON, Date, console, encodeURIComponent, decodeURIComponent, isNaN, isFinite, MutationObserver, Set });
|
|
484
|
+
const ctx = vm.createContext({ window, document, fetch, Promise, Math, String, Number, Array, Object, JSON, Date: PinnedDate, console, encodeURIComponent, decodeURIComponent, isNaN, isFinite, MutationObserver, Set });
|
|
479
485
|
ctx.globalThis = ctx;
|
|
480
486
|
for (const f of (withLiving ? ['gov-living.js', 'gov-feeds.js'] : []).concat(['city-draw.js', 'street-draw.js', 'street-card.js', 'street-inside.js', 'street-depth.js', 'street-desk.js', 'street.js'])) vm.runInContext(read(f), ctx);
|
|
481
487
|
await window.GovStreetPage.ready;
|