@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.
- package/.bongos-core.json +55 -40
- 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/module.json +1 -0
- 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/public-landing/create.html +11 -2
- package/public-landing/projects.html +591 -0
- 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/src/platform-server.js +9 -0
- package/tests/module_cli.mjs +126 -0
- package/tests/module_manifest.mjs +65 -0
- package/tests/public_landing_projects.mjs +121 -0
- 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);
|