@bongos/core 1.19.64 → 1.19.66

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 (36) hide show
  1. package/.bongos-core.json +55 -40
  2. package/docs/adr/0166-module-maintenance-and-deprecation-policy.md +91 -0
  3. package/docs/adr/README.md +1 -0
  4. package/docs/modules-contract.md +23 -1
  5. package/modules/autonomy/module.json +1 -0
  6. package/modules/builder-settings/module.json +1 -0
  7. package/modules/dev-box/module.json +1 -0
  8. package/modules/discord/module.json +1 -0
  9. package/modules/economy/module.json +1 -0
  10. package/modules/governance/module.json +1 -0
  11. package/modules/grading/module.json +1 -0
  12. package/modules/hall-ui/module.json +1 -0
  13. package/modules/hall-ui/public/modules.js +17 -1
  14. package/modules/ideas/module.json +1 -0
  15. package/modules/lifecycle/module.json +1 -0
  16. package/modules/memory/module.json +1 -0
  17. package/modules/onboarding/module.json +1 -0
  18. package/modules/platform-identity/module.json +1 -0
  19. package/modules/provisioning/module.json +1 -0
  20. package/modules/security/module.json +1 -0
  21. package/modules/sessions/module.json +1 -0
  22. package/modules/status-ui/module.json +1 -0
  23. package/package-lock.json +2 -2
  24. package/package.json +1 -1
  25. package/public-landing/create.html +11 -2
  26. package/public-landing/projects.html +591 -0
  27. package/scripts/gds/module.js +103 -9
  28. package/scripts/gds/upgrade.js +50 -5
  29. package/src/bongos/routes/modules.js +4 -0
  30. package/src/module-api.js +5 -1
  31. package/src/module-loader/manifest-schema.js +69 -2
  32. package/src/platform-server.js +9 -0
  33. package/tests/module_cli.mjs +126 -0
  34. package/tests/module_manifest.mjs +65 -0
  35. package/tests/public_landing_projects.mjs +121 -0
  36. package/tests/upgrade.mjs +94 -0
@@ -0,0 +1,121 @@
1
+ // tests/public_landing_projects.mjs — the owner's my-projects surface (task
2
+ // 1002693, goal 1000059 / gap 6 of the creation-flow audit): after the /create
3
+ // tab closes an owner was blind to their own instances — only create.html read
4
+ // GET /provisioning/instances, and it was sessionStorage-bound. /projects is the
5
+ // durable place to find, watch, and manage them without the original tab.
6
+ //
7
+ // Covers (DB-free — static HTML over the platform app, runs in the unit gate):
8
+ // • GET /projects on the apex serves the branding-injected projects page
9
+ // • /projects is apex-only — the builders/status hosts never serve it (the
10
+ // /create precedent: landing pages belong to the apex host)
11
+ // • the page drives ONLY the own-scoped provisioning surface: list, per-id
12
+ // watch (the create.html poll pattern), teardown, domain-attach — and
13
+ // carries the truthful refusal copy for each (task 1002691/1002692 lessons:
14
+ // intent_conflict, not_teardownable, bad_domain, domain_taken,
15
+ // domain_already_set, domain_not_attachable are all SAID, never swallowed)
16
+ // • an instance with no domain gets an HONEST no-address state, not a blank
17
+ // • create.html's poll give-up now points at /projects — the false "the hall
18
+ // has it" stop (the hall my-projects card was removed in task 1002675) is gone
19
+ //
20
+ // Run: node --test tests/public_landing_projects.mjs
21
+
22
+ import assert from 'node:assert/strict';
23
+ import { test, before, after } from 'node:test';
24
+ import http from 'node:http';
25
+ import platformMod from '../src/platform-server.js';
26
+
27
+ const { buildPlatformApp } = platformMod;
28
+
29
+ let server;
30
+ let port;
31
+
32
+ before(async () => {
33
+ const app = buildPlatformApp();
34
+ server = http.createServer(app);
35
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
36
+ port = server.address().port;
37
+ });
38
+
39
+ after(async () => {
40
+ await new Promise((resolve) => server.close(resolve));
41
+ });
42
+
43
+ // Explicit Host header so the apex / builders.* / status.* routing is exercised
44
+ // (fetch treats `host` as forbidden — the platform_boot.mjs helper).
45
+ function request(pathname, host = '127.0.0.1') {
46
+ return new Promise((resolve, reject) => {
47
+ const req = http.request(
48
+ { host: '127.0.0.1', port, path: pathname, method: 'GET', headers: { Host: host } },
49
+ (res) => {
50
+ let body = '';
51
+ res.on('data', (c) => (body += c));
52
+ res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body }));
53
+ }
54
+ );
55
+ req.on('error', reject);
56
+ req.end();
57
+ });
58
+ }
59
+
60
+ test('the apex host serves the branded /projects page', async () => {
61
+ const res = await request('/projects', 'game.test.local');
62
+ assert.equal(res.status, 200);
63
+ assert.match(res.headers['content-type'] || '', /text\/html/);
64
+ // branding-injected exactly like the landing + /create (same landingPage memo)
65
+ assert.match(res.body, /window\.__BRANDING__=/);
66
+ // the page's own shell, not the 404 tail or a stray document
67
+ assert.match(res.body, /id="projectsList"/);
68
+ assert.doesNotMatch(res.body, /404 — not found/);
69
+ });
70
+
71
+ test('/projects is apex-only — the builders and status hosts never serve it', async () => {
72
+ for (const host of ['builders.test.local', 'status.test.local']) {
73
+ const res = await request('/projects', host);
74
+ assert.doesNotMatch(res.body, /id="projectsList"/, `${host} must not serve the projects page`);
75
+ }
76
+ });
77
+
78
+ test('the page drives the own-scoped provisioning surface: list, watch, teardown, domain', async () => {
79
+ const { body } = await request('/projects', 'game.test.local');
80
+ // list + per-id watch (the create.html:1034 poll pattern — GET, own-scoped)
81
+ assert.match(body, /provisioning\/instances/);
82
+ // manage: request teardown + attach an address — both ENQUEUE-only routes
83
+ // (ADR 0111 §2: this page can only ask; the control-plane runner acts)
84
+ assert.match(body, /\/teardown/);
85
+ assert.match(body, /\/domain/);
86
+ // it never reaches for archon/fleet surfaces
87
+ assert.doesNotMatch(body, /provisioning\/fleet/);
88
+ });
89
+
90
+ test('every truthful refusal the API can give is mapped to human copy, never swallowed', async () => {
91
+ const { body } = await request('/projects', 'game.test.local');
92
+ for (const code of [
93
+ 'intent_conflict', // an open run holds the slot (tasks 1002691/1002692)
94
+ 'not_teardownable', // teardown refused for the real state
95
+ 'bad_domain', // malformed hostname
96
+ 'domain_taken', // another instance already answers there
97
+ 'domain_already_set', // re-pointing is not this surface
98
+ 'domain_not_attachable', // mid-flight/torn-down states refuse truthfully
99
+ ]) {
100
+ assert.match(body, new RegExp(code), `the page must handle '${code}' explicitly`);
101
+ }
102
+ });
103
+
104
+ test('a domainless instance gets an honest no-address state', async () => {
105
+ const { body } = await request('/projects', 'game.test.local');
106
+ assert.match(body, /[Nn]o address yet/);
107
+ });
108
+
109
+ test('the empty state points a new owner at /create — never at a hall', async () => {
110
+ const { body } = await request('/projects', 'game.test.local');
111
+ assert.match(body, /href="\/create"/);
112
+ // the hall my-projects card was REMOVED (task 1002675; hall my-projects is
113
+ // federation-only) — this page must not send an owner there
114
+ assert.doesNotMatch(body, /Enter the Hall/i);
115
+ });
116
+
117
+ test('create.html: the poll give-up points at /projects — "the hall has it" is dead', async () => {
118
+ const { body } = await request('/create', 'game.test.local');
119
+ assert.match(body, /\/projects/, 'the give-up path must hand the owner the projects page');
120
+ assert.doesNotMatch(body, /the hall has it/, 'the false poll-stop copy must be gone');
121
+ });
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);