@bongos/core 1.19.64 → 1.19.65

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.
@@ -211,3 +211,68 @@ test('dependencies: a non-object, or a non-string range, is rejected', () => {
211
211
  assert.ok(r.errors.some((e) => e.startsWith('dependencies')), `error should mention dependencies: ${r.errors}`);
212
212
  }
213
213
  });
214
+
215
+ // --- maintenance & deprecation policy (ADR 0107 §7 / ADR 0166) ---
216
+
217
+ test('maintenance: omitted is fine (optional — pre-policy manifests still validate)', () => {
218
+ assert.equal(validateManifest(baseManifest()).valid, true);
219
+ });
220
+
221
+ test('maintenance: each of the four statuses validates when its own obligations are met', () => {
222
+ const cases = [
223
+ { maintenance: { status: 'core-maintained' } },
224
+ { maintainer: 'A. Builder', maintenance: { status: 'maintained' } },
225
+ { maintenance: { status: 'deprecated', successor: 'econ-v2', removeAfter: '1.12.0' } },
226
+ { maintenance: { status: 'orphaned', note: 'Author unreachable since 2026-07; enable at your own risk.' } },
227
+ ];
228
+ for (const extra of cases) {
229
+ const r = validateManifest({ ...baseManifest(), ...extra });
230
+ assert.equal(r.valid, true, `${extra.maintenance.status}: ${r.errors.join('; ')}`);
231
+ }
232
+ });
233
+
234
+ test('maintenance: "maintained" without a named top-level maintainer is rejected', () => {
235
+ const r = validateManifest({ ...baseManifest(), maintenance: { status: 'maintained' } });
236
+ assert.equal(r.valid, false, 'an unnamed maintainer IS the orphaning failure the policy prevents');
237
+ assert.ok(r.errors.some((e) => /maintained.*maintainer/.test(e)), r.errors.join('; '));
238
+ });
239
+
240
+ test('maintenance: a sunset status with no deprecation path (no successor, no note) is rejected', () => {
241
+ for (const status of ['deprecated', 'orphaned']) {
242
+ const r = validateManifest({ ...baseManifest(), maintenance: { status } });
243
+ assert.equal(r.valid, false, `${status} must state a path out`);
244
+ assert.ok(r.errors.some((e) => /deprecation path/.test(e)), r.errors.join('; '));
245
+ }
246
+ });
247
+
248
+ test('maintenance: an unknown status, an unknown key, or a bad version is rejected', () => {
249
+ const bad = [
250
+ { status: 'someday' },
251
+ { status: 'core-maintained', removeafter: '1.2.0' },
252
+ { status: 'core-maintained', since: 'v1.2' },
253
+ { status: 'deprecated', successor: 'Econ_V2' },
254
+ 'core-maintained',
255
+ ];
256
+ for (const m of bad) {
257
+ const r = validateManifest({ ...baseManifest(), maintenance: m });
258
+ assert.equal(r.valid, false, `should reject ${JSON.stringify(m)}`);
259
+ }
260
+ });
261
+
262
+ test('maintenance: removeAfter only applies to a module on its way out', () => {
263
+ const r = validateManifest({ ...baseManifest(), maintenance: { status: 'core-maintained', removeAfter: '1.12.0' } });
264
+ assert.equal(r.valid, false);
265
+ assert.ok(r.errors.some((e) => /removeAfter only applies/.test(e)), r.errors.join('; '));
266
+ });
267
+
268
+ test('every bundled module declares a maintenance posture (the catalog has no unowned entries)', async () => {
269
+ const fsm = (await import('node:fs')).default;
270
+ const modulesDir = path.join(ROOT, 'modules');
271
+ const undeclared = fsm.readdirSync(modulesDir)
272
+ .filter((k) => fsm.existsSync(path.join(modulesDir, k, 'module.json')))
273
+ .filter((k) => {
274
+ const m = JSON.parse(fsm.readFileSync(path.join(modulesDir, k, 'module.json'), 'utf8'));
275
+ return !m.maintenance || !m.maintenance.status;
276
+ });
277
+ assert.deepEqual(undeclared, [], `these modules declare no maintenance.status: ${undeclared.join(', ')}`);
278
+ });
package/tests/upgrade.mjs CHANGED
@@ -935,6 +935,100 @@ t('healDocAssets: a generator absent from this core is recorded, not spawned', (
935
935
  assert.equal(spawned.length, 1, 'the absent script is never spawned');
936
936
  });
937
937
 
938
+ // --- module maintenance & deprecation surfacing (ADR 0107 §7 / ADR 0166, task 1001773) ---
939
+ //
940
+ // The bug this pins: preflightModules used to return ONLY `blocking`, so a DISABLED module
941
+ // whose coreVersion the target core no longer satisfies vanished from a real bump entirely.
942
+ // An accepted catalog module ships default:false (ADR 0107 §6) — so that was every rotted
943
+ // catalog entry, silently.
944
+
945
+ // A modules/ dir on disk that preflightModules will really discover, driven through a stub
946
+ // moduleLib so the verdict shape (not the loader) is what's under test.
947
+ function moduleLibStub(verdict) {
948
+ return { gatherUpgrade: () => ({ rows: verdict.rows || [], errors: [], core: verdict.core }), evaluateUpgrade: () => verdict };
949
+ }
950
+
951
+ t('preflightModules: a DISABLED incompatible module is returned as a warning, not dropped', () => {
952
+ const dir = scratchConsumer();
953
+ mkdirSync(join(dir, 'modules'), { recursive: true });
954
+ const warned = { key: 'stale-catalog-entry', coreVersion: '^1.0.0', enabled: false, satisfied: false, maintainer: 'Ada <ada@example.com>', maintenance: { status: 'maintained' } };
955
+ const res = u.preflightModules({ instanceDir: dir, targetVersion: '2.0.0' }, {
956
+ moduleLib: moduleLibStub({ ok: true, blocking: [], warnings: [warned], deprecations: [], expired: [], core: '2.0.0' }),
957
+ });
958
+ assert.equal(res.ok, true, 'a disabled mismatch must never block the bump');
959
+ assert.deepEqual(res.warnings.map((w) => w.key), ['stale-catalog-entry']);
960
+ assert.match(res.note, /1 disabled module\(s\) do NOT/, 'the headline note admits the rot instead of claiming all-clear');
961
+ });
962
+
963
+ t('preflightModules: with everything healthy the note stays the plain all-clear', () => {
964
+ const dir = scratchConsumer();
965
+ mkdirSync(join(dir, 'modules'), { recursive: true });
966
+ const res = u.preflightModules({ instanceDir: dir, targetVersion: '2.0.0' }, {
967
+ moduleLib: moduleLibStub({ ok: true, blocking: [], warnings: [], deprecations: [], expired: [], core: '2.0.0' }),
968
+ });
969
+ assert.equal(res.note, 'all enabled modules satisfy the target');
970
+ assert.deepEqual(res.warnings, []);
971
+ });
972
+
973
+ t('preflightModules: no modules/ dir returns every collection empty (a vanilla consumer)', () => {
974
+ const dir = scratchConsumer();
975
+ const res = u.preflightModules({ instanceDir: dir, targetVersion: '2.0.0' });
976
+ assert.equal(res.ok, true);
977
+ for (const k of ['blocking', 'warnings', 'deprecations', 'expired']) assert.deepEqual(res[k], [], k);
978
+ });
979
+
980
+ t('reportModulePreflight: prints the rotted-but-disabled module WITH its maintainer named', () => {
981
+ const out = [];
982
+ const mods = {
983
+ warnings: [{ key: 'stale-catalog-entry', coreVersion: '^1.0.0', maintainer: 'Ada <ada@example.com>', maintenance: { status: 'maintained' } }],
984
+ deprecations: [], expired: [],
985
+ };
986
+ u.reportModulePreflight(mods, '2.0.0', { log: (s) => out.push(s), err: (s) => out.push(s) });
987
+ const text = out.join('\n');
988
+ assert.match(text, /stale-catalog-entry/);
989
+ assert.match(text, /silently fail to mount/);
990
+ assert.match(text, /maintainer Ada <ada@example\.com>/);
991
+ });
992
+
993
+ t('reportModulePreflight: calls out a deprecation the core has already passed removeAfter', () => {
994
+ const out = [];
995
+ const dep = { key: 'sunset', maintenance: { status: 'deprecated', successor: 'newer', removeAfter: '1.12.0' } };
996
+ u.reportModulePreflight({ warnings: [], deprecations: [dep], expired: [dep] }, '1.13.0', { log: (s) => out.push(s), err: (s) => out.push(s) });
997
+ const text = out.join('\n');
998
+ assert.match(text, /module "sunset" is deprecated/);
999
+ assert.match(text, /PAST its removeAfter 1\.12\.0/);
1000
+ assert.match(text, /due for removal/);
1001
+ });
1002
+
1003
+ t('reportModulePreflight: nothing to say prints nothing', () => {
1004
+ const out = [];
1005
+ const lines = u.reportModulePreflight({ warnings: [], deprecations: [], expired: [] }, '2.0.0', { log: (s) => out.push(s), err: (s) => out.push(s) });
1006
+ assert.deepEqual(lines, []);
1007
+ assert.deepEqual(out, []);
1008
+ });
1009
+
1010
+ t('runUpgrade: a REFUSED bump still reports the non-blocking module findings', async () => {
1011
+ // A refusal is exactly when someone is reading this output, so the warnings must
1012
+ // already have been emitted by the time the blocking check returns.
1013
+ const dir = scratchConsumer();
1014
+ mkdirSync(join(dir, 'modules'), { recursive: true });
1015
+ const out = [];
1016
+ const res = await u.runUpgrade({ to: '2.0.0', instance: dir, force: true, dryRun: true }, {
1017
+ log: (s) => out.push(String(s)),
1018
+ err: (s) => out.push(String(s)),
1019
+ moduleLib: moduleLibStub({
1020
+ ok: false,
1021
+ blocking: [{ key: 'enabled-breaker', coreVersion: '^1.0.0', enabled: true, satisfied: false }],
1022
+ warnings: [{ key: 'stale-catalog-entry', coreVersion: '^1.0.0', maintenance: { status: 'orphaned', note: 'author unreachable' } }],
1023
+ deprecations: [], expired: [], core: '2.0.0',
1024
+ }),
1025
+ });
1026
+ assert.equal(res.ok, false, 'the enabled mismatch still refuses');
1027
+ const text = out.join('\n');
1028
+ assert.match(text, /stale-catalog-entry/, 'the disabled rot is reported even on a refused bump');
1029
+ assert.match(text, /author unreachable/);
1030
+ });
1031
+
938
1032
  await Promise.all(pending);
939
1033
  console.log(`\nupgrade: ${passed} passed, ${failed} failed`);
940
1034
  process.exit(failed ? 1 : 0);