@bongos/core 1.19.1013 → 1.19.1015
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 +90 -85
- package/docs/adr/0227-a-task-network-map-is-refused-the-goal-graph-is-the-map.md +4 -1
- package/docs/adr/0331-the-sky-is-the-ideators-home.md +6 -1
- package/docs/copy-inventory.md +152 -112
- package/docs/copy-registry.json +1001 -304
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet-costs.js +80 -0
- package/modules/hall-ui/public/fleet.html +64 -0
- package/modules/hall-ui/public/fleet.js +45 -0
- package/modules/hall-ui/public/fleet.states.json +14 -0
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +125 -52
- package/modules/hall-ui/public/goal-map.states.json +29 -5
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/people.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +15 -0
- package/modules/hall-ui/public/sky-data.js +19 -1
- package/modules/hall-ui/public/sky-interaction.js +14 -7
- package/modules/hall-ui/public/sky-layout.js +2 -1
- package/modules/hall-ui/public/sky-tour.js +10 -6
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.css +6 -3
- package/modules/hall-ui/public/thinking.html +16 -10
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/provisioning/routes/provisioning.js +4 -4
- package/modules/public-landing/public/projects.html +7 -0
- package/modules/ui-design/kit/fixtures/provisioning-instances.json +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/auth.js +8 -0
- package/src/bongos/serve-internal.js +9 -0
- package/src/module-api.js +1 -1
- package/tests/auth_resolve_failure.mjs +3 -2
- package/tests/fleet_cost_view.mjs +189 -0
- package/tests/goal_map_page.mjs +346 -211
- package/tests/hall_audit.mjs +1 -0
- package/tests/hall_nav.mjs +9 -2
- package/tests/hall_page_gate_map.mjs +5 -2
- package/tests/helpers.mjs +21 -5
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/render_check.mjs +6 -4
- package/modules/hall-ui/public/goal-map-lib.js +0 -349
- package/modules/hall-ui/public/goal-map.css +0 -117
- package/modules/hall-ui/public/goal-map.js +0 -232
- package/tests/goal_map_lib.mjs +0 -214
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// tests/fleet_cost_view.mjs — Cost view v1 (task 1001949, ADR 0327): the fleet cost page
|
|
2
|
+
// for the platform owner, and the one estimate line on a cloud-host owner's project page.
|
|
3
|
+
//
|
|
4
|
+
// The rule every case below leans on: an UNPRICED cost (null) renders as unpriced and never
|
|
5
|
+
// as $0, and never moves a total. A $0 there would be a price nobody quoted.
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { test } from 'node:test';
|
|
8
|
+
import http from 'node:http';
|
|
9
|
+
import vm from 'node:vm';
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
|
+
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
18
|
+
const read = (p) => readFileSync(p, 'utf8').replace(/\r\n/g, '\n');
|
|
19
|
+
const costs = require(path.join(HALL, 'fleet-costs.js'));
|
|
20
|
+
const FIXTURE = JSON.parse(read(path.join(ROOT, 'scripts', 'hall-preview', 'fixtures', 'provisioning__fleet__cost-ledger.json')));
|
|
21
|
+
|
|
22
|
+
// ── the fleet page's builders ────────────────────────────────────────────────
|
|
23
|
+
const row = (over) => ({ id: '1', slug: 'p', owner: 'o', hosting_shape: 'cloud-host', tier: null, status: 'active', platform_monthly_usd: 0, owner_monthly_usd: null, ...over });
|
|
24
|
+
const cells = (html) => [...html.matchAll(/<td[^>]*>([\s\S]*?)<\/td>/g)].map((m) => m[1].replace(/<[^>]+>/g, '').trim());
|
|
25
|
+
const foot = (html) => html.slice(html.indexOf('<tfoot>'));
|
|
26
|
+
|
|
27
|
+
test('money: whole dollars stay whole, cents show two places, and an unpriced value has no price', () => {
|
|
28
|
+
assert.equal(costs.money(13), '$13');
|
|
29
|
+
assert.equal(costs.money(12.5), '$12.50');
|
|
30
|
+
assert.equal(costs.money(0), '$0');
|
|
31
|
+
assert.equal(costs.money(null), null);
|
|
32
|
+
assert.equal(costs.money(undefined), null);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('an unpriced row reads "not priced yet", never $0 — and a real $0 still reads $0', () => {
|
|
36
|
+
const html = costs.ledgerHtml({
|
|
37
|
+
instances: [row({ slug: 'a', platform_monthly_usd: 0, owner_monthly_usd: null })],
|
|
38
|
+
totals: { platform: { monthly_usd: 0, unpriced: 0 }, owner: { monthly_usd: 0, unpriced: 1 } },
|
|
39
|
+
});
|
|
40
|
+
const [, , , , us, owner] = cells(html.slice(0, html.indexOf('<tfoot>')));
|
|
41
|
+
assert.equal(us, '$0', 'a cloud-host costs us a real, measured $0');
|
|
42
|
+
assert.equal(owner, 'not priced yet');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('a total sums only priced rows and names how many it left out', () => {
|
|
46
|
+
const html = costs.ledgerHtml({
|
|
47
|
+
instances: [
|
|
48
|
+
row({ slug: 'cp', hosting_shape: 'control-plane', tier: 's-1vcpu-2gb', platform_monthly_usd: 12, owner_monthly_usd: 0 }),
|
|
49
|
+
row({ slug: 'priced', tier: 'starter+basic-256mb', owner_monthly_usd: 13 }),
|
|
50
|
+
row({ slug: 'bare', owner_monthly_usd: null }),
|
|
51
|
+
],
|
|
52
|
+
totals: { platform: { monthly_usd: 12, unpriced: 0 }, owner: { monthly_usd: 13, unpriced: 1 } },
|
|
53
|
+
});
|
|
54
|
+
const [us, owner] = cells(foot(html));
|
|
55
|
+
assert.equal(us, '$12');
|
|
56
|
+
assert.equal(owner, '$13 + 1 not priced', 'the null row is counted, not summed');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('a column with NOTHING priced has no total — the sum of no prices is not $0', () => {
|
|
60
|
+
const html = costs.ledgerHtml({
|
|
61
|
+
instances: [row({ slug: 'a' }), row({ slug: 'b' })],
|
|
62
|
+
totals: { platform: { monthly_usd: 0, unpriced: 0 }, owner: { monthly_usd: 0, unpriced: 2 } },
|
|
63
|
+
});
|
|
64
|
+
const [us, owner] = cells(foot(html));
|
|
65
|
+
assert.equal(us, '$0', 'every row prices at a real $0 for us');
|
|
66
|
+
assert.equal(owner, 'not priced yet', 'the owner total is not printed as $0');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('the harness fixture reads as live does today: our box priced, every owner bill but ours unpriced', () => {
|
|
70
|
+
const [us, owner] = cells(foot(costs.ledgerHtml(FIXTURE)));
|
|
71
|
+
assert.equal(us, '$12');
|
|
72
|
+
assert.equal(owner, '$0 + 2 not priced, 1 not visible');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('the total says what its cells say: a self-hosted bill is "not visible", not "not priced"', () => {
|
|
76
|
+
const byo = (slug) => row({ slug, hosting_shape: 'byo-host' });
|
|
77
|
+
const mixed = costs.ledgerHtml({
|
|
78
|
+
instances: [row({ slug: 'p', tier: 'starter+basic-256mb', owner_monthly_usd: 13 }), byo('a'), byo('b')],
|
|
79
|
+
totals: { platform: { monthly_usd: 0, unpriced: 0 }, owner: { monthly_usd: 13, unpriced: 2 } },
|
|
80
|
+
});
|
|
81
|
+
assert.equal(cells(foot(mixed))[1], '$13 + 2 not visible');
|
|
82
|
+
const allByo = costs.ledgerHtml({ instances: [byo('a'), byo('b')], totals: { platform: { monthly_usd: 0, unpriced: 0 }, owner: { monthly_usd: 0, unpriced: 2 } } });
|
|
83
|
+
assert.equal(cells(foot(allByo))[1], 'not visible to us');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('a self-hosted owner\'s bill is not ours to see — a different absence from an unrecorded plan', () => {
|
|
87
|
+
const html = costs.ledgerHtml({ instances: [row({ hosting_shape: 'byo-host' })], totals: { platform: { monthly_usd: 0, unpriced: 0 }, owner: { monthly_usd: 0, unpriced: 1 } } });
|
|
88
|
+
assert.equal(cells(html)[5], 'not visible to us');
|
|
89
|
+
assert.equal(cells(html)[2], 'Self-hosted');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('every row of the fixture is listed, a missing plan says so, and row text is escaped', () => {
|
|
93
|
+
const html = costs.ledgerHtml(FIXTURE);
|
|
94
|
+
for (const r of FIXTURE.instances) assert.ok(html.includes(`>${r.slug}<`), `${r.slug} is a row`);
|
|
95
|
+
assert.match(html, /none recorded/);
|
|
96
|
+
const hostile = costs.ledgerHtml({ instances: [row({ slug: '<img src=x onerror=1>', owner: '"q"' })], totals: {} });
|
|
97
|
+
assert.doesNotMatch(hostile, /<img/);
|
|
98
|
+
assert.match(hostile, /<img src=x onerror=1>/);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('no projects renders a sentence, not an empty table', () => {
|
|
102
|
+
assert.match(costs.ledgerHtml({ instances: [], totals: {} }), /No projects are hosted here yet/);
|
|
103
|
+
assert.match(costs.ledgerHtml(null), /No projects are hosted here yet/);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ── the page: its wiring and its gate ────────────────────────────────────────
|
|
107
|
+
test('the page loads the builders before its chrome and reads the ledger route', () => {
|
|
108
|
+
const html = read(path.join(HALL, 'fleet.html'));
|
|
109
|
+
assert.ok(html.indexOf('fleet-costs.js') !== -1 && html.indexOf('fleet-costs.js') < html.indexOf('/fleet.js'), 'fleet-costs.js loads first');
|
|
110
|
+
const js = read(path.join(HALL, 'fleet.js'));
|
|
111
|
+
assert.match(js, /\/provisioning\/fleet\/cost-ledger/);
|
|
112
|
+
assert.match(js, /status === 403[\s\S]{0,200}fl-sealed/, 'a refusal shows the sealed state');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('the page gate is the atom the ledger route checks, and the nav mirrors its floor', () => {
|
|
116
|
+
const route = read(path.join(ROOT, 'modules', 'provisioning', 'routes', 'provisioning.js'));
|
|
117
|
+
assert.match(route, /router\.get\('\/provisioning\/fleet\/cost-ledger', auth\.requireBuilder, auth\.requirePermission\('provisioning\.fleet\.manage'\)/);
|
|
118
|
+
assert.match(read(path.join(ROOT, 'src', 'bongos', 'auth.js')), /requireFleetPage: Object\.freeze\(\{ permission: 'provisioning\.fleet\.manage'/);
|
|
119
|
+
// The nav is cosmetic (the page gate is the wall); 'government' is the metic floor.
|
|
120
|
+
const { byKey } = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
|
|
121
|
+
assert.equal(byKey('provisioning.fleet.manage').floor, 'metic');
|
|
122
|
+
assert.match(read(path.join(HALL, 'shell.js')), /href: '\/fleet', gate: 'government', module: 'provisioning'/);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ── the owner's line on their own project page ───────────────────────────────
|
|
126
|
+
const HUB = read(path.join(ROOT, 'modules', 'public-landing', 'public', 'projects.html'));
|
|
127
|
+
function hubFn(name) {
|
|
128
|
+
const start = HUB.indexOf(`function ${name}(`);
|
|
129
|
+
assert.ok(start !== -1, `${name} is defined in projects.html`);
|
|
130
|
+
const end = HUB.indexOf('\n function ', start + 1);
|
|
131
|
+
const box = {};
|
|
132
|
+
vm.runInNewContext(`${HUB.slice(start, end)}\nbox.fn = ${name};`, { box });
|
|
133
|
+
return box.fn;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
test('the owner\'s hosting-bill line: a price when the plan is known, "not priced yet" when not', () => {
|
|
137
|
+
const hostingBill = hubFn('hostingBill');
|
|
138
|
+
assert.equal(hostingBill(null), 'not priced yet');
|
|
139
|
+
assert.equal(hostingBill(undefined), 'not priced yet');
|
|
140
|
+
assert.equal(hostingBill(13), '$13/month, estimated. Render bills you directly.');
|
|
141
|
+
assert.equal(hostingBill(12.5), '$12.50/month, estimated. Render bills you directly.');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('the line is shown only on a live cloud-host — a self-hosted owner\'s bill is theirs', () => {
|
|
145
|
+
const body = HUB.slice(HUB.indexOf('function renderManage('), HUB.indexOf("$('mMeta').innerHTML", HUB.indexOf('function renderManage(')));
|
|
146
|
+
assert.match(body, /if \(inst\.hosting_shape === 'cloud-host' && inst\.status !== 'torn_down'\) rows\.push\(\['hosting bill', hostingBill\(inst\.owner_monthly_usd\)\]\);/);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ── the payload the owner's page reads ───────────────────────────────────────
|
|
150
|
+
const provisioning = require(path.join(ROOT, 'modules', 'provisioning', 'provisioning.js'));
|
|
151
|
+
const api = require(path.join(ROOT, 'src', 'module-api.js'));
|
|
152
|
+
let detailRow = null;
|
|
153
|
+
provisioning.getInstanceById = async () => detailRow;
|
|
154
|
+
provisioning.getOpenIntent = async () => null;
|
|
155
|
+
provisioning.getLatestOAuthManifestForInstance = async () => null;
|
|
156
|
+
require(path.join(ROOT, 'modules', 'provisioning', 'catalog-bridge.js')).healInstanceCatalog = async () => {};
|
|
157
|
+
api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, github_login: 'owner' }; next(); };
|
|
158
|
+
const express = require('express');
|
|
159
|
+
const app = express();
|
|
160
|
+
app.use((_req, res, next) => {
|
|
161
|
+
res.fail = (code, opts) => res.status(typeof opts === 'number' ? opts : (opts && opts.status) || 400).json({ error: { code } });
|
|
162
|
+
next();
|
|
163
|
+
});
|
|
164
|
+
app.use('/api/bongos', require(path.join(ROOT, 'modules', 'provisioning', 'routes', 'provisioning.js'))());
|
|
165
|
+
const getJson = (url) => new Promise((resolve, reject) => {
|
|
166
|
+
http.get(url, (res) => { let d = ''; res.on('data', (c) => { d += c; }); res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(d || 'null') })); }).on('error', reject);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('GET /provisioning/instances/:id carries the owner\'s estimate — null until a plan is recorded', async () => {
|
|
170
|
+
const server = app.listen(0);
|
|
171
|
+
await new Promise((r) => server.once('listening', r));
|
|
172
|
+
const url = `http://127.0.0.1:${server.address().port}/api/bongos/provisioning/instances/41`;
|
|
173
|
+
const base = { id: 41, owner_builder_id: 7, slug: 'p', status: 'active', hosting_shape: 'cloud-host', description: 'd' };
|
|
174
|
+
try {
|
|
175
|
+
detailRow = { ...base, tier: null };
|
|
176
|
+
let r = await getJson(url);
|
|
177
|
+
assert.equal(r.status, 200);
|
|
178
|
+
assert.ok('owner_monthly_usd' in r.body.instance, 'the field is present');
|
|
179
|
+
assert.equal(r.body.instance.owner_monthly_usd, null, 'no plan: unpriced, not 0');
|
|
180
|
+
detailRow = { ...base, tier: 'starter+basic-256mb' };
|
|
181
|
+
r = await getJson(url);
|
|
182
|
+
assert.equal(r.body.instance.owner_monthly_usd, 13, 'the ledger\'s own price for the named plan');
|
|
183
|
+
detailRow = { ...base, owner_builder_id: 8, tier: 'starter+basic-256mb' };
|
|
184
|
+
r = await getJson(url);
|
|
185
|
+
assert.equal(r.status, 404, 'another builder\'s project answers nothing');
|
|
186
|
+
} finally {
|
|
187
|
+
server.close();
|
|
188
|
+
}
|
|
189
|
+
});
|