@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.
Files changed (89) hide show
  1. package/.bongos-core.json +116 -91
  2. package/.claude/skills/collab-review/SKILL.md +24 -13
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +4 -0
  5. package/clients/bongos-client/index.cjs +4 -0
  6. package/clients/bongos-client/index.d.ts +7 -0
  7. package/clients/bongos-client/index.mjs +4 -0
  8. package/docs/adr/0330-an-ask-is-answered-where-it-was-asked.md +155 -0
  9. package/docs/adr/README.md +1 -0
  10. package/docs/api/openapi.json +155 -5
  11. package/docs/api-reference.md +6 -4
  12. package/docs/copy-inventory.md +18 -13
  13. package/docs/copy-registry.json +70 -15
  14. package/docs/module-api-changelog.md +4 -0
  15. package/modules/hall-ui/public/collab-lib.js +79 -0
  16. package/modules/hall-ui/public/collab.css +60 -0
  17. package/modules/hall-ui/public/collab.js +134 -5
  18. package/modules/lifecycle/help-requests.js +118 -8
  19. package/modules/lifecycle/migrations/lifecycle_011_help_request_replies.sql +113 -0
  20. package/modules/lifecycle/routes/help-requests.js +93 -13
  21. package/modules/provisioning/capacity.js +3 -3
  22. package/modules/provisioning/control-plane-guard.js +1 -1
  23. package/modules/provisioning/core-upgrade.js +7 -7
  24. package/modules/provisioning/cost-ledger.js +74 -0
  25. package/modules/provisioning/migrations/provisioning_027_cloud_host_shape_rename.sql +58 -0
  26. package/modules/provisioning/onboard-plan.js +1 -1
  27. package/modules/provisioning/paid-shape-gate.js +28 -15
  28. package/modules/provisioning/provisioning.js +20 -39
  29. package/modules/provisioning/routes/provisioning.js +4 -3
  30. package/modules/provisioning/routes/repo-private.js +1 -1
  31. package/modules/provisioning/tests/liveness.mjs +16 -8
  32. package/modules/provisioning/tests/provisioning.mjs +1 -1
  33. package/modules/public-landing/public/projects.html +26 -27
  34. package/modules/ui-design/kit/fixtures/provisioning-instances.json +1 -1
  35. package/package-lock.json +2 -2
  36. package/package.json +1 -1
  37. package/scripts/gds/init.js +12 -4
  38. package/scripts/gds/onboard.js +1 -1
  39. package/scripts/gds/provision-capacity.js +6 -1
  40. package/scripts/gds/provision-config.js +6 -3
  41. package/scripts/gds/provision-core-upgrade.js +8 -8
  42. package/scripts/gds/provision-disconnect.js +2 -2
  43. package/scripts/gds/provision-process.js +2 -2
  44. package/scripts/gds/provision-repo-private.js +1 -1
  45. package/scripts/gds/provision-repo.js +13 -3
  46. package/scripts/gds/provision-teardown.js +2 -2
  47. package/scripts/gds/provision-units.js +2 -2
  48. package/scripts/gds/provision.js +6 -6
  49. package/src/module-api.js +1 -1
  50. package/tests/collab_page.mjs +140 -7
  51. package/tests/core_upgrade_door.mjs +1 -1
  52. package/tests/core_upgrade_runner.mjs +20 -20
  53. package/tests/deploy_page_projects.mjs +5 -5
  54. package/tests/hall_collab_world.mjs +6 -2
  55. package/tests/help_requests.mjs +201 -6
  56. package/tests/hub_disconnect_ui.mjs +4 -1
  57. package/tests/init.mjs +6 -6
  58. package/tests/onboard.mjs +5 -4
  59. package/tests/project_catalog_backfill.mjs +1 -1
  60. package/tests/project_catalog_bridge.mjs +2 -2
  61. package/tests/project_catalog_routes.mjs +4 -4
  62. package/tests/projects_hub.mjs +1 -1
  63. package/tests/projects_hub_pre_uat.mjs +22 -5
  64. package/tests/projects_hub_repo_private.mjs +1 -1
  65. package/tests/provision.mjs +32 -32
  66. package/tests/provision_byo_host.mjs +1 -1
  67. package/tests/provision_caddy_wiring.mjs +1 -1
  68. package/tests/provision_disconnect.mjs +3 -3
  69. package/tests/provision_repo_private.mjs +1 -1
  70. package/tests/provision_restart.mjs +5 -5
  71. package/tests/provision_settings_apply.mjs +4 -4
  72. package/tests/provision_teardown_address.mjs +2 -2
  73. package/tests/provisioning_capacity.mjs +40 -34
  74. package/tests/provisioning_capacity_gate.mjs +5 -12
  75. package/tests/provisioning_cost_ledger.mjs +98 -0
  76. package/tests/provisioning_default_domain.mjs +1 -1
  77. package/tests/provisioning_domain_attach.mjs +4 -4
  78. package/tests/provisioning_domain_unique.mjs +2 -2
  79. package/tests/provisioning_error_note_scope.mjs +1 -1
  80. package/tests/provisioning_module_selection.mjs +5 -5
  81. package/tests/provisioning_onboard_mode.mjs +2 -2
  82. package/tests/provisioning_paid_shape_gate.mjs +57 -44
  83. package/tests/provisioning_repo_private_route.mjs +1 -1
  84. package/tests/provisioning_restart.mjs +11 -4
  85. package/tests/provisioning_settings_apply.mjs +12 -5
  86. package/tests/provisioning_settings_env.mjs +1 -1
  87. package/tests/provisioning_teardown_intent.mjs +3 -3
  88. package/tests/public_landing_projects.mjs +13 -6
  89. 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` is the ONE shape
7
- // whose runner leg creates a real DigitalOcean droplet (scripts/gds/provision.js — billed
8
- // monthly, defaulting to s-2vcpu-4gb), so any authenticated builder — a Xenos included —
9
- // could queue recurring infrastructure spend the owner never approved. The route that
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
- // standalone — judged harmless because it costs $0. True of the invoice; false of the
25
- // trust. The unit's WorkingDirectory is the caller's own checkout and the
26
- // loader discovers modules from process.cwd(), so the platform require()s
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', 'cloud host', 'dedicated cloud 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', 'dedicated', 'standalone'],
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
- // ── standalone runs the caller's OWN code on the platform's box (task 1003370) ────
192
+ // ── cloud-host runs the caller's OWN code on the platform's box (task 1003370) ────
188
193
 
189
- await ta('standalone: REFUSED for a builder without provisioning.fleet.manage', async () => {
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 standalone request is "please require() this
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: 'standalone', no_address: true });
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('standalone: ADMITTED for a builder holding provisioning.fleet.manage', async () => {
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: 'standalone', no_address: true });
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, 'standalone');
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
- // ── dedicated is the metered one ──────────────────────────────────────────────────
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: ADMITTED for a builder holding provisioning.fleet.manage', async () => {
257
- held = ['provisioning.fleet.manage'];
258
- lastCreateArgs = null;
259
- const res = await post('/provisioning/instances', { slug: 'demo', hosting_shape: 'dedicated', no_address: true });
260
- assert.equal(res.status, 201, `expected 201, got ${res.status} ${res.raw}`);
261
- assert.equal(lastCreateArgs && lastCreateArgs.hostingShape, 'dedicated');
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 dedicated create leaves nothing behind', async () => {
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: 'dedicated', tier: 's-4vcpu-8gb' });
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 dedicated row', async () => {
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 dedicated.
278
- // The property that matters is not WHICH error comes back, it is that no dedicated row
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 ['DEDICATED', 'Dedicated', ' dedicated', 'dedicated ']) {
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', 'dedicated', 'standalone']);
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: 'standalone', target_ref: 'example-owner/spike' };
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 — dedicated is out, and the set is a subset of HOSTING_SHAPES', () => {
93
- assert.deepEqual([...provisioning.RESTART_SHAPES].sort(), ['co-tenant', 'standalone']);
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: 'standalone', domain: 'spike.cloudbongos.com', settings: {} };
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 — dedicated is out, and the set is a subset of HOSTING_SHAPES', () => {
76
- assert.deepEqual([...provisioning.SETTINGS_APPLY_SHAPES].sort(), ['co-tenant', 'standalone']);
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.ok(!provisioning.SETTINGS_APPLY_SHAPES.has('dedicated'));
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: 'standalone', domain: 'spike.cloudbongos.com', settings: {} };
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 standalone: the row is written FIRST, then a settings-apply intent queues; the receipt says a restart is coming', async () => {
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: 'standalone', settings: { platform_visibility: 'gated' } }, {});
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: 'standalone' });
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: 'standalone' });
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: 'standalone' });
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, per hosting shape (F17)', async () => {
196
+ test('teardown copy states what the runner actually does (F17)', async () => {
197
197
  const { body } = await request('/projects', 'game.test.local');
198
- // the DECIDED policy: shared shapes keep the database; a dedicated machine
199
- // takes its database with it — the copy branches on the shape field
200
- assert.match(body, /The database is kept/, 'the kept-shape promise is present');
201
- assert.match(body, /database goes with (it|the machine)/, 'the dedicated branch exists');
202
- assert.match(body, /hosting_shape === 'dedicated'/, 'shape-driven, not flat prose');
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: 'standalone' };
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: 'standalone' },
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
  );