@bongos/core 1.20.36 → 1.20.38

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 (53) hide show
  1. package/.bongos-core.json +104 -49
  2. package/.claude/skills/backlog-review/SKILL.md +1 -1
  3. package/.claude/skills/bongos-feedback/SKILL.md +66 -0
  4. package/.claude/skills/collab-review/SKILL.md +1 -1
  5. package/.claude/skills/priority-session/SKILL.md +1 -1
  6. package/.claude/skills/session-handoff/SKILL.md +1 -1
  7. package/.claude/skills/status/SKILL.md +1 -1
  8. package/bin/bongos.js +1 -0
  9. package/clients/bongos-client/README.md +1 -1
  10. package/clients/bongos-client/bongos-client.global.js +8 -0
  11. package/clients/bongos-client/index.cjs +8 -0
  12. package/clients/bongos-client/index.d.ts +12 -1
  13. package/clients/bongos-client/index.mjs +8 -0
  14. package/docs/adr/0136-update-channel-subscription-policy.md +1 -1
  15. package/docs/adr/0357-the-update-rule-set-on-deploy-is-the-one-the-sweep-follows.md +41 -0
  16. package/docs/adr/README.md +1 -0
  17. package/docs/api/openapi.json +188 -2
  18. package/docs/api-reference.md +12 -4
  19. package/docs/architecture.md +2 -0
  20. package/docs/copy-inventory.md +9 -9
  21. package/docs/copy-registry.json +9 -9
  22. package/docs/file-map.md +1 -0
  23. package/docs/module-api-changelog.md +4 -0
  24. package/docs/page-readings.json +2 -2
  25. package/docs/recipes/upgrading-the-core.md +2 -0
  26. package/modules/autonomy/db.js +39 -8
  27. package/modules/autonomy/fence.js +26 -4
  28. package/modules/autonomy/migrations/autonomy_003_priority_goal.sql +32 -0
  29. package/modules/autonomy/routes/autonomy.js +28 -1
  30. package/modules/hall-ui/public/gate.js +33 -4
  31. package/modules/ideas/routing.js +8 -1
  32. package/modules/platform-identity/instance-feedback.js +142 -0
  33. package/modules/platform-identity/module.json +1 -1
  34. package/modules/platform-identity/routes/sso.js +62 -0
  35. package/package-lock.json +2 -2
  36. package/package.json +1 -1
  37. package/release-notes.json +30 -0
  38. package/scripts/gds/autobongos-run.js +121 -16
  39. package/scripts/gds/feedback-send.js +131 -0
  40. package/scripts/gds/module-assess-tests.js +248 -0
  41. package/scripts/gds/update-channel.js +82 -0
  42. package/src/bongos/routes/feedback.js +110 -0
  43. package/src/bongos/routes.js +2 -0
  44. package/src/module-api.js +1 -1
  45. package/tests/autobongos_fence.mjs +62 -0
  46. package/tests/autobongos_loop.mjs +281 -0
  47. package/tests/autonomy_fence_priority.mjs +211 -0
  48. package/tests/bongos_cli.mjs +7 -1
  49. package/tests/filing_abuse_matrix.mjs +7 -2
  50. package/tests/idea_routing.mjs +1 -1
  51. package/tests/instance_feedback.mjs +327 -0
  52. package/tests/module_assess_tests.mjs +232 -0
  53. package/tests/update_channel_db.mjs +124 -0
@@ -0,0 +1,327 @@
1
+ // tests/instance_feedback.mjs — `bongos feedback`: a builder on a managed project
2
+ // reports something about Bongos itself, their project relays it to the hub, and
3
+ // the hub files it (task 1004462). DB-free: the hub half is driven through its
4
+ // pure module with a fake `ideas.capture`, the project half through its real
5
+ // router with a fake hub.
6
+
7
+ import assert from 'node:assert/strict';
8
+ import { test } from 'node:test';
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { createRequire } from 'node:module';
12
+ import { fileURLToPath } from 'node:url';
13
+
14
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
15
+ const require_ = createRequire(import.meta.url);
16
+ const IF = require_(path.join(ROOT, 'modules/platform-identity/instance-feedback.js'));
17
+ const { decideLanding } = require_(path.join(ROOT, 'modules/ideas/routing.js'));
18
+
19
+ const CLIENT = { client_id: 'acme', name: 'Acme', origin: 'https://acme.example.com' };
20
+
21
+ // ── the hub: who gets to file, and where it lands ─────────────────────────────
22
+
23
+ test('a relayed bug lands as a homeless bug AWAITING A NOD, never claimable', () => {
24
+ // The owner's rule (2026-09-30): every report waits. The relay files with no
25
+ // rank, so no sender — the hub's own Archon included — can skip the nod.
26
+ const d = decideLanding({ goalId: null, kind: 'bug', filerRank: null, vector: 'instance-feedback' });
27
+ assert.deepEqual(d, { route: true, status: 'backlog', homeless: true, reason: 'bug_homeless_awaiting_nod' });
28
+ });
29
+
30
+ test('a relayed idea is not a defect — it stays in the inbox', () => {
31
+ const d = decideLanding({ goalId: null, kind: IF.FEEDBACK_KINDS.idea, filerRank: null, vector: 'instance-feedback' });
32
+ assert.equal(d.route, false);
33
+ });
34
+
35
+ test('fileInstanceFeedback files with its own vector, no rank, and the hub\'s own login', async () => {
36
+ let captured = null;
37
+ const landing = await IF.fileInstanceFeedback({
38
+ client: CLIENT, githubId: '42', message: 'claim hung\nfor two minutes', kind: 'bug',
39
+ context: { core_version: '1.19.900', cli_version: '0.2.0', os: 'darwin 24.6.0' },
40
+ }, {
41
+ capture: async (c) => { captured = c; return { id: 7, routing: { routed: true, task_id: 1234, goal_id: 1000118, status: 'backlog' } }; },
42
+ getAccount: async () => ({ github_login: 'hub-login' }),
43
+ hubBuilderId: async () => 99,
44
+ });
45
+ assert.equal(captured.vector, 'instance-feedback');
46
+ assert.equal(captured.filerRank, null);
47
+ assert.equal(captured.kind, 'bug');
48
+ assert.equal(captured.capturedBy, 99);
49
+ assert.equal(captured.title, 'Feedback: claim hung');
50
+ assert.match(captured.bodyMd, /Sent by: @hub-login/);
51
+ assert.match(captured.bodyMd, /Project: Acme \(https:\/\/acme\.example\.com\)/);
52
+ assert.match(captured.bodyMd, /Core 1\.19\.900 · CLI 0\.2\.0 · OS darwin 24\.6\.0/);
53
+ assert.deepEqual(landing, { landed: 'task', task_id: '1234', goal_id: '1000118', status: 'backlog', idea_id: '7' });
54
+ });
55
+
56
+ test('an unrouted filing reports the inbox, and an idea is filed as a feature', async () => {
57
+ let kind = null;
58
+ const landing = await IF.fileInstanceFeedback({ client: CLIENT, githubId: '42', message: 'wish', kind: 'idea' }, {
59
+ capture: async (c) => { kind = c.kind; return { id: 8, routing: { routed: false, reason: 'no_goal' } }; },
60
+ getAccount: async () => null,
61
+ hubBuilderId: async () => null,
62
+ });
63
+ assert.equal(kind, 'feature');
64
+ assert.deepEqual(landing, { landed: 'inbox', idea_id: '8' });
65
+ });
66
+
67
+ test('version stamps cannot carry markdown or links into the task', () => {
68
+ const body = IF.feedbackBody({
69
+ message: 'x', client: CLIENT, account: null, githubId: '42',
70
+ context: { core_version: '[click](https://evil.example)', cli_version: '0.2.0', os: 'linux\n- Sent by: @owner' },
71
+ });
72
+ assert.doesNotMatch(body, /evil/);
73
+ assert.doesNotMatch(body, /@owner/);
74
+ // The sender's own text is quoted below the attribution, so it cannot pose as it.
75
+ const spoof = IF.feedbackBody({ message: 'hi\n- Sent by: @owner', client: CLIENT, account: { github_login: 'real' }, githubId: '42' });
76
+ assert.ok(spoof.indexOf('- Sent by: @real') < spoof.indexOf('> - Sent by: @owner'));
77
+ assert.doesNotMatch(spoof, /^- Sent by: @owner/m);
78
+ assert.match(body, /CLI 0\.2\.0/);
79
+ assert.match(body, /Sent by: GitHub user 42/, 'no hub account → the id, never an instance-claimed login');
80
+ });
81
+
82
+ test('a long first line is trimmed to a title', () => {
83
+ const t = IF.feedbackTitle('a'.repeat(500));
84
+ assert.ok(t.length <= 'Feedback: '.length + 120);
85
+ assert.ok(t.endsWith('…'));
86
+ });
87
+
88
+ test('the per-sender cap stops one builder; the per-project cap stops one project', () => {
89
+ let now = 1_000_000;
90
+ const lim = IF.makeFeedbackLimiter({ now: () => now });
91
+ for (let i = 0; i < IF.PER_SENDER_LIMIT; i++) assert.equal(lim.take('acme', '1').ok, true);
92
+ const refused = lim.take('acme', '1');
93
+ assert.equal(refused.ok, false);
94
+ assert.equal(refused.scope, 'sender');
95
+ // A refusal spends nothing: the project still has exactly LIMIT - 10 left.
96
+ let sent = IF.PER_SENDER_LIMIT;
97
+ for (let s = 2; sent < IF.PER_PROJECT_LIMIT; s++) {
98
+ for (let i = 0; i < IF.PER_SENDER_LIMIT && sent < IF.PER_PROJECT_LIMIT; i++, sent++) {
99
+ assert.equal(lim.take('acme', String(s)).ok, true);
100
+ }
101
+ }
102
+ const projectRefused = lim.take('acme', '999');
103
+ assert.equal(projectRefused.ok, false);
104
+ assert.equal(projectRefused.scope, 'project');
105
+ assert.equal(lim.take('other', '1').ok, true, 'another project is untouched');
106
+ now += IF.FEEDBACK_WINDOW_MS;
107
+ assert.equal(lim.take('acme', '1').ok, true, 'the window slides');
108
+ });
109
+
110
+ test('the hub route authenticates the project and the sender BEFORE spending or filing', () => {
111
+ const src = fs.readFileSync(path.join(ROOT, 'modules/platform-identity/routes/sso.js'), 'utf8');
112
+ const route = src.slice(src.indexOf("router.post('/sso/feedback'"));
113
+ const at = (s) => { const i = route.indexOf(s); assert.ok(i > 0, `route has ${s}`); return i; };
114
+ assert.ok(at('clientSecretValid') < at('membershipBacksRollup'));
115
+ assert.ok(at('membershipBacksRollup') < at('feedbackLimiter.take'));
116
+ assert.ok(at('feedbackLimiter.take') < at('fileInstanceFeedback'));
117
+ assert.doesNotMatch(route.slice(0, route.indexOf('return router')), /filerRank|vector:/,
118
+ 'the route passes neither a rank nor a vector — both are fixed in instance-feedback.js');
119
+ });
120
+
121
+ // ── the project: the relay ────────────────────────────────────────────────────
122
+
123
+ async function withRoute({ idp, hub }, fn) {
124
+ const r2 = createRequire(import.meta.url);
125
+ const authPath = r2.resolve(path.join(ROOT, 'src/bongos/auth.js'));
126
+ const routePath = r2.resolve(path.join(ROOT, 'src/bongos/routes/feedback.js'));
127
+ const envPath = r2.resolve(path.join(ROOT, 'src/bongos/middleware/error-envelope.js'));
128
+ const realAuth = r2(authPath);
129
+ const saved = realAuth.requireBuilder;
130
+ realAuth.requireBuilder = (req, _res, next) => { req.builder = { id: 5, github_id: '42', github_login: 'someone' }; next(); };
131
+ delete r2.cache[routePath];
132
+ const sent = [];
133
+ const fetchImpl = async (url, opts) => {
134
+ sent.push({ url, body: JSON.parse(opts.body) });
135
+ const { status, body } = hub;
136
+ return { status, json: async () => body };
137
+ };
138
+ const express = r2('express');
139
+ const app = express();
140
+ app.use(express.json());
141
+ app.use(r2(envPath).attachFail);
142
+ app.use(r2(routePath)({ idpConfig: () => idp, fetchImpl }));
143
+ const server = app.listen(0);
144
+ await new Promise((res) => server.once('listening', res));
145
+ const post = (body) => fetch(`http://127.0.0.1:${server.address().port}/feedback`, {
146
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
147
+ }).then(async (r) => ({ status: r.status, body: await r.json() }));
148
+ try {
149
+ await fn({ post, sent });
150
+ } finally {
151
+ server.close();
152
+ realAuth.requireBuilder = saved;
153
+ delete r2.cache[routePath];
154
+ }
155
+ }
156
+
157
+ const IDP = { origin: 'https://hub.example.com', clientId: 'acme', clientSecret: 's3cret' };
158
+
159
+ test('the relay sends the SESSION\'s identity and the running core to the hub, and returns the landing', async () => {
160
+ await withRoute({ idp: IDP, hub: { status: 201, body: { ok: true, landed: 'task', task_id: '1234', status: 'backlog' } } }, async ({ post, sent }) => {
161
+ assert.equal((await post({ message: 'x', github_id: '1' })).status, 400, 'a body naming a sender is refused outright');
162
+ assert.equal(sent.length, 0);
163
+ const r = await post({ message: ' it broke ', context: { cli_version: '0.2.0', os: 'darwin', core_version: 'forged' } });
164
+ assert.equal(r.status, 201);
165
+ assert.equal(r.body.task_id, '1234');
166
+ assert.equal(r.body.hub_origin, IDP.origin);
167
+ assert.equal(sent.length, 1);
168
+ assert.equal(sent[0].url, 'https://hub.example.com/api/bongos/sso/feedback');
169
+ assert.equal(sent[0].body.github_id, '42', 'from the session, never the body');
170
+ assert.equal(sent[0].body.message, 'it broke');
171
+ assert.equal(sent[0].body.kind, 'bug');
172
+ assert.notEqual(sent[0].body.context.core_version, 'forged', 'the core version is this server\'s own');
173
+ assert.equal(sent[0].body.client_secret, 's3cret');
174
+ });
175
+ });
176
+
177
+ test('a project with no hub link says so, and sends nothing', async () => {
178
+ await withRoute({ idp: null, hub: { status: 500, body: {} } }, async ({ post, sent }) => {
179
+ const r = await post({ message: 'hello' });
180
+ assert.equal(r.status, 409);
181
+ assert.equal(r.body.error.code, 'not_hub_connected');
182
+ assert.equal(sent.length, 0);
183
+ });
184
+ });
185
+
186
+ test('an empty message or an unknown kind is refused locally', async () => {
187
+ await withRoute({ idp: IDP, hub: { status: 201, body: { ok: true } } }, async ({ post, sent }) => {
188
+ assert.equal((await post({ message: ' ' })).body.error.code, 'message_required');
189
+ const badKind = await post({ message: 'x', kind: 'task' });
190
+ assert.equal(badKind.status, 400);
191
+ assert.equal((await post({ message: 'x'.repeat(4001) })).status, 400, 'over the cap');
192
+ assert.equal(sent.length, 0);
193
+ });
194
+ });
195
+
196
+ test('a hub refusal the builder can act on is passed through; anything else is generic', async () => {
197
+ await withRoute({ idp: IDP, hub: { status: 429, body: { error: { code: 'rate_limited', details: { retry_after_seconds: 600 } } } } }, async ({ post }) => {
198
+ const r = await post({ message: 'x' });
199
+ assert.equal(r.status, 429);
200
+ assert.equal(r.body.error.code, 'rate_limited');
201
+ assert.equal(r.body.error.details.retry_after_seconds, 600);
202
+ });
203
+ await withRoute({ idp: IDP, hub: { status: 401, body: { error: { code: 'invalid_client', message: 'secret mismatch' } } } }, async ({ post }) => {
204
+ const r = await post({ message: 'x' });
205
+ assert.equal(r.status, 502);
206
+ assert.equal(r.body.error.code, 'hub_refused');
207
+ assert.doesNotMatch(JSON.stringify(r.body), /secret/);
208
+ });
209
+ });
210
+
211
+ // ── the CLI ───────────────────────────────────────────────────────────────────
212
+
213
+ test('`bongos feedback` is a registered verb, and every refusal code has a plain line', () => {
214
+ const { VERBS } = require_(path.join(ROOT, 'bin/bongos.js'));
215
+ assert.equal(VERBS.feedback.script, 'feedback-send.js');
216
+ const { REFUSALS, readMessage } = require_(path.join(ROOT, 'scripts/gds/feedback-send.js'));
217
+ for (const code of ['message_required', 'not_hub_connected', 'membership_required', 'rate_limited', 'feedback_unavailable', 'hub_unreachable']) {
218
+ assert.ok(REFUSALS[code], code);
219
+ }
220
+ for (const code of ['validation_failed']) {
221
+ assert.ok(REFUSALS[code], code);
222
+ }
223
+ assert.equal(readMessage(['--idea', 'filter', 'by', 'goal']), 'filter by goal');
224
+ });
225
+
226
+ // ── the hub route, driven for real ────────────────────────────────────────────
227
+ // The assembled /sso router with its DB edges stubbed: the client lookup, the
228
+ // membership read and the account read on the platform-identity domain, the
229
+ // builders lookup on the doorway's pool, and the `ideas.capture` port.
230
+
231
+ async function withHub(overrides, fn) {
232
+ const r2 = createRequire(import.meta.url);
233
+ const api = r2(path.join(ROOT, 'src/module-api.js'));
234
+ const seams = r2(path.join(ROOT, 'src/module-seams.js'));
235
+ const pi = r2(path.join(ROOT, 'modules/platform-identity/platform-identity.js'));
236
+ const saved = { ...pi };
237
+ const savedQuery = api.pool.query;
238
+ const captured = [];
239
+ Object.assign(pi, {
240
+ getClient: async (id) => (id === 'acme' ? { ...CLIENT, status: 'active' } : null),
241
+ clientSecretValid: (_c, s) => s === 'right',
242
+ getMembership: async () => ({ membership_kind: 'member', signin_verified_at: new Date() }),
243
+ getAccount: async () => ({ github_login: 'hub-login' }),
244
+ }, overrides);
245
+ api.pool.query = async () => ({ rows: [] });
246
+ if (!seams.hasProvider('ideas.capture')) {
247
+ seams.registerProvider('ideas.capture', async (c) => { captured.push(c); return { id: 1, routing: { routed: true, task_id: 55, goal_id: 9, status: 'backlog' } }; });
248
+ }
249
+ const routerPath = r2.resolve(path.join(ROOT, 'modules/platform-identity/routes/sso.js'));
250
+ delete r2.cache[routerPath];
251
+ const express = r2('express');
252
+ const app = express();
253
+ app.use(express.json());
254
+ app.use(r2(path.join(ROOT, 'src/bongos/middleware/error-envelope.js')).attachFail);
255
+ app.use(r2(routerPath)());
256
+ const server = app.listen(0);
257
+ await new Promise((res) => server.once('listening', res));
258
+ const post = (body) => fetch(`http://127.0.0.1:${server.address().port}/sso/feedback`, {
259
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
260
+ }).then(async (res) => ({ status: res.status, body: await res.json() }));
261
+ try { await fn({ post, captured }); } finally {
262
+ server.close();
263
+ Object.assign(pi, saved);
264
+ api.pool.query = savedQuery;
265
+ delete r2.cache[routerPath];
266
+ }
267
+ }
268
+
269
+ const GOOD = { client_id: 'acme', client_secret: 'right', github_id: '42', message: 'claim hung' };
270
+
271
+ test('hub: a witnessed member\'s bug is filed at backlog and the landing comes back 201', async () => {
272
+ await withHub({}, async ({ post, captured }) => {
273
+ const r = await post(GOOD);
274
+ assert.equal(r.status, 201, JSON.stringify(r.body));
275
+ assert.deepEqual(r.body, { ok: true, landed: 'task', task_id: '55', goal_id: '9', status: 'backlog', idea_id: '1' });
276
+ assert.equal(captured.at(-1).vector, 'instance-feedback');
277
+ assert.equal(captured.at(-1).filerRank, null);
278
+ });
279
+ });
280
+
281
+ test('hub: a wrong secret, an unwitnessed sender and a forged field are all refused before filing', async () => {
282
+ await withHub({}, async ({ post, captured }) => {
283
+ const before = captured.length;
284
+ assert.equal((await post({ ...GOOD, client_secret: 'wrong' })).status, 401);
285
+ assert.equal((await post({ ...GOOD, github_id: 'abc' })).status, 400);
286
+ assert.equal((await post({ ...GOOD, filer_rank: 'archon' })).status, 400, 'no rank can ride the body');
287
+ assert.equal(captured.length, before);
288
+ });
289
+ await withHub({ getMembership: async () => ({ membership_kind: 'member', signin_verified_at: null }) }, async ({ post, captured }) => {
290
+ const before = captured.length;
291
+ const r = await post(GOOD);
292
+ assert.equal(r.status, 409);
293
+ assert.equal(r.body.error.code, 'membership_required');
294
+ assert.equal(captured.length, before);
295
+ });
296
+ });
297
+
298
+ // ── proactive filing (task 1004467) ───────────────────────────────────────────
299
+ // The owner's rule: a builder venting that Bongos is broken gets it filed without
300
+ // asking. That lives in two places, and both must keep saying it.
301
+
302
+ test('the skill files unasked on a Bongos rant, and the kernel tells every session to', () => {
303
+ const skill = fs.readFileSync(path.join(ROOT, '.claude/skills/bongos-feedback/SKILL.md'), 'utf8');
304
+ const description = skill.split('\n---')[0];
305
+ assert.match(description, /unasked/, 'the listing entry itself says it acts unprompted');
306
+ assert.match(description, /Bongos is broken/, 'a venting phrase is a trigger');
307
+ assert.match(skill, /File it without being asked/);
308
+ assert.match(skill, /Never paste the rant/, 'a report, not the raw complaint');
309
+ assert.match(skill, /own project/, 'their own code is out of scope');
310
+ assert.match(skill, /One filing per distinct problem per session/);
311
+ const kernel = fs.readFileSync(path.join(ROOT, 'CLAUDE.md'), 'utf8');
312
+ assert.match(kernel, /vents that Bongos itself is broken.*\/bongos-feedback/);
313
+ });
314
+
315
+ test('every report is secret-scrubbed before it leaves the machine, and never sent unscrubbed', async () => {
316
+ const { scrubReport } = require_(path.join(ROOT, 'scripts/gds/feedback-send.js'));
317
+ const token = 'ghp_' + 'a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8';
318
+ const envValue = 'zq9-private-value-7781';
319
+ const r = scrubReport(`claim failed, my token is ${token} and the key is ${envValue}`, { MY_API_KEY: envValue });
320
+ assert.doesNotMatch(r.text, /ghp_/);
321
+ assert.ok(!r.text.includes(envValue), 'an env secret is stripped by value');
322
+ assert.ok(r.removed >= 2);
323
+ assert.equal(scrubReport('bongos claim hung for two minutes', {}).text, 'bongos claim hung for two minutes', 'plain prose is untouched');
324
+ const src = fs.readFileSync(path.join(ROOT, 'scripts/gds/feedback-send.js'), 'utf8');
325
+ assert.ok(src.indexOf('scrubReport(message)') < src.indexOf("api.request('POST', '/feedback'"), 'scrubbed before the request');
326
+ assert.match(src, /message: scrubbed\.text/, 'only the scrubbed text is sent');
327
+ });
@@ -0,0 +1,232 @@
1
+ // tests/module_assess_tests.mjs — the Tests assessment signal (task 1003791, ADR 0343).
2
+ // No database: the module's tests really run (real node child processes, from a REAL
3
+ // packed tarball verified by module-artifact.js), and the signal row goes to a fake
4
+ // pg client, so what is pinned is the outcome vocabulary core_265 stores:
5
+ // no tests -> no_data, never a fail
6
+ // tests ran -> scored, round(100 x passing files / files), sample_size = files
7
+ // unreadable -> not_scored
8
+ // plus the two safety properties: the sandbox is always removed, and a module's test
9
+ // process never inherits the parent's credentials. And it runs regardless of whether
10
+ // the module is enabled — nothing here consults isModuleEnabled.
11
+ //
12
+ // Run: node tests/module_assess_tests.mjs
13
+
14
+ import { test } from 'node:test';
15
+ import { strict as assert } from 'node:assert';
16
+ import { createRequire } from 'node:module';
17
+ import fs from 'node:fs';
18
+ import os from 'node:os';
19
+ import path from 'node:path';
20
+
21
+ const require = createRequire(import.meta.url);
22
+ const { discoverTestFiles, assessTests, recordTestsSignal, insertSignal, runRefusal, testEnv, SANDBOX_PREFIX } = require('../scripts/gds/module-assess-tests.js');
23
+ const { packModule, verifyModuleArtifact } = require('../scripts/gds/module-artifact.js');
24
+ const { relativeArtifactPath } = require('../src/bongos/module-store.js');
25
+
26
+ const PASS = "import assert from 'node:assert'; assert.equal(1 + 1, 2); console.log('1 passed, 0 failed');\n";
27
+ const FAIL = "import assert from 'node:assert'; assert.equal(1 + 1, 3);\n";
28
+ const file = (p, text) => ({ path: p, buf: Buffer.from(text) });
29
+ const tmpRoot = () => fs.mkdtempSync(path.join(os.tmpdir(), 'mod-assess-root-'));
30
+ const sandboxesIn = (root) => fs.readdirSync(root).filter((n) => n.startsWith(SANDBOX_PREFIX));
31
+
32
+ test('only tests/*.mjs at the top of tests/ count, as in the unit gate', () => {
33
+ assert.deepEqual(
34
+ discoverTestFiles(['module.json', 'tests/b.mjs', 'tests/a.mjs', 'tests/fixtures/x.mjs', 'tests/helper.js', 'lib/tests/c.mjs']),
35
+ ['tests/a.mjs', 'tests/b.mjs']);
36
+ });
37
+
38
+ test('a module that declares no tests is no_data, not a fail', async () => {
39
+ const root = tmpRoot();
40
+ const s = await assessTests('weather', [file('module.json', '{}'), file('index.js', '')], { trusted: true, root });
41
+ assert.equal(s.outcome, 'no_data');
42
+ assert.equal(s.score, null, 'no_data carries no number (core_265: score exactly when scored)');
43
+ assert.equal(s.sample_size, 0);
44
+ assert.match(s.reason, /declares no tests/);
45
+ assert.deepEqual(sandboxesIn(root), [], 'nothing is unpacked when there is nothing to run');
46
+ });
47
+
48
+ test('all passing scores 100; one of two failing scores 50', async () => {
49
+ const root = tmpRoot();
50
+ const all = await assessTests('weather', [file('tests/a.mjs', PASS), file('tests/b.mjs', PASS)], { trusted: true, root });
51
+ assert.equal(all.outcome, 'scored');
52
+ assert.equal(all.score, 100);
53
+ assert.equal(all.sample_size, 2);
54
+
55
+ const half = await assessTests('weather', [file('tests/a.mjs', PASS), file('tests/b.mjs', FAIL)], { trusted: true, root });
56
+ assert.equal(half.outcome, 'scored');
57
+ assert.equal(half.score, 50);
58
+ assert.equal(half.sample_size, 2);
59
+ assert.match(half.reason, /1 of 2/);
60
+ const failed = half.detail.files.find((f) => f.file === 'tests/b.mjs');
61
+ assert.equal(failed.passed, false);
62
+ assert.match(failed.tail, /AssertionError|assert/i, 'a failing file keeps a bounded tail of why');
63
+ assert.deepEqual(sandboxesIn(root), [], 'the sandbox is removed after every run');
64
+ });
65
+
66
+ test('every failing still scores 0 — distinct from no_data', async () => {
67
+ const s = await assessTests('weather', [file('tests/a.mjs', FAIL)], { trusted: true, root: tmpRoot() });
68
+ assert.equal(s.outcome, 'scored');
69
+ assert.equal(s.score, 0);
70
+ });
71
+
72
+ test('a module test reaches its own files by relative path, at the modules/<key>/ depth', async () => {
73
+ const s = await assessTests('weather', [
74
+ file('lib/add.js', 'module.exports = (a, b) => a + b;\n'),
75
+ file('tests/a.mjs', "import { createRequire } from 'node:module'; import assert from 'node:assert';\nconst add = createRequire(import.meta.url)('../lib/add.js'); assert.equal(add(2, 2), 4);\n"),
76
+ ], { trusted: true, root: tmpRoot() });
77
+ assert.equal(s.score, 100, JSON.stringify(s.detail));
78
+ });
79
+
80
+ test('a module test process never inherits the parent\'s credentials', async () => {
81
+ process.env.DATABASE_URL = 'postgres://secret@host/db';
82
+ process.env.BONGOS_TOKEN = 'secret-token';
83
+ try {
84
+ const s = await assessTests('weather', [file('tests/env.mjs',
85
+ "import assert from 'node:assert'; assert.equal(process.env.DATABASE_URL, undefined); assert.equal(process.env.BONGOS_TOKEN, undefined); assert.equal(process.env.NODE_ENV, 'test');\n")],
86
+ { trusted: true, root: tmpRoot() });
87
+ assert.equal(s.score, 100, JSON.stringify(s.detail));
88
+ } finally {
89
+ delete process.env.DATABASE_URL;
90
+ delete process.env.BONGOS_TOKEN;
91
+ }
92
+ });
93
+
94
+ test('a hung test file is killed at the timeout and counted as failed', async () => {
95
+ const s = await assessTests('weather', [file('tests/hang.mjs', 'setInterval(() => {}, 1000);\n'), file('tests/ok.mjs', PASS)],
96
+ { trusted: true, root: tmpRoot(), timeoutMs: 1500 });
97
+ assert.equal(s.score, 50);
98
+ assert.equal(s.detail.files.find((f) => f.file === 'tests/hang.mjs').timed_out, true);
99
+ });
100
+
101
+ // ---- recordTestsSignal: tarball in, one appended row out ----
102
+
103
+ const SANDBOX_ENV = { MODULE_TEST_SANDBOX: '1', PATH: process.env.PATH, SystemRoot: process.env.SystemRoot };
104
+
105
+ test('outside the separate testing environment, a published module is never run', async () => {
106
+ let ran = false;
107
+ const db = { async query() { ran = true; return { rows: [] }; } };
108
+ for (const env of [{}, { MODULE_TEST_SANDBOX: 'true' }, { MODULE_TEST_SANDBOX: '0' }]) {
109
+ const res = await recordTestsSignal(42, { db, env, root: tmpRoot() });
110
+ assert.equal(res.ok, false);
111
+ assert.equal(res.code, 'not_in_test_sandbox');
112
+ }
113
+ assert.equal(ran, false, 'refused before reading the store or the tarball');
114
+ });
115
+
116
+ test('the test environment keeps no credential and pins HOME to the sandbox', () => {
117
+ process.env.SOME_API_KEY = 'x';
118
+ try {
119
+ const env = testEnv('/sb');
120
+ assert.equal(env.SOME_API_KEY, undefined);
121
+ assert.equal(env.HOME, '/sb');
122
+ assert.equal(env.NODE_ENV, 'test');
123
+ } finally { delete process.env.SOME_API_KEY; }
124
+ });
125
+
126
+ function fakeDb(versionRow) {
127
+ const inserts = [];
128
+ return {
129
+ inserts,
130
+ async query(sql, params) {
131
+ if (/FROM store_module_versions/.test(sql)) return { rows: versionRow ? [versionRow] : [] };
132
+ if (/INSERT INTO module_assessment_signals/.test(sql)) {
133
+ assert.doesNotMatch(sql, /UPDATE/i, 'append-only');
134
+ inserts.push(params);
135
+ const [version_id, outcome, score, sample_size, reason] = params;
136
+ return { rows: [{ id: 7, version_id, part: 'tests', outcome, score, sample_size, reason, measured_at: new Date() }] };
137
+ }
138
+ throw new Error(`unexpected query: ${sql}`);
139
+ },
140
+ };
141
+ }
142
+
143
+ function publishedFixture(extraFiles) {
144
+ const src = fs.mkdtempSync(path.join(os.tmpdir(), 'mod-assess-src-'));
145
+ const dir = path.join(src, 'weather');
146
+ fs.mkdirSync(path.join(dir, 'tests'), { recursive: true });
147
+ fs.writeFileSync(path.join(dir, 'module.json'), JSON.stringify({ key: 'weather', title: 'Weather', description: 'x', version: '1.2.0', coreVersion: '^1.0.0', contributes: {} }));
148
+ for (const [p, text] of Object.entries(extraFiles)) fs.writeFileSync(path.join(dir, ...p.split('/')), text);
149
+ const { tgz } = packModule('weather', { modulesDir: src, now: () => new Date('2026-09-30T00:00:00Z'), modeOf: () => '644' });
150
+ const storeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mod-assess-store-'));
151
+ const final = path.join(storeDir, 'weather', 'weather-1.2.0.tgz');
152
+ fs.mkdirSync(path.dirname(final), { recursive: true });
153
+ fs.writeFileSync(final, tgz);
154
+ return { storeDir, final, row: { id: 42, module_key: 'weather', version: '1.2.0', artifact_path: relativeArtifactPath(final, { dir: storeDir }) } };
155
+ }
156
+
157
+ test('a published version is tested from its tarball and one tests row is appended', async () => {
158
+ const { storeDir, row } = publishedFixture({ 'tests/a.mjs': PASS, 'tests/b.mjs': FAIL });
159
+ const db = fakeDb(row);
160
+ const res = await recordTestsSignal(42, { db, storeDir, root: tmpRoot(), env: SANDBOX_ENV });
161
+ assert.equal(res.ok, true);
162
+ assert.equal(db.inserts.length, 1);
163
+ const [versionId, outcome, score, sample, , detail] = db.inserts[0];
164
+ assert.equal(versionId, 42);
165
+ assert.equal(outcome, 'scored');
166
+ assert.equal(score, 50);
167
+ assert.equal(sample, 2);
168
+ assert.equal(JSON.parse(detail).files.length, 2);
169
+ });
170
+
171
+ test('a version whose tarball is missing or tampered is not_scored, not failed', async () => {
172
+ const { storeDir, final, row } = publishedFixture({ 'tests/a.mjs': PASS });
173
+ fs.writeFileSync(final, Buffer.from('not a tarball'));
174
+ const db = fakeDb(row);
175
+ const res = await recordTestsSignal(42, { db, storeDir, root: tmpRoot(), env: SANDBOX_ENV });
176
+ assert.equal(res.ok, true);
177
+ const [, outcome, score] = db.inserts[0];
178
+ assert.equal(outcome, 'not_scored');
179
+ assert.equal(score, null);
180
+
181
+ fs.rmSync(final);
182
+ const db2 = fakeDb(row);
183
+ await recordTestsSignal(42, { db: db2, storeDir, root: tmpRoot(), env: SANDBOX_ENV });
184
+ assert.equal(db2.inserts[0][1], 'not_scored');
185
+ assert.doesNotMatch(db2.inserts[0][4], /[\/]/, 'the stored reason carries an error code, never a local path');
186
+ });
187
+
188
+ test('an unknown version writes nothing', async () => {
189
+ const db = fakeDb(null);
190
+ const res = await recordTestsSignal(99, { db, root: tmpRoot(), env: SANDBOX_ENV });
191
+ assert.equal(res.ok, false);
192
+ assert.equal(res.code, 'version_not_found');
193
+ assert.equal(db.inserts.length, 0);
194
+ });
195
+
196
+ test('the runner never consults whether the module is enabled', () => {
197
+ const src = fs.readFileSync(new URL('../scripts/gds/module-assess-tests.js', import.meta.url), 'utf8').replace(/\/\/.*$/gm, '');
198
+ assert.doesNotMatch(src, /isModuleEnabled/);
199
+ });
200
+
201
+ test('the sandbox dir is gitignored in case a run is killed mid-way', () => {
202
+ const gi = fs.readFileSync(new URL('../.gitignore', import.meta.url), 'utf8');
203
+ assert.match(gi, /^\/\.module-assess-\*\/$/m);
204
+ });
205
+
206
+ test('the gate lives in the runner itself: assessTests refuses untrusted code outside the testing environment', async () => {
207
+ const root = tmpRoot();
208
+ await assert.rejects(assessTests('weather', [file('tests/a.mjs', PASS)], { root, env: {} }), { code: 'not_in_test_sandbox' });
209
+ assert.deepEqual(sandboxesIn(root), [], 'refused before anything is unpacked');
210
+ });
211
+
212
+ test('a mis-set flag on a machine that holds secrets is still refused', async () => {
213
+ const root = tmpRoot();
214
+ for (const k of ['DATABASE_URL', 'BONGOS_TOKEN', 'STRIPE_SECRET', 'OPENAI_API_KEY']) {
215
+ assert.equal(runRefusal({ env: { ...SANDBOX_ENV, [k]: 'x' }, root }).code, 'secrets_present', k);
216
+ }
217
+ assert.equal(runRefusal({ env: SANDBOX_ENV, root }), null, 'a clean testing environment may run');
218
+ fs.writeFileSync(path.join(root, '.env'), 'DATABASE_URL=x\n');
219
+ assert.equal(runRefusal({ env: SANDBOX_ENV, root }).code, 'secrets_present', 'a .env at the core root counts');
220
+ await assert.rejects(assessTests('weather', [file('tests/a.mjs', PASS)], { root, env: SANDBOX_ENV }), { code: 'secrets_present' });
221
+ assert.equal(runRefusal({ trusted: true, env: {}, root }), null, 'your own checkout (--dir) is trusted');
222
+ });
223
+
224
+ test('insertSignal appends exactly one tests row with the signal fields', async () => {
225
+ const seen = [];
226
+ const db = { async query(sql, params) { seen.push({ sql, params }); return { rows: [{ id: 1 }] }; } };
227
+ await insertSignal(5, { outcome: 'scored', score: 80, sample_size: 5, reason: 'r', detail: { files: [] } }, { db });
228
+ assert.equal(seen.length, 1);
229
+ assert.match(seen[0].sql, /INSERT INTO module_assessment_signals[\s\S]*'tests'/);
230
+ assert.doesNotMatch(seen[0].sql, /UPDATE/i);
231
+ assert.deepEqual(seen[0].params, [5, 'scored', 80, 5, 'r', '{"files":[]}']);
232
+ });