@bongos/core 1.19.763 → 1.19.765

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.
@@ -0,0 +1,411 @@
1
+ // tests/provisioning_capacity.mjs
2
+ //
3
+ // The box-capacity ceiling (ADR 0285, task 1003927): the shared arithmetic in
4
+ // modules/provisioning/capacity.js, the runner preflight in
5
+ // scripts/gds/provision-capacity.js, its composition into provisionInstance and
6
+ // the drain loop, and the `capacity` block on GET /provisioning/fleet.
7
+ //
8
+ // Before this, nothing enforced the ceiling the ADR measured: the runner stood up
9
+ // project N+1 on a full box and swap hid it. These tests pin the two walls (row
10
+ // ceiling, live MemAvailable floor), that a refusal happens BEFORE any status write
11
+ // or standup command, that it reaches the owner's card as error_note without
12
+ // flipping the instance's status, and that the number every surface shows comes
13
+ // from the same helpers. Hermetic: fake db, fake executors, a loopback listener
14
+ // for the route; no Postgres, no shell, no /proc.
15
+ //
16
+ // Run: node tests/provisioning_capacity.mjs
17
+
18
+ import { strict as assert } from 'node:assert';
19
+ import { createRequire } from 'node:module';
20
+ import http from 'node:http';
21
+ import capacityDefault, {
22
+ DEFAULT_CEILING, DEFAULT_MEM_FLOOR_MB, PLACED_STATES,
23
+ parseMeminfo, placementBox, ceilingFor, placementVerdict, capacitySummary, countPlaced, fleetCapacity,
24
+ } from '../modules/provisioning/capacity.js';
25
+ import { MEMINFO_CMD, capacityPreflight, statusCapacityLine } from '../scripts/gds/provision-capacity.js';
26
+
27
+ const require = createRequire(import.meta.url);
28
+ const P = require('../scripts/gds/provision.js');
29
+
30
+ let passed = 0, failed = 0;
31
+ function t(name, fn) {
32
+ try { fn(); passed++; console.log(` PASS ${name}`); }
33
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
34
+ }
35
+ async function ta(name, fn) {
36
+ try { await fn(); passed++; console.log(` PASS ${name}`); }
37
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
38
+ }
39
+
40
+ // The live control plane on 2026-09-16 (1968 MiB total) and the ADR's 4 GB tier.
41
+ const MEMINFO_2GB = 'MemTotal: 2015216 kB\nMemFree: 123456 kB\nMemAvailable: 1135180 kB\nSwapTotal: 4194300 kB\n';
42
+ const MEMINFO_4GB = 'MemTotal: 4030432 kB\nMemAvailable: 2500000 kB\n';
43
+ const MEMINFO_TIGHT = 'MemTotal: 2015216 kB\nMemAvailable: 120000 kB\n'; // 117 MB free — under the 256 MB floor
44
+
45
+ // A fake db: answers the ceiling COUNT with `placed`, records every query, and
46
+ // returns no rows for anything else (allocatePort etc.).
47
+ function fakeDb(placed) {
48
+ const queries = [];
49
+ return {
50
+ queries,
51
+ query: async (sql, params) => {
52
+ queries.push({ sql, params });
53
+ if (/count\(\*\)/.test(sql)) return { rows: [{ n: placed }] };
54
+ return { rows: [] };
55
+ },
56
+ };
57
+ }
58
+
59
+ console.log('the arithmetic (ADR 0285):');
60
+
61
+ t('parseMeminfo: MemTotal / MemAvailable in MiB; absent fields read null, never 0', () => {
62
+ assert.deepEqual(parseMeminfo(MEMINFO_2GB), { memTotalMb: 1967, memAvailableMb: 1108 }, 'the ADR\'s own "1967 MiB RAM" reading');
63
+ assert.deepEqual(parseMeminfo(''), { memTotalMb: null, memAvailableMb: null });
64
+ assert.deepEqual(parseMeminfo(null), { memTotalMb: null, memAvailableMb: null });
65
+ assert.deepEqual(parseMeminfo('MemTotal: 4030432 kB\n'), { memTotalMb: 3935, memAvailableMb: null });
66
+ });
67
+
68
+ t('ceilingFor reproduces the ADR 0285 table from MemTotal: 15 / 39 / 41 — and a resize raises it with no knob', () => {
69
+ assert.deepEqual(ceilingFor({ memTotalMb: 1968, fixedMb: 750 }), { ceiling: 15, source: 'measured' }, 's-1vcpu-2gb control plane');
70
+ assert.deepEqual(ceilingFor({ memTotalMb: 3936, fixedMb: 750 }), { ceiling: 39, source: 'measured' }, 's-2vcpu-4gb control plane');
71
+ assert.deepEqual(ceilingFor({ memTotalMb: 3936, fixedMb: 630 }), { ceiling: 41, source: 'measured' }, 's-2vcpu-4gb overflow box');
72
+ });
73
+
74
+ t('ceilingFor: an explicit override wins; an unmeasured box falls to the ADR smallest-tier default and SAYS so', () => {
75
+ assert.deepEqual(ceilingFor({ override: 20, memTotalMb: 1968 }), { ceiling: 20, source: 'env' });
76
+ assert.deepEqual(ceilingFor({}), { ceiling: DEFAULT_CEILING, source: 'default' });
77
+ assert.deepEqual(ceilingFor({ memTotalMb: 500, fixedMb: 750 }), { ceiling: DEFAULT_CEILING, source: 'default' }, 'a box smaller than its fixed base cannot be measured into a ceiling');
78
+ assert.equal(DEFAULT_CEILING, 15);
79
+ });
80
+
81
+ t('placementBox: standalone → control plane always; co-tenant → the remote box only under remote-exec; dedicated → no shared box', () => {
82
+ assert.deepEqual(placementBox('standalone', {}), { box: 'control-plane', shapes: ['standalone', 'co-tenant'], fixedMb: 750 });
83
+ assert.deepEqual(placementBox('standalone', { remoteHost: 'cohost' }), { box: 'control-plane', shapes: ['standalone'], fixedMb: 750 });
84
+ assert.deepEqual(placementBox('co-tenant', {}), { box: 'control-plane', shapes: ['standalone', 'co-tenant'], fixedMb: 750 });
85
+ assert.deepEqual(placementBox('co-tenant', { remoteHost: 'cohost' }), { box: 'overflow', shapes: ['co-tenant'], fixedMb: 630 });
86
+ assert.equal(placementBox('dedicated', {}), null);
87
+ });
88
+
89
+ t('placementVerdict: the ceiling refuses at N of N, the floor refuses under 256 MB free, an unmeasured floor cannot refuse', () => {
90
+ const full = placementVerdict({ placed: 15, ceiling: 15, memAvailableMb: 1108 });
91
+ assert.equal(full.ok, false); assert.equal(full.wall, 'ceiling');
92
+ assert.match(full.reason, /15 of 15 project slots/, 'names N of M for the owner');
93
+ assert.match(full.reason, /Nothing was placed/);
94
+ assert.match(full.reason, /task 1003923/, 'points at the resize');
95
+ const tight = placementVerdict({ placed: 10, ceiling: 15, memAvailableMb: 117 });
96
+ assert.equal(tight.ok, false); assert.equal(tight.wall, 'memory');
97
+ assert.match(tight.reason, /117 MB of memory free, under the 256 MB/);
98
+ assert.deepEqual(placementVerdict({ placed: 10, ceiling: 15, memAvailableMb: 1108 }), { ok: true, wall: null, reason: null });
99
+ assert.equal(placementVerdict({ placed: 14, ceiling: 15, memAvailableMb: null }).ok, true, 'unmeasured memory: the ceiling alone decides');
100
+ assert.equal(placementVerdict({ placed: 16, ceiling: 15 }).ok, false, 'past the ceiling is still full');
101
+ assert.equal(DEFAULT_MEM_FLOOR_MB, 256);
102
+ });
103
+
104
+ t('capacitySummary: resize_due at 80% (12 of 15), remaining never negative, verdict carried', () => {
105
+ const eleven = capacitySummary({ box: 'control-plane', placed: 11, ceiling: 15, source: 'measured', memAvailableMb: 1108 });
106
+ assert.equal(eleven.resize_at, 12); assert.equal(eleven.resize_due, false); assert.equal(eleven.remaining, 4);
107
+ const twelve = capacitySummary({ box: 'control-plane', placed: 12, ceiling: 15, source: 'measured', memAvailableMb: 1108 });
108
+ assert.equal(twelve.resize_due, true, 'the ADR schedules the resize at 12');
109
+ assert.equal(twelve.verdict.ok, true, 'due for a resize is not yet full');
110
+ const over = capacitySummary({ box: 'control-plane', placed: 16, ceiling: 15, source: 'env' });
111
+ assert.equal(over.remaining, 0); assert.equal(over.verdict.ok, false);
112
+ assert.equal(over.mem_floor_mb, DEFAULT_MEM_FLOOR_MB);
113
+ });
114
+
115
+ await ta('countPlaced counts ONLY provisioning + active rows of the box\'s shapes, and can exclude the row being re-run', async () => {
116
+ const db = fakeDb(10);
117
+ assert.equal(await countPlaced(db, ['standalone']), 10);
118
+ assert.deepEqual(db.queries[0].params, [['standalone'], PLACED_STATES]);
119
+ assert.deepEqual(PLACED_STATES, ['provisioning', 'active'], 'error rows hold no running unit (live fleet, 2026-09-16) and must not take a slot');
120
+ assert.doesNotMatch(db.queries[0].sql, /id <>/, 'no exclusion unless asked');
121
+ await countPlaced(db, ['standalone', 'co-tenant'], { excludeId: 42 });
122
+ assert.match(db.queries[1].sql, /id <> \$3/);
123
+ assert.deepEqual(db.queries[1].params, [['standalone', 'co-tenant'], PLACED_STATES, 42]);
124
+ assert.equal(await countPlaced({ query: async () => ({ rows: [] }) }, ['standalone']), 0, 'a recording fake with no rows reads 0, not NaN');
125
+ });
126
+
127
+ await ta('fleetCapacity (the API + CLI summary) measures THIS host and counts the control plane\'s shapes', async () => {
128
+ delete process.env.PROVISION_BOX_CEILING;
129
+ const db = fakeDb(10);
130
+ const c = await fleetCapacity(db, { meminfo: MEMINFO_2GB, remoteHost: null });
131
+ assert.equal(c.box, 'control-plane'); assert.equal(c.placed, 10); assert.equal(c.ceiling, 15); assert.equal(c.source, 'measured');
132
+ assert.equal(c.mem_available_mb, 1108); assert.equal(c.resize_due, false); assert.equal(c.verdict.ok, true);
133
+ assert.deepEqual(db.queries[0].params[0], ['standalone', 'co-tenant']);
134
+ const remote = await fleetCapacity(fakeDb(3), { meminfo: MEMINFO_4GB, remoteHost: 'cohost' });
135
+ assert.equal(remote.ceiling, 39, 'the control plane\'s own 4 GB ceiling, not the overflow box\'s');
136
+ // The env override reaches the summary, and is read per call.
137
+ process.env.PROVISION_BOX_CEILING = '9';
138
+ const pinned = await fleetCapacity(fakeDb(10), { meminfo: MEMINFO_2GB, remoteHost: null });
139
+ assert.deepEqual([pinned.ceiling, pinned.source, pinned.verdict.ok], [9, 'env', false]);
140
+ delete process.env.PROVISION_BOX_CEILING;
141
+ });
142
+
143
+ console.log('\nthe runner preflight (scripts/gds/provision-capacity.js):');
144
+
145
+ const standalone = { id: 7, slug: 'lantern', hosting_shape: 'standalone', status: 'requested', owner_builder_id: 3, port: null, db_name: 'lantern', domain: null };
146
+
147
+ // Recording deps: which executor was asked, what was logged, which events landed.
148
+ function preflightDeps({ apply = true, placed = 10, controlMeminfo = MEMINFO_2GB, boxMeminfo = MEMINFO_4GB } = {}) {
149
+ const asked = [], logs = [], events = [];
150
+ return {
151
+ asked, logs, events,
152
+ deps: {
153
+ apply, log: (l) => logs.push(l),
154
+ db: fakeDb(placed),
155
+ provisioning: { recordEvent: async (_db, e) => { events.push(e); } },
156
+ exec: (cmd) => { asked.push(['box', cmd]); return { ok: true, stdout: boxMeminfo }; },
157
+ controlExec: (cmd) => { asked.push(['control', cmd]); return { ok: true, stdout: controlMeminfo }; },
158
+ },
159
+ };
160
+ }
161
+
162
+ await ta('a standalone with room: ok, measured on the CONTROL executor, the count excludes its own row', async () => {
163
+ const h = preflightDeps({ placed: 10 });
164
+ const r = await capacityPreflight(standalone, h.deps);
165
+ assert.equal(r.ok, true);
166
+ assert.deepEqual(h.asked, [['control', MEMINFO_CMD]], 'a standalone runs control-plane-local, so its box is measured there');
167
+ assert.equal(r.ceiling, 15); assert.equal(r.source, 'measured'); assert.equal(r.placed, 10);
168
+ assert.deepEqual(h.deps.db.queries[0].params[2], 7, 'the row being provisioned never counts itself');
169
+ assert.equal(h.events.length, 0, 'nothing to record when it fits');
170
+ assert.ok(h.logs.some((l) => /control-plane box at 10 of 15 slots \(measured, 1108 MB free\)/.test(l)), `logs the box line: ${h.logs.join(' | ')}`);
171
+ });
172
+
173
+ await ta('a full box REFUSES with the owner-readable reason, records placement-refused, and returns (never throws)', async () => {
174
+ const h = preflightDeps({ placed: 15 });
175
+ const r = await capacityPreflight(standalone, h.deps);
176
+ assert.equal(r.ok, false);
177
+ assert.match(r.error, /15 of 15 project slots are taken/);
178
+ assert.equal(h.events.length, 1);
179
+ assert.equal(h.events[0].event, 'placement-refused');
180
+ assert.equal(h.events[0].instanceId, 7);
181
+ assert.equal(h.events[0].detail, r.error, 'the ledger carries the same sentence the card will');
182
+ assert.ok(h.logs.some((l) => /✖ \[preflight\].*refusing to place lantern \(ceiling wall\)/.test(l)));
183
+ });
184
+
185
+ await ta('the live memory floor refuses even with slots to spare (a stray unit the ledger cannot see)', async () => {
186
+ const h = preflightDeps({ placed: 10, controlMeminfo: MEMINFO_TIGHT });
187
+ const r = await capacityPreflight(standalone, h.deps);
188
+ assert.equal(r.ok, false);
189
+ assert.equal(r.verdict.wall, 'memory');
190
+ assert.match(r.error, /117 MB of memory free/);
191
+ assert.equal(h.events[0].event, 'placement-refused');
192
+ });
193
+
194
+ await ta('at 80% it still places, but the log names the resize trigger', async () => {
195
+ const h = preflightDeps({ placed: 12 });
196
+ const r = await capacityPreflight(standalone, h.deps);
197
+ assert.equal(r.ok, true); assert.equal(r.resize_due, true);
198
+ assert.ok(h.logs.some((l) => /at or past 80%: schedule the resize \(task 1003923\)/.test(l)), h.logs.join(' | '));
199
+ });
200
+
201
+ await ta('a co-tenant under remote-exec is measured on the BOX executor against the overflow base (41 on 4 GB)', async () => {
202
+ const { CONFIG } = require('../scripts/gds/provision-config.js');
203
+ const prior = CONFIG.remoteHost;
204
+ CONFIG.remoteHost = 'cohost';
205
+ try {
206
+ const h = preflightDeps({ placed: 40 });
207
+ const r = await capacityPreflight({ ...standalone, hosting_shape: 'co-tenant' }, h.deps);
208
+ assert.deepEqual(h.asked, [['box', MEMINFO_CMD]]);
209
+ assert.equal(r.box, 'overflow'); assert.equal(r.ceiling, 41); assert.equal(r.ok, true);
210
+ assert.deepEqual(h.deps.db.queries[0].params[0], ['co-tenant'], 'only the shapes that share that box count');
211
+ } finally { CONFIG.remoteHost = prior; }
212
+ });
213
+
214
+ await ta('a dedicated droplet is its own box: no measurement, no count, no refusal', async () => {
215
+ const h = preflightDeps({ placed: 99 });
216
+ const r = await capacityPreflight({ ...standalone, hosting_shape: 'dedicated' }, h.deps);
217
+ assert.deepEqual(r, { ok: true, skipped: 'dedicated' });
218
+ assert.equal(h.asked.length, 0); assert.equal(h.deps.db.queries.length, 0);
219
+ });
220
+
221
+ await ta('dry-run measures nothing and refuses nothing, even on a full box; an unmeasured --apply falls to the default and warns', async () => {
222
+ const dry = preflightDeps({ apply: false, placed: 15 });
223
+ const r = await capacityPreflight(standalone, dry.deps);
224
+ assert.equal(r.ok, true); assert.equal(r.dryRun, true);
225
+ assert.equal(dry.events.length, 0);
226
+ assert.ok(dry.logs.some((l) => /would refuse if full/.test(l)));
227
+ // --apply with an executor that answers no stdout (a fake, or a box without procfs)
228
+ const blind = preflightDeps({ placed: 3, controlMeminfo: '' });
229
+ const b = await capacityPreflight(standalone, blind.deps);
230
+ assert.equal(b.ok, true); assert.equal(b.source, 'default'); assert.equal(b.ceiling, DEFAULT_CEILING);
231
+ assert.ok(blind.logs.some((l) => /could not measure the box.*PROVISION_BOX_CEILING/.test(l)), blind.logs.join(' | '));
232
+ // …and an executor that throws is the same as one that answers nothing.
233
+ const throwing = preflightDeps({ placed: 3 });
234
+ throwing.deps.controlExec = () => { throw new Error('ssh down'); };
235
+ assert.equal((await capacityPreflight(standalone, throwing.deps)).source, 'default');
236
+ });
237
+
238
+ await ta('statusCapacityLine prints "box at N of M" for the CLI', async () => {
239
+ delete process.env.PROVISION_BOX_CEILING;
240
+ // fleetCapacity reads THIS host's /proc/meminfo (absent on a Windows dev box → default);
241
+ // the line must carry the count and the source whichever it is.
242
+ const line = await statusCapacityLine(fakeDb(12));
243
+ assert.match(line, /control-plane box: 12 of \d+ project slots taken \((measured|default)/);
244
+ });
245
+
246
+ console.log('\ncomposition into the runner (provisionInstance + the drain loop):');
247
+
248
+ // The recorder from tests/provision.mjs, with a db that answers the ceiling count.
249
+ function recorder({ apply, placed, meminfo = MEMINFO_2GB }) {
250
+ const cmds = [], writes = [], statuses = [], events = [];
251
+ const db = fakeDb(placed);
252
+ const deps = {
253
+ apply, log: () => {}, db,
254
+ provisioning: {
255
+ setInstanceStatus: async (_db, _id, s) => { statuses.push(s); },
256
+ recordEvent: async (_db, e) => { events.push(e); },
257
+ getInstanceBySlug: async () => null,
258
+ },
259
+ exec: (cmd) => { cmds.push(cmd); return { ok: true, stdout: /meminfo/.test(cmd) ? meminfo : '' }; },
260
+ writeFile: (p) => { writes.push(p); return { ok: true }; },
261
+ doApi: null,
262
+ checkDnsTokenScope: async () => ({ reachable: true, zone: 'example.com' }),
263
+ getGithubToken: async () => null,
264
+ };
265
+ return { deps, cmds, writes, statuses, events };
266
+ }
267
+
268
+ await ta('provisionInstance(--apply) on a full box returns ok:false BEFORE any status write or standup command', async () => {
269
+ const h = recorder({ apply: true, placed: 15 });
270
+ const r = await P.provisionInstance(standalone, h.deps);
271
+ assert.equal(r.ok, false);
272
+ assert.match(r.error, /15 of 15 project slots/);
273
+ assert.deepEqual(h.statuses, [], 'never flipped to provisioning — a project that was not placed is not "provisioning"');
274
+ assert.deepEqual(h.cmds, [MEMINFO_CMD], 'the ONLY command run was the measurement');
275
+ assert.deepEqual(h.writes, [], 'no unit, no env file, no Caddy block');
276
+ assert.deepEqual(h.events.map((e) => e.event), ['placement-refused']);
277
+ });
278
+
279
+ await ta('provisionInstance(--apply) with room proceeds into the runbook exactly as before', async () => {
280
+ const h = recorder({ apply: true, placed: 10 });
281
+ const r = await P.provisionInstance({ ...standalone, hosting_shape: 'co-tenant', port: 3003 }, h.deps);
282
+ assert.equal(r.ok, true);
283
+ assert.equal(h.statuses[0], 'provisioning');
284
+ assert.equal(h.cmds[0], MEMINFO_CMD, 'measured first');
285
+ assert.ok(h.cmds.some((c) => /createdb lantern/.test(c)), 'then the runbook');
286
+ });
287
+
288
+ await ta('provisionInstance(dry-run) on a full box still prints the whole plan — dry-run places nothing, so it refuses nothing', async () => {
289
+ const h = recorder({ apply: false, placed: 15 });
290
+ const r = await P.provisionInstance({ ...standalone, hosting_shape: 'co-tenant', port: 3003 }, h.deps);
291
+ assert.equal(r.ok, true);
292
+ assert.ok(h.cmds.some((c) => /createdb lantern/.test(c)));
293
+ });
294
+
295
+ // The drain loop's retirement path: MAX_INTENT_ATTEMPTS reached → the intent is
296
+ // resolved 'error' and the reason lands on the instance WITHOUT changing its status.
297
+ await ta('cmdRunIntents: a refused placement retires onto the card as error_note, status untouched, event recorded', async () => {
298
+ let claims = 0; const resolved = [], statusWrites = [], events = [];
299
+ const deps = {
300
+ apply: true, log: () => {},
301
+ db: fakeDb(15),
302
+ provisioning: {
303
+ claimNextIntent: async () => (claims++ === 0 ? { id: 9, instance_id: 7, action: 'provision', attempts: P.MAX_INTENT_ATTEMPTS } : null),
304
+ getInstanceById: async () => ({ ...standalone }),
305
+ resolveIntent: async (_db, id, state, err) => { resolved.push({ id, state, err }); },
306
+ setInstanceStatus: async (_db, id, status, patch) => { statusWrites.push({ id, status, patch }); },
307
+ recordEvent: async (_db, e) => { events.push(e); },
308
+ claimNextOAuthManifest: async () => null,
309
+ resolveOAuthManifest: async () => {},
310
+ },
311
+ exec: (cmd) => ({ ok: true, stdout: /meminfo/.test(cmd) ? MEMINFO_2GB : '' }),
312
+ writeFile: () => ({ ok: true }), doApi: null,
313
+ };
314
+ const r = await P.cmdRunIntents(deps);
315
+ assert.equal(r.errors, 1); assert.equal(r.ok, 0);
316
+ assert.equal(resolved[0].state, 'error');
317
+ assert.match(resolved[0].err, /15 of 15 project slots/, 'the intent carries the reason');
318
+ assert.equal(statusWrites.length, 1);
319
+ assert.equal(statusWrites[0].status, 'requested', 'the CURRENT status is passed back — never flipped to error');
320
+ assert.match(statusWrites[0].patch.error_note, /Nothing was placed/, 'the card reads why');
321
+ assert.deepEqual(events.map((e) => e.event), ['placement-refused']);
322
+ });
323
+
324
+ await ta('cmdRunIntents: below the attempt cap a refusal goes back to pending with last_error — a teardown may free the slot', async () => {
325
+ let claims = 0; const updates = [];
326
+ const db = fakeDb(15);
327
+ const rawQuery = db.query;
328
+ db.query = async (sql, params) => { if (/SET state='pending'/.test(sql)) { updates.push(params); return { rows: [] }; } return rawQuery(sql, params); };
329
+ const deps = {
330
+ apply: true, log: () => {}, db,
331
+ provisioning: {
332
+ claimNextIntent: async () => (claims++ === 0 ? { id: 9, instance_id: 7, action: 'provision', attempts: 1 } : null),
333
+ getInstanceById: async () => ({ ...standalone }),
334
+ resolveIntent: async () => { throw new Error('must not resolve on a retryable refusal'); },
335
+ setInstanceStatus: async () => { throw new Error('must not touch the instance on a retryable refusal'); },
336
+ recordEvent: async () => {},
337
+ claimNextOAuthManifest: async () => null,
338
+ resolveOAuthManifest: async () => {},
339
+ },
340
+ exec: (cmd) => ({ ok: true, stdout: /meminfo/.test(cmd) ? MEMINFO_2GB : '' }),
341
+ writeFile: () => ({ ok: true }), doApi: null,
342
+ };
343
+ const r = await P.cmdRunIntents(deps);
344
+ assert.equal(r.errors, 1);
345
+ assert.equal(updates.length, 1);
346
+ assert.equal(updates[0][0], 9);
347
+ assert.match(updates[0][1], /15 of 15 project slots/);
348
+ });
349
+
350
+ console.log('\nthe fleet API carries the same summary:');
351
+
352
+ // Stub the route's data access at the same seams tests/provisioning_paid_shape_gate.mjs
353
+ // uses; the capacity module object is shared with the route through the require cache.
354
+ const provisioning = require('../modules/provisioning/provisioning.js');
355
+ provisioning.listAllInstances = async () => [{ id: 1, owner_builder_id: 7, slug: 'lantern', status: 'active', hosting_shape: 'standalone', settings: {}, detail: {}, github_login: 'o', display_name: 'O', rank: 'archon' }];
356
+ const api = require('../src/module-api.js');
357
+ api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, github_login: 'owner' }; next(); };
358
+ api.requirePermission = () => (_req, _res, next) => next();
359
+ const realFleetCapacity = capacityDefault.fleetCapacity;
360
+
361
+ const express = require('express');
362
+ const provisioningRoutes = require('../modules/provisioning/routes/provisioning.js');
363
+ const app = express();
364
+ app.use(express.json());
365
+ app.use((_req, res, next) => {
366
+ res.fail = (code, opts, details) => {
367
+ const status = typeof opts === 'number' ? opts : (opts && opts.status) || 400;
368
+ res.status(status).json({ error: { code, ...(details || (typeof opts === 'object' ? opts : {})) } });
369
+ };
370
+ next();
371
+ });
372
+ app.use('/api/bongos', provisioningRoutes());
373
+ const server = app.listen(0);
374
+ await new Promise((r) => server.once('listening', r));
375
+ const base = `http://127.0.0.1:${server.address().port}/api/bongos`;
376
+ function get(path) {
377
+ return new Promise((resolve, reject) => {
378
+ http.get(`${base}${path}`, (res) => {
379
+ let d = '';
380
+ res.on('data', (c) => { d += c; });
381
+ res.on('end', () => { let j = null; try { j = JSON.parse(d); } catch { /* non-JSON */ } resolve({ status: res.statusCode, body: j, raw: d }); });
382
+ }).on('error', reject);
383
+ });
384
+ }
385
+
386
+ await ta('GET /provisioning/fleet returns `capacity` beside the roster — placed, ceiling, source, resize_due', async () => {
387
+ capacityDefault.fleetCapacity = async (db) => fleetCapacity(fakeDb(12), { meminfo: MEMINFO_2GB, remoteHost: null });
388
+ try {
389
+ const res = await get('/provisioning/fleet');
390
+ assert.equal(res.status, 200, res.raw);
391
+ assert.equal(res.body.instances.length, 1);
392
+ const c = res.body.capacity;
393
+ assert.equal(c.box, 'control-plane'); assert.equal(c.placed, 12); assert.equal(c.ceiling, 15);
394
+ assert.equal(c.source, 'measured'); assert.equal(c.resize_due, true); assert.equal(c.remaining, 3);
395
+ assert.equal(c.verdict.ok, true);
396
+ } finally { capacityDefault.fleetCapacity = realFleetCapacity; }
397
+ });
398
+
399
+ await ta('a capacity read that fails never fails the roster: `capacity` is null, the instances still come back', async () => {
400
+ capacityDefault.fleetCapacity = async () => { throw new Error('count unavailable'); };
401
+ try {
402
+ const res = await get('/provisioning/fleet');
403
+ assert.equal(res.status, 200, res.raw);
404
+ assert.equal(res.body.instances.length, 1);
405
+ assert.equal(res.body.capacity, null);
406
+ } finally { capacityDefault.fleetCapacity = realFleetCapacity; }
407
+ });
408
+
409
+ server.close();
410
+ console.log(`\n${passed} passed, ${failed} failed`);
411
+ process.exit(failed ? 1 : 0);