@bongos/core 1.19.63 → 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.
- package/.bongos-core.json +46 -41
- package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +4 -0
- package/docs/adr/0166-module-maintenance-and-deprecation-policy.md +91 -0
- package/docs/adr/README.md +1 -0
- package/docs/modules-contract.md +23 -1
- package/modules/autonomy/module.json +1 -0
- package/modules/builder-settings/module.json +1 -0
- package/modules/dev-box/module.json +1 -0
- package/modules/discord/module.json +1 -0
- package/modules/economy/module.json +1 -0
- package/modules/governance/module.json +1 -0
- package/modules/grading/module.json +1 -0
- package/modules/hall-ui/module.json +1 -0
- package/modules/hall-ui/public/modules.js +17 -1
- package/modules/ideas/module.json +1 -0
- package/modules/lifecycle/db.js +35 -3
- package/modules/lifecycle/module.json +1 -0
- package/modules/lifecycle/routes/claims.js +14 -8
- package/modules/memory/module.json +1 -0
- package/modules/onboarding/module.json +1 -0
- package/modules/platform-identity/module.json +1 -0
- package/modules/provisioning/module.json +1 -0
- package/modules/security/module.json +1 -0
- package/modules/sessions/module.json +1 -0
- package/modules/status-ui/module.json +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/module.js +103 -9
- package/scripts/gds/upgrade.js +50 -5
- package/src/bongos/routes/modules.js +4 -0
- package/src/module-api.js +5 -1
- package/src/module-loader/manifest-schema.js +69 -2
- package/tests/module_cli.mjs +126 -0
- package/tests/module_manifest.mjs +65 -0
- package/tests/one_active_claim_per_session.mjs +74 -0
- package/tests/upgrade.mjs +94 -0
package/tests/module_cli.mjs
CHANGED
|
@@ -418,3 +418,129 @@ test('`bongos module submit` is documented in help / dispatches to module.js sub
|
|
|
418
418
|
assert.ok(lines.some((l) => /module submit/.test(l)));
|
|
419
419
|
assert.equal(await mod.main(['node', 'module.js', 'submit']), 2, 'no key => usage error');
|
|
420
420
|
});
|
|
421
|
+
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
// Maintenance & deprecation policy (ADR 0107 §7 / ADR 0166, task 1001773)
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
|
|
426
|
+
test('evaluateUpgrade: sorts self-declared deprecations, and flags the ones the core has passed removeAfter', () => {
|
|
427
|
+
const v = mod.evaluateUpgrade({
|
|
428
|
+
core: '1.13.0',
|
|
429
|
+
rows: [
|
|
430
|
+
{ key: 'healthy', coreVersion: '^1.0.0', enabled: true, satisfied: true, maintenance: { status: 'core-maintained' } },
|
|
431
|
+
{ key: 'sunset', coreVersion: '^1.0.0', enabled: false, satisfied: true, maintenance: { status: 'deprecated', successor: 'newer', removeAfter: '1.12.0' } },
|
|
432
|
+
{ key: 'sunset-later', coreVersion: '^1.0.0', enabled: false, satisfied: true, maintenance: { status: 'deprecated', note: 'going', removeAfter: '1.20.0' } },
|
|
433
|
+
{ key: 'nobody', coreVersion: '^1.0.0', enabled: false, satisfied: true, maintenance: { status: 'orphaned', note: 'unowned' } },
|
|
434
|
+
],
|
|
435
|
+
});
|
|
436
|
+
assert.equal(v.ok, true, 'a deprecation is not a compatibility failure — it must not block a bump');
|
|
437
|
+
assert.deepEqual(v.deprecations.map((d) => d.key), ['sunset', 'sunset-later', 'nobody']);
|
|
438
|
+
assert.deepEqual(v.expired.map((d) => d.key), ['sunset'], 'core 1.13.0 is past removeAfter 1.12.0 but not 1.20.0');
|
|
439
|
+
assert.deepEqual(v.undeclared, []);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test('evaluateUpgrade: a module with no maintenance block is reported as undeclared (nobody is on the hook)', () => {
|
|
443
|
+
const v = mod.evaluateUpgrade({
|
|
444
|
+
core: '1.0.0',
|
|
445
|
+
rows: [
|
|
446
|
+
{ key: 'owned', coreVersion: '^1.0.0', enabled: true, satisfied: true, maintenance: { status: 'core-maintained' } },
|
|
447
|
+
{ key: 'orphan-risk', coreVersion: '^1.0.0', enabled: false, satisfied: true },
|
|
448
|
+
],
|
|
449
|
+
});
|
|
450
|
+
assert.deepEqual(v.undeclared.map((r) => r.key), ['orphan-risk']);
|
|
451
|
+
assert.equal(v.ok, true, 'an undeclared posture is a reporting gap, not a compatibility failure');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
test('formatMaintenanceHint: names the owner, or says plainly that there is none', () => {
|
|
455
|
+
assert.match(mod.formatMaintenanceHint({ key: 'x' }), /no maintenance declared/);
|
|
456
|
+
assert.match(
|
|
457
|
+
mod.formatMaintenanceHint({ key: 'x', maintainer: 'Ada <ada@example.com>', maintenance: { status: 'maintained' } }),
|
|
458
|
+
/maintainer Ada <ada@example\.com>/,
|
|
459
|
+
);
|
|
460
|
+
assert.match(mod.formatMaintenanceHint({ key: 'x', maintenance: { status: 'maintained' } }), /unnamed!/);
|
|
461
|
+
const sunset = mod.formatMaintenanceHint({ key: 'x', maintenance: { status: 'deprecated', successor: 'y', removeAfter: '1.12.0', note: 'use y' } });
|
|
462
|
+
assert.match(sunset, /superseded by "y"/);
|
|
463
|
+
assert.match(sunset, /removable after core 1\.12\.0/);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test('gatherUpgrade: carries each module\'s maintenance posture onto its row', () => {
|
|
467
|
+
loader._reset();
|
|
468
|
+
const root = tmpModulesRoot();
|
|
469
|
+
mod.scaffoldModule({ key: 'econ', modulesDir: root, core: '1.0.0' });
|
|
470
|
+
const data = mod.gatherUpgrade({ modulesDir: root, core: '1.0.0', env: {}, neutral: {}, instance: {}, prefix: 'CLOUDBONGOS' });
|
|
471
|
+
const row = data.rows.find((r) => r.key === 'econ');
|
|
472
|
+
assert.equal(row.maintenance.status, 'maintained', 'the scaffolder stamps an honest default');
|
|
473
|
+
assert.ok(row.maintainer, 'the maintainer travels with the row so the hint can name them');
|
|
474
|
+
loader._reset();
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test('cmdUpgrade: a DISABLED incompatible module is reported with its owner, not silently dropped', () => {
|
|
478
|
+
// The catalog-rot case: an accepted module ships default:false (ADR 0107 §6), so
|
|
479
|
+
// every rotted catalog entry is disabled by construction. Exit stays 0 — it breaks
|
|
480
|
+
// nothing at boot — but it must be visible and attributed.
|
|
481
|
+
const lines = [];
|
|
482
|
+
const sink = { log: (s) => lines.push(String(s)), errlog: (s) => lines.push(String(s)) };
|
|
483
|
+
const code = mod.cmdUpgrade([], {
|
|
484
|
+
...sink,
|
|
485
|
+
gather: () => ({
|
|
486
|
+
core: '2.0.0',
|
|
487
|
+
errors: [],
|
|
488
|
+
rows: [{
|
|
489
|
+
key: 'stale-catalog-entry', coreVersion: '^1.0.0', enabled: false, satisfied: false,
|
|
490
|
+
maintainer: 'Ada <ada@example.com>', maintenance: { status: 'maintained' },
|
|
491
|
+
}],
|
|
492
|
+
}),
|
|
493
|
+
});
|
|
494
|
+
assert.equal(code, 0, 'a disabled mismatch must not block');
|
|
495
|
+
const out = lines.join('\n');
|
|
496
|
+
assert.match(out, /stale-catalog-entry/);
|
|
497
|
+
assert.match(out, /BROKEN if enabled/);
|
|
498
|
+
assert.match(out, /maintainer Ada <ada@example\.com>/, 'the owner is named so someone can be told');
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test('cmdUpgrade: reports an expired deprecation and modules with no declared upkeep', () => {
|
|
502
|
+
const lines = [];
|
|
503
|
+
const sink = { log: (s) => lines.push(String(s)), errlog: (s) => lines.push(String(s)) };
|
|
504
|
+
const code = mod.cmdUpgrade([], {
|
|
505
|
+
...sink,
|
|
506
|
+
gather: () => ({
|
|
507
|
+
core: '1.13.0',
|
|
508
|
+
errors: [],
|
|
509
|
+
rows: [
|
|
510
|
+
{ key: 'sunset', coreVersion: '^1.0.0', enabled: false, satisfied: true, maintenance: { status: 'deprecated', successor: 'newer', removeAfter: '1.12.0' } },
|
|
511
|
+
{ key: 'unowned', coreVersion: '^1.0.0', enabled: false, satisfied: true },
|
|
512
|
+
],
|
|
513
|
+
}),
|
|
514
|
+
});
|
|
515
|
+
assert.equal(code, 0);
|
|
516
|
+
const out = lines.join('\n');
|
|
517
|
+
assert.match(out, /sunset: deprecated/);
|
|
518
|
+
assert.match(out, /PAST removeAfter 1\.12\.0/);
|
|
519
|
+
assert.match(out, /declare no `maintenance` block/);
|
|
520
|
+
assert.match(out, /unowned/);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test('checkModulePublishability: refuses a module that declares no maintenance posture', () => {
|
|
524
|
+
const root = tmpModulesRoot();
|
|
525
|
+
const { dir } = mod.scaffoldModule({ key: 'unowned', modulesDir: root, core: '1.0.0' });
|
|
526
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'module.json'), 'utf8'));
|
|
527
|
+
delete manifest.maintenance;
|
|
528
|
+
fs.writeFileSync(path.join(dir, 'module.json'), JSON.stringify(manifest, null, 2));
|
|
529
|
+
const r = mod.checkModulePublishability('unowned', { modulesDir: root });
|
|
530
|
+
assert.equal(r.ok, false, 'acceptance is a commitment to upkeep — it must be stated');
|
|
531
|
+
assert.ok(r.findings.some((f) => f.kind === 'maintenance' && /core-maintained/.test(f.detail)), JSON.stringify(r.findings));
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test('checkModulePublishability: refuses a module that is ALREADY deprecated or orphaned', () => {
|
|
535
|
+
const root = tmpModulesRoot();
|
|
536
|
+
for (const status of ['deprecated', 'orphaned']) {
|
|
537
|
+
const key = `sunset-${status}`;
|
|
538
|
+
const { dir } = mod.scaffoldModule({ key, modulesDir: root, core: '1.0.0' });
|
|
539
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'module.json'), 'utf8'));
|
|
540
|
+
manifest.maintenance = { status, note: 'on the way out' };
|
|
541
|
+
fs.writeFileSync(path.join(dir, 'module.json'), JSON.stringify(manifest, null, 2));
|
|
542
|
+
const r = mod.checkModulePublishability(key, { modulesDir: root });
|
|
543
|
+
assert.equal(r.ok, false, `${status} must not be submittable INTO the catalog`);
|
|
544
|
+
assert.ok(r.findings.some((f) => f.kind === 'maintenance' && /way out/.test(f.detail)));
|
|
545
|
+
}
|
|
546
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -183,6 +183,80 @@ t('names the in-flight task in the MATCHING worktree, not the first row', () =>
|
|
|
183
183
|
assert.equal(d.inFlightTitle, 'Two');
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
+
// =========================================================================
|
|
187
|
+
// task 1002522 — the guard and core_204's backstop index must agree EXACTLY on
|
|
188
|
+
// which claims share a tree. The index keys (creator_session_id,
|
|
189
|
+
// COALESCE(worktree_name, '')), so to Postgres '' IS the worktree-less group;
|
|
190
|
+
// the guard compared raw values, where '' !== null. db.foldWorktreeName is the
|
|
191
|
+
// single normalization both sides now use (and claimTask applies before the
|
|
192
|
+
// INSERT, so stored rows are already trimmed-or-NULL).
|
|
193
|
+
// =========================================================================
|
|
194
|
+
const { foldWorktreeName } = require('../modules/lifecycle/db.js');
|
|
195
|
+
|
|
196
|
+
t('foldWorktreeName folds null / empty / whitespace to the same NULL key', () => {
|
|
197
|
+
assert.equal(foldWorktreeName(null), null);
|
|
198
|
+
assert.equal(foldWorktreeName(undefined), null);
|
|
199
|
+
assert.equal(foldWorktreeName(''), null);
|
|
200
|
+
assert.equal(foldWorktreeName(' '), null);
|
|
201
|
+
assert.equal(foldWorktreeName('\t\n'), null);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
t('foldWorktreeName trims but never renames a real worktree', () => {
|
|
205
|
+
assert.equal(foldWorktreeName('wt-a'), 'wt-a');
|
|
206
|
+
assert.equal(foldWorktreeName(' wt-a '), 'wt-a');
|
|
207
|
+
// Distinct trees stay distinct — the fold must not collapse parallel work.
|
|
208
|
+
assert.notEqual(foldWorktreeName('wt-a'), foldWorktreeName('wt-a-2'));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
t("empty-string worktree is the SAME tree as worktree-less → refused (matches the index)", () => {
|
|
212
|
+
// Before the fold this returned allow, and the INSERT then tripped
|
|
213
|
+
// uniq_active_claim_per_session_worktree — a 23505 on a task nobody had taken.
|
|
214
|
+
const d = shouldRefuseSessionClaim({
|
|
215
|
+
creatorSessionId: 'sess-6',
|
|
216
|
+
worktreeName: '',
|
|
217
|
+
activeClaimsForSession: [
|
|
218
|
+
{ claim_id: 50, task_id: 70, creator_session_id: 'sess-6', worktree_name: null, title: 'shared clone' },
|
|
219
|
+
],
|
|
220
|
+
});
|
|
221
|
+
assert.equal(d.refuse, true);
|
|
222
|
+
assert.equal(d.inFlightTaskId, 70);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
t('the mirror case: worktree-less incoming vs a stored empty-string name → refused', () => {
|
|
226
|
+
const d = shouldRefuseSessionClaim({
|
|
227
|
+
creatorSessionId: 'sess-6',
|
|
228
|
+
worktreeName: null,
|
|
229
|
+
activeClaimsForSession: [
|
|
230
|
+
{ claim_id: 51, task_id: 71, creator_session_id: 'sess-6', worktree_name: ' ', title: 'blank name' },
|
|
231
|
+
],
|
|
232
|
+
});
|
|
233
|
+
assert.equal(d.refuse, true);
|
|
234
|
+
assert.equal(d.inFlightTaskId, 71);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
t('whitespace around a real name does not fake a second tree', () => {
|
|
238
|
+
const d = shouldRefuseSessionClaim({
|
|
239
|
+
creatorSessionId: 'sess-6',
|
|
240
|
+
worktreeName: ' wt-a ',
|
|
241
|
+
activeClaimsForSession: [
|
|
242
|
+
{ claim_id: 52, task_id: 72, creator_session_id: 'sess-6', worktree_name: 'wt-a', title: 'A' },
|
|
243
|
+
],
|
|
244
|
+
});
|
|
245
|
+
assert.equal(d.refuse, true);
|
|
246
|
+
assert.equal(d.inFlightTaskId, 72);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
t('the fold does NOT re-break parallel worktrees (the bug this family fixed)', () => {
|
|
250
|
+
const d = shouldRefuseSessionClaim({
|
|
251
|
+
creatorSessionId: 'sess-6',
|
|
252
|
+
worktreeName: 'wt-b',
|
|
253
|
+
activeClaimsForSession: [
|
|
254
|
+
{ claim_id: 53, task_id: 73, creator_session_id: 'sess-6', worktree_name: 'wt-a', title: 'A' },
|
|
255
|
+
],
|
|
256
|
+
});
|
|
257
|
+
assert.equal(d.refuse, false);
|
|
258
|
+
});
|
|
259
|
+
|
|
186
260
|
// =========================================================================
|
|
187
261
|
// task 1002589 (idea 1000638) — the DB backstop's ERROR SURFACE. A claims
|
|
188
262
|
// INSERT can trip TWO partial unique indexes; claimTask used to map every pg
|
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);
|