@bongos/core 1.17.0 → 1.17.2

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 (230) hide show
  1. package/.bongos-core.json +363 -312
  2. package/.claude/skills/blocker-review/SKILL.md +1 -1
  3. package/.claude/skills/builder-claim/SKILL.md +2 -0
  4. package/.claude/skills/builder-exit/SKILL.md +2 -2
  5. package/.claude/skills/builder-redteam/SKILL.md +3 -3
  6. package/.claude/skills/builder-setup/SKILL.md +2 -2
  7. package/.claude/skills/builder-start/SKILL.md +1 -1
  8. package/.claude/skills/dev/SKILL.md +1 -1
  9. package/.claude/skills/idea-triage/SKILL.md +2 -2
  10. package/.claude/skills/ideate/SKILL.md +1 -1
  11. package/.claude/skills/merge-mode/SKILL.md +1 -1
  12. package/.claude/skills/new-project/SKILL.md +3 -0
  13. package/.claude/skills/paint/SKILL.md +1 -1
  14. package/CLAUDE.md +11 -238
  15. package/GOVERNANCE.md +1 -1
  16. package/README.md +1 -1
  17. package/bin/bongos.js +1 -0
  18. package/clients/bongos-client/README.md +4 -4
  19. package/clients/bongos-client/bongos-client.global.js +14 -1
  20. package/clients/bongos-client/index.cjs +14 -1
  21. package/clients/bongos-client/index.d.ts +19 -2
  22. package/clients/bongos-client/index.mjs +11 -1
  23. package/clients/bongos-client/package.json +1 -1
  24. package/docs/adr/0001-stack-choice-single-vps.md +1 -1
  25. package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
  26. package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
  27. package/docs/adr/0020-security-model-public-stub.md +1 -1
  28. package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
  29. package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
  30. package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
  31. package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
  32. package/docs/adr/0030-builders-hall-live-updates.md +1 -1
  33. package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
  34. package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
  35. package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
  36. package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
  37. package/docs/adr/0044-per-box-live-game-preview.md +1 -1
  38. package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
  39. package/docs/adr/0057-container-cost-ledger.md +3 -3
  40. package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
  41. package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
  42. package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
  43. package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
  44. package/docs/adr/0096-require-reward-before-workable.md +9 -9
  45. package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
  46. package/docs/adr/0100-two-repo-instance-model.md +1 -1
  47. package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
  48. package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
  49. package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
  50. package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
  51. package/docs/adr/0133-github-app-manifest-onboarding-signin.md +2 -0
  52. package/docs/adr/0134-private-first-npm-distribution.md +20 -6
  53. package/docs/adr/0135-module-upstream-submission-interim-queue.md +34 -0
  54. package/docs/adr/0136-update-channel-subscription-policy.md +51 -0
  55. package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
  56. package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
  57. package/docs/adr/README.md +14 -10
  58. package/docs/api/openapi.json +315 -6
  59. package/docs/api-reference.md +10 -5
  60. package/docs/architecture.md +3 -3
  61. package/docs/branding-contract.md +5 -4
  62. package/docs/canonical-permissions.md +1 -1
  63. package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
  64. package/docs/file-map.md +20 -19
  65. package/docs/handoff-template.md +2 -2
  66. package/docs/modules-contract.md +8 -1
  67. package/docs/onboarding/diagrams/README.md +7 -7
  68. package/docs/onboarding/drachmae-and-karma.md +18 -18
  69. package/docs/onboarding/primer.md +9 -9
  70. package/docs/recipes/audit-replay.md +2 -2
  71. package/docs/recipes/builder-box-lifecycle.md +2 -2
  72. package/docs/recipes/claude-design-loop.md +1 -1
  73. package/docs/recipes/instance-deploy.md +4 -4
  74. package/docs/recipes/instance-first-run.md +51 -0
  75. package/docs/recipes/ops-gotchas.md +1 -1
  76. package/docs/recipes/packaging-the-core.md +5 -5
  77. package/docs/recipes/private-npm-distribution.md +39 -12
  78. package/docs/recipes/self-host.md +1 -1
  79. package/docs/recipes/staging-environment.md +1 -1
  80. package/docs/recipes/upgrading-the-core.md +63 -8
  81. package/docs/research/cursor-competitive-analysis.md +1 -1
  82. package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
  83. package/migrations/core_191_module_submissions.sql +45 -0
  84. package/modules/autonomy/CLAUDE.md +11 -45
  85. package/modules/builder-settings/CLAUDE.md +11 -54
  86. package/modules/dev-box/CLAUDE.md +11 -34
  87. package/modules/discord/CLAUDE.md +11 -46
  88. package/modules/discord/module.json +2 -1
  89. package/modules/economy/CLAUDE.md +11 -44
  90. package/modules/economy/achievements.js +9 -1
  91. package/modules/economy/credits.js +10 -1
  92. package/modules/grading/CLAUDE.md +11 -38
  93. package/modules/hall-ui/CLAUDE.md +11 -57
  94. package/modules/hall-ui/public/builders.js +71 -4
  95. package/modules/hall-ui/public/goal-inbox.js +191 -0
  96. package/modules/hall-ui/public/goals-page.js +227 -22
  97. package/modules/hall-ui/public/goals.html +16 -0
  98. package/modules/hall-ui/public/hall-widgets.js +14 -1
  99. package/modules/hall-ui/public/index.html +3 -0
  100. package/modules/hall-ui/public/new-project-wizard.js +6 -4
  101. package/modules/hall-ui/public/style.css +43 -0
  102. package/modules/ideas/CLAUDE.md +11 -33
  103. package/modules/lifecycle/CLAUDE.md +11 -59
  104. package/modules/lifecycle/routes/goals.js +80 -0
  105. package/modules/memory/CLAUDE.md +11 -36
  106. package/modules/onboarding/CLAUDE.md +11 -51
  107. package/modules/provisioning/CLAUDE.md +15 -0
  108. package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
  109. package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
  110. package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
  111. package/modules/provisioning/module.json +17 -0
  112. package/modules/provisioning/onboard-plan.js +37 -0
  113. package/modules/provisioning/provisioning.js +539 -0
  114. package/modules/provisioning/rate-limit.js +35 -0
  115. package/modules/provisioning/routes/provisioning.js +607 -0
  116. package/modules/provisioning/tests/provisioning.mjs +232 -0
  117. package/modules/provisioning/ui/.gitkeep +0 -0
  118. package/modules/security/CLAUDE.md +11 -40
  119. package/modules/security/secret-scrub.js +10 -0
  120. package/modules/sessions/CLAUDE.md +11 -35
  121. package/modules/status-ui/CLAUDE.md +11 -43
  122. package/package-lock.json +2 -2
  123. package/package.json +7 -2
  124. package/public-landing/create.html +58 -12
  125. package/public-landing/index.html +40 -21
  126. package/scripts/discord/README.md +1 -1
  127. package/scripts/gds/CLAUDE.md +11 -140
  128. package/scripts/gds/claim.js +98 -48
  129. package/scripts/gds/claude-materialize.js +174 -20
  130. package/scripts/gds/claude-md-template.js +84 -0
  131. package/scripts/gds/cli-lib.js +112 -2
  132. package/scripts/gds/cutover-codemod.js +241 -0
  133. package/scripts/gds/fitness.js +54 -0
  134. package/scripts/gds/gen-api-client.js +10 -10
  135. package/scripts/gds/init.js +112 -16
  136. package/scripts/gds/lint-infra-exec.js +70 -0
  137. package/scripts/gds/lint-workflow-exec.js +63 -0
  138. package/scripts/gds/migration-namespace.js +1 -1
  139. package/scripts/gds/mirror-lineage-check.js +14 -5
  140. package/scripts/gds/mirror-redact.js +8 -0
  141. package/scripts/gds/module.js +118 -4
  142. package/scripts/gds/npmrc.js +93 -0
  143. package/scripts/gds/onboard.js +17 -24
  144. package/scripts/gds/package-core.js +74 -17
  145. package/scripts/gds/provision.js +102 -17
  146. package/scripts/gds/publish-manifest.js +60 -2
  147. package/scripts/gds/run-routine.js +51 -13
  148. package/scripts/gds/run-unit-tests.js +4 -0
  149. package/scripts/gds/seed-adopt-tasks.js +1 -1
  150. package/scripts/gds/status.js +6 -1
  151. package/scripts/gds/task.js +166 -0
  152. package/scripts/gds/update-channel.js +186 -0
  153. package/scripts/gds/upgrade.js +172 -25
  154. package/src/bongos/CLAUDE.md +11 -60
  155. package/src/bongos/auth.js +24 -0
  156. package/src/bongos/module-submissions.js +56 -0
  157. package/src/bongos/route-rank-check.js +6 -0
  158. package/src/bongos/routes/CLAUDE.md +11 -39
  159. package/src/bongos/routes/auth.js +6 -1
  160. package/src/bongos/routes/modules.js +123 -21
  161. package/src/bongos/serve-internal.js +10 -1
  162. package/src/branding.js +1 -0
  163. package/src/instance-config.js +1 -1
  164. package/src/module-api.js +24 -1
  165. package/src/module-loader/loader.js +36 -4
  166. package/src/module-loader/manifest-schema.js +16 -1
  167. package/tests/branding.mjs +11 -0
  168. package/tests/claim_error_surface.mjs +107 -0
  169. package/tests/claude_materialize.mjs +125 -0
  170. package/tests/consumer_layout_boot.mjs +302 -0
  171. package/tests/cutover_codemod.mjs +184 -0
  172. package/tests/device_flow_start.mjs +96 -0
  173. package/tests/first_run_engine_check.mjs +79 -0
  174. package/tests/goal_membership_requests.mjs +33 -3
  175. package/tests/hook_core_resolve.mjs +125 -0
  176. package/tests/hook_user_agents.mjs +62 -0
  177. package/tests/infra_exec_paths.mjs +104 -0
  178. package/tests/init.mjs +106 -6
  179. package/tests/instance_mismatch_guard.mjs +101 -0
  180. package/tests/mirror_lineage_proof.mjs +14 -0
  181. package/tests/module_cli.mjs +130 -2
  182. package/tests/module_loader.mjs +42 -0
  183. package/tests/module_manifest.mjs +19 -0
  184. package/tests/module_submissions.mjs +135 -0
  185. package/tests/npmrc.mjs +105 -0
  186. package/tests/onboard.mjs +62 -0
  187. package/tests/package_core.mjs +76 -22
  188. package/tests/platform_boot.mjs +5 -2
  189. package/tests/provision.mjs +107 -8
  190. package/tests/provisioning_github_app_routes.mjs +44 -2
  191. package/tests/publish_manifest.mjs +52 -1
  192. package/tests/run_routine.mjs +26 -1
  193. package/tests/run_routine_two_root.mjs +77 -0
  194. package/tests/scrubber_corpus.mjs +17 -0
  195. package/tests/served_paths_shipped.mjs +2 -2
  196. package/tests/task_cli.mjs +99 -0
  197. package/tests/update_channel.mjs +186 -0
  198. package/tests/upgrade.mjs +220 -25
  199. package/tests/workflow_exec_paths.mjs +98 -0
  200. package/docs/design/container-exterior-tiles.md +0 -49
  201. package/docs/design/container-exterior.md +0 -81
  202. package/docs/design/container-interior.md +0 -77
  203. package/docs/design/world-map-scroll.md +0 -41
  204. package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
  205. package/docs/recipes/cloudbongos-staging-gate.md +0 -139
  206. package/docs/recipes/cloudbongos-standup.md +0 -111
  207. package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
  208. package/docs/recipes/parallel-load-test.md +0 -32
  209. package/docs/recipes/pixel-art-pipeline.md +0 -411
  210. package/docs/recipes/staging-data.md +0 -50
  211. package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
  212. package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
  213. package/migrations/instance/110_medusa_planning_seed.sql +0 -70
  214. package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
  215. package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
  216. package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
  217. package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
  218. package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
  219. package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
  220. package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
  221. package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
  222. package/scripts/gds/art-stage.js +0 -152
  223. package/server.js +0 -212
  224. package/tests/art_key.mjs +0 -121
  225. package/tests/day_cycle.mjs +0 -197
  226. package/tests/snapshot_picker.mjs +0 -233
  227. package/tests/sprite_facing.mjs +0 -35
  228. package/tests/terrain_picker.mjs +0 -298
  229. package/tests/turn_in_place.mjs +0 -280
  230. package/tests/walkability.mjs +0 -188
@@ -0,0 +1,79 @@
1
+ // tests/first_run_engine_check.mjs
2
+ //
3
+ // The SessionStart first-run engine check (task 2105): a freshly-cloned standalone
4
+ // instance that hasn't run `npm install` has no `bongos`/tools, so a session used
5
+ // to improvise against the wrong instance (the 2026-07-08 demo-session failure).
6
+ // firstRunEngineCheck() detects that state and prints the two first-run commands.
7
+ // It reads package.json + config/branding.json directly (never requires the missing
8
+ // core), is inert on a source/monorepo checkout and once installed, and is injectable
9
+ // (projectDir + log) so it unit-tests standalone. Run: node tests/first_run_engine_check.mjs
10
+
11
+ import { strict as assert } from 'node:assert';
12
+ import { createRequire } from 'node:module';
13
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
14
+ import { tmpdir } from 'node:os';
15
+ import { join } from 'node:path';
16
+
17
+ const require = createRequire(import.meta.url);
18
+ const { firstRunEngineCheck } = require('../.claude/hooks/session-start.js');
19
+
20
+ let passed = 0, failed = 0;
21
+ function t(name, fn) {
22
+ try { fn(); passed++; console.log(` ok ${name}`); }
23
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
24
+ }
25
+
26
+ // Build a temp instance dir; deps + whether node_modules/<dep> exists + branding are configurable.
27
+ function mkInstance({ dep, installed = false, branding } = {}) {
28
+ const dir = mkdtempSync(join(tmpdir(), 'fre-'));
29
+ writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'x', dependencies: dep ? { [dep]: 'file:vendor/x.tgz' } : { express: '^4' } }));
30
+ if (installed && dep) mkdirSync(join(dir, 'node_modules', dep), { recursive: true });
31
+ if (branding) { mkdirSync(join(dir, 'config'), { recursive: true }); writeFileSync(join(dir, 'config', 'branding.json'), JSON.stringify(branding)); }
32
+ return dir;
33
+ }
34
+ const run = (dir) => { const lines = []; const fired = firstRunEngineCheck(dir, { log: (s) => lines.push(s) }); return { fired, out: lines.join('\n') }; };
35
+
36
+ t('consumer instance, engine NOT installed → prints the two first-run commands + the login origin', () => {
37
+ const dir = mkInstance({ dep: '@cloudbongos/core', installed: false, branding: { envPrefix: 'CLOUDBONGOS', domains: { publicOrigin: 'https://demo.cloudbongos.com' } } });
38
+ const { fired, out } = run(dir);
39
+ assert.equal(fired, true);
40
+ assert.ok(out.includes("engine isn't installed"), 'names the problem');
41
+ assert.ok(out.includes('npm install'), 'step 1');
42
+ assert.ok(out.includes('npx bongos login https://demo.cloudbongos.com'), 'step 2 with the instance origin');
43
+ assert.ok(out.includes('[cloudbongos]'), 'tag from branding envPrefix');
44
+ rmSync(dir, { recursive: true, force: true });
45
+ });
46
+
47
+ t('source / monorepo checkout (no core dependency) → INERT (no output)', () => {
48
+ const dir = mkInstance({ dep: null });
49
+ const { fired, out } = run(dir);
50
+ assert.equal(fired, false);
51
+ assert.equal(out, '');
52
+ rmSync(dir, { recursive: true, force: true });
53
+ });
54
+
55
+ t('consumer instance, engine INSTALLED → INERT (nothing to say)', () => {
56
+ const dir = mkInstance({ dep: '@cloudbongos/core', installed: true, branding: { domains: { publicOrigin: 'https://demo.cloudbongos.com' } } });
57
+ const { fired } = run(dir);
58
+ assert.equal(fired, false);
59
+ rmSync(dir, { recursive: true, force: true });
60
+ });
61
+
62
+ t('accepts the @bongos/core and cloudbongos dependency names too', () => {
63
+ for (const dep of ['@bongos/core', 'cloudbongos']) {
64
+ const dir = mkInstance({ dep, installed: false, branding: { domains: { publicOrigin: 'https://x.example.com' } } });
65
+ assert.equal(run(dir).fired, true, `${dep} recognized as the core dep`);
66
+ rmSync(dir, { recursive: true, force: true });
67
+ }
68
+ });
69
+
70
+ t('missing config/branding.json → still fires, with a placeholder login target (never crashes)', () => {
71
+ const dir = mkInstance({ dep: '@cloudbongos/core', installed: false });
72
+ const { fired, out } = run(dir);
73
+ assert.equal(fired, true);
74
+ assert.ok(out.includes('npx bongos login <this-instance-url>'), 'placeholder when origin unknown');
75
+ rmSync(dir, { recursive: true, force: true });
76
+ });
77
+
78
+ console.log(`\nfirst-run engine check (task 2105): ${passed} passed, ${failed} failed`);
79
+ process.exit(failed ? 1 : 0);
@@ -40,6 +40,7 @@ const {
40
40
  authorizeCreateJoinRequest,
41
41
  authorizeInvitationResponse,
42
42
  authorizeJoinRequestResponse,
43
+ authorizeWithdrawJoinRequest,
43
44
  } = require('../modules/lifecycle/routes/goals.js');
44
45
  const { MODULE_EXPECTED_RANKS } = require('../src/bongos/route-rank-check.js');
45
46
 
@@ -310,12 +311,39 @@ await test('authorizeJoinRequestResponse: an Archon (non-member) resolves → AL
310
311
  assert.deepEqual(authorizeJoinRequestResponse({ goal: OWNER_GOAL, actorId: 42, actorRank: 'archon', actorMembership: null, request: REQUEST, goalId: 5 }), { ok: true });
311
312
  });
312
313
 
314
+ // authorizeWithdrawJoinRequest — only the requester withdraws their own pending
315
+ // join-request (task 1953), a symmetric mirror of authorizeInvitationResponse.
316
+ const REQUEST_FOR_WITHDRAW = { id: 11, goal_id: '5', builder_id: '2', kind: 'request', status: 'pending' };
317
+ await test('authorizeWithdrawJoinRequest: unknown request → 404', () => {
318
+ const d = authorizeWithdrawJoinRequest({ request: null, goalId: 5, actorId: 2 });
319
+ assert.equal(d.status, 404); assert.equal(d.body.error, 'request_not_found');
320
+ });
321
+ await test('authorizeWithdrawJoinRequest: request on a different goal → 404 (containment)', () => {
322
+ const d = authorizeWithdrawJoinRequest({ request: { ...REQUEST_FOR_WITHDRAW, goal_id: '99' }, goalId: 5, actorId: 2 });
323
+ assert.equal(d.status, 404);
324
+ });
325
+ await test('authorizeWithdrawJoinRequest: row is an invitation, not a request → 400', () => {
326
+ const d = authorizeWithdrawJoinRequest({ request: { ...REQUEST_FOR_WITHDRAW, kind: 'invite' }, goalId: 5, actorId: 2 });
327
+ assert.equal(d.status, 400); assert.equal(d.body.error, 'not_a_join_request');
328
+ });
329
+ await test('authorizeWithdrawJoinRequest: already resolved → 409 not_pending', () => {
330
+ const d = authorizeWithdrawJoinRequest({ request: { ...REQUEST_FOR_WITHDRAW, status: 'accepted' }, goalId: 5, actorId: 2 });
331
+ assert.equal(d.status, 409); assert.equal(d.body.error, 'not_pending');
332
+ });
333
+ await test('authorizeWithdrawJoinRequest: someone else withdrawing my request → 403 not_your_request', () => {
334
+ const d = authorizeWithdrawJoinRequest({ request: REQUEST_FOR_WITHDRAW, goalId: 5, actorId: 7 });
335
+ assert.equal(d.status, 403); assert.equal(d.body.error, 'not_your_request');
336
+ });
337
+ await test('authorizeWithdrawJoinRequest: the requester (string vs number id) → ALLOW', () => {
338
+ assert.deepEqual(authorizeWithdrawJoinRequest({ request: REQUEST_FOR_WITHDRAW, goalId: '5', actorId: 2 }), { ok: true });
339
+ });
340
+
313
341
  // =========================================================================
314
- // route-rank pins (task 1952) — the walls are in-handler, so the routes stay
315
- // 'any-builder'; an accidental requireRank re-gate must be a red build.
342
+ // route-rank pins (task 1952/1953) — the walls are in-handler, so the routes
343
+ // stay 'any-builder'; an accidental requireRank re-gate must be a red build.
316
344
  // =========================================================================
317
345
 
318
- await test('route-rank pins: the six new routes are pinned any-builder', () => {
346
+ await test('route-rank pins: the eight private-goal routes are pinned any-builder', () => {
319
347
  const pins = MODULE_EXPECTED_RANKS.lifecycle;
320
348
  for (const r of [
321
349
  'GET /goals/inbox',
@@ -324,6 +352,8 @@ await test('route-rank pins: the six new routes are pinned any-builder', () => {
324
352
  'POST /goals/:id/requests/:reqId/respond',
325
353
  'POST /goals/:id/invitations',
326
354
  'POST /goals/:id/invitations/:reqId/respond',
355
+ 'GET /goals/:id/requests',
356
+ 'POST /goals/:id/requests/:reqId/withdraw',
327
357
  ]) {
328
358
  assert.equal(pins[r], 'any-builder', `${r} must be pinned any-builder (wall is in-handler)`);
329
359
  }
@@ -0,0 +1,125 @@
1
+ // tests/hook_core_resolve.mjs
2
+ //
3
+ // Task 2139 — the materialized .claude/hooks resolve CORE modules (src/bongos/*,
4
+ // scripts/gds/*) in BOTH layouts: the repo root today, and the pinned @bongos/core
5
+ // package post-split (ADR 0108). Before this, conductor.js's UNGUARDED
6
+ // require('../../src/bongos/…') MODULE_NOT_FOUND'd on EVERY UserPromptSubmit once
7
+ // src/ left the instance root; the guarded hooks silently killed their feature.
8
+ //
9
+ // _core-resolve.coreModulePathFrom(instanceRoot, rel) is the whole mechanism, so
10
+ // it takes the root as an argument and this suite diverges it against synthetic
11
+ // trees (impossible to prove with the real single checkout, where root == core).
12
+ // Locks:
13
+ // 1. finds a core file inside node_modules/@bongos/core when it is NOT at the
14
+ // repo root — the post-split consumer layout, the point of the task;
15
+ // 2. repo root WINS when the file exists in both (today = byte-identical);
16
+ // 3. absent in every layout → null (fresh clone → callers degrade, never crash);
17
+ // 4. all three published core package names resolve;
18
+ // 5. requireCore loads a real core module today + throws a tagged error when absent;
19
+ // 6. conductor.js, loaded in a core-LESS instance root, does NOT crash and still
20
+ // emits its methodology nudges (the "conductor no longer hard-crashes" done-when).
21
+ //
22
+ // DB-free + no network → part of the unit gate. Run: node tests/hook_core_resolve.mjs
23
+
24
+ import { strict as assert } from 'node:assert';
25
+ import { execFileSync } from 'node:child_process';
26
+ import { mkdtempSync, writeFileSync, mkdirSync, cpSync } from 'node:fs';
27
+ import { tmpdir } from 'node:os';
28
+ import { join, dirname } from 'node:path';
29
+ import { fileURLToPath } from 'node:url';
30
+ import { createRequire } from 'node:module';
31
+
32
+ const require = createRequire(import.meta.url);
33
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
34
+ const resolver = require('../.claude/hooks/_core-resolve.js');
35
+
36
+ let passed = 0;
37
+ let failed = 0;
38
+ function t(name, fn) {
39
+ try { fn(); passed++; console.log(` PASS ${name}`); }
40
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
41
+ }
42
+
43
+ // Build a throwaway instance root from { relPath: contents }.
44
+ function makeTree(files) {
45
+ const root = mkdtempSync(join(tmpdir(), 'hook-core-'));
46
+ for (const [rel, body] of Object.entries(files)) {
47
+ const abs = join(root, rel);
48
+ mkdirSync(dirname(abs), { recursive: true });
49
+ writeFileSync(abs, body);
50
+ }
51
+ return root;
52
+ }
53
+
54
+ // 1. Post-split: the core file exists ONLY inside the installed package.
55
+ t('resolves a core module inside node_modules/@bongos/core (post-split layout)', () => {
56
+ const root = makeTree({ 'node_modules/@bongos/core/src/bongos/path-match.js': 'module.exports={};' });
57
+ const p = resolver.coreModulePathFrom(root, 'src/bongos/path-match');
58
+ assert.ok(p, 'expected a resolution');
59
+ assert.ok(p.includes(join('node_modules', '@bongos', 'core')), `expected a node_modules path, got ${p}`);
60
+ });
61
+
62
+ // 2. Priority: repo root wins when present in both (today's single checkout — unchanged).
63
+ t('prefers the repo root when a core file exists at both root and node_modules', () => {
64
+ const root = makeTree({
65
+ 'scripts/gds/cli-lib.js': 'module.exports={root:true};',
66
+ 'node_modules/@bongos/core/scripts/gds/cli-lib.js': 'module.exports={pkg:true};',
67
+ });
68
+ const p = resolver.coreModulePathFrom(root, 'scripts/gds/cli-lib');
69
+ assert.ok(p && !p.includes('node_modules'), `expected the root copy, got ${p}`);
70
+ });
71
+
72
+ // 3. Absent everywhere → null (fresh clone pre-`npm install`; callers degrade).
73
+ t('returns null when the core file exists in no layout', () => {
74
+ const root = makeTree({ 'README.md': '# empty instance' });
75
+ assert.equal(resolver.coreModulePathFrom(root, 'src/bongos/path-match'), null);
76
+ });
77
+
78
+ // 4. Every published core package name is honored (scoped-private, scoped-public, bare).
79
+ for (const pkg of resolver.CORE_PKG_NAMES) {
80
+ t(`resolves via published package name "${pkg}"`, () => {
81
+ const root = makeTree({ [`node_modules/${pkg}/scripts/gds/x.js`]: 'module.exports={};' });
82
+ const p = resolver.coreModulePathFrom(root, 'scripts/gds/x.js');
83
+ const frag = join('node_modules', ...pkg.split('/'));
84
+ assert.ok(p && p.includes(frag), `expected "${frag}" in ${p}`);
85
+ });
86
+ }
87
+
88
+ // 5. requireCore: loads a real core module at the repo root today; throws tagged when absent.
89
+ t('requireCore loads a real core module at the repo root today', () => {
90
+ const cli = resolver.requireCore('scripts/gds/cli-lib');
91
+ assert.equal(typeof cli.apiCall, 'function', 'expected cli-lib.apiCall');
92
+ });
93
+ t('requireCore throws CORE_MODULE_NOT_FOUND when the core module is absent', () => {
94
+ assert.throws(
95
+ () => resolver.requireCore('src/bongos/__does_not_exist_2139__'),
96
+ (e) => e && e.code === 'CORE_MODULE_NOT_FOUND',
97
+ );
98
+ });
99
+
100
+ // 6. The headline done-when: conductor.js in a core-LESS instance root does not crash
101
+ // and still emits methodology nudges (the sensitive-surface advisory degrades off).
102
+ // Runs in a child process with CLAUDE_PROJECT_DIR pointed at the core-less root so
103
+ // _core-resolve computes that root (this process's env points at the real repo).
104
+ t('conductor.js in a core-less instance root does not crash and still nudges', () => {
105
+ const root = mkdtempSync(join(tmpdir(), 'hook-coreless-'));
106
+ const hooks = join(root, '.claude', 'hooks');
107
+ mkdirSync(hooks, { recursive: true });
108
+ cpSync(join(ROOT, '.claude', 'hooks', 'conductor.js'), join(hooks, 'conductor.js'));
109
+ cpSync(join(ROOT, '.claude', 'hooks', '_core-resolve.js'), join(hooks, '_core-resolve.js'));
110
+ const snippet =
111
+ `const c = require(${JSON.stringify(join(hooks, 'conductor.js'))});` +
112
+ `const out = c.evaluateTriggers({ changedFiles: ['src/bongos/sensitive-surfaces.js'], prompt: 'ship it', activeClaims: [] });` +
113
+ `process.stdout.write(JSON.stringify(out.map((a) => a.id)));`;
114
+ const stdout = execFileSync(process.execPath, ['-e', snippet], {
115
+ encoding: 'utf8',
116
+ env: { ...process.env, CLAUDE_PROJECT_DIR: root },
117
+ });
118
+ const ids = JSON.parse(stdout);
119
+ assert.ok(ids.includes('no-claim'), 'still emits the no-claim methodology nudge without the core');
120
+ assert.ok(ids.includes('ship-intent'), 'still emits the ship-intent nudge without the core');
121
+ assert.ok(!ids.includes('sensitive-surface'), 'sensitive-surface degrades off (isSensitive fallback) when the core is absent');
122
+ });
123
+
124
+ console.log(`\n${passed} passed, ${failed} failed`);
125
+ process.exit(failed ? 1 : 0);
@@ -0,0 +1,62 @@
1
+ // tests/hook_user_agents.mjs
2
+ //
3
+ // task 2122 (demo-audit fix B) — the scaffolded .claude/hooks must NOT hardcode a
4
+ // founder-instance brand in their outbound HTTP User-Agent. Those hooks are copied
5
+ // into a consumer instance by claude-materialize, whose rebrand maps domain HOSTS
6
+ // (amazonprimea.com), config dirs, and env prefixes — but NOT a bare brand token
7
+ // like `amazonprimea-gds-cli` (no TLD). So a branded UA there leaks the founder's
8
+ // host-brand into every scaffolded instance (criterion gcarry-prod-vanilla).
9
+ //
10
+ // Invariant: every `'User-Agent': '<value>'` literal in .claude/hooks/*.js uses the
11
+ // platform-neutral `cloud-bongos-` prefix (matching src/branding.js userAgent()'s
12
+ // neutral fallback), never an instance brand. Hermetic: reads files only, no DB/net.
13
+ //
14
+ // Run: node tests/hook_user_agents.mjs
15
+
16
+ import { strict as assert } from 'node:assert';
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
22
+ const HOOKS_DIR = path.join(HERE, '..', '.claude', 'hooks');
23
+
24
+ // Every User-Agent request-header literal, single- or double-quoted.
25
+ const UA_RE = /['"]User-Agent['"]\s*:\s*['"]([^'"]+)['"]/g;
26
+ const NEUTRAL_PREFIX = 'cloud-bongos-';
27
+
28
+ let passed = 0;
29
+ let failed = 0;
30
+ function t(name, fn) {
31
+ try { fn(); passed++; console.log(` PASS ${name}`); }
32
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
33
+ }
34
+
35
+ const hookFiles = fs.existsSync(HOOKS_DIR)
36
+ ? fs.readdirSync(HOOKS_DIR).filter((f) => f.endsWith('.js')).sort()
37
+ : [];
38
+
39
+ t('.claude/hooks exists and has hook files to check', () => {
40
+ assert.ok(hookFiles.length > 0, `no hook .js files found under ${HOOKS_DIR}`);
41
+ });
42
+
43
+ t('every hook User-Agent uses the neutral cloud-bongos- prefix (no founder-brand leak)', () => {
44
+ const offenders = [];
45
+ let uaCount = 0;
46
+ for (const f of hookFiles) {
47
+ const src = fs.readFileSync(path.join(HOOKS_DIR, f), 'utf8');
48
+ for (const m of src.matchAll(UA_RE)) {
49
+ uaCount++;
50
+ const ua = m[1];
51
+ if (!ua.startsWith(NEUTRAL_PREFIX)) offenders.push(`${f}: '${ua}'`);
52
+ }
53
+ }
54
+ assert.ok(uaCount > 0, 'expected at least one User-Agent literal across the hooks (regex drift?)');
55
+ assert.equal(
56
+ offenders.length, 0,
57
+ `hook User-Agent(s) not neutral (must start with "${NEUTRAL_PREFIX}"):\n ${offenders.join('\n ')}`
58
+ );
59
+ });
60
+
61
+ console.log(`\n${passed} passed, ${failed} failed`);
62
+ process.exit(failed ? 1 : 0);
@@ -0,0 +1,104 @@
1
+ // tests/infra_exec_paths.mjs — task 2137 (ADR 0108 configurable-root).
2
+ //
3
+ // Two proofs that the host systemd units resolve their CORE scripts in a consumer layout after the
4
+ // R89 split:
5
+ // A. lint-infra-exec: every infra/*.service ExecStart that runs a core script goes through
6
+ // infra/core-node.sh AND names a PUBLISHABLE core file (ships in @bongos/core → present at
7
+ // node_modules/@bongos/core post-install) that also exists at the core root today. No unit
8
+ // still uses the bare repo-relative `node scripts/…` form.
9
+ // B. core-node.sh actually resolves both layouts: it prefers the instance root (monolith) and
10
+ // falls back to node_modules/@bongos/core (consumer), driven by $BONGOS_INSTANCE_ROOT.
11
+ //
12
+ // DB-free, no network. Run: node tests/infra_exec_paths.mjs
13
+
14
+ import { strict as assert } from 'node:assert';
15
+ import { execFileSync } from 'node:child_process';
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
+ import { fileURLToPath } from 'node:url';
21
+
22
+ const require = createRequire(import.meta.url);
23
+ const { lintInfraExec } = require('../scripts/gds/lint-infra-exec.js');
24
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
+ const REPO = path.resolve(__dirname, '..');
26
+ const LAUNCHER = path.join(REPO, 'infra', 'core-node.sh');
27
+
28
+ let passed = 0, failed = 0;
29
+ function test(name, fn) {
30
+ try { fn(); console.log(` ok ${name}`); passed++; }
31
+ catch (err) { console.error(` FAIL ${name}`); console.error(' ', err.message); failed++; }
32
+ }
33
+
34
+ // ---- A. the lint is green on the real tree, and covers all 11 core-script units ----------------
35
+ test('lint-infra-exec: every core-script unit resolves in a consumer layout (publishable + present)', () => {
36
+ const r = lintInfraExec({ instanceRoot: REPO, coreRoot: REPO });
37
+ assert.deepEqual(r.violations, [], `expected no violations, got: ${JSON.stringify(r.violations)}`);
38
+ assert.ok(r.ok, 'lint should pass');
39
+ assert.equal(r.checked.length, 11, `expected all 11 core-script units repointed + resolving, got ${r.checked.length}`);
40
+ // The three core CLIs the units invoke are all covered.
41
+ const rels = new Set(r.checked.map((c) => c.rel));
42
+ for (const rel of ['scripts/gds/box.js', 'scripts/gds/provision.js', 'scripts/gds/search-ingest-db.js']) {
43
+ assert.ok(rels.has(rel), `expected a unit routing through ${rel}`);
44
+ }
45
+ });
46
+
47
+ test('lint-infra-exec: a bare repo-relative core ExecStart is flagged (counterfactual)', () => {
48
+ // Synthetic infra/ with the OLD form → must be reported as a violation.
49
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'infra-lint-'));
50
+ try {
51
+ fs.mkdirSync(path.join(tmp, 'infra'), { recursive: true });
52
+ fs.mkdirSync(path.join(tmp, 'scripts', 'gds'), { recursive: true });
53
+ fs.writeFileSync(path.join(tmp, 'scripts', 'gds', 'box.js'), '// probe\n');
54
+ fs.writeFileSync(path.join(tmp, 'infra', 'legacy.service'),
55
+ '[Service]\nExecStart=/usr/bin/node scripts/gds/box.js run-intents --apply\n');
56
+ const r = lintInfraExec({ instanceRoot: tmp, coreRoot: tmp });
57
+ assert.ok(!r.ok, 'a bare repo-relative core ExecStart must fail the lint');
58
+ assert.equal(r.violations.length, 1);
59
+ assert.match(r.violations[0].why, /core-node\.sh/);
60
+ } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
61
+ });
62
+
63
+ // ---- B. the launcher resolves BOTH layouts ------------------------------------------------------
64
+ function runLauncher(rel, instanceRoot) {
65
+ return execFileSync('bash', [LAUNCHER, rel], {
66
+ encoding: 'utf8',
67
+ env: { ...process.env, BONGOS_INSTANCE_ROOT: instanceRoot },
68
+ }).trim();
69
+ }
70
+
71
+ test('core-node.sh resolves the monolith layout (script at the instance root)', () => {
72
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cn-mono-'));
73
+ try {
74
+ fs.mkdirSync(path.join(tmp, 'scripts', 'gds'), { recursive: true });
75
+ fs.writeFileSync(path.join(tmp, 'scripts', 'gds', 'probe.js'), 'console.log("MONOLITH");\n');
76
+ assert.equal(runLauncher('scripts/gds/probe.js', tmp), 'MONOLITH');
77
+ } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
78
+ });
79
+
80
+ test('core-node.sh falls back to the consumer layout (script only under node_modules/@bongos/core)', () => {
81
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cn-cons-'));
82
+ try {
83
+ const pkgDir = path.join(tmp, 'node_modules', '@bongos', 'core', 'scripts', 'gds');
84
+ fs.mkdirSync(pkgDir, { recursive: true });
85
+ fs.writeFileSync(path.join(pkgDir, 'probe.js'), 'console.log("CONSUMER");\n');
86
+ // No scripts/gds/probe.js at the instance root → the resolver must reach node_modules.
87
+ assert.equal(runLauncher('scripts/gds/probe.js', tmp), 'CONSUMER');
88
+ } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
89
+ });
90
+
91
+ test('core-node.sh exits non-zero when the script is in NO layout', () => {
92
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cn-none-'));
93
+ let threw = false;
94
+ try {
95
+ execFileSync('bash', [LAUNCHER, 'scripts/gds/nope.js'], {
96
+ encoding: 'utf8', stdio: 'pipe', env: { ...process.env, BONGOS_INSTANCE_ROOT: tmp },
97
+ });
98
+ } catch (_) { threw = true; }
99
+ finally { fs.rmSync(tmp, { recursive: true, force: true }); }
100
+ assert.ok(threw, 'expected a non-zero exit when the core script resolves in no layout');
101
+ });
102
+
103
+ console.log(`\ninfra_exec_paths: ${passed} passed, ${failed} failed`);
104
+ process.exit(failed ? 1 : 0);
package/tests/init.mjs CHANGED
@@ -336,12 +336,12 @@ test('buildInstancePackageJson: no package.json → mints a private one with the
336
336
  assert.equal(pkg.scripts.start, init.INSTANCE_START_SCRIPT);
337
337
  assert.equal(pkg.scripts.migrate, init.INSTANCE_MIGRATE_SCRIPT);
338
338
  // the start/migrate scripts must target the installed core package so `npm ci` output is runnable
339
- assert.match(pkg.scripts.start, /node_modules\/@cloudbongos\/core\/src\/platform-server\.js$/);
340
- assert.match(pkg.scripts.migrate, /node_modules\/@cloudbongos\/core\/scripts\/migrate\.sh$/);
339
+ assert.match(pkg.scripts.start, /node_modules\/@bongos\/core\/src\/platform-server\.js$/);
340
+ assert.match(pkg.scripts.migrate, /node_modules\/@bongos\/core\/scripts\/migrate\.sh$/);
341
341
  });
342
342
 
343
343
  test('buildInstancePackageJson: coreDep (file: tarball pin) → the dep is the file: pin, not the caret', () => {
344
- const pin = 'file:vendor/cloudbongos-core-1.17.0.tgz';
344
+ const pin = 'file:vendor/bongos-core-1.17.0.tgz';
345
345
  const pkg = init.buildInstancePackageJson(null, { name: 'demo', coreDep: pin });
346
346
  assert.equal(pkg.dependencies[init.CORE_PKG], pin);
347
347
  assert.notEqual(pkg.dependencies[init.CORE_PKG], init.coreDepRange());
@@ -349,13 +349,60 @@ test('buildInstancePackageJson: coreDep (file: tarball pin) → the dep is the f
349
349
 
350
350
  test('buildInstancePackageJson: preserves existing fields + never clobbers a custom start/migrate', () => {
351
351
  const existing = { name: 'keep-me', version: '2.0.0', scripts: { start: 'node server.js' }, keywords: ['x'] };
352
- const pkg = init.buildInstancePackageJson(existing, { name: 'ignored', coreDep: 'file:vendor/cloudbongos-core-1.17.0.tgz' });
352
+ const pkg = init.buildInstancePackageJson(existing, { name: 'ignored', coreDep: 'file:vendor/bongos-core-1.17.0.tgz' });
353
353
  assert.equal(pkg.name, 'keep-me'); // existing name wins
354
354
  assert.equal(pkg.version, '2.0.0'); // untouched field preserved
355
355
  assert.deepEqual(pkg.keywords, ['x']);
356
356
  assert.equal(pkg.scripts.start, 'node server.js'); // custom start NOT clobbered
357
357
  assert.equal(pkg.scripts.migrate, init.INSTANCE_MIGRATE_SCRIPT); // missing one is filled
358
- assert.equal(pkg.dependencies[init.CORE_PKG], 'file:vendor/cloudbongos-core-1.17.0.tgz'); // dep (re)pinned
358
+ assert.equal(pkg.dependencies[init.CORE_PKG], 'file:vendor/bongos-core-1.17.0.tgz'); // dep (re)pinned
359
+ });
360
+
361
+ // --- enabled-module dep composition (ADR 0138 / task 2160) --------------------
362
+
363
+ test('enabledModuleDeps: merges only the ENABLED modules’ declared deps', () => {
364
+ const readDeps = (k) => ({
365
+ discord: { 'discord.js': '^14.26.4' },
366
+ game: { colyseus: '^0.16.5', phaser: '^3.87.0' },
367
+ 'art-pipeline': { sharp: '^0.34.5' },
368
+ }[k] || {});
369
+ const merged = init.enabledModuleDeps(
370
+ { modules: { discord: true, game: false, 'art-pipeline': true, 'dev-box': false } },
371
+ { readDeps }
372
+ );
373
+ assert.deepEqual(merged, { 'discord.js': '^14.26.4', sharp: '^0.34.5' }); // game excluded (off)
374
+ });
375
+
376
+ test('enabledModuleDeps: no enabled modules → empty object', () => {
377
+ assert.deepEqual(init.enabledModuleDeps({ modules: { discord: false } }, { readDeps: () => ({ 'discord.js': '^14' }) }), {});
378
+ });
379
+
380
+ test('buildInstancePackageJson: composes moduleDeps (fills), but an existing pin WINS and core is always set', () => {
381
+ const existing = { dependencies: { 'discord.js': '^14.0.0', express: '^4' } }; // instance pinned discord.js older
382
+ const moduleDeps = { 'discord.js': '^14.26.4', sharp: '^0.34.5' };
383
+ const pkg = init.buildInstancePackageJson(existing, { name: 'demo', coreDep: 'file:x.tgz', moduleDeps });
384
+ assert.equal(pkg.dependencies['discord.js'], '^14.0.0'); // existing pin wins over the module range
385
+ assert.equal(pkg.dependencies.sharp, '^0.34.5'); // module dep fills what was missing
386
+ assert.equal(pkg.dependencies.express, '^4'); // untouched host dep preserved
387
+ assert.equal(pkg.dependencies[init.CORE_PKG], 'file:x.tgz'); // core always (re)set
388
+ });
389
+
390
+ test('buildInstancePackageJson: no moduleDeps → byte-identical to the pre-composition behavior', () => {
391
+ const existing = { dependencies: { express: '^4' } };
392
+ const a = init.buildInstancePackageJson(existing, { name: 'demo', coreDep: 'file:x.tgz' });
393
+ const b = init.buildInstancePackageJson(existing, { name: 'demo', coreDep: 'file:x.tgz', moduleDeps: {} });
394
+ assert.deepEqual(a.dependencies, b.dependencies);
395
+ assert.deepEqual(a.dependencies, { express: '^4', [init.CORE_PKG]: 'file:x.tgz' });
396
+ });
397
+
398
+ test('moduleDeclaredDeps: reads a module.json dependencies block; {} when absent', () => {
399
+ const readOk = (p) => JSON.stringify({ key: 'discord', dependencies: { 'discord.js': '^14.26.4' } });
400
+ assert.deepEqual(init.moduleDeclaredDeps('discord', { coreRoot: '/core', fsImpl: { readFileSync: readOk } }),
401
+ { 'discord.js': '^14.26.4' });
402
+ const readNone = () => JSON.stringify({ key: 'lifecycle', contributes: {} });
403
+ assert.deepEqual(init.moduleDeclaredDeps('lifecycle', { coreRoot: '/core', fsImpl: { readFileSync: readNone } }), {});
404
+ const readThrow = () => { throw new Error('ENOENT'); };
405
+ assert.deepEqual(init.moduleDeclaredDeps('ghost', { coreRoot: '/core', fsImpl: { readFileSync: readThrow } }), {});
359
406
  });
360
407
 
361
408
  test('writeConfigs (greenfield): mints package.json with the caret core dep + start/migrate scripts', () => {
@@ -377,7 +424,7 @@ test('writeConfigs (greenfield): mints package.json with the caret core dep + st
377
424
  test('writeConfigs (greenfield, --vendor-core): package.json pins the file: tarball instead of the caret', () => {
378
425
  const dir = mkdtempSync(join(tmpdir(), 'greenfield-'));
379
426
  try {
380
- const pin = 'file:vendor/cloudbongos-core-1.17.0.tgz';
427
+ const pin = 'file:vendor/bongos-core-1.17.0.tgz';
381
428
  const res = init.writeConfigs(fullSpec(), { dir, force: false, dryRun: false, coreDep: pin, log: () => {} });
382
429
  const pkg = JSON.parse(readFileSync(res.pkgPath, 'utf8'));
383
430
  assert.equal(pkg.dependencies[init.CORE_PKG], pin);
@@ -456,7 +503,60 @@ async function runSeedTests() {
456
503
  });
457
504
  }
458
505
 
506
+ // ---- greenfield registry-install path (ADR 0134, task 2090) ----------------
507
+ // main() with --registry-core --dry-run only LOGS (no fs, no network), so we capture
508
+ // console output and assert the plan. The real write+install path is proven by the
509
+ // CLI scratch run in the session log; the pure gates live in tests/npmrc.mjs.
510
+ async function runRegistryCoreTests() {
511
+ const at = async (name, fn) => {
512
+ try { await fn(); passed++; console.log(` ok ${name}`); }
513
+ catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
514
+ };
515
+ const specFile = () => {
516
+ const dir = mkdtempSync(join(tmpdir(), 'init-reg-'));
517
+ const p = join(dir, 'spec.json');
518
+ writeFileSync(p, JSON.stringify({
519
+ identity: { productName: 'Reg Test' }, repo: { owner: 'acme', name: 'reg-test' }, firstAdmin: 'acme',
520
+ currency: { label: 'credits' }, domains: { publicOrigin: 'http://localhost:3000' }, modules: [],
521
+ version: { id: 'V1', name: 'V1', done_when: 'loop works' },
522
+ }));
523
+ return { dir, p };
524
+ };
525
+ // Run init.main() with console captured; returns { code, out }.
526
+ const capture = async (argv) => {
527
+ const lines = [];
528
+ const origLog = console.log, origErr = console.error;
529
+ console.log = (...a) => lines.push(a.join(' '));
530
+ console.error = (...a) => lines.push(a.join(' '));
531
+ let code;
532
+ try { code = await init.main(['node', 'init', ...argv]); }
533
+ finally { console.log = origLog; console.error = origErr; }
534
+ return { code, out: lines.join('\n') };
535
+ };
536
+
537
+ await at('main --registry-core --dry-run: plans the env-fed .npmrc; does NOT vendor a tarball', async () => {
538
+ const { dir, p } = specFile();
539
+ try {
540
+ const { code, out } = await capture(['--from', p, '--dir', join(dir, 'out'), '--no-seed', '--registry-core', '--dry-run']);
541
+ assert.equal(code, 0);
542
+ assert.match(out, /would write \.npmrc \(env-fed @bongos auth\)/);
543
+ assert.ok(!/vendor/i.test(out), 'no vendored-tarball step on the registry path');
544
+ assert.equal(existsSync(join(dir, 'out', '.npmrc')), false, 'dry-run writes nothing');
545
+ } finally { rmSync(dir, { recursive: true, force: true }); }
546
+ });
547
+
548
+ await at('main: --vendor-core + --registry-core is refused (mutually exclusive) → exit 1', async () => {
549
+ const { dir, p } = specFile();
550
+ try {
551
+ const { code, out } = await capture(['--from', p, '--dir', join(dir, 'out'), '--no-seed', '--vendor-core', '/tmp/x.tgz', '--registry-core', '--dry-run']);
552
+ assert.equal(code, 1);
553
+ assert.match(out, /mutually exclusive/);
554
+ } finally { rmSync(dir, { recursive: true, force: true }); }
555
+ });
556
+ }
557
+
459
558
  await runSeedTests();
559
+ await runRegistryCoreTests();
460
560
 
461
561
  console.log('');
462
562
  console.log(`init tests: ${passed} passed, ${failed} failed`);