@bongos/core 1.19.765 → 1.19.767

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 CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.765",
6
- "core_contract": "1.19.765",
7
- "source_commit": "3b3d14502e395142e33386df046a8b1d89b05608",
5
+ "core_version": "1.19.767",
6
+ "core_contract": "1.19.767",
7
+ "source_commit": "eb349d772c37baba9353ce3d7e4fb60ee0c50120",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-16T13:30:48.350Z",
9
+ "built_at": "2026-09-16T14:32:00.184Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 483,
13
13
  "agent_docs_stubbed": 25,
14
- "functional_verbatim": 2121,
14
+ "functional_verbatim": 2123,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2629,
20
- "tree_sha256": "39fa9398befe5e5189039c4da72c998c4449f48299694a1c6a9d30d10723d49d",
19
+ "file_count": 2631,
20
+ "tree_sha256": "a176823f439c4744fa68e223d9a6c892eae966f0584eca0fc53b785e65a80bf1",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2502,7 +2502,7 @@
2502
2502
  {
2503
2503
  "path": "docs/file-map.md",
2504
2504
  "mode": "0000644",
2505
- "sha256": "f59041018367ebd25642c8f94c6ab2ad429fc67b2e917e5706185186717b0403"
2505
+ "sha256": "9369c319c356283bdce655d126e78f339c9a329ecefc02d17c2997c45e583475"
2506
2506
  },
2507
2507
  {
2508
2508
  "path": "docs/handoff-template.md",
@@ -2512,7 +2512,7 @@
2512
2512
  {
2513
2513
  "path": "docs/module-api-changelog.md",
2514
2514
  "mode": "0000644",
2515
- "sha256": "650ec2daa3a256932ccb33c3d25a4364565d0a2cf9ff8717493144f48f4738b4"
2515
+ "sha256": "758ba4125664158bbbbedce4610178b629d74da7b8defb902caa63750be147f2"
2516
2516
  },
2517
2517
  {
2518
2518
  "path": "docs/modules-contract.md",
@@ -7577,12 +7577,12 @@
7577
7577
  {
7578
7578
  "path": "package-lock.json",
7579
7579
  "mode": "0000644",
7580
- "sha256": "025da0b521b730f0ef2a24421607242c01cbc6d23a501c9a3f25d88a0a386b46"
7580
+ "sha256": "c20cc0b8296799714beb07358a69f4c0c520adcbe121dfe53899cf5ee5dca0b6"
7581
7581
  },
7582
7582
  {
7583
7583
  "path": "package.json",
7584
7584
  "mode": "0000644",
7585
- "sha256": "69ee9c8d4847dabb4965d646a635dbe39a539070a2966df5d6c277101cb62019"
7585
+ "sha256": "734c747e81d3c512988159903a57a4ce901b67c353ea77fe7e253f4814cab10a"
7586
7586
  },
7587
7587
  {
7588
7588
  "path": "public-docs/index.html",
@@ -7754,6 +7754,11 @@
7754
7754
  "mode": "0000644",
7755
7755
  "sha256": "4646bd6da8be2d2c40c1063bba47f5fdce0f2f63143e26fc16e36ea941adbc88"
7756
7756
  },
7757
+ {
7758
+ "path": "scripts/gds/backup-dropin-sweep.sh",
7759
+ "mode": "0000644",
7760
+ "sha256": "bfbac57a3e282e1b482a967733eb8f39a15d97ffc8c24b4348370e4a11acc160"
7761
+ },
7757
7762
  {
7758
7763
  "path": "scripts/gds/backup.js",
7759
7764
  "mode": "0000644",
@@ -8412,7 +8417,7 @@
8412
8417
  {
8413
8418
  "path": "scripts/gds/provision-config.js",
8414
8419
  "mode": "0000644",
8415
- "sha256": "cda8e1022a26dce2252954351c70aa3827067ca300ebfe86b9fc7fa0ae05abeb"
8420
+ "sha256": "c870abebd21411e1fec744281130d116f3b9e8f255ddb6823aae4072a420cc15"
8416
8421
  },
8417
8422
  {
8418
8423
  "path": "scripts/gds/provision-net.js",
@@ -8432,7 +8437,7 @@
8432
8437
  {
8433
8438
  "path": "scripts/gds/provision-units.js",
8434
8439
  "mode": "0000644",
8435
- "sha256": "f6a26c5a6e6cb57fbcef1cff93738d4dc0ce955486535fca7796d72c5a280cad"
8440
+ "sha256": "e299687a54305e45ea57ff0a439f5ff099cb5d51be64bb0a48056c93309a67e3"
8436
8441
  },
8437
8442
  {
8438
8443
  "path": "scripts/gds/provision.js",
@@ -9237,7 +9242,7 @@
9237
9242
  {
9238
9243
  "path": "src/bongos/pool.js",
9239
9244
  "mode": "0000644",
9240
- "sha256": "96d41f41c4e93358c66e5101b2fc43d4ff8a1c6301a1fd3ec7020ebff6029c8c"
9245
+ "sha256": "f3fa3e0360c1bd7fb9bdf5756edccb8c5f5cad4d9ef2b89b7f0c6382c5240294"
9241
9246
  },
9242
9247
  {
9243
9248
  "path": "src/bongos/prelaunch-gate.js",
@@ -9397,7 +9402,7 @@
9397
9402
  {
9398
9403
  "path": "src/module-api.js",
9399
9404
  "mode": "0000644",
9400
- "sha256": "ad1c50e46ad096b7ba3032b8e225fec5fa74b8c50640cc577e9323a439f5b8f0"
9405
+ "sha256": "9be4adbc202e2c681d8a2ebba85d6ce541cd8a75d10a8802e1042b43e75f50c0"
9401
9406
  },
9402
9407
  {
9403
9408
  "path": "src/module-loader/catalog.js",
@@ -11874,6 +11879,11 @@
11874
11879
  "mode": "0000644",
11875
11880
  "sha256": "ff9d438a33a32fecc04caa6e652eeaaded6394438bcec0d508ae7c516fa62333"
11876
11881
  },
11882
+ {
11883
+ "path": "tests/pool_max.mjs",
11884
+ "mode": "0000644",
11885
+ "sha256": "68e5b941a1889ddc44fd548e10bb1fdb33822628b0da392592f747a32f7ccd1a"
11886
+ },
11877
11887
  {
11878
11888
  "path": "tests/post_login_landing.mjs",
11879
11889
  "mode": "0000644",
@@ -12052,7 +12062,7 @@
12052
12062
  {
12053
12063
  "path": "tests/provision.mjs",
12054
12064
  "mode": "0000644",
12055
- "sha256": "25fbd2adccf79e921c642f8f57b082f5f19fa01cd981cd5b813dc1070511aadb"
12065
+ "sha256": "245deb7b090f528180dde1457b821a8c8c2bd74a643524f441519859659643e8"
12056
12066
  },
12057
12067
  {
12058
12068
  "path": "tests/provision_restart.mjs",
package/docs/file-map.md CHANGED
@@ -187,6 +187,7 @@
187
187
  │ ├── seed-v3-tasks.js ← V3 Planning Session #1: 80 net-new GDS-V3 tasks (V3.R01–V3.R89) + 10 idea_inbox promotions; idempotent on source_ref
188
188
  │ ├── backfill-cost.js ← read art ledger → POST /api/gds/cost (idempotent)
189
189
  │ ├── smoke-write.sh ← write-path smoke (create→claim→ship round-trip)
190
+ │ ├── backup-dropin-sweep.sh ← (task 1003930) control-plane sweep: retires the temporary task-1003925 backup drop-in from each project whose pinned core carries the fix (1.19.750+), proving one backup succeeds without it first; survey by default, `--apply` to act; re-run after each pin moves
190
191
  │ ├── smoke-gds.js ← public-surface + 401-check smoke test (asserts /api/pms/* now 404s — removed in V3.R01-followup #308)
191
192
  │ ├── box.js ← (#598/ADR 0031) per-builder dev box lifecycle CLI: provision·park·wake·deprovision·sweep-idle·sweep-dormant·status (dry-run default, --apply to act; rank-gated)
192
193
  │ ├── smoke-box.sh ← (#598) hermetic box-lifecycle smoke (no DB/DO/network): runs tests/boxes.mjs + tests/cf_tunnel.mjs (#771 named tunnel) + route-rank + module-load
@@ -2004,5 +2004,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
2004
2004
  landed since 1.19.763 with no explicit bump. run 35059447999. (task 1002620)
2005
2005
  1.19.765 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2006
2006
  landed since 1.19.764 with no explicit bump. run 35102327521. (task 1002620)
2007
+ 1.19.766 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2008
+ landed since 1.19.765 with no explicit bump. run 35104246900. (task 1002620)
2009
+ 1.19.767 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2010
+ landed since 1.19.766 with no explicit bump. run 35109111074. (task 1002620)
2007
2011
  ---------------------------------------------------------------------------
2008
2012
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.765",
3
+ "version": "1.19.767",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.765",
9
+ "version": "1.19.767",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.765",
3
+ "version": "1.19.767",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/gds/backup-dropin-sweep.sh — retire the TEMPORARY task-1003925 backup drop-ins
3
+ # once a project's pinned core carries the fix (task 1003930, ADR 0285 follow-up).
4
+ #
5
+ # WHY. Before the nightly-backup fix (task 1003925, core 1.19.750) reached any
6
+ # project, the 10 live project backup units on the control plane got a systemd
7
+ # drop-in — /etc/systemd/system/<slug>-db-backup.service.d/task-1003925-min-bytes.conf
8
+ # (the backup script's min-bytes override, `*_BACKUP_MIN_BYTES=4096`) — so their
9
+ # small-but-valid dumps stopped being rejected.
10
+ # That workaround lives outside every repo, nothing removes it, and while it stays it
11
+ # hides whether the shipped fix actually works per project. Projects move to the fixed
12
+ # core one pin at a time, so this is a SWEEP to re-run after each pin moves, not a
13
+ # one-shot: each run retires only the drop-ins whose project can prove it no longer
14
+ # needs one (a backup run succeeds without it), and names the rest.
15
+ #
16
+ # Run on the control plane as a sudoer:
17
+ # bash scripts/gds/backup-dropin-sweep.sh # survey only — read-only, no sudo writes
18
+ # bash scripts/gds/backup-dropin-sweep.sh --apply # retire the eligible drop-ins + clear ghosts
19
+ #
20
+ # --apply, per eligible project (pinned core >= FIX_VERSION and drop-in present):
21
+ # rm the drop-in, daemon-reload, `systemctl start <slug>-db-backup.service`, wait for it,
22
+ # and require exit 0 AND a new dump under /var/backups/gds — else the drop-in is put back
23
+ # and the project is reported, so a project is never left with no backup at all.
24
+ # Then: `systemctl reset-failed` on any *-db-backup unit whose files are gone (a torn-down
25
+ # project's ghost; teardown removes the files but not the failed state — task 1003940).
26
+ set -uo pipefail
27
+
28
+ FIX_VERSION="${FIX_VERSION:-1.19.750}"
29
+ DROPIN=task-1003925-min-bytes.conf
30
+ BACKUP_DIR=/var/backups/gds
31
+ APPLY=0; [ "${1:-}" = "--apply" ] && APPLY=1
32
+
33
+ # The safety copies of retired drop-ins live in a root-owned, mode-0700, mktemp-named
34
+ # dir for the length of one run — never a predictable /tmp/<slug> path a lower-privileged
35
+ # local account could pre-stage a symlink at (CWE-377 / CWE-367 on a shared box). Created
36
+ # only under --apply; removed on exit whatever happened.
37
+ WORK=""
38
+ if [ "$APPLY" = 1 ]; then
39
+ WORK=$(sudo -n mktemp -d /var/tmp/backup-dropin-sweep.XXXXXX) || { echo "cannot create a root-owned work dir"; exit 1; }
40
+ trap 'sudo -n rm -rf "$WORK"' EXIT
41
+ fi
42
+
43
+ # The project's pinned core version: the installed inner manifest under its own checkout.
44
+ core_of() {
45
+ local slug=$1 pj
46
+ for pj in "/srv/cloudbongos/$slug/node_modules/@bongos/core/package.json" "/home/lars/$slug/node_modules/@bongos/core/package.json"; do
47
+ if sudo -n test -r "$pj"; then sudo -n node -e "console.log(require(process.argv[1]).version)" "$pj" 2>/dev/null && return; fi
48
+ done
49
+ echo "unknown"
50
+ }
51
+ # semver-ish: is $1 >= $2 ? (sort -V puts the smaller first)
52
+ at_least() { [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -1)" = "$2" ]; }
53
+ # The slug rides as a positional argument, never interpolated into the sh -c string.
54
+ dump_count() { sudo -n sh -c 'ls "$1"-*.sql.gz 2>/dev/null | wc -l' _ "$BACKUP_DIR/$1"; }
55
+
56
+ retired=(); kept=(); failed=()
57
+ echo "== drop-ins vs pinned core (fix = $FIX_VERSION) =="
58
+ for d in /etc/systemd/system/*-db-backup.service.d; do
59
+ [ -f "$d/$DROPIN" ] || continue
60
+ unit=$(basename "$d" .service.d); slug=${unit%-db-backup}
61
+ core=$(core_of "$slug")
62
+ if [ "$core" != "unknown" ] && at_least "$core" "$FIX_VERSION"; then verdict="ELIGIBLE"; else verdict="keeps drop-in"; fi
63
+ printf ' %-24s core=%-10s %s\n' "$slug" "$core" "$verdict"
64
+ if [ "$verdict" != "ELIGIBLE" ]; then kept+=("$slug ($core)"); continue; fi
65
+ [ "$APPLY" = 1 ] || { retired+=("$slug (would retire)"); continue; }
66
+ before=$(dump_count "$slug")
67
+ sudo -n cp "$d/$DROPIN" "$WORK/$slug.conf"
68
+ sudo -n rm -f "$d/$DROPIN"; sudo -n rmdir "$d" 2>/dev/null || true
69
+ sudo -n systemctl daemon-reload
70
+ # Type=oneshot (provision-units.js backupService): `start` blocks until the dump script exits.
71
+ sudo -n systemctl start "$unit.service"
72
+ after=$(dump_count "$slug")
73
+ if systemctl show -p Result --value "$unit.service" | grep -qx success && [ "$after" -gt "$before" ]; then
74
+ retired+=("$slug ($core; dumps $before→$after)")
75
+ else
76
+ sudo -n install -D -m 0644 "$WORK/$slug.conf" "$d/$DROPIN"; sudo -n systemctl daemon-reload
77
+ failed+=("$slug ($core; backup did not succeed without it — drop-in restored)")
78
+ fi
79
+ done
80
+
81
+ echo; echo "== failed *-db-backup units (ghosts of torn-down projects) =="
82
+ ghosts=$(systemctl --failed --no-legend 2>/dev/null | awk '/db-backup/ {print $1 == "●" ? $2 : $1}')
83
+ if [ -z "$ghosts" ]; then echo " (none)"; else
84
+ for u in $ghosts; do
85
+ if [ -e "/etc/systemd/system/$u" ]; then echo " $u: unit file still present — a real failure, not a ghost; left alone"; continue; fi
86
+ if [ "$APPLY" = 1 ]; then sudo -n systemctl reset-failed "$u" && echo " $u: ghost cleared"; else echo " $u: ghost (would reset-failed)"; fi
87
+ done
88
+ fi
89
+
90
+ echo; echo "== summary =="
91
+ echo " retired (${#retired[@]}): ${retired[*]:-none}"
92
+ echo " kept (${#kept[@]}): ${kept[*]:-none}"
93
+ echo " failed (${#failed[@]}): ${failed[*]:-none}"
94
+ [ "$APPLY" = 1 ] || echo " (survey only — re-run with --apply to act)"
95
+ [ "${#failed[@]}" -eq 0 ]
@@ -134,6 +134,14 @@ const CONFIG = {
134
134
  // mirror an override into web.env so the fleet API reports the same number.
135
135
  boxCeiling: numEnv('PROVISION_BOX_CEILING', null),
136
136
  memFloorMb: numEnv('PROVISION_MEM_FLOOR_MB', null),
137
+ // The Postgres pool cap written into a CO-LOCATED instance's web.env as PGPOOL_MAX
138
+ // (task 1003928, ADR 0285 §3). pg's default is 10 per process and the shared server
139
+ // allows 97 non-superuser backends, so nine busy projects exhaust it whatever the
140
+ // RAM headroom. Idle projects hold zero backends, so this bounds bursts, not the
141
+ // steady state: 3 × 15 slots + the control plane's own 10 stays under the 97. The
142
+ // control plane keeps pg's default (it is not provisioned by this runner), and a
143
+ // dedicated droplet has its own server, so neither is written.
144
+ instancePoolMax: numEnv('PROVISION_INSTANCE_PGPOOL_MAX', 3),
137
145
  };
138
146
 
139
147
  // task 1002757 — the provisioner's git identity resolves from the instance's own
@@ -267,13 +267,18 @@ GITHUB_APP_PRIVATE_KEY=${String(appPem).replace(/\r?\n/g, '\\n')}
267
267
  const dbLines = dbPassword
268
268
  ? `PGUSER=${instanceDbRole(inst)}\nPGPASSWORD=${dbPassword}\n`
269
269
  : '';
270
+ // The pool cap (task 1003928, ADR 0285 §3): a co-located instance shares the box's
271
+ // Postgres with every sibling and the control plane, so its pool may not take pg's
272
+ // default 10 of the server's 97. src/bongos/pool.js reads it bare. A dedicated
273
+ // droplet has its own server and keeps the default — the line is not written.
274
+ const poolLine = inst.hosting_shape === 'dedicated' ? '' : `PGPOOL_MAX=${CONFIG.instancePoolMax}\n`;
270
275
  return `# /etc/${inst.slug}/web.env — per-instance secrets (chmod 600, OUTSIDE the repo).
271
276
  ${credComment}
272
277
  ${prefix}_GITHUB_CLIENT_ID=${clientId || ''}
273
278
  ${prefix}_GITHUB_CLIENT_SECRET=${clientSecret || ''}
274
279
  ${firstAdminLine}${originLines}${appLines}PORT=${inst.port}
275
280
  PGDATABASE=${dbName(inst)}
276
- ${dbLines}${fedLines}${settingsEnvLines(inst, prefix)}`;
281
+ ${poolLine}${dbLines}${fedLines}${settingsEnvLines(inst, prefix)}`;
277
282
  }
278
283
  function webEnvPath(inst, { etcBase = CONFIG.etcBase } = {}) {
279
284
  return path.posix.join(etcBase, inst.slug, 'web.env');
@@ -37,11 +37,27 @@ function instanceDbName() {
37
37
  return undefined;
38
38
  }
39
39
 
40
+ // The pool's connection cap (task 1003928, ADR 0285 §3). pg's default `max` is 10 and
41
+ // the shared server allows 97 non-superuser backends, so nine busy co-located
42
+ // instances exhaust it whatever the RAM headroom — connections are the second wall
43
+ // after memory. A provisioned instance's web.env carries PGPOOL_MAX (the runner
44
+ // writes a small cap, PROVISION_INSTANCE_PGPOOL_MAX); UNSET — self-host, dev, the
45
+ // control plane itself — returns undefined so pg keeps its own default and nothing
46
+ // outside a co-located box changes. Read bare like the PG* family beside it: it is
47
+ // a pg option, not a branding one, so it does not ride resolveEnv's prefix.
48
+ // Anything that is not a positive integer is ignored rather than passed to pg.
49
+ function poolMax(env = process.env) {
50
+ const n = Number(env.PGPOOL_MAX);
51
+ return Number.isInteger(n) && n > 0 ? n : undefined;
52
+ }
53
+
40
54
  let pool;
55
+ const max = poolMax();
41
56
  if (process.env.DATABASE_URL) {
42
- pool = new Pool({ connectionString: process.env.DATABASE_URL });
57
+ pool = new Pool({ connectionString: process.env.DATABASE_URL, ...(max ? { max } : {}) });
43
58
  } else {
44
59
  pool = new Pool({
60
+ ...(max ? { max } : {}),
45
61
  // Peer auth via the local Unix socket on the droplet; PG* env overrides for other environments.
46
62
  host: process.env.PGHOST || '/var/run/postgresql',
47
63
  database: process.env.PGDATABASE || instanceDbName(),
@@ -66,4 +82,4 @@ pool.on('error', (err) => {
66
82
  console.error('[gds/pool] unexpected pool error', err);
67
83
  });
68
84
 
69
- module.exports = { pool, instanceDbName };
85
+ module.exports = { pool, instanceDbName, poolMax };
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.765'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.767'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,79 @@
1
+ // tests/pool_max.mjs
2
+ //
3
+ // The Postgres pool cap (task 1003928, ADR 0285 §3). Every co-located instance used
4
+ // to open pg's default 10 connections against a shared server that allows 97, so
5
+ // nine busy projects exhausted it whatever the RAM headroom. src/bongos/pool.js now
6
+ // honours PGPOOL_MAX; the runner writes it into a co-located instance's web.env
7
+ // (tests/provision.mjs pins that half). These pin the pool half:
8
+ //
9
+ // • the env reaches pg's `max` — asserted on a REAL Pool built by requiring the
10
+ // module in a child process with the env set (pool.js builds its Pool at require
11
+ // time, so the assertion has to cross a process boundary; no connection is ever
12
+ // opened — a Pool connects lazily);
13
+ // • absence leaves pg's default (10), so self-host, dev and the control plane are
14
+ // unchanged;
15
+ // • garbage is ignored, never handed to pg.
16
+ //
17
+ // DB-free. Run: node tests/pool_max.mjs
18
+
19
+ import { strict as assert } from 'node:assert';
20
+ import { execFileSync } from 'node:child_process';
21
+ import path from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+ import { poolMax } from '../src/bongos/pool.js';
24
+
25
+ let passed = 0, failed = 0;
26
+ function t(name, fn) {
27
+ try { fn(); passed++; console.log(` PASS ${name}`); }
28
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
29
+ }
30
+
31
+ const ROOT = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
32
+ const POOL = path.join(ROOT, 'src', 'bongos', 'pool.js');
33
+
34
+ // Build the pool in a child with a controlled env and report what pg was handed.
35
+ // The DB name is pinned so instanceDbName() never reads a pack; nothing connects.
36
+ function poolMaxInChild(extraEnv) {
37
+ const env = { ...process.env, PGDATABASE: 'pool_max_probe' };
38
+ delete env.PGPOOL_MAX; delete env.DATABASE_URL; delete env.NODE_TEST_CONTEXT;
39
+ Object.assign(env, extraEnv);
40
+ const out = execFileSync(process.execPath, [
41
+ '-e', 'const { pool } = require(process.argv[1]); console.log(JSON.stringify({ max: pool.options.max })); pool.end();',
42
+ POOL,
43
+ ], { env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
44
+ return JSON.parse(out.trim().split('\n').pop()).max;
45
+ }
46
+
47
+ console.log('poolMax (pure):');
48
+
49
+ t('a positive integer is the cap; absent/blank/garbage/zero/negative/fractional → undefined (pg keeps its default)', () => {
50
+ assert.equal(poolMax({ PGPOOL_MAX: '3' }), 3);
51
+ assert.equal(poolMax({ PGPOOL_MAX: '10' }), 10);
52
+ assert.equal(poolMax({}), undefined);
53
+ assert.equal(poolMax({ PGPOOL_MAX: '' }), undefined);
54
+ assert.equal(poolMax({ PGPOOL_MAX: 'three' }), undefined);
55
+ assert.equal(poolMax({ PGPOOL_MAX: '0' }), undefined);
56
+ assert.equal(poolMax({ PGPOOL_MAX: '-2' }), undefined);
57
+ assert.equal(poolMax({ PGPOOL_MAX: '2.5' }), undefined);
58
+ });
59
+
60
+ console.log('\nthe env reaches the real Pool (child process, no connection):');
61
+
62
+ t('PGPOOL_MAX=3 → pool.options.max is 3 (the co-located instance shape)', () => {
63
+ assert.equal(poolMaxInChild({ PGPOOL_MAX: '3' }), 3);
64
+ });
65
+
66
+ t('unset → pg\'s default 10 (self-host, dev, the control plane: unchanged)', () => {
67
+ assert.equal(poolMaxInChild({}), 10);
68
+ });
69
+
70
+ t('garbage → pg\'s default, never a NaN handed to pg', () => {
71
+ assert.equal(poolMaxInChild({ PGPOOL_MAX: 'lots' }), 10);
72
+ });
73
+
74
+ t('the cap applies on the DATABASE_URL branch too', () => {
75
+ assert.equal(poolMaxInChild({ PGPOOL_MAX: '4', DATABASE_URL: 'postgres://u:p@127.0.0.1:1/pool_max_probe' }), 4);
76
+ });
77
+
78
+ console.log(`\npool_max: ${passed} passed, ${failed} failed`);
79
+ process.exit(failed ? 1 : 0);
@@ -2618,6 +2618,27 @@ t('webEnvBody: NO password ⇒ no PG credential lines — an instance provisione
2618
2618
  assert.match(b, /^PGDATABASE=mercury$/m, 'the DB name is unchanged either way');
2619
2619
  });
2620
2620
 
2621
+ // ── the Postgres pool cap (task 1003928, ADR 0285 §3) ─────────────────────────
2622
+ t('webEnvBody: a CO-LOCATED instance (co-tenant or standalone) carries PGPOOL_MAX at the runner\'s cap, beside PGDATABASE', () => {
2623
+ for (const shape of ['co-tenant', 'standalone']) {
2624
+ const b = P.webEnvBody({ ...coTenant, hosting_shape: shape });
2625
+ assert.match(b, new RegExp(`^PGPOOL_MAX=${P.CONFIG.instancePoolMax}$`, 'm'), `${shape} carries the cap`);
2626
+ const lines = b.split('\n');
2627
+ assert.equal(lines[lines.indexOf('PGDATABASE=mercury') + 1], `PGPOOL_MAX=${P.CONFIG.instancePoolMax}`, 'written right after the DB name, with or without a credential');
2628
+ }
2629
+ assert.equal(P.CONFIG.instancePoolMax, 3, 'the default cap: 3 × 15 slots + the control plane\'s 10 stays under the server\'s 97');
2630
+ // …and it rides along with the credential lines, not instead of them.
2631
+ const withCred = P.webEnvBody(coTenant, { dbPassword: 'pw_ABC-123' });
2632
+ assert.match(withCred, /^PGPOOL_MAX=3$/m);
2633
+ assert.match(withCred, /^PGUSER=bongos-mercury$/m);
2634
+ });
2635
+
2636
+ t('webEnvBody: a DEDICATED droplet has its own Postgres — no PGPOOL_MAX line, pg keeps its default', () => {
2637
+ const b = P.webEnvBody({ ...coTenant, hosting_shape: 'dedicated' });
2638
+ assert.doesNotMatch(b, /^PGPOOL_MAX=/m);
2639
+ assert.match(b, /^PGDATABASE=mercury$/m);
2640
+ });
2641
+
2621
2642
  t('dedicatedUserData: the droplet runs the SAME account + role steps, in the same order around the migrate', () => {
2622
2643
  const ud = P.dedicatedUserData(coTenant);
2623
2644
  const at = (needle) => ud.indexOf(needle);