@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.
- package/.bongos-core.json +363 -312
- package/.claude/skills/blocker-review/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +2 -0
- package/.claude/skills/builder-exit/SKILL.md +2 -2
- package/.claude/skills/builder-redteam/SKILL.md +3 -3
- package/.claude/skills/builder-setup/SKILL.md +2 -2
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/dev/SKILL.md +1 -1
- package/.claude/skills/idea-triage/SKILL.md +2 -2
- package/.claude/skills/ideate/SKILL.md +1 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/.claude/skills/new-project/SKILL.md +3 -0
- package/.claude/skills/paint/SKILL.md +1 -1
- package/CLAUDE.md +11 -238
- package/GOVERNANCE.md +1 -1
- package/README.md +1 -1
- 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/0001-stack-choice-single-vps.md +1 -1
- package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
- package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
- package/docs/adr/0020-security-model-public-stub.md +1 -1
- package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
- package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
- package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
- package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
- package/docs/adr/0030-builders-hall-live-updates.md +1 -1
- package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
- package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
- package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
- package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
- package/docs/adr/0044-per-box-live-game-preview.md +1 -1
- package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
- package/docs/adr/0057-container-cost-ledger.md +3 -3
- package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
- package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
- package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
- package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
- package/docs/adr/0096-require-reward-before-workable.md +9 -9
- package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
- package/docs/adr/0100-two-repo-instance-model.md +1 -1
- package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
- package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
- package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
- 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/0136-update-channel-subscription-policy.md +51 -0
- package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
- package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
- package/docs/adr/README.md +14 -10
- package/docs/api/openapi.json +315 -6
- package/docs/api-reference.md +10 -5
- package/docs/architecture.md +3 -3
- package/docs/branding-contract.md +5 -4
- package/docs/canonical-permissions.md +1 -1
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +20 -19
- package/docs/handoff-template.md +2 -2
- package/docs/modules-contract.md +8 -1
- package/docs/onboarding/diagrams/README.md +7 -7
- package/docs/onboarding/drachmae-and-karma.md +18 -18
- package/docs/onboarding/primer.md +9 -9
- package/docs/recipes/audit-replay.md +2 -2
- package/docs/recipes/builder-box-lifecycle.md +2 -2
- package/docs/recipes/claude-design-loop.md +1 -1
- package/docs/recipes/instance-deploy.md +4 -4
- package/docs/recipes/instance-first-run.md +51 -0
- package/docs/recipes/ops-gotchas.md +1 -1
- package/docs/recipes/packaging-the-core.md +5 -5
- package/docs/recipes/private-npm-distribution.md +39 -12
- package/docs/recipes/self-host.md +1 -1
- package/docs/recipes/staging-environment.md +1 -1
- package/docs/recipes/upgrading-the-core.md +63 -8
- package/docs/research/cursor-competitive-analysis.md +1 -1
- package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
- 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/discord/module.json +2 -1
- package/modules/economy/CLAUDE.md +11 -44
- package/modules/economy/achievements.js +9 -1
- package/modules/economy/credits.js +10 -1
- package/modules/grading/CLAUDE.md +11 -38
- package/modules/hall-ui/CLAUDE.md +11 -57
- package/modules/hall-ui/public/builders.js +71 -4
- package/modules/hall-ui/public/goal-inbox.js +191 -0
- package/modules/hall-ui/public/goals-page.js +227 -22
- package/modules/hall-ui/public/goals.html +16 -0
- package/modules/hall-ui/public/hall-widgets.js +14 -1
- package/modules/hall-ui/public/index.html +3 -0
- package/modules/hall-ui/public/new-project-wizard.js +6 -4
- package/modules/hall-ui/public/style.css +43 -0
- 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/provisioning/CLAUDE.md +15 -0
- package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
- package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
- package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
- package/modules/provisioning/module.json +17 -0
- package/modules/provisioning/onboard-plan.js +37 -0
- package/modules/provisioning/provisioning.js +539 -0
- package/modules/provisioning/rate-limit.js +35 -0
- package/modules/provisioning/routes/provisioning.js +607 -0
- package/modules/provisioning/tests/provisioning.mjs +232 -0
- package/modules/provisioning/ui/.gitkeep +0 -0
- package/modules/security/CLAUDE.md +11 -40
- package/modules/security/secret-scrub.js +10 -0
- package/modules/sessions/CLAUDE.md +11 -35
- package/modules/status-ui/CLAUDE.md +11 -43
- package/package-lock.json +2 -2
- package/package.json +7 -2
- package/public-landing/create.html +58 -12
- package/public-landing/index.html +40 -21
- package/scripts/discord/README.md +1 -1
- package/scripts/gds/CLAUDE.md +11 -140
- package/scripts/gds/claim.js +98 -48
- package/scripts/gds/claude-materialize.js +174 -20
- package/scripts/gds/claude-md-template.js +84 -0
- package/scripts/gds/cli-lib.js +112 -2
- package/scripts/gds/cutover-codemod.js +241 -0
- package/scripts/gds/fitness.js +54 -0
- package/scripts/gds/gen-api-client.js +10 -10
- package/scripts/gds/init.js +112 -16
- package/scripts/gds/lint-infra-exec.js +70 -0
- package/scripts/gds/lint-workflow-exec.js +63 -0
- package/scripts/gds/migration-namespace.js +1 -1
- package/scripts/gds/mirror-lineage-check.js +14 -5
- package/scripts/gds/mirror-redact.js +8 -0
- package/scripts/gds/module.js +118 -4
- package/scripts/gds/npmrc.js +93 -0
- package/scripts/gds/onboard.js +17 -24
- package/scripts/gds/package-core.js +74 -17
- package/scripts/gds/provision.js +102 -17
- package/scripts/gds/publish-manifest.js +60 -2
- package/scripts/gds/run-routine.js +51 -13
- package/scripts/gds/run-unit-tests.js +4 -0
- 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/update-channel.js +186 -0
- package/scripts/gds/upgrade.js +172 -25
- package/src/bongos/CLAUDE.md +11 -60
- package/src/bongos/auth.js +24 -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/bongos/serve-internal.js +10 -1
- package/src/branding.js +1 -0
- package/src/instance-config.js +1 -1
- package/src/module-api.js +24 -1
- package/src/module-loader/loader.js +36 -4
- package/src/module-loader/manifest-schema.js +16 -1
- package/tests/branding.mjs +11 -0
- package/tests/claim_error_surface.mjs +107 -0
- package/tests/claude_materialize.mjs +125 -0
- package/tests/consumer_layout_boot.mjs +302 -0
- package/tests/cutover_codemod.mjs +184 -0
- package/tests/device_flow_start.mjs +96 -0
- package/tests/first_run_engine_check.mjs +79 -0
- package/tests/goal_membership_requests.mjs +33 -3
- package/tests/hook_core_resolve.mjs +125 -0
- package/tests/hook_user_agents.mjs +62 -0
- package/tests/infra_exec_paths.mjs +104 -0
- package/tests/init.mjs +106 -6
- package/tests/instance_mismatch_guard.mjs +101 -0
- package/tests/mirror_lineage_proof.mjs +14 -0
- package/tests/module_cli.mjs +130 -2
- package/tests/module_loader.mjs +42 -0
- package/tests/module_manifest.mjs +19 -0
- package/tests/module_submissions.mjs +135 -0
- package/tests/npmrc.mjs +105 -0
- package/tests/onboard.mjs +62 -0
- package/tests/package_core.mjs +76 -22
- package/tests/platform_boot.mjs +5 -2
- package/tests/provision.mjs +107 -8
- package/tests/provisioning_github_app_routes.mjs +44 -2
- package/tests/publish_manifest.mjs +52 -1
- package/tests/run_routine.mjs +26 -1
- package/tests/run_routine_two_root.mjs +77 -0
- package/tests/scrubber_corpus.mjs +17 -0
- package/tests/served_paths_shipped.mjs +2 -2
- package/tests/task_cli.mjs +99 -0
- package/tests/update_channel.mjs +186 -0
- package/tests/upgrade.mjs +220 -25
- package/tests/workflow_exec_paths.mjs +98 -0
- package/docs/design/container-exterior-tiles.md +0 -49
- package/docs/design/container-exterior.md +0 -81
- package/docs/design/container-interior.md +0 -77
- package/docs/design/world-map-scroll.md +0 -41
- package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
- package/docs/recipes/cloudbongos-staging-gate.md +0 -139
- package/docs/recipes/cloudbongos-standup.md +0 -111
- package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
- package/docs/recipes/parallel-load-test.md +0 -32
- package/docs/recipes/pixel-art-pipeline.md +0 -411
- package/docs/recipes/staging-data.md +0 -50
- package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
- package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
- package/migrations/instance/110_medusa_planning_seed.sql +0 -70
- package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
- package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
- package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
- package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
- package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
- package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
- package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
- package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
- package/scripts/gds/art-stage.js +0 -152
- package/server.js +0 -212
- package/tests/art_key.mjs +0 -121
- package/tests/day_cycle.mjs +0 -197
- package/tests/snapshot_picker.mjs +0 -233
- package/tests/sprite_facing.mjs +0 -35
- package/tests/terrain_picker.mjs +0 -298
- package/tests/turn_in_place.mjs +0 -280
- 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
|
|
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
|
|
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\/@
|
|
340
|
-
assert.match(pkg.scripts.migrate, /node_modules\/@
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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`);
|