@bongos/core 1.19.940 → 1.19.941

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,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.940",
6
- "core_contract": "1.19.940",
7
- "source_commit": "44e10510143c0775561f28fe1e8d3b55fdbc3190",
5
+ "core_version": "1.19.941",
6
+ "core_contract": "1.19.941",
7
+ "source_commit": "4063c1c70284ec0850ace02a6ac26e9ecf0ecc20",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-21T19:54:33.696Z",
9
+ "built_at": "2026-09-21T20:39:58.651Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 511,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 2876,
20
- "tree_sha256": "a7edb7e2d7278cda02f1dc7d69eb957b0702345c2f059ead3049463b6fb17ff7",
20
+ "tree_sha256": "ede1bb5b48026ef02ab4df39fae783c3eb2e648c7bafec28010174f4ab8ed076",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2677,7 +2677,7 @@
2677
2677
  {
2678
2678
  "path": "docs/module-api-changelog.md",
2679
2679
  "mode": "0000644",
2680
- "sha256": "b0dcdad0ab90dc948a111f4bb26a39ffa028bd17907ad7ffbadbc3ec424d54c6"
2680
+ "sha256": "57bcf700f7f794da9b36f6cd6a88d5c44ac153a6ef246c7be7841ace8e6908ce"
2681
2681
  },
2682
2682
  {
2683
2683
  "path": "docs/modules-contract.md",
@@ -2927,7 +2927,7 @@
2927
2927
  {
2928
2928
  "path": "docs/recipes/upgrading-the-core.md",
2929
2929
  "mode": "0000644",
2930
- "sha256": "147fe7408c7e86487c2cd99198074f3591c7b956c0debd80b7b0730803940571"
2930
+ "sha256": "f16df23bfcb5e1ce02813222bf9a8b190b458749939f5074fbe142d9430b1bad"
2931
2931
  },
2932
2932
  {
2933
2933
  "path": "docs/recipes/windows-builders.md",
@@ -8002,12 +8002,12 @@
8002
8002
  {
8003
8003
  "path": "package-lock.json",
8004
8004
  "mode": "0000644",
8005
- "sha256": "d319db2c2f341bea9b0f59dd6f2c826a68a155e620b3b6e8809ef8e35390b7bf"
8005
+ "sha256": "7930ba5349243c9641adef8865a4d868521f9c841c174dda327942a599304ae8"
8006
8006
  },
8007
8007
  {
8008
8008
  "path": "package.json",
8009
8009
  "mode": "0000644",
8010
- "sha256": "0692c686bc978e5f8ada5fa1f563bf40bdc9fde2a548a8ca47f21e089010bbbb"
8010
+ "sha256": "f19114e6c03c1eb79f70855f08efae1b58167aa4939cb149bcd50894f2ca7c36"
8011
8011
  },
8012
8012
  {
8013
8013
  "path": "public-docs/index.html",
@@ -8442,7 +8442,7 @@
8442
8442
  {
8443
8443
  "path": "scripts/gds/deploy-divergence.js",
8444
8444
  "mode": "0000644",
8445
- "sha256": "8aae6c1a2cd978e53bce7d788ab5f4fe86ab357821f083d1b414df4f5742beac"
8445
+ "sha256": "bffaceec8b47b238f4a7911aee10a651b45cd75e0c5a67b2b8e3daa90e3cd54a"
8446
8446
  },
8447
8447
  {
8448
8448
  "path": "scripts/gds/deploy-main-audit-gate.sh",
@@ -9107,7 +9107,7 @@
9107
9107
  {
9108
9108
  "path": "scripts/gds/run-unit-tests.js",
9109
9109
  "mode": "0000644",
9110
- "sha256": "034d5678310329273360d7dd3e60cb0bb13e29fa6895c2c79338f80416b751bd"
9110
+ "sha256": "40d1b5bf4c82536cac2a666e81c4909f99bc26796db0c9e9c928678032948c91"
9111
9111
  },
9112
9112
  {
9113
9113
  "path": "scripts/gds/runner-drift.js",
@@ -9972,7 +9972,7 @@
9972
9972
  {
9973
9973
  "path": "src/bongos/schema-lag.js",
9974
9974
  "mode": "0000644",
9975
- "sha256": "bafe3453eff53cb80ef0eb3ca5894d7ff2fc4a2480e80595869d8d9838d06a42"
9975
+ "sha256": "b6443663bc60287242164c530f864504514050b648b6c9736454fd0e9ab4319f"
9976
9976
  },
9977
9977
  {
9978
9978
  "path": "src/bongos/secret-box.js",
@@ -9987,7 +9987,7 @@
9987
9987
  {
9988
9988
  "path": "src/bongos/serve-internal.js",
9989
9989
  "mode": "0000644",
9990
- "sha256": "59aa0ee2be23c8c956357eb67ee58720bd2e378a71a9efb6a73ab966c7d4094f"
9990
+ "sha256": "989a1bef0e179efe32fc3edb11f7f46b1732920ca026b36a841c8e0e344a175a"
9991
9991
  },
9992
9992
  {
9993
9993
  "path": "src/bongos/uptime-poller.js",
@@ -10012,7 +10012,7 @@
10012
10012
  {
10013
10013
  "path": "src/module-api.js",
10014
10014
  "mode": "0000644",
10015
- "sha256": "d4d3c893fb318a929f2721a142191807d04b74c20ace3e77c1afc4d50f747c03"
10015
+ "sha256": "168b1b8002437b7a7bc5b42ae2f68af01c31b007473a09cdc0f1bb98338f662a"
10016
10016
  },
10017
10017
  {
10018
10018
  "path": "src/module-loader/catalog.js",
@@ -11062,7 +11062,7 @@
11062
11062
  {
11063
11063
  "path": "tests/deploy_divergence_line.mjs",
11064
11064
  "mode": "0000644",
11065
- "sha256": "7e87aa9005acbf2fff537a9b79f19b5901b304b5fb4472224b5f3abaf5fef15a"
11065
+ "sha256": "fd62561dcf5fb935116ec5933393e75cef25eaa807a6d39fc55d9c981e6fe289"
11066
11066
  },
11067
11067
  {
11068
11068
  "path": "tests/deploy_instance.mjs",
@@ -13452,12 +13452,12 @@
13452
13452
  {
13453
13453
  "path": "tests/schema_lag.mjs",
13454
13454
  "mode": "0000644",
13455
- "sha256": "90e5cdc6648856a027c67df41e39fee6699d7bae56b79bc41cefbb2d249c86a9"
13455
+ "sha256": "d592f20f08695dfbe2806e195c4d2a6e81bc0ea6cec4f1c2168609280a197c09"
13456
13456
  },
13457
13457
  {
13458
13458
  "path": "tests/schema_lag_db.mjs",
13459
13459
  "mode": "0000644",
13460
- "sha256": "74f4f62025773a917b77120637fcfb3e5cd843aee4ca30c7ed2303fb613f18ec"
13460
+ "sha256": "5e3ada8619ea87eea3026a2ccf3069926d5825a06fe11b5e435020034b2eb7e1"
13461
13461
  },
13462
13462
  {
13463
13463
  "path": "tests/scope_map_guard.mjs",
@@ -2354,5 +2354,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2354
2354
  landed since 1.19.938 with no explicit bump. run 35641614084. (task 1002620)
2355
2355
  1.19.940 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2356
2356
  landed since 1.19.939 with no explicit bump. run 35647628385. (task 1002620)
2357
+ 1.19.941 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2358
+ landed since 1.19.940 with no explicit bump. run 35652433730. (task 1002620)
2357
2359
  ---------------------------------------------------------------------------
2358
2360
  ```
@@ -167,7 +167,7 @@ running a go-live.
167
167
  - **Survey co-tenant git state as `lars`, never as root.** `ssh cloudbongos` lands you as root, where `git config --get remote.origin.url` on a `lars`-owned repo returns **nothing** — which reads exactly like "this instance has no remote" and is wrong. They do: `git@github-instances:example-owner/<slug>.git`. Prefix every probe with `sudo -u lars`.
168
168
  - **A scaffold's `migrate` script is not guaranteed.** 21 of the 22 co-tenants have `scripts.migrate`; `mercury` has **no `scripts` block at all** and is started directly by its systemd `ExecStart`, so the normal lane cannot complete on it. `--skip-migrate` gets the install through but silently skips every schema change in the window — check what that window carries before reaching for it, rather than after.
169
169
  - **An instance's database moves with its code, on every path** (task 1002002). `bongos upgrade` migrates (step 4) before it restarts; the control-plane runner behind the Deploy door drives it with `PGDATABASE` set to the target instance's own database; provisioning migrates at stand-up. A pinned co-tenant needs nothing extra, because its code only moves through `bongos upgrade`. `--skip-migrate` is the one way out of lockstep. Pinned by `tests/instance_db_migrated_on_code_move.mjs`.
170
- - **`/version` reports `schemaPending`** (task 1002002): how many core migrations the running code ships that this instance's database has not recorded — `null` when unknown (database unreachable, or a core older than the field). Above 0 is the 2026-07-04 outage shape, where the first query touching those tables fails. Fix: `migrate` against that instance DB with `PGDATABASE` set. The SessionStart `deploy-divergence` line alarms on it for the instance a checkout talks to; `--skip-migrate` above is the usual way to cause one.
170
+ - **`/version` reports `schemaPending`** (task 1002002): how many migrations the running code ships — core, plus each module this process has enabled (DB overrides included, task 1004136) — that this instance's database has not recorded — `null` when unknown (database unreachable, or a core older than the field). Above 0 is the 2026-07-04 outage shape, where the first query touching those tables fails. Fix: `migrate` against that instance DB with `PGDATABASE` set. Exception: `migrate` does not read DB overrides yet ([task 1004137](https://cloudbongos.com/builders#/task/1004137)), so a module switched on only by the hall's Modules toggle stays pending. Until that lands, also switch it on in `config/modules.json` (or the `<PREFIX>_MODULE_<KEY>` env) before migrating. The SessionStart `deploy-divergence` line alarms on it for the instance a checkout talks to; `--skip-migrate` above is the usual way to cause one.
171
171
  - **The health check polls the restart** (fixed in task [#1984](https://example.com/builders#/task/1984)). The service binds its port ~1s after `systemctl restart`, so `--health-url` now polls up to ~30s for a 2xx before reporting a warning — an immediate `fetch failed` no longer means a failed upgrade. (Pre-1984 cores fetched once and spuriously reported `fetch failed`; there, confirm with `is-active` + a manual `curl`.)
172
172
  - **The ledger auto-inserts via `PGDATABASE` too** (fixed in task [#1984](https://example.com/builders#/task/1984)). A consumer on `PGDATABASE` + peer auth now gets its `core_upgrades` row written automatically — the channel delegates the insert to `psql -d "$PGDATABASE"`, exactly like `migrate.sh`, so no manual `psql INSERT` is needed. (`DATABASE_URL` still works, via the `pg` driver.) Pre-1984 cores only auto-wrote with `DATABASE_URL`; on those, record the row manually.
173
173
  - **No deploy key on the droplet** → deploys/upgrades ride a `git bundle`, not `git pull` (idea 509).
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.940",
3
+ "version": "1.19.941",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.940",
9
+ "version": "1.19.941",
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.940",
3
+ "version": "1.19.941",
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",
@@ -16,7 +16,7 @@
16
16
  // 2. UNDEPLOYED — the live instance's coreVersion is behind the CORE_VERSION
17
17
  // on main. A release exists; this instance has not moved its pin to it.
18
18
  // 3. SCHEMA BEHIND — live's /version reports schemaPending > 0: its database
19
- // lacks core migrations its running code ships (the 2026-07-04 502 outage
19
+ // lacks migrations its running code ships (the 2026-07-04 502 outage
20
20
  // shape; task 1002002). An instance too old to report it is not flagged.
21
21
  //
22
22
  // Usage:
@@ -267,7 +267,7 @@ function report(r, { quiet = false } = {}) {
267
267
  }
268
268
  if (r.schemaBehind) {
269
269
  const n = r.live.schemaPending;
270
- say(` ⚠ SCHEMA BEHIND — live's database is missing ${n} core migration${n === 1 ? '' : 's'} its running code ships.`);
270
+ say(` ⚠ SCHEMA BEHIND — live's database is missing ${n} migration${n === 1 ? '' : 's'} its running code ships.`);
271
271
  say(' The first query that touches them fails (the 2026-07-04 502 was this shape).');
272
272
  say(' Fix: run migrate against that instance DB with PGDATABASE set (see docs/recipes/upgrading-the-core.md).');
273
273
  }
@@ -319,7 +319,7 @@ function line(r) {
319
319
  const versionParts = parts.length;
320
320
  const pending = r.schemaBehind ? safeCount(r.live?.schemaPending) : null;
321
321
  if (pending) {
322
- parts.push(`its database is MISSING ${pending} core migration${pending === 1 ? '' : 's'} the running code ships, so the first query that touches ${pending === 1 ? 'it' : 'them'} fails`);
322
+ parts.push(`its database is MISSING ${pending} migration${pending === 1 ? '' : 's'} the running code ships, so the first query that touches ${pending === 1 ? 'it' : 'them'} fails`);
323
323
  }
324
324
  if (!parts.length) return null;
325
325
 
@@ -181,7 +181,7 @@ const INTEGRATION = new Set([
181
181
  'session_token_hash_db',
182
182
  // task 1002002: the no-false-alarm half of the schema-lag detector. The DB-free
183
183
  // sibling (schema_lag) covers the arithmetic against a stub; only a database that
184
- // migrate.sh has just run can show the expected core stems equal the recorded ones,
184
+ // migrate.sh has just run can show the expected stems (core + enabled modules) equal the recorded ones,
185
185
  // so a healthy instance reads zero rather than a permanent SCHEMA BEHIND.
186
186
  'schema_lag_db',
187
187
  'scouting_rollup_fanout', 'session_records',
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
- // src/bongos/schema-lag.js — does this instance's database carry every core migration the
3
- // running code ships? (task 1002002)
2
+ // src/bongos/schema-lag.js — does this instance's database carry every migration the running
3
+ // code ships? (task 1002002; modules added in task 1004136)
4
4
  //
5
5
  // WHY. On 2026-07-04 cloudbongos.com returned 502 on every /me: the deploy had moved the code
6
6
  // 22 core migrations past the instance's own database, and nothing said so — /healthz answered
@@ -8,11 +8,16 @@
8
8
  // moves an instance's core now migrates that instance's own DB first (bongos upgrade, the runner
9
9
  // behind the Deploy door, provisioning — pinned by tests/instance_db_migrated_on_code_move.mjs),
10
10
  // but a skipped or failed step would recreate that outage just as silently. This is the detector:
11
- // the core migration stems the installed package ships that schema_migrations does not record.
11
+ // the migration stems the running code expects that schema_migrations does not record.
12
12
  //
13
- // CORE ONLY, on purpose. migrate.sh applies every core migrations/*.sql on every instance, so a
14
- // missing core stem is always a real gap. Module and instance-seed migrations are per-instance
15
- // opt-ins, and counting them would alarm on a module that is simply off.
13
+ // WHAT IS EXPECTED is every migration the code this process actually serves depends on: every
14
+ // core migrations/*.sql, plus modules/<key>/migrations/*.sql for each module isModuleEnabled
15
+ // says is on — the same gate its routes mount behind, DB overrides included. A module that is
16
+ // off is expected nothing, so it can never alarm. Instance seeds (migrations/instance/) are an
17
+ // opt-in no running code depends on, so they are never counted.
18
+ //
19
+ // KNOWN GAP: migrate.sh does not read DB overrides (task 1004137), so for a module on ONLY by
20
+ // override this reports a true gap that running migrate does not yet close.
16
21
  //
17
22
  // Surfaced as `schemaPending` on /version (a count, never the names) and logged at boot;
18
23
  // scripts/gds/deploy-divergence.js turns a non-zero count into the SessionStart alarm.
@@ -20,6 +25,8 @@
20
25
  const fs = require('node:fs');
21
26
  const path = require('node:path');
22
27
  const { resolveCoreRoot } = require('../instance-config');
28
+ const { isModuleEnabled } = require('../modules');
29
+ const { discovered } = require('../module-loader/loader');
23
30
  const { pool } = require('./pool');
24
31
  const log = require('./logger').logger.child({ src: 'schema-lag' });
25
32
 
@@ -28,14 +35,23 @@ const CORE_MIGRATIONS_DIR = path.join(resolveCoreRoot(), 'migrations');
28
35
  // query runs at most this often; between refreshes the last answer is served.
29
36
  const REFRESH_MS = 60 * 1000;
30
37
 
31
- /** The stems migrate.sh keys schema_migrations by, for the core's top-level *.sql files. */
32
- function coreMigrationStems(dir = CORE_MIGRATIONS_DIR) {
38
+ /** The stems migrate.sh keys schema_migrations by, for the *.sql files directly in `dir`. */
39
+ function migrationStems(dir = CORE_MIGRATIONS_DIR) {
33
40
  return fs.readdirSync(dir, { withFileTypes: true })
34
41
  .filter((e) => e.isFile() && e.name.endsWith('.sql'))
35
42
  .map((e) => e.name.slice(0, -'.sql'.length))
36
43
  .sort();
37
44
  }
38
45
 
46
+ /** Migration stems of every module this process serves, from the dir it was discovered in. */
47
+ function enabledModuleMigrationStems({ loaded = discovered().loaded, isEnabled = isModuleEnabled } = {}) {
48
+ return loaded
49
+ .filter((m) => isEnabled(m.key))
50
+ .map((m) => path.join(m.dir, 'migrations'))
51
+ .filter((dir) => fs.existsSync(dir))
52
+ .flatMap((dir) => migrationStems(dir));
53
+ }
54
+
39
55
  /** Shipped stems the database has not recorded. PURE. */
40
56
  function pendingStems(shipped, applied) {
41
57
  const have = new Set(applied);
@@ -49,25 +65,27 @@ let inflight = null;
49
65
  * Re-read schema_migrations and cache the gap. Never throws: an unreadable database or
50
66
  * migrations dir caches null — "unknown" — which no reader treats as an alarm.
51
67
  */
52
- async function refreshSchemaLag({ db = pool, dir = CORE_MIGRATIONS_DIR, now = Date.now } = {}) {
68
+ async function refreshSchemaLag({
69
+ db = pool, dir = CORE_MIGRATIONS_DIR, moduleStems = enabledModuleMigrationStems, now = Date.now,
70
+ } = {}) {
53
71
  const before = last.pending;
54
72
  let pending = null;
55
73
  try {
56
74
  const { rows } = await db.query('SELECT version FROM schema_migrations');
57
- pending = pendingStems(coreMigrationStems(dir), rows.map((r) => r.version));
75
+ pending = pendingStems([...migrationStems(dir), ...moduleStems()], rows.map((r) => r.version));
58
76
  } catch (err) {
59
- log.warn({ err: err && err.message }, 'could not compare schema_migrations with the core migrations');
77
+ log.warn({ err: err && err.message }, 'could not compare schema_migrations with the migrations the running code ships');
60
78
  }
61
79
  last = { pending, checkedAt: now() };
62
80
  if (pending && pending.length && (!before || before.length !== pending.length)) {
63
81
  log.error({ pending: pending.length, first: pending.slice(0, 5) },
64
- 'this database is missing core migrations the running code ships — run migrate against this instance DB before its next query touches them');
82
+ 'this database is missing migrations the running code ships (core, or an enabled module) — run migrate against this instance DB before its next query touches them');
65
83
  }
66
84
  return pending;
67
85
  }
68
86
 
69
87
  /**
70
- * The last-known count of missing core migrations, or null when unknown. Kicks a background
88
+ * The last-known count of missing migrations, or null when unknown. Kicks a background
71
89
  * refresh when the answer is older than REFRESH_MS, and never waits for it.
72
90
  */
73
91
  function schemaPending({ now = Date.now, refresh = refreshSchemaLag } = {}) {
@@ -77,4 +95,6 @@ function schemaPending({ now = Date.now, refresh = refreshSchemaLag } = {}) {
77
95
  return last.pending ? last.pending.length : null;
78
96
  }
79
97
 
80
- module.exports = { coreMigrationStems, pendingStems, refreshSchemaLag, schemaPending, REFRESH_MS };
98
+ module.exports = {
99
+ migrationStems, enabledModuleMigrationStems, pendingStems, refreshSchemaLag, schemaPending, REFRESH_MS,
100
+ };
@@ -583,7 +583,7 @@ function mountInternalSurfaces(app) {
583
583
  // the throwaway `updateCanary` proof marker was removed in phase E.)
584
584
  let coreVersion = null;
585
585
  try { coreVersion = require('../module-api').CORE_VERSION; } catch { /* fail-soft: version unknown */ }
586
- // schemaPending — core migrations this code ships that the instance DB has not applied
586
+ // schemaPending — migrations this code ships (core + enabled modules') that the instance DB has not applied
587
587
  // (null = unknown). Non-zero is the 2026-07-04 outage shape (task 1002002).
588
588
  res.json({ ...buildInfo(), coreVersion, schemaPending: schemaPending() });
589
589
  });
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.940'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.941'; // 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');
@@ -260,7 +260,7 @@ test('settings.json stays OUT of the published package', () => {
260
260
  });
261
261
 
262
262
  // ── schema behind (task 1002002) ────────────────────────────────────────────
263
- // /version's schemaPending: core migrations the running code ships that live's DB
263
+ // /version's schemaPending: migrations (core + enabled modules') the running code ships that live's DB
264
264
  // lacks. The 2026-07-04 502 was this shape, and nothing reported it.
265
265
 
266
266
  const schemaOnly = {
@@ -269,17 +269,17 @@ const schemaOnly = {
269
269
  behind: false, hasUnreleased: false, unreleased: { count: 0 }, schemaBehind: true,
270
270
  };
271
271
 
272
- test('a database missing core migrations is named on the one line, with its own advice', () => {
272
+ test('a database missing migrations is named on the one line, with its own advice', () => {
273
273
  const s = line(schemaOnly);
274
- assert.match(s, /MISSING 3 core migrations the running code ships/);
274
+ assert.match(s, /MISSING 3 migrations the running code ships/);
275
275
  assert.match(s, /Run migrate against that instance DB/);
276
276
  assert.doesNotMatch(s, /Merging to main deploys nothing/, 'the version advice is for version gaps only');
277
277
  assert.equal(s.split('\n').length, 1);
278
- assert.match(line({ ...schemaOnly, live: { ...schemaOnly.live, schemaPending: 1 } }), /MISSING 1 core migration the/);
278
+ assert.match(line({ ...schemaOnly, live: { ...schemaOnly.live, schemaPending: 1 } }), /MISSING 1 migration the/);
279
279
 
280
280
  const both = line({ ...schemaOnly, behind: true, live: { coreVersion: '1.19.56', schemaPending: 2 } });
281
281
  assert.match(both, /1\.19\.56/);
282
- assert.match(both, /MISSING 2 core migrations/);
282
+ assert.match(both, /MISSING 2 migrations/);
283
283
  assert.match(both, /Merging to main deploys nothing/);
284
284
  });
285
285
 
@@ -1,4 +1,5 @@
1
- // tests/schema_lag.mjs — the schema-lag detector behind /version's schemaPending (task 1002002).
1
+ // tests/schema_lag.mjs — the schema-lag detector behind /version's schemaPending (task 1002002;
2
+ // enabled modules' migrations, task 1004136).
2
3
  //
3
4
  // DB-free: every database read goes through an injected stub. The real-SQL half — that a
4
5
  // database migrate.sh has just run reports ZERO pending, i.e. the stems this module expects
@@ -13,10 +14,23 @@ import os from 'node:os';
13
14
  import path from 'node:path';
14
15
  import { fileURLToPath } from 'node:url';
15
16
  import {
16
- coreMigrationStems, pendingStems, refreshSchemaLag, schemaPending, REFRESH_MS,
17
+ migrationStems, enabledModuleMigrationStems, pendingStems, refreshSchemaLag, schemaPending, REFRESH_MS,
17
18
  } from '../src/bongos/schema-lag.js';
19
+ import { setModuleOverrides } from '../src/modules.js';
18
20
 
19
21
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
22
+ const none = () => [];
23
+
24
+ /** A modules/ root: `on` and `off` each ship a migration; `bare` has no migrations/ dir. */
25
+ function tmpModules() {
26
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'schema-lag-mod-'));
27
+ for (const [key, files] of [['on', ['on_001_a.sql', 'on_002_b.sql']], ['off', ['off_001_a.sql']]]) {
28
+ fs.mkdirSync(path.join(root, key, 'migrations'), { recursive: true });
29
+ for (const f of files) fs.writeFileSync(path.join(root, key, 'migrations', f), '-- x\n');
30
+ }
31
+ fs.mkdirSync(path.join(root, 'bare'));
32
+ return ['on', 'off', 'bare'].map((key) => ({ key, dir: path.join(root, key) }));
33
+ }
20
34
 
21
35
  function tmpMigrations() {
22
36
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'schema-lag-'));
@@ -36,12 +50,12 @@ test('pendingStems lists shipped stems the database has not recorded', () => {
36
50
  assert.deepEqual(pendingStems(['a'], ['a']), []);
37
51
  });
38
52
 
39
- test('coreMigrationStems reads top-level .sql stems only, sorted', () => {
40
- assert.deepEqual(coreMigrationStems(tmpMigrations()), ['001_init', 'core_002_add', 'core_003_more']);
53
+ test('migrationStems reads top-level .sql stems only, sorted', () => {
54
+ assert.deepEqual(migrationStems(tmpMigrations()), ['001_init', 'core_002_add', 'core_003_more']);
41
55
  });
42
56
 
43
57
  test('the default directory is the installed core migrations', () => {
44
- const stems = coreMigrationStems();
58
+ const stems = migrationStems();
45
59
  const onDisk = fs.readdirSync(path.join(ROOT, 'migrations')).filter((f) => f.endsWith('.sql'));
46
60
  assert.equal(stems.length, onDisk.length);
47
61
  assert.ok(stems.includes('003_pms'), 'legacy bare stems are core migrations too');
@@ -49,19 +63,49 @@ test('the default directory is the installed core migrations', () => {
49
63
 
50
64
  test('a database missing core migrations reports them; a current one reports an empty list', async () => {
51
65
  const dir = tmpMigrations();
52
- assert.deepEqual(await refreshSchemaLag({ db: dbWith(['001_init']), dir }), ['core_002_add', 'core_003_more']);
53
- assert.deepEqual(await refreshSchemaLag({ db: dbWith(['001_init', 'core_002_add', 'core_003_more']), dir }), []);
66
+ assert.deepEqual(await refreshSchemaLag({ db: dbWith(['001_init']), dir, moduleStems: none }), ['core_002_add', 'core_003_more']);
67
+ assert.deepEqual(await refreshSchemaLag({ db: dbWith(['001_init', 'core_002_add', 'core_003_more']), dir, moduleStems: none }), []);
68
+ });
69
+
70
+ test('only enabled modules are expected to carry their migrations; a module that is off never alarms', () => {
71
+ const loaded = tmpModules();
72
+ const isEnabled = (key) => key !== 'off';
73
+ assert.deepEqual(enabledModuleMigrationStems({ loaded, isEnabled }), ['on_001_a', 'on_002_b'],
74
+ 'an enabled module with no migrations/ dir contributes nothing and does not throw');
75
+ assert.deepEqual(enabledModuleMigrationStems({ loaded, isEnabled: () => false }), []);
76
+ });
77
+
78
+ test('an enabled module whose migrations never ran is counted beside the core gap', async () => {
79
+ const dir = tmpMigrations();
80
+ const core = ['001_init', 'core_002_add', 'core_003_more'];
81
+ const moduleStems = () => enabledModuleMigrationStems({ loaded: tmpModules(), isEnabled: (key) => key === 'on' });
82
+ assert.deepEqual(await refreshSchemaLag({ db: dbWith([...core, 'on_001_a']), dir, moduleStems }), ['on_002_b']);
83
+ assert.deepEqual(await refreshSchemaLag({ db: dbWith([...core, 'on_001_a', 'on_002_b']), dir, moduleStems }), []);
84
+ });
85
+
86
+ test('the default expectation follows the server\'s own enablement, DB overrides included', () => {
87
+ // `discord` ships a migration and meters no spend, so the override alone decides it. A DB
88
+ // override is the layer migrate.sh's fresh process never loads (task 1004137).
89
+ const STEM = 'discord_001_board_messages';
90
+ try {
91
+ setModuleOverrides({ discord: true });
92
+ assert.ok(enabledModuleMigrationStems().includes(STEM), 'switched on by override: its migration is expected');
93
+ setModuleOverrides({ discord: false });
94
+ assert.ok(!enabledModuleMigrationStems().includes(STEM), 'switched off by override: nothing expected');
95
+ } finally {
96
+ setModuleOverrides({});
97
+ }
54
98
  });
55
99
 
56
100
  test('an unreadable database is unknown (null), never an alarm and never a throw', async () => {
57
101
  const down = { query: async () => { throw new Error('ECONNREFUSED'); } };
58
- assert.equal(await refreshSchemaLag({ db: down, dir: tmpMigrations() }), null);
102
+ assert.equal(await refreshSchemaLag({ db: down, dir: tmpMigrations(), moduleStems: none }), null);
59
103
  });
60
104
 
61
105
  test('schemaPending serves the cached count and refreshes at most once per window', async () => {
62
106
  const dir = tmpMigrations();
63
107
  const t0 = 1_000_000;
64
- await refreshSchemaLag({ db: dbWith(['001_init']), dir, now: () => t0 });
108
+ await refreshSchemaLag({ db: dbWith(['001_init']), dir, moduleStems: none, now: () => t0 });
65
109
 
66
110
  let calls = 0;
67
111
  const refresh = () => { calls += 1; return new Promise(() => {}); }; // never settles: stays in flight
@@ -29,7 +29,7 @@ try {
29
29
 
30
30
  try {
31
31
  const pending = await refreshSchemaLag();
32
- assert.deepEqual(pending, [], `a freshly migrated database must report no pending core migrations, got: ${JSON.stringify(pending)}`);
32
+ assert.deepEqual(pending, [], `a freshly migrated database must report no pending migrations (core or enabled module), got: ${JSON.stringify(pending)}`);
33
33
  assert.equal(schemaPending(), 0, 'and /version serves 0 — known-current, not unknown (null)');
34
34
  console.log(' ok a freshly migrated database reports schemaPending 0');
35
35
  } finally {