@bongos/core 1.19.999 → 1.19.1001
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 +116 -91
- package/.claude/skills/collab-review/SKILL.md +24 -13
- 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 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0330-an-ask-is-answered-where-it-was-asked.md +155 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +155 -5
- package/docs/api-reference.md +6 -4
- package/docs/copy-inventory.md +18 -13
- package/docs/copy-registry.json +70 -15
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/collab-lib.js +79 -0
- package/modules/hall-ui/public/collab.css +60 -0
- package/modules/hall-ui/public/collab.js +134 -5
- package/modules/lifecycle/help-requests.js +118 -8
- package/modules/lifecycle/migrations/lifecycle_011_help_request_replies.sql +113 -0
- package/modules/lifecycle/routes/help-requests.js +93 -13
- package/modules/provisioning/capacity.js +3 -3
- package/modules/provisioning/control-plane-guard.js +1 -1
- package/modules/provisioning/core-upgrade.js +7 -7
- package/modules/provisioning/cost-ledger.js +74 -0
- package/modules/provisioning/migrations/provisioning_027_cloud_host_shape_rename.sql +58 -0
- package/modules/provisioning/onboard-plan.js +1 -1
- package/modules/provisioning/paid-shape-gate.js +28 -15
- package/modules/provisioning/provisioning.js +20 -39
- package/modules/provisioning/routes/provisioning.js +4 -3
- package/modules/provisioning/routes/repo-private.js +1 -1
- package/modules/provisioning/tests/liveness.mjs +16 -8
- package/modules/provisioning/tests/provisioning.mjs +1 -1
- package/modules/public-landing/public/projects.html +26 -27
- package/modules/ui-design/kit/fixtures/provisioning-instances.json +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/init.js +12 -4
- package/scripts/gds/onboard.js +1 -1
- package/scripts/gds/provision-capacity.js +6 -1
- package/scripts/gds/provision-config.js +6 -3
- package/scripts/gds/provision-core-upgrade.js +8 -8
- package/scripts/gds/provision-disconnect.js +2 -2
- package/scripts/gds/provision-process.js +2 -2
- package/scripts/gds/provision-repo-private.js +1 -1
- package/scripts/gds/provision-repo.js +13 -3
- package/scripts/gds/provision-teardown.js +2 -2
- package/scripts/gds/provision-units.js +2 -2
- package/scripts/gds/provision.js +6 -6
- package/src/module-api.js +1 -1
- package/tests/collab_page.mjs +140 -7
- package/tests/core_upgrade_door.mjs +1 -1
- package/tests/core_upgrade_runner.mjs +20 -20
- package/tests/deploy_page_projects.mjs +5 -5
- package/tests/hall_collab_world.mjs +6 -2
- package/tests/help_requests.mjs +201 -6
- package/tests/hub_disconnect_ui.mjs +4 -1
- package/tests/init.mjs +6 -6
- package/tests/onboard.mjs +5 -4
- package/tests/project_catalog_backfill.mjs +1 -1
- package/tests/project_catalog_bridge.mjs +2 -2
- package/tests/project_catalog_routes.mjs +4 -4
- package/tests/projects_hub.mjs +1 -1
- package/tests/projects_hub_pre_uat.mjs +22 -5
- package/tests/projects_hub_repo_private.mjs +1 -1
- package/tests/provision.mjs +32 -32
- package/tests/provision_byo_host.mjs +1 -1
- package/tests/provision_caddy_wiring.mjs +1 -1
- package/tests/provision_disconnect.mjs +3 -3
- package/tests/provision_repo_private.mjs +1 -1
- package/tests/provision_restart.mjs +5 -5
- package/tests/provision_settings_apply.mjs +4 -4
- package/tests/provision_teardown_address.mjs +2 -2
- package/tests/provisioning_capacity.mjs +40 -34
- package/tests/provisioning_capacity_gate.mjs +5 -12
- package/tests/provisioning_cost_ledger.mjs +98 -0
- package/tests/provisioning_default_domain.mjs +1 -1
- package/tests/provisioning_domain_attach.mjs +4 -4
- package/tests/provisioning_domain_unique.mjs +2 -2
- package/tests/provisioning_error_note_scope.mjs +1 -1
- package/tests/provisioning_module_selection.mjs +5 -5
- package/tests/provisioning_onboard_mode.mjs +2 -2
- package/tests/provisioning_paid_shape_gate.mjs +57 -44
- package/tests/provisioning_repo_private_route.mjs +1 -1
- package/tests/provisioning_restart.mjs +11 -4
- package/tests/provisioning_settings_apply.mjs +12 -5
- package/tests/provisioning_settings_env.mjs +1 -1
- package/tests/provisioning_teardown_intent.mjs +3 -3
- package/tests/public_landing_projects.mjs +13 -6
- package/tests/upgrade.mjs +2 -2
|
@@ -3,13 +3,18 @@
|
|
|
3
3
|
// The paid-shape gate on POST /provisioning/instances (task 1003682).
|
|
4
4
|
//
|
|
5
5
|
// Before this, the create route's only gate was requireBuilder, and `hosting_shape` was
|
|
6
|
-
// caller-supplied and validated only against the allowed set. `dedicated`
|
|
7
|
-
// whose runner leg
|
|
8
|
-
// monthly, defaulting to s-2vcpu-4gb), so any authenticated builder — a Xenos
|
|
9
|
-
// could queue recurring infrastructure spend the owner never approved. The route
|
|
10
|
-
// SPENDS was less gated than force-teardown, which stops spending and sits behind
|
|
6
|
+
// caller-supplied and validated only against the allowed set. `dedicated` was then the ONE
|
|
7
|
+
// shape whose runner leg created a real DigitalOcean droplet (scripts/gds/provision.js —
|
|
8
|
+
// billed monthly, defaulting to s-2vcpu-4gb), so any authenticated builder — a Xenos
|
|
9
|
+
// included — could queue recurring infrastructure spend the owner never approved. The route
|
|
10
|
+
// that SPENDS was less gated than force-teardown, which stops spending and sits behind
|
|
11
11
|
// provisioning.fleet.manage.
|
|
12
12
|
//
|
|
13
|
+
// THAT SHAPE IS GONE (ADR 0327 decision 4, task 1004182, migration provisioning_027): on the
|
|
14
|
+
// owner's own platform account a bigger plan is a setting they choose, not a shape we model.
|
|
15
|
+
// Its rows folded into 'cloud-host'. The gate's reasoning did not move with it — see the
|
|
16
|
+
// 'dedicated is no longer requestable' case below, which is what holds the removal.
|
|
17
|
+
//
|
|
13
18
|
// The fix gates the SHAPE, not the route: ordinary project creation must not require a rank,
|
|
14
19
|
// so the front door — `byo-host` since ADR 0323, `co-tenant` before it — stays open to any
|
|
15
20
|
// authenticated builder. Everything else
|
|
@@ -21,10 +26,10 @@
|
|
|
21
26
|
// subject. The original gate named the one shape it refused and admitted the rest, and two
|
|
22
27
|
// shapes stayed open underneath that reasoning:
|
|
23
28
|
//
|
|
24
|
-
//
|
|
25
|
-
// trust. The unit's WorkingDirectory is the
|
|
26
|
-
//
|
|
27
|
-
// a repo the caller named.
|
|
29
|
+
// cloud-host — then stored as 'standalone'. Judged harmless because it costs $0. True
|
|
30
|
+
// of the invoice; false of the trust. The unit's WorkingDirectory is the
|
|
31
|
+
// caller's own checkout and the loader discovers modules from
|
|
32
|
+
// process.cwd(), so the platform require()s a repo the caller named.
|
|
28
33
|
// control-plane — did not exist when the gate was written, and was permitted by omission
|
|
29
34
|
// the moment task 1003159 added it. A probe returned 201 and a second
|
|
30
35
|
// control-plane row, while this file's own comment asserted it could not.
|
|
@@ -173,41 +178,41 @@ await ta('the refusal speaks WIRE vocabulary — no owner-facing label is transl
|
|
|
173
178
|
const res = await post('/provisioning/instances', { slug: 'demo', hosting_shape: shape, no_address: true });
|
|
174
179
|
assert.equal(res.status, 403, `expected 403 for ${shape}, got ${res.status} ${res.raw}`);
|
|
175
180
|
const message = res.body.error.message || '';
|
|
176
|
-
for (const label of ['bring your own host', '
|
|
181
|
+
for (const label of ['bring your own host', 'your own host', 'shared server']) {
|
|
177
182
|
assert.ok(!message.toLowerCase().includes(label),
|
|
178
183
|
`the ${shape} refusal translates a stored value into the owner-facing label "${label}" — that map lives only in projects.html's SHAPE_LABEL (ADR 0323): ${message}`);
|
|
179
184
|
}
|
|
180
185
|
}
|
|
181
186
|
// And the caller is still told what they CAN ask for, in the vocabulary they sent.
|
|
182
187
|
const res = await post('/provisioning/instances', { slug: 'demo', hosting_shape: 'co-tenant', no_address: true });
|
|
183
|
-
assert.deepEqual([...res.body.error.details.requestable].sort(), ['byo-host', '
|
|
188
|
+
assert.deepEqual([...res.body.error.details.requestable].sort(), ['byo-host', 'cloud-host'],
|
|
184
189
|
'a refusal that names no alternative is a dead end — the stored values are the alternative');
|
|
185
190
|
});
|
|
186
191
|
|
|
187
|
-
// ──
|
|
192
|
+
// ── cloud-host runs the caller's OWN code on the platform's box (task 1003370) ────
|
|
188
193
|
|
|
189
|
-
await ta('
|
|
194
|
+
await ta('cloud-host: REFUSED for a builder without provisioning.fleet.manage', async () => {
|
|
190
195
|
// Task 1003682 left this shape open, reasoning that a $0 shape hands the caller
|
|
191
196
|
// nothing. That is true of the invoice and false of the trust: the systemd unit's
|
|
192
197
|
// WorkingDirectory is the caller's own checkout and the module loader discovers
|
|
193
|
-
// `modules/` from process.cwd(), so a
|
|
198
|
+
// `modules/` from process.cwd(), so a cloud-host request is "please require() this
|
|
194
199
|
// repo on your control plane". The uid split (task 1003369) bounds the blast radius;
|
|
195
200
|
// it does not make the default acceptable.
|
|
196
201
|
held = [];
|
|
197
202
|
lastCreateArgs = null;
|
|
198
|
-
const res = await post('/provisioning/instances', { slug: 'demo', hosting_shape: '
|
|
203
|
+
const res = await post('/provisioning/instances', { slug: 'demo', hosting_shape: 'cloud-host', no_address: true });
|
|
199
204
|
assert.equal(res.status, 403, `expected 403, got ${res.status} ${res.raw}`);
|
|
200
205
|
assert.equal(res.body.error.code, 'permission_forbidden');
|
|
201
206
|
assert.deepEqual(res.body.error.details.required, ['provisioning.fleet.manage']);
|
|
202
207
|
assert.equal(lastCreateArgs, null, 'no row is created when the shape is refused');
|
|
203
208
|
});
|
|
204
209
|
|
|
205
|
-
await ta('
|
|
210
|
+
await ta('cloud-host: ADMITTED for a builder holding provisioning.fleet.manage', async () => {
|
|
206
211
|
held = ['provisioning.fleet.manage'];
|
|
207
212
|
lastCreateArgs = null;
|
|
208
|
-
const res = await post('/provisioning/instances', { slug: 'demo', hosting_shape: '
|
|
213
|
+
const res = await post('/provisioning/instances', { slug: 'demo', hosting_shape: 'cloud-host', no_address: true });
|
|
209
214
|
assert.equal(res.status, 201, `expected 201, got ${res.status} ${res.raw}`);
|
|
210
|
-
assert.equal(lastCreateArgs && lastCreateArgs.hostingShape, '
|
|
215
|
+
assert.equal(lastCreateArgs && lastCreateArgs.hostingShape, 'cloud-host');
|
|
211
216
|
});
|
|
212
217
|
|
|
213
218
|
// ── control-plane is not requestable AT ALL (task 1003370) ────────────────────────
|
|
@@ -239,45 +244,46 @@ await ta('control-plane: still refused for a builder holding EVERY permission',
|
|
|
239
244
|
assert.equal(lastCreateArgs, null, 'no row is created');
|
|
240
245
|
});
|
|
241
246
|
|
|
242
|
-
// ──
|
|
243
|
-
|
|
244
|
-
await ta('dedicated: REFUSED for a builder without provisioning.fleet.manage', async () => {
|
|
245
|
-
held = [];
|
|
246
|
-
lastCreateArgs = null;
|
|
247
|
-
const res = await post('/provisioning/instances', { slug: 'demo', hosting_shape: 'dedicated', no_address: true });
|
|
248
|
-
assert.equal(res.status, 403, `expected 403, got ${res.status} ${res.raw}`);
|
|
249
|
-
assert.equal(res.body.error.code, 'permission_forbidden');
|
|
250
|
-
// requirePermission reports the gate's literal list under `details` (the same envelope
|
|
251
|
-
// requireRank uses), so the denial names the atom a caller has to be granted.
|
|
252
|
-
assert.deepEqual(res.body.error.details.required, ['provisioning.fleet.manage']);
|
|
253
|
-
assert.equal(lastCreateArgs, null, 'no row is created when the shape is refused');
|
|
254
|
-
});
|
|
247
|
+
// ── the removed shape stays removed ───────────────────────────────────────────────
|
|
255
248
|
|
|
256
|
-
await ta('dedicated
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
249
|
+
await ta('dedicated is no longer requestable AT ALL — not by permission, not by anyone', async () => {
|
|
250
|
+
// This is the assertion that holds ADR 0327 decision 4. 'dedicated' used to be the metered
|
|
251
|
+
// shape: refused without provisioning.fleet.manage, ADMITTED with it. Task 1004182 removed
|
|
252
|
+
// the value from HOSTING_SHAPES and migration provisioning_027 folded its rows into
|
|
253
|
+
// 'cloud-host', so it is now an unknown shape — the gate DEFERS it and the validator
|
|
254
|
+
// refuses it, whatever the caller holds.
|
|
255
|
+
//
|
|
256
|
+
// Asserted for BOTH permission states deliberately. Testing only the unprivileged case
|
|
257
|
+
// would still pass if the shape had merely gone back to being gated, which is exactly the
|
|
258
|
+
// state this removal is distinguishable from.
|
|
259
|
+
for (const permissions of [[], ['provisioning.fleet.manage']]) {
|
|
260
|
+
held = permissions;
|
|
261
|
+
lastCreateArgs = null;
|
|
262
|
+
const res = await post('/provisioning/instances', { slug: 'demo', hosting_shape: 'dedicated', no_address: true });
|
|
263
|
+
assert.ok(res.status === 400 || res.status === 403,
|
|
264
|
+
`holding ${JSON.stringify(permissions)}: expected a refusal, got ${res.status} ${res.raw}`);
|
|
265
|
+
assert.equal(lastCreateArgs, null,
|
|
266
|
+
`holding ${JSON.stringify(permissions)}: a removed shape must never reach createInstance`);
|
|
267
|
+
}
|
|
262
268
|
});
|
|
263
269
|
|
|
264
|
-
await ta('the gate refuses BEFORE the row exists — a denied
|
|
270
|
+
await ta('the gate refuses BEFORE the row exists — a denied create leaves nothing behind', async () => {
|
|
265
271
|
// The denial must land ahead of createInstance, not after it: a refused-but-inserted row
|
|
266
272
|
// would still be drained by the runner, which reads the column, not the HTTP status.
|
|
267
273
|
held = [];
|
|
268
274
|
lastCreateArgs = null;
|
|
269
|
-
await post('/provisioning/instances', { slug: 'spendy', hosting_shape: '
|
|
275
|
+
await post('/provisioning/instances', { slug: 'spendy', hosting_shape: 'cloud-host', no_address: true });
|
|
270
276
|
assert.equal(lastCreateArgs, null);
|
|
271
277
|
});
|
|
272
278
|
|
|
273
279
|
// ── the gate cannot be walked around ──────────────────────────────────────────────
|
|
274
280
|
|
|
275
|
-
await ta('a case-variant shape does not slip past the gate INTO a
|
|
281
|
+
await ta('a case-variant shape does not slip past the gate INTO a privileged row', async () => {
|
|
276
282
|
// The gate matches the exact wire value, and so does the shape validator — so a variant
|
|
277
|
-
// is refused as a bad shape rather than admitted as a cheap one and stored as
|
|
278
|
-
// The property that matters is not WHICH error comes back, it is that no
|
|
283
|
+
// is refused as a bad shape rather than admitted as a cheap one and stored as a gated
|
|
284
|
+
// shape. The property that matters is not WHICH error comes back, it is that no gated row
|
|
279
285
|
// is ever created without the permission.
|
|
280
|
-
for (const shape of ['
|
|
286
|
+
for (const shape of ['CLOUD-HOST', 'Cloud-Host', ' cloud-host', 'cloud-host ']) {
|
|
281
287
|
held = [];
|
|
282
288
|
lastCreateArgs = null;
|
|
283
289
|
const res = await post('/provisioning/instances', { slug: 'demo', hosting_shape: shape, no_address: true });
|
|
@@ -307,8 +313,15 @@ await ta('HOSTING_SHAPES is pinned, so a new shape forces a written decision abo
|
|
|
307
313
|
// no caller may request: the value stays storable so one torn-down row remains
|
|
308
314
|
// readable, and the door is shut in UNREQUESTABLE_SHAPES instead. Dropping it from
|
|
309
315
|
// HOSTING_SHAPES would make that historical row violate its own CHECK constraint.
|
|
316
|
+
//
|
|
317
|
+
// It fired a THIRD time at task 1004182, which is the first time it fired for a shape
|
|
318
|
+
// going AWAY rather than arriving. 'dedicated' left the set entirely (ADR 0327 decision 4)
|
|
319
|
+
// and 'standalone' was renamed to 'cloud-host' so the stored value matches the
|
|
320
|
+
// owner-facing word; migration provisioning_027 moved both. A shape LEAVING needs the same
|
|
321
|
+
// written decision as one arriving, and more care: the rows carrying the old value have to
|
|
322
|
+
// land somewhere legal before the CHECK constraint narrows under them.
|
|
310
323
|
assert.deepEqual([...provisioning.HOSTING_SHAPES].sort(),
|
|
311
|
-
['byo-host', 'co-tenant', 'control-plane'
|
|
324
|
+
['byo-host', 'cloud-host', 'co-tenant', 'control-plane']);
|
|
312
325
|
});
|
|
313
326
|
|
|
314
327
|
await ta('an unknown shape is answered by the validator, not by the gate', () => {
|
|
@@ -83,7 +83,7 @@ function post(path, body) {
|
|
|
83
83
|
});
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
const LIVE = { owner_builder_id: 7, slug: 'spike', status: 'active', hosting_shape: '
|
|
86
|
+
const LIVE = { owner_builder_id: 7, slug: 'spike', status: 'active', hosting_shape: 'cloud-host', target_ref: 'example-owner/spike' };
|
|
87
87
|
const OWNER = { id: 7, github_login: 'owner', rank: 'thetes' };
|
|
88
88
|
function reset(patch = {}) {
|
|
89
89
|
instance = { ...LIVE, ...patch };
|
|
@@ -89,10 +89,17 @@ t('RESTART_FROM is exactly {active} — every other state is already served by t
|
|
|
89
89
|
}
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
-
t('RESTART_SHAPES is the runner-reachable pair
|
|
93
|
-
assert.deepEqual([...provisioning.RESTART_SHAPES].sort(), ['
|
|
94
|
-
assert.ok(!provisioning.RESTART_SHAPES.has('dedicated'));
|
|
92
|
+
t('RESTART_SHAPES is the runner-reachable pair, and the set is a subset of HOSTING_SHAPES', () => {
|
|
93
|
+
assert.deepEqual([...provisioning.RESTART_SHAPES].sort(), ['cloud-host', 'co-tenant']);
|
|
95
94
|
for (const s of provisioning.RESTART_SHAPES) assert.ok(provisioning.HOSTING_SHAPES.includes(s), `${s} is a real shape`);
|
|
95
|
+
// Was `!has('dedicated')`. Task 1004182 removed that value from HOSTING_SHAPES, which made
|
|
96
|
+
// the assertion vacuously true — it would now pass against any set at all. Name the shapes
|
|
97
|
+
// that still EXIST and are still unreachable, and assert they are real first, so this
|
|
98
|
+
// cannot rot the same way twice.
|
|
99
|
+
for (const unreachable of ['byo-host', 'control-plane']) {
|
|
100
|
+
assert.ok(provisioning.HOSTING_SHAPES.includes(unreachable), `${unreachable} must be a real shape or this assertion is vacuous`);
|
|
101
|
+
assert.ok(!provisioning.RESTART_SHAPES.has(unreachable), unreachable);
|
|
102
|
+
}
|
|
96
103
|
});
|
|
97
104
|
|
|
98
105
|
await ta('enqueueRestart: a free slot creates the intent', async () => {
|
|
@@ -190,7 +197,7 @@ function post(path) {
|
|
|
190
197
|
});
|
|
191
198
|
}
|
|
192
199
|
|
|
193
|
-
const ACTIVE = { owner_builder_id: 7, slug: 'spike', status: 'active', hosting_shape: '
|
|
200
|
+
const ACTIVE = { owner_builder_id: 7, slug: 'spike', status: 'active', hosting_shape: 'cloud-host', domain: 'spike.cloudbongos.com', settings: {} };
|
|
194
201
|
function reset(patch = {}) {
|
|
195
202
|
instance = { ...ACTIVE, ...patch };
|
|
196
203
|
enqueueCalls = []; enqueueResult = null; events = [];
|
|
@@ -72,10 +72,17 @@ t('SETTINGS_APPLY_FROM is exactly {active} — the provision intent cannot stand
|
|
|
72
72
|
assert.ok(!provisioning.PROVISION_ENQUEUE_FROM.has('active'), 'the premise the gate rests on');
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
t('SETTINGS_APPLY_SHAPES is the reachable pair
|
|
76
|
-
assert.deepEqual([...provisioning.SETTINGS_APPLY_SHAPES].sort(), ['
|
|
75
|
+
t('SETTINGS_APPLY_SHAPES is the reachable pair, and the set is a subset of HOSTING_SHAPES', () => {
|
|
76
|
+
assert.deepEqual([...provisioning.SETTINGS_APPLY_SHAPES].sort(), ['cloud-host', 'co-tenant']);
|
|
77
77
|
for (const s of provisioning.SETTINGS_APPLY_SHAPES) assert.ok(provisioning.HOSTING_SHAPES.includes(s), s);
|
|
78
|
-
assert
|
|
78
|
+
// The exclusion this used to assert was 'dedicated', which task 1004182 removed from
|
|
79
|
+
// HOSTING_SHAPES entirely — so that assertion became vacuously true and stopped testing
|
|
80
|
+
// anything. Name the shapes that DO exist and are still deliberately unreachable: nothing
|
|
81
|
+
// of ours runs a byo-host, and the control plane is not pushed settings by this path.
|
|
82
|
+
for (const unreachable of ['byo-host', 'control-plane']) {
|
|
83
|
+
assert.ok(provisioning.HOSTING_SHAPES.includes(unreachable), `${unreachable} must be a real shape or this assertion is vacuous`);
|
|
84
|
+
assert.ok(!provisioning.SETTINGS_APPLY_SHAPES.has(unreachable), unreachable);
|
|
85
|
+
}
|
|
79
86
|
});
|
|
80
87
|
|
|
81
88
|
await ta('enqueueSettingsApply: a fresh intent → created:true; the INSERT carries the settings-apply action', async () => {
|
|
@@ -208,7 +215,7 @@ function patch(path, body) {
|
|
|
208
215
|
});
|
|
209
216
|
}
|
|
210
217
|
|
|
211
|
-
const ACTIVE = { owner_builder_id: 7, slug: 'spike', status: 'active', hosting_shape: '
|
|
218
|
+
const ACTIVE = { owner_builder_id: 7, slug: 'spike', status: 'active', hosting_shape: 'cloud-host', domain: 'spike.cloudbongos.com', settings: {} };
|
|
212
219
|
function reset({ inst, as } = {}) {
|
|
213
220
|
instance = inst !== undefined ? inst : { ...ACTIVE };
|
|
214
221
|
builder = as || { id: 7, github_login: 'owner', rank: 'thetes' };
|
|
@@ -230,7 +237,7 @@ function receiptShape(push, { queued, restart, reachable }) {
|
|
|
230
237
|
|
|
231
238
|
console.log('\nPATCH /provisioning/instances/:id/settings — the enqueue:');
|
|
232
239
|
|
|
233
|
-
await ta('ACTIVE
|
|
240
|
+
await ta('ACTIVE cloud-host: the row is written FIRST, then a settings-apply intent queues; the receipt says a restart is coming', async () => {
|
|
234
241
|
reset();
|
|
235
242
|
const r = await patch('/provisioning/instances/18/settings', { platform_visibility: 'gated' });
|
|
236
243
|
assert.equal(r.status, 200, r.raw);
|
|
@@ -168,7 +168,7 @@ t('the runner writes them into web.env alongside PORT and PGDATABASE', () => {
|
|
|
168
168
|
const provision = require('../scripts/gds/provision.js');
|
|
169
169
|
const body = provision.webEnvBody(
|
|
170
170
|
{ slug: 'spike-harbor', port: 3010, db_name: 'spike-harbor', target_ref: 'jaxri/spike-harbor',
|
|
171
|
-
hosting_shape: '
|
|
171
|
+
hosting_shape: 'cloud-host', settings: { platform_visibility: 'gated' } }, {});
|
|
172
172
|
assert.match(body, /_PLATFORM_VISIBILITY=gated/);
|
|
173
173
|
assert.match(body, /_JOINABILITY=apply/);
|
|
174
174
|
assert.match(body, /PORT=3010/, 'the rest of the file is untouched');
|
|
@@ -250,7 +250,7 @@ console.log('\nPOST /provisioning/instances — no provision intent for an insta
|
|
|
250
250
|
await ta('re-requesting an ACTIVE instance does NOT enqueue a provision intent (the enabling misstep, fixed)', async () => {
|
|
251
251
|
createResult = { instance: { id: 42, owner_builder_id: 7, slug: 'spike', status: 'active' }, created: false };
|
|
252
252
|
enqueueIntentCalls = [];
|
|
253
|
-
const res = await post('/provisioning/instances', { slug: 'spike', hosting_shape: '
|
|
253
|
+
const res = await post('/provisioning/instances', { slug: 'spike', hosting_shape: 'cloud-host' });
|
|
254
254
|
assert.equal(res.status, 200, res.raw);
|
|
255
255
|
assert.equal(enqueueIntentCalls.length, 0, 'no provision intent was enqueued');
|
|
256
256
|
assert.equal(res.body.queued, false);
|
|
@@ -260,7 +260,7 @@ await ta('re-requesting an ACTIVE instance does NOT enqueue a provision intent (
|
|
|
260
260
|
await ta('re-requesting an ERRORED instance still enqueues (the retry path is preserved)', async () => {
|
|
261
261
|
createResult = { instance: { id: 42, owner_builder_id: 7, slug: 'spike', status: 'error' }, created: false };
|
|
262
262
|
enqueueIntentCalls = [];
|
|
263
|
-
const res = await post('/provisioning/instances', { slug: 'spike', hosting_shape: '
|
|
263
|
+
const res = await post('/provisioning/instances', { slug: 'spike', hosting_shape: 'cloud-host' });
|
|
264
264
|
assert.equal(res.status, 200, res.raw);
|
|
265
265
|
assert.equal(enqueueIntentCalls.length, 1, 'the provision retry was enqueued');
|
|
266
266
|
assert.equal(enqueueIntentCalls[0].action, 'provision');
|
|
@@ -269,7 +269,7 @@ await ta('re-requesting an ERRORED instance still enqueues (the retry path is pr
|
|
|
269
269
|
await ta('a FRESH instance always enqueues its provision intent (unchanged happy path)', async () => {
|
|
270
270
|
createResult = { instance: { id: 43, owner_builder_id: 7, slug: 'fresh', status: 'requested' }, created: true };
|
|
271
271
|
enqueueIntentCalls = [];
|
|
272
|
-
const res = await post('/provisioning/instances', { slug: 'fresh', hosting_shape: '
|
|
272
|
+
const res = await post('/provisioning/instances', { slug: 'fresh', hosting_shape: 'cloud-host' });
|
|
273
273
|
assert.equal(res.status, 201, res.raw);
|
|
274
274
|
assert.equal(enqueueIntentCalls.length, 1);
|
|
275
275
|
assert.equal(enqueueIntentCalls[0].action, 'provision');
|
|
@@ -193,14 +193,21 @@ test('the manual-repo step states placement and visibility — the owner never h
|
|
|
193
193
|
assert.match(body, /public or private/i, 'visibility must be stated in our copy, not first seen on GitHub');
|
|
194
194
|
});
|
|
195
195
|
|
|
196
|
-
test('teardown copy states what the runner actually does
|
|
196
|
+
test('teardown copy states what the runner actually does (F17)', async () => {
|
|
197
197
|
const { body } = await request('/projects', 'game.test.local');
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
198
|
+
// The DECIDED policy is unchanged — teardown keeps the database — but it is no longer a
|
|
199
|
+
// PER-SHAPE claim. This used to branch: every shared shape kept the database while a
|
|
200
|
+
// 'dedicated' machine took its database with it. ADR 0327 decision 4 removed that shape
|
|
201
|
+
// and task 1004182 folded its rows into 'cloud-host', so the branch could never be taken
|
|
202
|
+
// again and the page now states the surviving outcome flatly.
|
|
203
|
+
//
|
|
204
|
+
// The important part is the NEGATIVE: the copy must no longer offer the other outcome at
|
|
205
|
+
// all. A conditional that can't fire is not merely dead here — it is a promise to owners
|
|
206
|
+
// about a machine none of them have.
|
|
207
|
+
assert.match(body, /The database is kept/, 'the kept promise is present');
|
|
203
208
|
assert.match(body, /repository is untouched|repository stays yours|repository is still yours/i);
|
|
209
|
+
assert.doesNotMatch(body, /database goes with (it|the machine)/, 'the removed shape\'s copy is gone with it');
|
|
210
|
+
assert.doesNotMatch(body, /hosting_shape === 'dedicated'/, 'and so is the branch that selected it');
|
|
204
211
|
// the pre-walk over-claim can never come back
|
|
205
212
|
assert.doesNotMatch(body, /server and database go away/i);
|
|
206
213
|
assert.doesNotMatch(body, /database (is|will be) (deleted|dropped|destroyed|removed)/i);
|
package/tests/upgrade.mjs
CHANGED
|
@@ -1621,7 +1621,7 @@ t('pinnedDeployKey / remoteGithubRepo: read the ADR 0176 wiring off the checkout
|
|
|
1621
1621
|
});
|
|
1622
1622
|
|
|
1623
1623
|
t('resolvePinPushAuth: the owner token is spent only when one is stored AND the provisioning row names the remote repo', async () => {
|
|
1624
|
-
const inst = { slug: 'acme', owner_builder_id: 7, target_ref: 'acme/widgets', hosting_shape: '
|
|
1624
|
+
const inst = { slug: 'acme', owner_builder_id: 7, target_ref: 'acme/widgets', hosting_shape: 'cloud-host' };
|
|
1625
1625
|
const at = (url) => pinRun({ remote: { status: 0, stdout: `${url}\n` } });
|
|
1626
1626
|
const dir = { instanceDir: '/srv/bongos/acme' };
|
|
1627
1627
|
|
|
@@ -1683,7 +1683,7 @@ t('runUpgrade --commit-pin: on a wired private instance the pin lands via the ow
|
|
|
1683
1683
|
materialize: () => ({ skills: 35, settings: true, hooks: 3 }),
|
|
1684
1684
|
regenerateApiArtifacts: () => ({ ok: true, ran: ['gen-api-docs.js', 'gen-api-client.js'] }),
|
|
1685
1685
|
databaseUrl: '', pgDatabase: '',
|
|
1686
|
-
instance: { slug: 'acme', owner_builder_id: 7, target_ref: 'acme/widgets', hosting_shape: '
|
|
1686
|
+
instance: { slug: 'acme', owner_builder_id: 7, target_ref: 'acme/widgets', hosting_shape: 'cloud-host' },
|
|
1687
1687
|
getGithubToken: async () => ({ token, scope: 'read:user,repo' }),
|
|
1688
1688
|
},
|
|
1689
1689
|
);
|