@bongos/core 1.21.150 → 1.21.152

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 (48) hide show
  1. package/.bongos-core.json +52 -52
  2. package/docs/copy-inventory.md +16 -16
  3. package/docs/copy-registry.json +16 -16
  4. package/docs/module-api-changelog.md +4 -0
  5. package/docs/modules-contract.md +29 -0
  6. package/docs/page-readings.json +27 -26
  7. package/modules/agents/module.json +1 -0
  8. package/modules/autonomy/module.json +1 -0
  9. package/modules/builder-settings/module.json +1 -0
  10. package/modules/copy-desk/module.json +1 -0
  11. package/modules/design-styles/module.json +1 -0
  12. package/modules/discord/module.json +1 -0
  13. package/modules/economy/module.json +1 -0
  14. package/modules/government/module.json +1 -0
  15. package/modules/grading/module.json +1 -0
  16. package/modules/hall-ui/module.json +1 -0
  17. package/modules/hall-ui/public/modules.js +16 -4
  18. package/modules/ideas/module.json +1 -0
  19. package/modules/lifecycle/module.json +1 -0
  20. package/modules/memory/module.json +1 -0
  21. package/modules/npm-release/module.json +1 -0
  22. package/modules/onboarding/module.json +1 -0
  23. package/modules/page-notes/module.json +1 -0
  24. package/modules/pixel-art/module.json +1 -0
  25. package/modules/platform-identity/module.json +1 -0
  26. package/modules/provisioning/module.json +1 -0
  27. package/modules/public-landing/module.json +1 -0
  28. package/modules/render-deploy/module.json +1 -0
  29. package/modules/security/module.json +1 -0
  30. package/modules/sessions/module.json +1 -0
  31. package/modules/specialities/module.json +1 -0
  32. package/modules/status-ui/module.json +1 -0
  33. package/modules/ui-design/module.json +1 -0
  34. package/package-lock.json +2 -2
  35. package/package.json +1 -1
  36. package/release-notes.json +12 -0
  37. package/scripts/gds/credential-presence.js +10 -0
  38. package/scripts/gds/module-test-runner/README.md +3 -3
  39. package/scripts/gds/module-test-runner/module-tests.yml +18 -6
  40. package/scripts/gds/module-test-runner.js +92 -36
  41. package/scripts/gds/module.js +4 -2
  42. package/src/module-api.js +1 -1
  43. package/src/module-loader/catalog.js +15 -1
  44. package/src/module-loader/manifest-schema.js +40 -1
  45. package/tests/hall_catalog_world.mjs +4 -1
  46. package/tests/module_catalog.mjs +34 -0
  47. package/tests/module_manifest.mjs +36 -0
  48. package/tests/module_test_runner.mjs +70 -17
@@ -18,9 +18,12 @@ import { fileURLToPath } from 'node:url';
18
18
  const require = createRequire(import.meta.url);
19
19
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
20
20
  const runner = require('../scripts/gds/module-test-runner.js');
21
- const { runnerConfig, maybeDispatch, collectResults, sweepTick, readZipEntry, validateSignal, runName, artifactName } = runner;
21
+ const { runnerConfig, archiveCore, maybeDispatch, collectResults, sweepTick, readZipEntry, validateSignal, runName, artifactName } = runner;
22
22
 
23
- const CFG = { repo: 'owner/module-tests', token: 't0ken', coreRepo: 'owner/core' };
23
+ const CFG = { repo: 'owner/module-tests', token: 't0ken' };
24
+ const SHA = 'a1'.repeat(20);
25
+ const coreRef = () => SHA;
26
+ const archive = async () => Buffer.from('core-tgz');
24
27
  const VER = { id: 7, module_key: 'weather', version: '1.0.0', artifact_path: 'weather/weather-1.0.0.tgz' };
25
28
  const quiet = { warn() {}, error() {} };
26
29
 
@@ -104,36 +107,75 @@ test('unconfigured: no runner, so Tests stays plain pending and nothing is sent'
104
107
  assert.equal(db.signals.length, 0);
105
108
  });
106
109
 
107
- test('config: owner/name is required, the core repo falls back to git origin', () => {
110
+ test('config: owner/name is required; there is no core repo to name', () => {
108
111
  assert.throws(() => runnerConfig({ MODULE_TEST_REPO: 'nope', MODULE_TEST_TOKEN: 'x' }), /owner\/name/);
109
- const c = runnerConfig({ MODULE_TEST_REPO: 'o/r', MODULE_TEST_TOKEN: 'x' }, { origin: () => 'o/core' });
110
- assert.deepEqual(c, { repo: 'o/r', token: 'x', coreRepo: 'o/core', ref: 'main' });
111
- assert.equal(runnerConfig({ MODULE_TEST_REPO: 'o/r', MODULE_TEST_TOKEN: 'x', MODULE_TEST_REF: 'trunk' }, { origin: () => 'o/core' }).ref, 'trunk');
112
+ assert.deepEqual(runnerConfig({ MODULE_TEST_REPO: 'o/r', MODULE_TEST_TOKEN: 'x', MODULE_TEST_CORE_REPO: 'o/core' }), { repo: 'o/r', token: 'x', ref: 'main' });
113
+ assert.equal(runnerConfig({ MODULE_TEST_REPO: 'o/r', MODULE_TEST_TOKEN: 'x', MODULE_TEST_REF: 'trunk' }).ref, 'trunk');
112
114
  });
113
115
 
114
- test('dispatch pushes the tarball to a throwaway branch, dispatches, and records the nonce', async () => {
116
+ const short = (c) => `${c.method} ${c.url.replace('https://api.github.com/repos/owner/module-tests', '')}`;
117
+ const PUSH = ['POST /git/blobs', 'POST /git/trees', 'POST /git/commits', 'POST /git/refs'];
118
+ const DISPATCH = 'POST /actions/workflows/module-tests.yml/dispatches';
119
+
120
+ test('the first dispatch on a core commit pushes the core to core/<sha>, then the tarball, then dispatches', async () => {
115
121
  const db = fakeDb();
116
122
  const gh = fakeGitHub(dispatchRoutes);
117
- const r = await maybeDispatch(VER, { db, cfg: CFG, fetchImpl: gh.fetchImpl, readTarball: async () => Buffer.from('tgz'), coreRef: () => 'abc123', log: quiet });
123
+ const r = await maybeDispatch(VER, { db, cfg: CFG, fetchImpl: gh.fetchImpl, readTarball: async () => Buffer.from('tgz'), coreRef, archive, log: quiet });
118
124
  assert.match(r.nonce, /^[0-9a-f]{32}$/);
119
125
  assert.equal(r.branch, `module-test/${r.nonce}`);
120
- assert.deepEqual(gh.calls.map((c) => `${c.method} ${c.url.replace('https://api.github.com/repos/owner/module-tests', '')}`), [
121
- 'POST /git/blobs', 'POST /git/trees', 'POST /git/commits', 'POST /git/refs', 'POST /actions/workflows/module-tests.yml/dispatches']);
122
- assert.deepEqual(gh.calls[2].body.parents, [], 'an orphan commit: the branch carries only the tarball');
123
- assert.equal(gh.calls[3].body.ref, `refs/heads/module-test/${r.nonce}`);
124
- assert.equal(gh.calls[4].body.ref, 'main');
125
- assert.deepEqual(gh.calls[4].body.inputs, { version_id: '7', module_key: 'weather', nonce: r.nonce, core_repo: 'owner/core', core_ref: 'abc123' });
126
+ assert.equal(r.core_ref, SHA);
127
+ assert.deepEqual(gh.calls.map(short), [`GET /git/ref/heads/core/${SHA}`, ...PUSH, ...PUSH, DISPATCH]);
128
+ assert.equal(Buffer.from(gh.calls[1].body.content, 'base64').toString(), 'core-tgz');
129
+ assert.equal(gh.calls[2].body.tree[0].path, 'core.tgz');
130
+ assert.deepEqual(gh.calls[3].body.parents, [], 'the core snapshot is an orphan commit');
131
+ assert.equal(gh.calls[4].body.ref, `refs/heads/core/${SHA}`);
132
+ assert.deepEqual(gh.calls[7].body.parents, [], 'an orphan commit: the branch carries only the tarball');
133
+ assert.equal(gh.calls[8].body.ref, `refs/heads/module-test/${r.nonce}`);
134
+ assert.equal(gh.calls[9].body.ref, 'main');
135
+ assert.deepEqual(gh.calls[9].body.inputs, { version_id: '7', module_key: 'weather', nonce: r.nonce, core_ref: SHA });
126
136
  assert.equal(db.signals.length, 1);
127
137
  assert.equal(db.signals[0].outcome, 'pending');
128
138
  assert.equal(db.signals[0].detail.runner.nonce, r.nonce);
129
139
  });
130
140
 
141
+ test('a later dispatch on the same core commit reuses core/<sha> and never archives again', async () => {
142
+ const gh = fakeGitHub({ ...dispatchRoutes, [`GET /git/ref/heads/core/${SHA}$`]: { ref: `refs/heads/core/${SHA}` } });
143
+ let archived = 0;
144
+ const r = await maybeDispatch(VER, { db: fakeDb(), cfg: CFG, fetchImpl: gh.fetchImpl, readTarball: async () => Buffer.from('tgz'), coreRef, archive: async () => { archived += 1; return Buffer.from('x'); }, log: quiet });
145
+ assert.ok(r);
146
+ assert.equal(archived, 0);
147
+ assert.deepEqual(gh.calls.map(short), [`GET /git/ref/heads/core/${SHA}`, ...PUSH, DISPATCH]);
148
+ assert.equal(gh.calls[1].body.content, Buffer.from('tgz').toString('base64'), 'only the module tarball is pushed');
149
+ });
150
+
151
+ test('two dispatches racing on a new commit: the loser\'s refused ref create still dispatches', async () => {
152
+ const gh = fakeGitHub({ ...dispatchRoutes, 'POST /git/refs$': (url, init) => (JSON.parse(init.body).ref.startsWith('refs/heads/core/') ? new Response('{"message":"Reference already exists"}', { status: 422 }) : { ref: 'x' }) });
153
+ assert.ok(await maybeDispatch(VER, { db: fakeDb(), cfg: CFG, fetchImpl: gh.fetchImpl, readTarball: async () => Buffer.from('tgz'), coreRef, archive, log: quiet }));
154
+ assert.equal(gh.calls.at(-1).url.endsWith('/dispatches'), true);
155
+ });
156
+
157
+ test('an oversized core snapshot, or an unknown core commit, fails the dispatch loudly and pushes nothing', async () => {
158
+ for (const opts of [{ archive: async () => Buffer.alloc(73 * 1024 * 1024) }, { coreRef: () => null, archive }]) {
159
+ const db = fakeDb();
160
+ const gh = fakeGitHub(dispatchRoutes);
161
+ const errors = [];
162
+ assert.equal(await maybeDispatch(VER, { db, cfg: CFG, fetchImpl: gh.fetchImpl, readTarball: async () => Buffer.from('tgz'), coreRef, log: { warn() {}, error: (m) => errors.push(m) }, ...opts }), null);
163
+ assert.match(errors[0], /over the 72 MB|core commit is unknown/);
164
+ assert.ok(!gh.calls.some((c) => c.method === 'POST'), 'nothing was pushed or dispatched');
165
+ assert.equal(db.signals.length, 0);
166
+ }
167
+ });
168
+
169
+ test('a core commit git cannot find fails with a cause a person can act on, not git\'s bare error', async () => {
170
+ await assert.rejects(archiveCore('0'.repeat(40)), /must run from a git checkout that holds it/);
171
+ });
172
+
131
173
  test('a version with a real Tests result, or a run already in flight, is not dispatched again', async () => {
132
174
  for (const existing of [{ outcome: 'scored', detail: {} }, { outcome: 'pending', detail: { runner: { nonce: 'a'.repeat(32) } } }]) {
133
175
  const db = fakeDb();
134
176
  db.signals.push({ id: 1, version_id: 7, part: 'tests', ...existing });
135
177
  const gh = fakeGitHub(dispatchRoutes);
136
- assert.equal(await maybeDispatch(VER, { db, cfg: CFG, fetchImpl: gh.fetchImpl, readTarball: async () => Buffer.from('x'), coreRef: () => 'c', log: quiet }), null);
178
+ assert.equal(await maybeDispatch(VER, { db, cfg: CFG, fetchImpl: gh.fetchImpl, readTarball: async () => Buffer.from('x'), coreRef, archive, log: quiet }), null);
137
179
  assert.equal(gh.calls.length, 0);
138
180
  }
139
181
  });
@@ -142,13 +184,13 @@ test('a plain pending (from before the runner was set up) is dispatched', async
142
184
  const db = fakeDb();
143
185
  db.signals.push({ id: 1, version_id: 7, part: 'tests', outcome: 'pending', detail: {} });
144
186
  const gh = fakeGitHub(dispatchRoutes);
145
- assert.ok(await maybeDispatch(VER, { db, cfg: CFG, fetchImpl: gh.fetchImpl, readTarball: async () => Buffer.from('x'), coreRef: () => 'c', log: quiet }));
187
+ assert.ok(await maybeDispatch(VER, { db, cfg: CFG, fetchImpl: gh.fetchImpl, readTarball: async () => Buffer.from('x'), coreRef, archive, log: quiet }));
146
188
  });
147
189
 
148
190
  test('a failed dispatch deletes the branch, records nothing and never throws', async () => {
149
191
  const db = fakeDb();
150
192
  const gh = fakeGitHub({ ...dispatchRoutes, 'POST /dispatches$': () => new Response('{"message":"nope"}', { status: 422 }), 'DELETE /git/refs/heads/module-test/': null });
151
- assert.equal(await maybeDispatch(VER, { db, cfg: CFG, fetchImpl: gh.fetchImpl, readTarball: async () => Buffer.from('x'), coreRef: () => 'c', log: quiet }), null);
193
+ assert.equal(await maybeDispatch(VER, { db, cfg: CFG, fetchImpl: gh.fetchImpl, readTarball: async () => Buffer.from('x'), coreRef, archive, log: quiet }), null);
152
194
  assert.equal(gh.calls.at(-1).method, 'DELETE');
153
195
  assert.equal(db.signals.length, 0);
154
196
  });
@@ -274,7 +316,18 @@ test('the workflow template: no permissions for the job that runs module code, n
274
316
  assert.match(yml, /^run-name: module-tests \$\{\{ inputs\.version_id \}\} \$\{\{ inputs\.nonce \}\}$/m);
275
317
  assert.equal(runName(7, 'n'), 'module-tests 7 n', 'collect matches the template\'s run-name');
276
318
  const testJob = yml.slice(yml.indexOf('\n test:'));
319
+ const fetchJob = yml.slice(yml.indexOf('\n fetch:'), yml.indexOf('\n test:'));
277
320
  assert.match(testJob, /permissions: \{\}/);
321
+ // The core repo is private (task 1004739): the test job checks out nothing by
322
+ // name; the fetch job reads the snapshot dispatch pushed to core/<sha> and hands
323
+ // it over as an artifact.
324
+ assert.doesNotMatch(testJob, /actions\/checkout/, 'the job that runs module code checks out no repo');
325
+ assert.doesNotMatch(yml, /repository:|core_repo/, 'no repo is named anywhere');
326
+ assert.match(fetchJob, /ref: "core\/\$\{\{ inputs\.core_ref \}\}"/);
327
+ assert.match(fetchJob, /name: "core-\$\{\{ inputs\.nonce \}\}", path: core-snapshot\/core\.tgz/);
328
+ assert.match(fetchJob, /\^\[0-9a-f\]\{40\}\$/, 'the core ref is checked before it is used');
329
+ assert.match(testJob, /name: "core-\$\{\{ inputs\.nonce \}\}", path: core-in/);
330
+ assert.ok(testJob.indexOf('Unpack the core') < testJob.indexOf('npm ci'), 'the core is unpacked before it is installed');
278
331
  assert.match(testJob, /MODULE_TEST_SANDBOX=1/);
279
332
  // Module code runs as an unprivileged user whose packets the firewall rejects off loopback.
280
333
  const cut = testJob.indexOf('Cut the network');