@bongos/core 1.17.0 → 1.17.1
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 +154 -83
- package/CLAUDE.md +11 -238
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +4 -4
- package/clients/bongos-client/bongos-client.global.js +14 -1
- package/clients/bongos-client/index.cjs +14 -1
- package/clients/bongos-client/index.d.ts +19 -2
- package/clients/bongos-client/index.mjs +11 -1
- package/clients/bongos-client/package.json +1 -1
- package/docs/adr/0133-github-app-manifest-onboarding-signin.md +2 -0
- package/docs/adr/0134-private-first-npm-distribution.md +20 -6
- package/docs/adr/0135-module-upstream-submission-interim-queue.md +34 -0
- package/docs/adr/README.md +5 -4
- package/docs/api/openapi.json +315 -6
- package/docs/api-reference.md +10 -5
- package/docs/architecture.md +1 -1
- package/docs/file-map.md +2 -2
- package/docs/modules-contract.md +2 -0
- package/docs/recipes/instance-deploy.md +4 -4
- package/docs/recipes/instance-first-run.md +51 -0
- package/docs/recipes/packaging-the-core.md +5 -5
- package/docs/recipes/private-npm-distribution.md +22 -12
- package/docs/recipes/self-host.md +1 -1
- package/docs/recipes/upgrading-the-core.md +8 -8
- package/migrations/core_191_module_submissions.sql +45 -0
- package/modules/autonomy/CLAUDE.md +11 -45
- package/modules/builder-settings/CLAUDE.md +11 -54
- package/modules/dev-box/CLAUDE.md +11 -34
- package/modules/discord/CLAUDE.md +11 -46
- package/modules/economy/CLAUDE.md +11 -44
- package/modules/grading/CLAUDE.md +11 -38
- package/modules/hall-ui/CLAUDE.md +11 -57
- package/modules/hall-ui/public/goals-page.js +227 -22
- package/modules/hall-ui/public/goals.html +16 -0
- package/modules/hall-ui/public/new-project-wizard.js +5 -4
- package/modules/ideas/CLAUDE.md +11 -33
- package/modules/lifecycle/CLAUDE.md +11 -59
- package/modules/lifecycle/routes/goals.js +80 -0
- package/modules/memory/CLAUDE.md +11 -36
- package/modules/onboarding/CLAUDE.md +11 -51
- package/modules/security/CLAUDE.md +11 -40
- package/modules/sessions/CLAUDE.md +11 -35
- package/modules/status-ui/CLAUDE.md +11 -43
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +58 -12
- package/public-landing/index.html +40 -21
- package/scripts/gds/CLAUDE.md +11 -140
- package/scripts/gds/claude-materialize.js +151 -18
- package/scripts/gds/claude-md-template.js +84 -0
- package/scripts/gds/cli-lib.js +112 -2
- package/scripts/gds/gen-api-client.js +10 -10
- package/scripts/gds/init.js +53 -13
- package/scripts/gds/migration-namespace.js +1 -1
- package/scripts/gds/module.js +118 -4
- package/scripts/gds/npmrc.js +93 -0
- package/scripts/gds/onboard.js +13 -24
- package/scripts/gds/package-core.js +56 -14
- package/scripts/gds/provision.js +102 -17
- package/scripts/gds/seed-adopt-tasks.js +1 -1
- package/scripts/gds/status.js +6 -1
- package/scripts/gds/task.js +166 -0
- package/scripts/gds/upgrade.js +58 -18
- package/src/bongos/CLAUDE.md +11 -60
- package/src/bongos/auth.js +11 -0
- package/src/bongos/module-submissions.js +56 -0
- package/src/bongos/route-rank-check.js +6 -0
- package/src/bongos/routes/CLAUDE.md +11 -39
- package/src/bongos/routes/auth.js +6 -1
- package/src/bongos/routes/modules.js +123 -21
- package/src/instance-config.js +1 -1
- package/src/module-api.js +13 -1
- package/tests/claude_materialize.mjs +105 -0
- package/tests/device_flow_start.mjs +73 -0
- package/tests/first_run_engine_check.mjs +79 -0
- package/tests/goal_membership_requests.mjs +33 -3
- package/tests/hook_user_agents.mjs +62 -0
- package/tests/init.mjs +59 -6
- package/tests/instance_mismatch_guard.mjs +101 -0
- package/tests/module_cli.mjs +130 -2
- package/tests/module_submissions.mjs +135 -0
- package/tests/npmrc.mjs +105 -0
- package/tests/onboard.mjs +49 -0
- package/tests/package_core.mjs +68 -22
- package/tests/platform_boot.mjs +5 -2
- package/tests/provision.mjs +107 -8
- package/tests/provisioning_github_app_routes.mjs +29 -2
- package/tests/served_paths_shipped.mjs +2 -2
- package/tests/task_cli.mjs +99 -0
- package/tests/upgrade.mjs +89 -25
package/tests/module_cli.mjs
CHANGED
|
@@ -268,6 +268,126 @@ test('cmdCheck: usage error (exit 2) when no key is given', () => {
|
|
|
268
268
|
assert.equal(code, 2);
|
|
269
269
|
});
|
|
270
270
|
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// packageModuleForSubmission / readSignOff — the fs half of `module submit`
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
test('readSignOff: null when never signed off; the recorded record once it is', () => {
|
|
276
|
+
const root = tmpModulesRoot();
|
|
277
|
+
mod.scaffoldModule({ key: 'widget', modulesDir: root, core: '1.0.0' });
|
|
278
|
+
assert.equal(mod.readSignOff('widget', { modulesDir: root }), null);
|
|
279
|
+
mod.recordSignOff('widget', 'Ada Lovelace <ada@example.com>', { modulesDir: root });
|
|
280
|
+
const signOff = mod.readSignOff('widget', { modulesDir: root });
|
|
281
|
+
assert.equal(signOff.signOff.signedBy, 'Ada Lovelace <ada@example.com>');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('packageModuleForSubmission: snapshots the manifest + every file (base64) + the sign-off', () => {
|
|
285
|
+
const root = tmpModulesRoot();
|
|
286
|
+
mod.scaffoldModule({ key: 'widget', modulesDir: root, core: '1.0.0' });
|
|
287
|
+
mod.recordSignOff('widget', 'Ada Lovelace <ada@example.com>', { modulesDir: root });
|
|
288
|
+
|
|
289
|
+
const pkg = mod.packageModuleForSubmission('widget', { modulesDir: root });
|
|
290
|
+
assert.equal(pkg.key, 'widget');
|
|
291
|
+
assert.equal(pkg.manifest.key, 'widget');
|
|
292
|
+
assert.equal(pkg.signOff.signOff.signedBy, 'Ada Lovelace <ada@example.com>');
|
|
293
|
+
assert.ok(pkg.totalBytes > 0);
|
|
294
|
+
|
|
295
|
+
const paths = pkg.files.map((f) => f.path).sort();
|
|
296
|
+
assert.deepEqual(paths, [...paths].sort(), 'files are sorted by path');
|
|
297
|
+
assert.ok(paths.includes('module.json'));
|
|
298
|
+
assert.ok(paths.includes('routes/widget.js'));
|
|
299
|
+
// the sign-off file itself travels in the snapshot too — it's part of the module tree
|
|
300
|
+
assert.ok(paths.includes('.upstream-signoff.json'));
|
|
301
|
+
|
|
302
|
+
const manifestFile = pkg.files.find((f) => f.path === 'module.json');
|
|
303
|
+
const roundTripped = JSON.parse(Buffer.from(manifestFile.contentBase64, 'base64').toString('utf8'));
|
|
304
|
+
assert.deepEqual(roundTripped, pkg.manifest);
|
|
305
|
+
assert.equal(manifestFile.bytes, Buffer.byteLength(JSON.stringify(pkg.manifest, null, 2) + '\n'));
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// bongos module submit <key> — ADR 0107 §1 (network call is always injected —
|
|
310
|
+
// never hits cli-lib.js / the real API in a unit test)
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
test('cmdSubmit: refuses (exit 1) when the pre-check fails, without ever attempting the network call', async () => {
|
|
314
|
+
const lines = [];
|
|
315
|
+
const sink = { log: (s) => lines.push(String(s)), errlog: (s) => lines.push(String(s)) };
|
|
316
|
+
let submitCalled = false;
|
|
317
|
+
const check = () => ({ key: 'game', ok: false, findings: [{ kind: 'content', detail: 'contains denied content' }], files: [], denied: [] });
|
|
318
|
+
const submit = async () => { submitCalled = true; return { ok: true, status: 201, data: { submission: { id: 1, status: 'pending' } } }; };
|
|
319
|
+
|
|
320
|
+
const code = await mod.cmdSubmit(['game'], { ...sink, check, submit });
|
|
321
|
+
assert.equal(code, 1);
|
|
322
|
+
assert.equal(submitCalled, false, 'a failing pre-check must never reach the network');
|
|
323
|
+
assert.ok(lines.some((l) => /REFUSING/.test(l)));
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('cmdSubmit: refuses (exit 1) when the pre-check passes but no sign-off is on file and none was given', async () => {
|
|
327
|
+
const lines = [];
|
|
328
|
+
const sink = { log: (s) => lines.push(String(s)), errlog: (s) => lines.push(String(s)) };
|
|
329
|
+
let submitCalled = false;
|
|
330
|
+
const check = () => ({ key: 'widget', ok: true, findings: [], files: [], denied: [] });
|
|
331
|
+
const readSign = () => null;
|
|
332
|
+
const submit = async () => { submitCalled = true; return { ok: true, status: 201, data: {} }; };
|
|
333
|
+
|
|
334
|
+
const code = await mod.cmdSubmit(['widget'], { ...sink, check, readSign, submit });
|
|
335
|
+
assert.equal(code, 1);
|
|
336
|
+
assert.equal(submitCalled, false);
|
|
337
|
+
assert.ok(lines.some((l) => /REFUSING/.test(l) && /sign-off/.test(l)));
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('cmdSubmit: --sign-off records a NEW sign-off when none is on file, then submits', async () => {
|
|
341
|
+
const lines = [];
|
|
342
|
+
const sink = { log: (s) => lines.push(String(s)), errlog: (s) => lines.push(String(s)) };
|
|
343
|
+
const check = () => ({ key: 'widget', ok: true, findings: [], files: [], denied: [] });
|
|
344
|
+
let signedCall = null;
|
|
345
|
+
const recorded = { signOff: { signedBy: 'Ada Lovelace <ada@example.com>', date: '2026-07-08T00:00:00.000Z' }, path: 'modules/widget/.upstream-signoff.json' };
|
|
346
|
+
const sign = (key, by) => { signedCall = { key, by }; return recorded; };
|
|
347
|
+
let readSignCalls = 0;
|
|
348
|
+
const readSign = () => { readSignCalls += 1; return readSignCalls === 1 ? null : recorded; };
|
|
349
|
+
let submitArgs = null;
|
|
350
|
+
const submit = async (key, body) => { submitArgs = { key, body }; return { ok: true, status: 201, data: { submission: { id: 7, status: 'pending' } } }; };
|
|
351
|
+
|
|
352
|
+
const code = await mod.cmdSubmit(['widget', '--sign-off', 'Ada Lovelace <ada@example.com>'], { ...sink, check, sign, readSign, submit });
|
|
353
|
+
assert.equal(code, 0);
|
|
354
|
+
assert.deepEqual(signedCall, { key: 'widget', by: 'Ada Lovelace <ada@example.com>' });
|
|
355
|
+
assert.deepEqual(submitArgs, { key: 'widget', body: { signed_by: 'Ada Lovelace <ada@example.com>' } });
|
|
356
|
+
assert.ok(lines.some((l) => /Filed — submission id 7/.test(l)));
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test('cmdSubmit: an existing sign-off on file is reused — --sign-off is not required again', async () => {
|
|
360
|
+
const lines = [];
|
|
361
|
+
const sink = { log: (s) => lines.push(String(s)), errlog: (s) => lines.push(String(s)) };
|
|
362
|
+
const check = () => ({ key: 'widget', ok: true, findings: [], files: [], denied: [] });
|
|
363
|
+
let signCalled = false;
|
|
364
|
+
const sign = () => { signCalled = true; };
|
|
365
|
+
const readSign = () => ({ signOff: { signedBy: 'Ada Lovelace <ada@example.com>', date: 'now' } });
|
|
366
|
+
const submit = async () => ({ ok: true, status: 201, data: { submission: { id: 9, status: 'pending' } } });
|
|
367
|
+
|
|
368
|
+
const code = await mod.cmdSubmit(['widget'], { ...sink, check, sign, readSign, submit });
|
|
369
|
+
assert.equal(code, 0);
|
|
370
|
+
assert.equal(signCalled, false, 'an existing sign-off is never re-recorded');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test('cmdSubmit: surfaces a rejected submission (e.g. the server-side pre-check disagrees) as exit 1', async () => {
|
|
374
|
+
const lines = [];
|
|
375
|
+
const sink = { log: (s) => lines.push(String(s)), errlog: (s) => lines.push(String(s)) };
|
|
376
|
+
const check = () => ({ key: 'widget', ok: true, findings: [], files: [], denied: [] });
|
|
377
|
+
const readSign = () => ({ signOff: { signedBy: 'Ada Lovelace <ada@example.com>', date: 'now' } });
|
|
378
|
+
const submit = async () => ({ ok: false, status: 422, data: { error: 'precheck_failed', message: 'server disagrees' } });
|
|
379
|
+
|
|
380
|
+
const code = await mod.cmdSubmit(['widget'], { ...sink, check, readSign, submit });
|
|
381
|
+
assert.equal(code, 1);
|
|
382
|
+
assert.ok(lines.some((l) => /REFUSING/.test(l) && /server disagrees/.test(l)));
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('cmdSubmit: usage error (exit 2) when no key is given', async () => {
|
|
386
|
+
const lines = [];
|
|
387
|
+
const code = await mod.cmdSubmit([], { log: (s) => lines.push(s), errlog: (s) => lines.push(s) });
|
|
388
|
+
assert.equal(code, 2);
|
|
389
|
+
});
|
|
390
|
+
|
|
271
391
|
// ---------------------------------------------------------------------------
|
|
272
392
|
// dispatch wiring (bin/bongos.js)
|
|
273
393
|
// ---------------------------------------------------------------------------
|
|
@@ -283,10 +403,18 @@ test('the `module` verb dispatches to module.js, `upgrade` to upgrade.js', () =>
|
|
|
283
403
|
assert.ok(h.includes('upgrade'));
|
|
284
404
|
});
|
|
285
405
|
|
|
286
|
-
test('`bongos module check` is documented in help / dispatches to module.js check subcommand', () => {
|
|
406
|
+
test('`bongos module check` is documented in help / dispatches to module.js check subcommand', async () => {
|
|
287
407
|
const lines = [];
|
|
288
408
|
mod.printHelp((s) => lines.push(s));
|
|
289
409
|
assert.ok(lines.some((l) => /module check/.test(l)));
|
|
290
410
|
// module.js itself dispatches the "check" subcommand (bongos.js forwards args[1:] verbatim to module.js)
|
|
291
|
-
|
|
411
|
+
// main() is async (bongos module submit makes a network call) — every subcommand's exit code resolves via the same Promise.
|
|
412
|
+
assert.equal(await mod.main(['node', 'module.js', 'check']), 2, 'no key => usage error');
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test('`bongos module submit` is documented in help / dispatches to module.js submit subcommand', async () => {
|
|
416
|
+
const lines = [];
|
|
417
|
+
mod.printHelp((s) => lines.push(s));
|
|
418
|
+
assert.ok(lines.some((l) => /module submit/.test(l)));
|
|
419
|
+
assert.equal(await mod.main(['node', 'module.js', 'submit']), 2, 'no key => usage error');
|
|
292
420
|
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// tests/module_submissions.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit smoke for the ADR 0107 §1 upstreaming review queue (task 1770,
|
|
4
|
+
// migration core_191_module_submissions). Proves, with NO live Postgres:
|
|
5
|
+
//
|
|
6
|
+
// DB helpers (against a stubbed pool seam):
|
|
7
|
+
// - createModuleSubmission() INSERTs, jsonb-casting + JSON.stringify'ing
|
|
8
|
+
// manifest/files/sign_off, parameterized
|
|
9
|
+
// - listModuleSubmissions() SELECTs metadata only (no `files` — the file
|
|
10
|
+
// snapshot can run to hundreds of KB and no list consumer needs it),
|
|
11
|
+
// newest-first, optionally filtered by status and/or module_key
|
|
12
|
+
//
|
|
13
|
+
// We stub the pool by pre-seeding the require cache for src/bongos/pool.js
|
|
14
|
+
// BEFORE requiring src/bongos/module-submissions.js — the same monkeypatch
|
|
15
|
+
// seam tests/security_docs.mjs uses.
|
|
16
|
+
//
|
|
17
|
+
// Run: node tests/module_submissions.mjs
|
|
18
|
+
|
|
19
|
+
import { strict as assert } from 'node:assert';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
27
|
+
|
|
28
|
+
let passed = 0;
|
|
29
|
+
let failed = 0;
|
|
30
|
+
function test(name, fn) {
|
|
31
|
+
return Promise.resolve()
|
|
32
|
+
.then(fn)
|
|
33
|
+
.then(() => { passed++; console.log(` ok ${name}`); })
|
|
34
|
+
.catch((e) => { failed++; console.error(` FAIL ${name}\n ${e.message}`); });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const calls = [];
|
|
38
|
+
let nextRows = [];
|
|
39
|
+
const fakePool = {
|
|
40
|
+
query: async (text, params) => {
|
|
41
|
+
calls.push({ text, params });
|
|
42
|
+
return { rows: nextRows };
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
const poolPath = require.resolve(path.join(ROOT, 'src', 'bongos', 'pool.js'));
|
|
46
|
+
require.cache[poolPath] = { id: poolPath, filename: poolPath, loaded: true, exports: { pool: fakePool, instanceDbName: () => undefined } };
|
|
47
|
+
|
|
48
|
+
const moduleSubmissions = require(path.join(ROOT, 'src', 'bongos', 'module-submissions.js'));
|
|
49
|
+
|
|
50
|
+
function lastCall() { return calls[calls.length - 1]; }
|
|
51
|
+
|
|
52
|
+
console.log('module_submissions — DB helpers (stubbed pool):');
|
|
53
|
+
|
|
54
|
+
await test('createModuleSubmission() INSERTs, jsonb-casts manifest/files/sign_off, parameterized', async () => {
|
|
55
|
+
calls.length = 0;
|
|
56
|
+
const manifest = { key: 'widget', title: 'Widget', license: 'AGPL-3.0' };
|
|
57
|
+
const files = [{ path: 'module.json', contentBase64: 'e30=', bytes: 2 }];
|
|
58
|
+
const signOff = { module: 'widget', signOff: { signedBy: 'Ada Lovelace <ada@example.com>', date: '2026-07-08T00:00:00.000Z' } };
|
|
59
|
+
const precheckPassedAt = new Date('2026-07-08T00:00:00.000Z');
|
|
60
|
+
nextRows = [{
|
|
61
|
+
id: 1, module_key: 'widget', submitted_by: 42, manifest, sign_off: signOff,
|
|
62
|
+
precheck_passed_at: precheckPassedAt, source_repo: 'lars589/amazonprimea',
|
|
63
|
+
status: 'pending', reviewed_by: null, reviewed_at: null, review_notes: null,
|
|
64
|
+
submitted_at: '2026-07-08T00:00:01.000Z',
|
|
65
|
+
}];
|
|
66
|
+
|
|
67
|
+
const row = await moduleSubmissions.createModuleSubmission({
|
|
68
|
+
moduleKey: 'widget', submittedBy: 42, manifest, files, signOff, precheckPassedAt,
|
|
69
|
+
sourceRepo: 'lars589/amazonprimea',
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const { text, params } = lastCall();
|
|
73
|
+
assert.ok(/INSERT INTO module_submissions/i.test(text));
|
|
74
|
+
assert.ok(/RETURNING/i.test(text), 'must return the inserted row');
|
|
75
|
+
// files IS written (params[3] below) but is NOT in the RETURNING list — the
|
|
76
|
+
// write path never echoes the (possibly large) snapshot back.
|
|
77
|
+
const returningClause = text.slice(text.search(/RETURNING/i));
|
|
78
|
+
assert.ok(!/\bfiles\b/i.test(returningClause), 'RETURNING must not include files');
|
|
79
|
+
|
|
80
|
+
assert.equal(params[0], 'widget');
|
|
81
|
+
assert.equal(params[1], 42);
|
|
82
|
+
assert.equal(params[2], JSON.stringify(manifest));
|
|
83
|
+
assert.equal(params[3], JSON.stringify(files));
|
|
84
|
+
assert.equal(params[4], JSON.stringify(signOff));
|
|
85
|
+
assert.equal(params[5], precheckPassedAt);
|
|
86
|
+
assert.equal(params[6], 'lars589/amazonprimea');
|
|
87
|
+
|
|
88
|
+
assert.equal(row.id, 1);
|
|
89
|
+
assert.equal(row.status, 'pending');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await test('createModuleSubmission() defaults a missing sourceRepo to null (parameterized, not the string "undefined")', async () => {
|
|
93
|
+
calls.length = 0;
|
|
94
|
+
nextRows = [{ id: 2, module_key: 'widget', status: 'pending' }];
|
|
95
|
+
await moduleSubmissions.createModuleSubmission({
|
|
96
|
+
moduleKey: 'widget', submittedBy: 1, manifest: {}, files: [], signOff: {}, precheckPassedAt: new Date(),
|
|
97
|
+
});
|
|
98
|
+
const { params } = lastCall();
|
|
99
|
+
assert.equal(params[6], null);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await test('listModuleSubmissions(): no filters => SELECT with no WHERE, newest-first, no files column', async () => {
|
|
103
|
+
calls.length = 0;
|
|
104
|
+
nextRows = [{ id: 2, module_key: 'b', status: 'pending' }, { id: 1, module_key: 'a', status: 'pending' }];
|
|
105
|
+
const rows = await moduleSubmissions.listModuleSubmissions();
|
|
106
|
+
const { text, params } = lastCall();
|
|
107
|
+
assert.ok(/SELECT/i.test(text));
|
|
108
|
+
assert.ok(!/WHERE/i.test(text));
|
|
109
|
+
assert.ok(/ORDER BY\s+submitted_at\s+DESC/i.test(text));
|
|
110
|
+
const selectClause = text.slice(0, text.search(/FROM/i));
|
|
111
|
+
assert.ok(!/\bfiles\b/i.test(selectClause), 'list SELECT must not include the files column');
|
|
112
|
+
assert.deepEqual(params, []);
|
|
113
|
+
assert.deepEqual(rows, nextRows);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await test('listModuleSubmissions({status}): filters by status, parameterized', async () => {
|
|
117
|
+
calls.length = 0;
|
|
118
|
+
nextRows = [];
|
|
119
|
+
await moduleSubmissions.listModuleSubmissions({ status: 'pending' });
|
|
120
|
+
const { text, params } = lastCall();
|
|
121
|
+
assert.ok(/WHERE\s+status\s*=\s*\$1/i.test(text));
|
|
122
|
+
assert.deepEqual(params, ['pending']);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await test('listModuleSubmissions({status, moduleKey}): both filters AND together, parameterized in order', async () => {
|
|
126
|
+
calls.length = 0;
|
|
127
|
+
nextRows = [];
|
|
128
|
+
await moduleSubmissions.listModuleSubmissions({ status: 'accepted', moduleKey: 'widget' });
|
|
129
|
+
const { text, params } = lastCall();
|
|
130
|
+
assert.ok(/WHERE\s+status\s*=\s*\$1\s+AND\s+module_key\s*=\s*\$2/i.test(text));
|
|
131
|
+
assert.deepEqual(params, ['accepted', 'widget']);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
135
|
+
if (failed > 0) process.exit(1);
|
package/tests/npmrc.mjs
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// tests/npmrc.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit tests for scripts/gds/npmrc.js (ADR 0134, task 2090) — the single source of the
|
|
4
|
+
// env-fed `.npmrc` used to install the PRIVATE @bongos/core from the npm registry, plus
|
|
5
|
+
// the opt-in / token gates that keep the vendored-tarball path the default/fallback.
|
|
6
|
+
//
|
|
7
|
+
// Run: node tests/npmrc.mjs
|
|
8
|
+
|
|
9
|
+
import { strict as assert } from 'node:assert';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const n = require('../scripts/gds/npmrc.js');
|
|
17
|
+
|
|
18
|
+
let passed = 0, failed = 0;
|
|
19
|
+
function test(name, fn) {
|
|
20
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
21
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---- npmrcContent ----------------------------------------------------------
|
|
25
|
+
test('npmrcContent: scopes @bongos to the registry + auth token EXPANDS from the env (literal ${NPM_TOKEN})', () => {
|
|
26
|
+
const body = n.npmrcContent();
|
|
27
|
+
assert.match(body, /@bongos:registry=https:\/\/registry\.npmjs\.org\//);
|
|
28
|
+
assert.match(body, /\/\/registry\.npmjs\.org\/:_authToken=\$\{NPM_TOKEN\}/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('npmrcContent: carries NO real secret — only the ${NPM_TOKEN} placeholder', () => {
|
|
32
|
+
const body = n.npmrcContent();
|
|
33
|
+
// the only _authToken= assignment must be the env placeholder, never a concrete value
|
|
34
|
+
const tokenLines = body.split('\n').filter((l) => /_authToken=/.test(l));
|
|
35
|
+
assert.equal(tokenLines.length, 1);
|
|
36
|
+
assert.equal(tokenLines[0].trim(), '//registry.npmjs.org/:_authToken=${NPM_TOKEN}');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('npmrcContent: comment lines use the ini `;` comment char', () => {
|
|
40
|
+
const comments = n.npmrcContent().split('\n').filter((l) => l.startsWith(';'));
|
|
41
|
+
assert.ok(comments.length >= 3, 'has explanatory comments');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('npmrcContent: honors custom scope/host/tokenEnv', () => {
|
|
45
|
+
const body = n.npmrcContent({ scope: '@acme', host: 'npm.acme.dev', tokenEnv: 'ACME_TOKEN' });
|
|
46
|
+
assert.match(body, /@acme:registry=https:\/\/npm\.acme\.dev\//);
|
|
47
|
+
assert.match(body, /\/\/npm\.acme\.dev\/:_authToken=\$\{ACME_TOKEN\}/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ---- gates -----------------------------------------------------------------
|
|
51
|
+
test('hasNpmToken: true only when NPM_TOKEN is a non-empty string', () => {
|
|
52
|
+
assert.equal(n.hasNpmToken({ NPM_TOKEN: 'npm_abc' }), true);
|
|
53
|
+
assert.equal(n.hasNpmToken({ NPM_TOKEN: '' }), false);
|
|
54
|
+
assert.equal(n.hasNpmToken({ NPM_TOKEN: ' ' }), false);
|
|
55
|
+
assert.equal(n.hasNpmToken({}), false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('registryCoreRequested: 1/true/yes/on → true; everything else → false', () => {
|
|
59
|
+
for (const v of ['1', 'true', 'TRUE', 'yes', 'on']) assert.equal(n.registryCoreRequested({ PROVISION_CORE_FROM_REGISTRY: v }), true, v);
|
|
60
|
+
for (const v of ['0', 'false', 'no', '', 'off', 'garbage']) assert.equal(n.registryCoreRequested({ PROVISION_CORE_FROM_REGISTRY: v }), false, v);
|
|
61
|
+
assert.equal(n.registryCoreRequested({}), false, 'unset');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ---- writeNpmrc ------------------------------------------------------------
|
|
65
|
+
test('writeNpmrc: writes the env-fed .npmrc + returns its path', () => {
|
|
66
|
+
const dir = mkdtempSync(join(tmpdir(), 'npmrc-'));
|
|
67
|
+
try {
|
|
68
|
+
const p = n.writeNpmrc(dir, { log: () => {} });
|
|
69
|
+
assert.equal(p, join(dir, '.npmrc'));
|
|
70
|
+
assert.equal(readFileSync(p, 'utf8'), n.npmrcContent());
|
|
71
|
+
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('writeNpmrc: dry-run writes nothing but returns the intended path', () => {
|
|
75
|
+
const dir = mkdtempSync(join(tmpdir(), 'npmrc-'));
|
|
76
|
+
try {
|
|
77
|
+
const p = n.writeNpmrc(dir, { dryRun: true, log: () => {} });
|
|
78
|
+
assert.equal(p, join(dir, '.npmrc'));
|
|
79
|
+
assert.equal(existsSync(join(dir, '.npmrc')), false, 'dry-run must not write');
|
|
80
|
+
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('writeNpmrc: overwrite:false leaves a pre-existing .npmrc untouched (returns null)', () => {
|
|
84
|
+
const dir = mkdtempSync(join(tmpdir(), 'npmrc-'));
|
|
85
|
+
try {
|
|
86
|
+
const existing = '@other:registry=https://npm.other.dev/\n';
|
|
87
|
+
writeFileSync(join(dir, '.npmrc'), existing);
|
|
88
|
+
const p = n.writeNpmrc(dir, { overwrite: false, log: () => {} });
|
|
89
|
+
assert.equal(p, null, 'skipped');
|
|
90
|
+
assert.equal(readFileSync(join(dir, '.npmrc'), 'utf8'), existing, 'existing content preserved');
|
|
91
|
+
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('writeNpmrc: overwrite:true (default) replaces an existing .npmrc', () => {
|
|
95
|
+
const dir = mkdtempSync(join(tmpdir(), 'npmrc-'));
|
|
96
|
+
try {
|
|
97
|
+
writeFileSync(join(dir, '.npmrc'), 'stale\n');
|
|
98
|
+
n.writeNpmrc(dir, { log: () => {} });
|
|
99
|
+
assert.equal(readFileSync(join(dir, '.npmrc'), 'utf8'), n.npmrcContent());
|
|
100
|
+
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
console.log('');
|
|
104
|
+
console.log(`npmrc tests: ${passed} passed, ${failed} failed`);
|
|
105
|
+
process.exit(failed ? 1 : 0);
|
package/tests/onboard.mjs
CHANGED
|
@@ -37,12 +37,30 @@ test('onboardPlan(adopt): same sequence + human steps, but the scaffold-leg labe
|
|
|
37
37
|
assert.doesNotMatch(label(green, 'init'), /--adopt/);
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
+
test('onboardPlan lists seed-first-version as an explicit AUTO step right after provision (task 2119 anti-drift)', () => {
|
|
41
|
+
// The first-version seed used to be an unlisted afterthought bolted to the local
|
|
42
|
+
// `bongos init` leg (run --no-seed in every hosted flow, so it never fired). Making
|
|
43
|
+
// it a named step in the ONE canonical sequence is what stops it silently drifting.
|
|
44
|
+
const plan = ob.onboardPlan();
|
|
45
|
+
const keys = plan.map((s) => s.key);
|
|
46
|
+
const seed = plan.find((s) => s.key === 'seed-version');
|
|
47
|
+
assert.ok(seed, 'the plan carries a seed-version step');
|
|
48
|
+
assert.equal(seed.kind, 'auto', 'seeding is automated (the runner does it, no owner action)');
|
|
49
|
+
assert.equal(keys.indexOf('seed-version'), keys.indexOf('provision') + 1, 'seed immediately follows provision (the DB now exists)');
|
|
50
|
+
assert.ok(keys.indexOf('seed-version') < keys.indexOf('first-sign-in'), 'a version exists before the owner ever signs in');
|
|
51
|
+
// present in BOTH modes (not a greenfield/adopt branch).
|
|
52
|
+
assert.ok(ob.onboardPlan('adopt').some((s) => s.key === 'seed-version'), 'adopt gets it too');
|
|
53
|
+
});
|
|
54
|
+
|
|
40
55
|
test('oauthAppInstructions pins the apex callback + homepage and points at ACCOUNT Developer settings', () => {
|
|
41
56
|
const oa = ob.oauthAppInstructions('mercury.example.com');
|
|
42
57
|
assert.equal(oa.homepageUrl, 'https://mercury.example.com');
|
|
43
58
|
assert.equal(oa.callbackUrl, 'https://mercury.example.com/api/gds/auth/web/callback');
|
|
44
59
|
assert.match(oa.location, /ACCOUNT/);
|
|
45
60
|
assert.match(oa.location, /Developer settings/);
|
|
61
|
+
// task 2127 — must tell the owner to enable Device Flow (off by default; the CLI login
|
|
62
|
+
// uses it). Its absence was the 2026-07-08 demo login-500 root cause.
|
|
63
|
+
assert.match(oa.enableDeviceFlow, /Device Flow/i);
|
|
46
64
|
});
|
|
47
65
|
|
|
48
66
|
test('buildInitSpec maps answers to a STANDALONE init spec (domain → origin + provisioning shape)', () => {
|
|
@@ -68,3 +86,34 @@ test('buildInitSpec without a domain still defaults a standalone provisioning sh
|
|
|
68
86
|
test('healthzUrl', () => {
|
|
69
87
|
assert.equal(ob.healthzUrl('mercury.example.com'), 'https://mercury.example.com/healthz');
|
|
70
88
|
});
|
|
89
|
+
|
|
90
|
+
// ── task 2085: ONE source for the onboarding sequence (anti-drift) ────────────
|
|
91
|
+
|
|
92
|
+
test('onboardPlan is the SAME function the provisioning module owns (single source, not a copy)', () => {
|
|
93
|
+
// The CLI must CONSUME modules/provisioning/onboard-plan.js, not carry its own copy —
|
|
94
|
+
// that is the whole point of task 2085. A re-declared duplicate would pass every other
|
|
95
|
+
// test yet silently drift; identity of the function reference proves there is one source.
|
|
96
|
+
const canonical = require('../modules/provisioning/onboard-plan.js').onboardPlan;
|
|
97
|
+
assert.equal(ob.onboardPlan, canonical, 'scripts/gds/onboard.js must re-export the module’s onboardPlan');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('the web forms (/create + hall wizard) declare EXACTLY the canonical human steps — drift guard', () => {
|
|
101
|
+
// goal 35 / criterion 165: "the canonical step sequence is the source of truth;
|
|
102
|
+
// skill/CLI/hall are forms over it." Each web form tags its human-step <li>s with
|
|
103
|
+
// data-onboard-step="<key>"; this asserts that set equals the plan's human-step keys,
|
|
104
|
+
// so adding/removing/renaming a human step in the single source fails CI until every
|
|
105
|
+
// form is updated (and vice-versa) — the sequence can no longer drift between forms.
|
|
106
|
+
const { readFileSync } = require('node:fs');
|
|
107
|
+
const path = require('node:path');
|
|
108
|
+
const humanKeys = ob.onboardPlan().filter((s) => s.kind === 'human').map((s) => s.key).sort();
|
|
109
|
+
const surfaces = {
|
|
110
|
+
'/create landing page': 'public-landing/create.html',
|
|
111
|
+
'hall new-project wizard': 'modules/hall-ui/public/new-project-wizard.js',
|
|
112
|
+
};
|
|
113
|
+
for (const [name, rel] of Object.entries(surfaces)) {
|
|
114
|
+
const src = readFileSync(path.join(import.meta.dirname, '..', rel), 'utf8');
|
|
115
|
+
const found = [...src.matchAll(/data-onboard-step="([^"]+)"/g)].map((m) => m[1]);
|
|
116
|
+
const uniq = [...new Set(found)].sort();
|
|
117
|
+
assert.deepEqual(uniq, humanKeys, `${name} (${rel}) must tag exactly the canonical human steps [${humanKeys}], found [${uniq}]`);
|
|
118
|
+
}
|
|
119
|
+
});
|
package/tests/package_core.mjs
CHANGED
|
@@ -25,6 +25,8 @@ const {
|
|
|
25
25
|
ARTIFACT_NAME, MANIFEST_SCHEMA, TAR_ROOT,
|
|
26
26
|
// task 1824 — the consumer-core file-classification layer
|
|
27
27
|
isDocPath, functionalSecretLeaks, processCoreFile,
|
|
28
|
+
// task 2121 — agent-doc (CLAUDE.md/AGENTS.md) stub
|
|
29
|
+
isAgentDoc, CORE_AGENT_DOC_STUB,
|
|
28
30
|
// task 1882 (ADR 0108 §2) — the real installable npm package
|
|
29
31
|
CORE_DEP_NAMES, buildPackageJson, resolveLockDep, lockfileClosure, prunePackageLock,
|
|
30
32
|
// task 2089 (ADR 0134) — private-by-default npm publish
|
|
@@ -141,27 +143,27 @@ t('withTarballInfo adds the exact-bytes coordinate for the side-car pin', () =>
|
|
|
141
143
|
version: '1.13.0', coreContract: '1.13.0', sourceCommit: 'abc', sourceRef: 'HEAD',
|
|
142
144
|
builtAtIso: '2026-07-01T00:00:00.000Z', files: [], treeHash: 'x', redaction: null,
|
|
143
145
|
});
|
|
144
|
-
const side = withTarballInfo(inner, { name: '
|
|
146
|
+
const side = withTarballInfo(inner, { name: 'bongos-core-1.13.0.tgz', sha256: 'cafe', bytes: 42 });
|
|
145
147
|
assert.equal(side.tarball.sha256, 'cafe');
|
|
146
148
|
assert.equal(side.tarball.bytes, 42);
|
|
147
149
|
assert.equal(inner.tarball, undefined); // withTarballInfo does not mutate the inner
|
|
148
150
|
});
|
|
149
151
|
|
|
150
152
|
t('artifact filenames', () => {
|
|
151
|
-
assert.equal(tarballName('1.13.0'), '
|
|
152
|
-
assert.equal(manifestName('1.13.0'), '
|
|
153
|
-
assert.equal(stageDirName('1.13.0'), '
|
|
153
|
+
assert.equal(tarballName('1.13.0'), 'bongos-core-1.13.0.tgz');
|
|
154
|
+
assert.equal(manifestName('1.13.0'), 'bongos-core-1.13.0.manifest.json');
|
|
155
|
+
assert.equal(stageDirName('1.13.0'), 'bongos-core-1.13.0');
|
|
154
156
|
});
|
|
155
157
|
|
|
156
158
|
// ── ustar name/prefix split ──────────────────────────────────────────────────────────
|
|
157
159
|
t('splitUstarName: short path → name only', () => {
|
|
158
|
-
const r = splitUstarName('
|
|
160
|
+
const r = splitUstarName('bongos-core/a.js');
|
|
159
161
|
assert.equal(r.prefix, '');
|
|
160
|
-
assert.equal(r.name, '
|
|
162
|
+
assert.equal(r.name, 'bongos-core/a.js');
|
|
161
163
|
});
|
|
162
164
|
|
|
163
165
|
t('splitUstarName: long path splits at a / into name<=100 + prefix<=155', () => {
|
|
164
|
-
const long = '
|
|
166
|
+
const long = 'bongos-core/' + 'dir/'.repeat(24) + 'file.js'; // > 100 bytes → forces a name/prefix split
|
|
165
167
|
const r = splitUstarName(long);
|
|
166
168
|
assert.ok(r, 'expected a split');
|
|
167
169
|
assert.ok(Buffer.byteLength(r.name) <= 100);
|
|
@@ -177,18 +179,18 @@ t('splitUstarName: unsplittable (>100-byte tail) → null (longlink fallback)',
|
|
|
177
179
|
// ── buildTar — produces a real, readable, deterministic archive ──────────────────────
|
|
178
180
|
t('buildTar round-trips a small set (content + mode preserved)', () => {
|
|
179
181
|
const entries = [
|
|
180
|
-
{ name: '
|
|
181
|
-
{ name: '
|
|
182
|
+
{ name: 'bongos-core/a.txt', mode: 0o644, buf: Buffer.from('hello\n') },
|
|
183
|
+
{ name: 'bongos-core/bin/run.sh', mode: 0o755, buf: Buffer.from('#!/bin/sh\necho hi\n') },
|
|
182
184
|
];
|
|
183
185
|
const files = readTar(buildTar(entries));
|
|
184
|
-
assert.equal(files.get('
|
|
185
|
-
assert.equal(files.get('
|
|
186
|
-
assert.equal(files.get('
|
|
187
|
-
assert.equal(files.get('
|
|
186
|
+
assert.equal(files.get('bongos-core/a.txt').content.toString(), 'hello\n');
|
|
187
|
+
assert.equal(files.get('bongos-core/bin/run.sh').content.toString(), '#!/bin/sh\necho hi\n');
|
|
188
|
+
assert.equal(files.get('bongos-core/bin/run.sh').mode & 0o777, 0o755);
|
|
189
|
+
assert.equal(files.get('bongos-core/a.txt').mode & 0o777, 0o644);
|
|
188
190
|
});
|
|
189
191
|
|
|
190
192
|
t('buildTar round-trips a >100-byte path via the GNU longlink path', () => {
|
|
191
|
-
const longName = '
|
|
193
|
+
const longName = 'bongos-core/' + 'deep/'.repeat(25) + 'leaf.js'; // ~150 bytes, but splittable
|
|
192
194
|
const unsplittable = 'z'.repeat(130); // forces the longlink branch (no valid split)
|
|
193
195
|
const entries = [
|
|
194
196
|
{ name: longName, mode: 0o644, buf: Buffer.from('deep\n') },
|
|
@@ -201,15 +203,15 @@ t('buildTar round-trips a >100-byte path via the GNU longlink path', () => {
|
|
|
201
203
|
|
|
202
204
|
t('buildTar is byte-deterministic (same input → identical bytes; entry order irrelevant)', () => {
|
|
203
205
|
const e1 = [
|
|
204
|
-
{ name: '
|
|
205
|
-
{ name: '
|
|
206
|
+
{ name: 'bongos-core/b.txt', mode: 0o644, buf: Buffer.from('B') },
|
|
207
|
+
{ name: 'bongos-core/a.txt', mode: 0o644, buf: Buffer.from('A') },
|
|
206
208
|
];
|
|
207
209
|
const e2 = [...e1].reverse();
|
|
208
210
|
assert.equal(sha256hex(buildTar(e1)), sha256hex(buildTar(e2)));
|
|
209
211
|
});
|
|
210
212
|
|
|
211
213
|
t('buildTar output gunzips and ends with two zero blocks', () => {
|
|
212
|
-
const gz = buildTar([{ name: '
|
|
214
|
+
const gz = buildTar([{ name: 'bongos-core/a', mode: 0o644, buf: Buffer.from('x') }]);
|
|
213
215
|
const tar = zlib.gunzipSync(gz);
|
|
214
216
|
assert.equal(tar.length % 512, 0);
|
|
215
217
|
const tail = tar.subarray(tar.length - 1024);
|
|
@@ -298,6 +300,50 @@ t('DOCS are genericized: instance brand/owner redacted, output is clean', () =>
|
|
|
298
300
|
assert.equal(r.leaks.length, 0); // clean after redaction → no abort
|
|
299
301
|
});
|
|
300
302
|
|
|
303
|
+
// ── task 2121 — CLAUDE.md/AGENTS.md ship as a brand-free stub (demo-audit fix) ──────
|
|
304
|
+
t('isAgentDoc: matches CLAUDE.md/AGENTS.md at root + any depth, nothing else', () => {
|
|
305
|
+
assert.equal(isAgentDoc('CLAUDE.md'), true);
|
|
306
|
+
assert.equal(isAgentDoc('scripts/gds/CLAUDE.md'), true);
|
|
307
|
+
assert.equal(isAgentDoc('src/bongos/routes/CLAUDE.md'), true);
|
|
308
|
+
assert.equal(isAgentDoc('modules/lifecycle/CLAUDE.md'), true);
|
|
309
|
+
assert.equal(isAgentDoc('AGENTS.md'), true);
|
|
310
|
+
assert.equal(isAgentDoc('docs/CLAUDE.md'), true);
|
|
311
|
+
// NOT an agent doc: other markdown, or a filename that merely contains the word
|
|
312
|
+
assert.equal(isAgentDoc('README.md'), false);
|
|
313
|
+
assert.equal(isAgentDoc('docs/adr/0100.md'), false);
|
|
314
|
+
assert.equal(isAgentDoc('docs/CLAUDE-notes.md'), false);
|
|
315
|
+
assert.equal(isAgentDoc('src/claude.js'), false);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
t('processCoreFile STUBS a CLAUDE.md — the core memory never ships (no auto-load pollution)', () => {
|
|
319
|
+
// A realistic core CLAUDE.md: the wrong project title + instance brand + a long body.
|
|
320
|
+
const coreClaude = '# Off the Boats — Project Memory\n' +
|
|
321
|
+
'Visit https://acme.example (world Xanadu), owner zoltan. ' + 'lorem ipsum '.repeat(200);
|
|
322
|
+
const r = proc('CLAUDE.md', coreClaude);
|
|
323
|
+
assert.equal(r.kind, 'stub'); // classified as a stub, not a doc
|
|
324
|
+
assert.equal(r.leaks.length, 0); // brand-free by construction → never a leak
|
|
325
|
+
const out = r.buf.toString();
|
|
326
|
+
assert.equal(out, CORE_AGENT_DOC_STUB); // exactly the stub, verbatim
|
|
327
|
+
assert.ok(!/Off the Boats/i.test(out), 'wrong project title gone');
|
|
328
|
+
assert.ok(!/acme/i.test(out) && !/zoltan/i.test(out) && !/Xanadu/i.test(out), 'no instance brand');
|
|
329
|
+
assert.ok(!/lorem ipsum/.test(out), 'original 200-line body gone');
|
|
330
|
+
assert.ok(out.length < coreClaude.length, 'stub is far shorter than the real memory');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
t('processCoreFile STUBS nested + module CLAUDE.md too (all auto-load sites)', () => {
|
|
334
|
+
for (const p of ['scripts/gds/CLAUDE.md', 'src/bongos/CLAUDE.md', 'modules/economy/CLAUDE.md']) {
|
|
335
|
+
const r = proc(p, '# nested map\nsecret https://acme.example detail');
|
|
336
|
+
assert.equal(r.kind, 'stub', `${p} stubbed`);
|
|
337
|
+
assert.equal(r.buf.toString(), CORE_AGENT_DOC_STUB, `${p} → stub body`);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
t('the stub itself carries no instance brand and states it does not govern', () => {
|
|
342
|
+
assert.ok(!/acme|zoltan|Xanadu|Off the Boats/i.test(CORE_AGENT_DOC_STUB));
|
|
343
|
+
assert.ok(/does not govern/i.test(CORE_AGENT_DOC_STUB));
|
|
344
|
+
assert.ok(/@bongos\/core/.test(CORE_AGENT_DOC_STUB));
|
|
345
|
+
});
|
|
346
|
+
|
|
301
347
|
// ── task 1882 (ADR 0108 §2) — the real installable npm package ─────────────────────
|
|
302
348
|
t('TAR_ROOT is the npm-required "package/" (not the ARTIFACT_NAME dir)', () => {
|
|
303
349
|
assert.equal(TAR_ROOT, 'package/');
|
|
@@ -309,7 +355,7 @@ t('buildPackageJson: prunes to express+pg, keeps the source ranges + the undici
|
|
|
309
355
|
sourceDeps: { express: '^4.21.2', pg: '^8.20.0', phaser: '^3.87.0', 'discord.js': '^14.26.4' },
|
|
310
356
|
sourceOverrides: { undici: '^6.27.0' },
|
|
311
357
|
});
|
|
312
|
-
assert.equal(pkg.name, '@
|
|
358
|
+
assert.equal(pkg.name, '@bongos/core');
|
|
313
359
|
assert.equal(pkg.version, '1.14.0');
|
|
314
360
|
assert.deepEqual(pkg.dependencies, { express: '^4.21.2', pg: '^8.20.0' });
|
|
315
361
|
assert.equal(pkg.dependencies.phaser, undefined);
|
|
@@ -325,7 +371,7 @@ t('buildPackageJson: prunes to express+pg, keeps the source ranges + the undici
|
|
|
325
371
|
});
|
|
326
372
|
|
|
327
373
|
t('publishArgs: private (restricted) publish, dry-run unless live (task 2089 / ADR 0134)', () => {
|
|
328
|
-
const tgz = 'dist/
|
|
374
|
+
const tgz = 'dist/bongos-core-1.14.0.tgz';
|
|
329
375
|
// Default: dry-run, restricted (never public, never a real upload).
|
|
330
376
|
assert.deepEqual(publishArgs(tgz, false), ['publish', tgz, '--access', 'restricted', '--dry-run']);
|
|
331
377
|
// Live: still restricted (private) — the permanent publish is never public here.
|
|
@@ -408,8 +454,8 @@ t('lockfileClosure: matches the real repo lockfile — express+pg close over the
|
|
|
408
454
|
});
|
|
409
455
|
|
|
410
456
|
t('prunePackageLock: emits a self-consistent pruned lockfile (root ranges + only the kept packages)', () => {
|
|
411
|
-
const pruned = prunePackageLock(FIXTURE_LOCK, { name: '@
|
|
412
|
-
assert.equal(pruned.name, '@
|
|
457
|
+
const pruned = prunePackageLock(FIXTURE_LOCK, { name: '@bongos/core', version: '1.14.0', rootRanges: { a: '^1.0.0' }, rootDepNames: ['a'] });
|
|
458
|
+
assert.equal(pruned.name, '@bongos/core');
|
|
413
459
|
assert.equal(pruned.version, '1.14.0');
|
|
414
460
|
assert.equal(pruned.lockfileVersion, 3);
|
|
415
461
|
assert.equal(pruned.requires, true);
|
|
@@ -423,7 +469,7 @@ t('prunePackageLock: emits a self-consistent pruned lockfile (root ranges + only
|
|
|
423
469
|
t('prunePackageLock: on the real repo lockfile, drops the vast majority of packages', () => {
|
|
424
470
|
const realLock = require('../package-lock.json');
|
|
425
471
|
const pruned = prunePackageLock(realLock, {
|
|
426
|
-
name: '@
|
|
472
|
+
name: '@bongos/core', version: '1.14.0', rootRanges: { express: '^4.21.2', pg: '^8.20.0' },
|
|
427
473
|
});
|
|
428
474
|
const keptCount = Object.keys(pruned.packages).length - 1; // exclude the root '' entry
|
|
429
475
|
const sourceCount = Object.keys(realLock.packages).length - 1;
|
package/tests/platform_boot.mjs
CHANGED
|
@@ -107,8 +107,11 @@ test('the apex host serves the landing page — not the game — on a game-less
|
|
|
107
107
|
// it IS the landing, with the branding pack injected (look + identity from config)
|
|
108
108
|
assert.match(res.body, /window\.__BRANDING__=/);
|
|
109
109
|
assert.match(res.body, /Enter the Hall/);
|
|
110
|
-
// the
|
|
111
|
-
|
|
110
|
+
// the per-project landing carries the top-right identity chip + hall entry
|
|
111
|
+
// (goal 35, task 2094). "Create a project" is an ORG-level action that lives
|
|
112
|
+
// only on the gate now — it must NOT appear on a project's own landing.
|
|
113
|
+
assert.match(res.body, /id="who"/);
|
|
114
|
+
assert.doesNotMatch(res.body, /Create a project/);
|
|
112
115
|
});
|
|
113
116
|
|
|
114
117
|
test('the apex host serves the branded /create page (goal 35, task 2081)', async () => {
|