@bongos/core 1.17.1 → 1.17.3
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 +521 -281
- 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/GOVERNANCE.md +1 -1
- package/README.md +1 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +4 -2
- package/clients/bongos-client/index.mjs +2 -0
- 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/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 +9 -6
- package/docs/api/openapi.json +65 -26
- package/docs/api-reference.md +3 -2
- package/docs/architecture.md +2 -2
- 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 +18 -17
- package/docs/handoff-template.md +2 -2
- package/docs/modules-contract.md +6 -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/ops-gotchas.md +1 -1
- package/docs/recipes/private-npm-distribution.md +17 -0
- package/docs/recipes/staging-environment.md +1 -1
- package/docs/recipes/upgrading-the-core.md +55 -0
- package/docs/research/cursor-competitive-analysis.md +1 -1
- package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
- package/modules/discord/module.json +2 -1
- package/modules/economy/achievements.js +9 -1
- package/modules/economy/credits.js +10 -1
- package/modules/hall-ui/public/builders.js +137 -620
- package/modules/hall-ui/public/goal-inbox.js +232 -0
- package/modules/hall-ui/public/goals-page.js +4 -1
- package/modules/hall-ui/public/goals.html +8 -0
- package/modules/hall-ui/public/hall-kit.js +474 -0
- package/modules/hall-ui/public/hall-widgets.js +14 -5
- package/modules/hall-ui/public/index.html +3 -0
- package/modules/hall-ui/public/instances-panel.js +24 -3
- package/modules/hall-ui/public/new-project-wizard.js +7 -5
- package/modules/hall-ui/public/style.css +168 -0
- package/modules/hall-ui/public/work.css +194 -0
- package/modules/hall-ui/public/work.html +46 -374
- package/modules/hall-ui/public/work.js +137 -102
- package/modules/lifecycle/criterion-suggest.js +80 -0
- package/modules/lifecycle/db.js +42 -5
- package/modules/lifecycle/routes/goals.js +26 -0
- package/modules/lifecycle/routes/tasks.js +21 -0
- 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/secret-scrub.js +10 -0
- package/package-lock.json +2 -2
- package/package.json +7 -2
- package/scripts/discord/README.md +1 -1
- package/scripts/gds/claim.js +98 -48
- package/scripts/gds/claude-materialize.js +30 -9
- package/scripts/gds/cutover-codemod.js +241 -0
- package/scripts/gds/fitness.js +54 -0
- package/scripts/gds/init.js +59 -3
- package/scripts/gds/lint-infra-exec.js +70 -0
- package/scripts/gds/lint-workflow-exec.js +63 -0
- package/scripts/gds/mirror-lineage-check.js +14 -5
- package/scripts/gds/mirror-redact.js +8 -0
- package/scripts/gds/onboard.js +4 -0
- package/scripts/gds/package-core.js +18 -3
- 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/update-channel.js +186 -0
- package/scripts/gds/upgrade.js +116 -9
- package/scripts/hall-preview/README.md +103 -0
- package/scripts/hall-preview/fixtures/access-requests.json +1 -0
- package/scripts/hall-preview/fixtures/achievements.json +76 -0
- package/scripts/hall-preview/fixtures/analytics__builder.json +251 -0
- package/scripts/hall-preview/fixtures/audit-log.json +1 -0
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +1 -0
- package/scripts/hall-preview/fixtures/builders__active-claims.json +1 -0
- package/scripts/hall-preview/fixtures/builders__directory.json +81 -0
- package/scripts/hall-preview/fixtures/builders__roster.json +1 -0
- package/scripts/hall-preview/fixtures/goals.json +502 -0
- package/scripts/hall-preview/fixtures/goals__25.json +96 -0
- package/scripts/hall-preview/fixtures/goals__26.json +67 -0
- package/scripts/hall-preview/fixtures/goals__3.json +103 -0
- package/scripts/hall-preview/fixtures/goals__35.json +49 -0
- package/scripts/hall-preview/fixtures/goals__conflicts.json +154 -0
- package/scripts/hall-preview/fixtures/goals__detail.json +96 -0
- package/scripts/hall-preview/fixtures/goals__inbox.json +5 -0
- package/scripts/hall-preview/fixtures/goals__requests.json +3 -0
- package/scripts/hall-preview/fixtures/grades__by-builder.json +1 -0
- package/scripts/hall-preview/fixtures/inbox.json +330 -0
- package/scripts/hall-preview/fixtures/instance.json +124 -0
- package/scripts/hall-preview/fixtures/leaderboard.json +184 -0
- package/scripts/hall-preview/fixtures/me.json +476 -0
- package/scripts/hall-preview/fixtures/me__recent-ships.json +304 -0
- package/scripts/hall-preview/fixtures/override-requests.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__instances.json +1 -0
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +516 -0
- package/scripts/hall-preview/fixtures/public__bounty-table.json +37 -0
- package/scripts/hall-preview/fixtures/public__cost-summary.json +742 -0
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +41 -0
- package/scripts/hall-preview/fixtures/public__grades.json +50 -0
- package/scripts/hall-preview/fixtures/public__progress.json +124 -0
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +426 -0
- package/scripts/hall-preview/fixtures/public__repo-health.json +160 -0
- package/scripts/hall-preview/fixtures/public__repo-info.json +5 -0
- package/scripts/hall-preview/fixtures/security__docs.json +1 -0
- package/scripts/hall-preview/fixtures/security__reports.json +1 -0
- package/scripts/hall-preview/fixtures/sessions__search.json +1 -0
- package/scripts/hall-preview/fixtures/tasks.json +2420 -0
- package/scripts/hall-preview/fixtures/tasks__claimable.json +2555 -0
- package/scripts/hall-preview/fixtures/tasks__detail.json +111 -0
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +330 -0
- package/scripts/hall-preview/fixtures/versions.json +89 -0
- package/scripts/hall-preview/refresh-fixtures.js +77 -0
- package/scripts/hall-preview/server.js +178 -0
- package/scripts/migrate.sh +29 -4
- package/src/bongos/auth.js +14 -1
- package/src/bongos/optimizer.js +37 -2
- package/src/bongos/serve-internal.js +10 -1
- package/src/branding.js +1 -0
- package/src/module-api.js +12 -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 +20 -0
- package/tests/consumer_layout_boot.mjs +302 -0
- package/tests/criterion_suggest.mjs +106 -0
- package/tests/cutover_codemod.mjs +184 -0
- package/tests/device_flow_start.mjs +25 -2
- package/tests/hook_core_resolve.mjs +125 -0
- package/tests/infra_exec_paths.mjs +104 -0
- package/tests/init.mjs +47 -0
- package/tests/migrate_root_resolution.mjs +97 -0
- package/tests/mirror_lineage_proof.mjs +14 -0
- package/tests/module_loader.mjs +42 -0
- package/tests/module_manifest.mjs +19 -0
- package/tests/onboard.mjs +13 -0
- package/tests/package_core.mjs +8 -0
- package/tests/provisioning_github_app_routes.mjs +15 -0
- 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/update_channel.mjs +186 -0
- package/tests/upgrade.mjs +131 -0
- 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,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,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
|
@@ -358,6 +358,53 @@ test('buildInstancePackageJson: preserves existing fields + never clobbers a cus
|
|
|
358
358
|
assert.equal(pkg.dependencies[init.CORE_PKG], 'file:vendor/bongos-core-1.17.0.tgz'); // dep (re)pinned
|
|
359
359
|
});
|
|
360
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 } }), {});
|
|
406
|
+
});
|
|
407
|
+
|
|
361
408
|
test('writeConfigs (greenfield): mints package.json with the caret core dep + start/migrate scripts', () => {
|
|
362
409
|
const dir = mkdtempSync(join(tmpdir(), 'greenfield-'));
|
|
363
410
|
try {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// tests/migrate_root_resolution.mjs
|
|
2
|
+
//
|
|
3
|
+
// R89 rehearsal F6 (task 2180): after the core→consumer split, scripts/migrate.sh
|
|
4
|
+
// must resolve the INSTANCE root from the INVOCATION cwd (OTB_INSTANCE_ROOT env,
|
|
5
|
+
// else the invocation cwd's bongos.config.json), NOT script-relative into the
|
|
6
|
+
// installed @bongos/core package. The old script-relative resolve `cd`'d into the
|
|
7
|
+
// package dir first — which has no bongos.config.json — so it silently picked the
|
|
8
|
+
// package as the "instance" and read the core's VANILLA module config, skipping
|
|
9
|
+
// every host + flag-gated module migration (rehearsal: `npm run migrate` applied
|
|
10
|
+
// 115 rows and created NO game schema; from the instance cwd it applied 121).
|
|
11
|
+
//
|
|
12
|
+
// This exercises migrate.sh's `--print-roots` seam (DB-free: it prints the resolved
|
|
13
|
+
// roots and exits before any DB work), so no Postgres is needed. CORE_ROOT must stay
|
|
14
|
+
// script-relative in every case; INSTANCE_ROOT must follow the invocation cwd.
|
|
15
|
+
//
|
|
16
|
+
// Run: node tests/migrate_root_resolution.mjs
|
|
17
|
+
|
|
18
|
+
import { strict as assert } from 'node:assert';
|
|
19
|
+
import { execFileSync } from 'node:child_process';
|
|
20
|
+
import { mkdtempSync, mkdirSync, writeFileSync, realpathSync } from 'node:fs';
|
|
21
|
+
import { tmpdir } from 'node:os';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
|
|
25
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
const MIGRATE = path.join(REPO_ROOT, 'scripts', 'migrate.sh');
|
|
27
|
+
const REPO_REAL = realpathSync(REPO_ROOT);
|
|
28
|
+
|
|
29
|
+
let passed = 0;
|
|
30
|
+
let failed = 0;
|
|
31
|
+
function t(name, fn) {
|
|
32
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
33
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Run `migrate.sh --print-roots` with a controlled cwd + INIT_CWD, and an env that
|
|
37
|
+
// carries NO *_INSTANCE_ROOT override, so we test cwd-based resolution alone.
|
|
38
|
+
function printRoots({ cwd, initCwd } = {}) {
|
|
39
|
+
const env = { ...process.env };
|
|
40
|
+
for (const k of Object.keys(env)) if (/_INSTANCE_ROOT$/.test(k)) delete env[k];
|
|
41
|
+
delete env.INIT_CWD;
|
|
42
|
+
if (initCwd) env.INIT_CWD = initCwd;
|
|
43
|
+
const out = execFileSync('bash', [MIGRATE, '--print-roots'], { cwd, env, encoding: 'utf8' });
|
|
44
|
+
const roots = {};
|
|
45
|
+
for (const line of out.split('\n')) {
|
|
46
|
+
const m = /^(INVOKE_CWD|CORE_ROOT|INSTANCE_ROOT)=(.*)$/.exec(line);
|
|
47
|
+
if (m) roots[m[1]] = m[2];
|
|
48
|
+
}
|
|
49
|
+
return roots;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A synthetic consumer: a dir with a bongos.config.json pointing at itself, exactly
|
|
53
|
+
// as the post-split amazonprimea repo carries (bongos.config.json { instanceRoot: "." }).
|
|
54
|
+
function makeConsumer() {
|
|
55
|
+
const dir = realpathSync(mkdtempSync(path.join(tmpdir(), 'r89-consumer-')));
|
|
56
|
+
writeFileSync(path.join(dir, 'bongos.config.json'), JSON.stringify({ instanceRoot: '.' }));
|
|
57
|
+
return dir;
|
|
58
|
+
}
|
|
59
|
+
// A dir with NO bongos.config.json — stands in for the monolith / a stray cwd.
|
|
60
|
+
function makeBare() {
|
|
61
|
+
return realpathSync(mkdtempSync(path.join(tmpdir(), 'r89-bare-')));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---- the F6 regression: instance root follows the invocation cwd ----------
|
|
65
|
+
t('consumer run from its own dir: INSTANCE_ROOT resolves to the consumer (not the core)', () => {
|
|
66
|
+
const consumer = makeConsumer();
|
|
67
|
+
const r = printRoots({ cwd: consumer });
|
|
68
|
+
assert.equal(realpathSync(r.INSTANCE_ROOT), consumer, 'instance root must be the consumer dir');
|
|
69
|
+
assert.notEqual(realpathSync(r.INSTANCE_ROOT), REPO_REAL, 'must NOT fall back to the script-relative core root');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
t('consumer via npm from elsewhere: INIT_CWD wins over the spawn cwd', () => {
|
|
73
|
+
const consumer = makeConsumer();
|
|
74
|
+
const elsewhere = makeBare();
|
|
75
|
+
// npm sets INIT_CWD to where `npm run` was invoked (the instance), even if a
|
|
76
|
+
// wrapper cd'd somewhere else first.
|
|
77
|
+
const r = printRoots({ cwd: elsewhere, initCwd: consumer });
|
|
78
|
+
assert.equal(realpathSync(r.INSTANCE_ROOT), consumer, 'INIT_CWD (the instance) must win');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ---- the monolith / no-pointer fallback stays script-relative -------------
|
|
82
|
+
t('no bongos.config + no env: INSTANCE_ROOT falls back to the script-relative repo root', () => {
|
|
83
|
+
const bare = makeBare();
|
|
84
|
+
const r = printRoots({ cwd: bare });
|
|
85
|
+
assert.equal(realpathSync(r.INSTANCE_ROOT), REPO_REAL, 'a stray cwd must NOT masquerade as the instance');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ---- CORE root is always script-relative, regardless of cwd ---------------
|
|
89
|
+
t('CORE_ROOT is always the script-relative repo root, independent of the invocation cwd', () => {
|
|
90
|
+
const consumer = makeConsumer();
|
|
91
|
+
const bare = makeBare();
|
|
92
|
+
assert.equal(realpathSync(printRoots({ cwd: consumer }).CORE_ROOT), REPO_REAL);
|
|
93
|
+
assert.equal(realpathSync(printRoots({ cwd: bare }).CORE_ROOT), REPO_REAL);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
console.log(`\nmigrate_root_resolution: ${passed} passed, ${failed} failed`);
|
|
97
|
+
process.exit(failed ? 1 : 0);
|
|
@@ -22,6 +22,20 @@ test('migrationNumbersFromPaths picks CORE migrations only (not module/ADR numbe
|
|
|
22
22
|
assert.deepEqual([...set].sort((a, b) => a - b), [3, 44, 60]);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
+
test('migrationNumbersFromPaths excludes migrations/instance/ seeds (task 2152)', () => {
|
|
26
|
+
// migrations/instance/*.sql are this instance's host seeds (ADR 0105), excluded from
|
|
27
|
+
// the publishable mirror (task 2140). Counting them as core put every instance seed in
|
|
28
|
+
// the PRIVATE set but not the MIRROR set → a false private⇄mirror divergence that
|
|
29
|
+
// red-lined the unit gate. Only the bare top-level core migration counts.
|
|
30
|
+
const set = migrationNumbersFromPaths([
|
|
31
|
+
'migrations/080_core_thing.sql', // bare core → counted
|
|
32
|
+
'migrations/instance/080_gds_v4_planning_seed.sql', // instance seed → NOT core
|
|
33
|
+
'migrations/instance/186_governance_seed.sql', // instance seed → NOT core
|
|
34
|
+
'migrations/core_192_namespaced.sql', // core_ prefix ≠ bare NNN_ form → not counted either
|
|
35
|
+
]);
|
|
36
|
+
assert.deepEqual([...set].sort((a, b) => a - b), [80]);
|
|
37
|
+
});
|
|
38
|
+
|
|
25
39
|
test('parseEnumChecks extracts the quoted value-lists from CHECK (... IN (...))', () => {
|
|
26
40
|
const sql = `
|
|
27
41
|
CREATE TABLE tasks (
|
package/tests/module_loader.mjs
CHANGED
|
@@ -84,6 +84,48 @@ test('discoverModules: a missing modules/ root is empty, not an error', () => {
|
|
|
84
84
|
assert.deepEqual(r, { loaded: [], skipped: [], errors: [] });
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
+
// --- task 2133 (R89-prereq): TWO-ROOT discovery. DEFAULT_ROOTS spans the core
|
|
88
|
+
// package's modules/ AND the consumer/instance's modules/, deduped by
|
|
89
|
+
// resolved path so a single checkout (roots coincide) never double-scans. ---
|
|
90
|
+
|
|
91
|
+
test('discoverModules: UNIONs modules across two diverged roots (the post-split shape)', () => {
|
|
92
|
+
const core = fixtureRoot({ 'core-mod': { manifest: validManifest('core-mod') } });
|
|
93
|
+
const instance = fixtureRoot({ 'inst-mod': { manifest: validManifest('inst-mod') } });
|
|
94
|
+
const { loaded, errors, skipped } = loader.discoverModules({ roots: [core, instance], core: '1.0.0' });
|
|
95
|
+
assert.deepEqual(loaded.map((m) => m.key).sort(), ['core-mod', 'inst-mod'], 'both roots contribute');
|
|
96
|
+
assert.deepEqual(errors, []);
|
|
97
|
+
assert.deepEqual(skipped, []);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('discoverModules: scanning the SAME root twice makes every key a duplicate-key ERROR (why dedupeRoots exists)', () => {
|
|
101
|
+
const root = fixtureRoot({ 'only-mod': { manifest: validManifest('only-mod') } });
|
|
102
|
+
const { loaded, errors } = loader.discoverModules({ roots: [root, root], core: '1.0.0' });
|
|
103
|
+
assert.equal(loaded.length, 1, 'the first scan loads it');
|
|
104
|
+
assert.equal(errors.length, 1, 'the second scan of the identical root is a duplicate-key error');
|
|
105
|
+
assert.match(errors[0].errors[0], /duplicate module key/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('dedupeRoots: collapses roots that resolve to the same path, preserving order', () => {
|
|
109
|
+
assert.deepEqual(
|
|
110
|
+
loader.dedupeRoots(['/a/modules', '/a/modules', '/b/modules']),
|
|
111
|
+
['/a/modules', '/b/modules'],
|
|
112
|
+
);
|
|
113
|
+
assert.deepEqual(loader.dedupeRoots(['/a/modules', '/a/./modules']), ['/a/modules'], 'normalizes before comparing');
|
|
114
|
+
assert.equal(loader.dedupeRoots(['/a/modules', '/b/modules']).length, 2, 'a genuinely diverged pair is kept');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('DEFAULT_ROOTS is deduped → the single checkout scans no root twice (zero dup-key errors)', () => {
|
|
118
|
+
// In a single checkout resolveInstanceRoot() falls back to cwd === resolveCoreRoot(),
|
|
119
|
+
// so the two candidate roots are identical and MUST collapse to one (a diverged
|
|
120
|
+
// consumer repo keeps both). The invariant: no resolved path appears twice.
|
|
121
|
+
const resolved = loader.DEFAULT_ROOTS.map((r) => path.resolve(r));
|
|
122
|
+
assert.equal(new Set(resolved).size, resolved.length, 'DEFAULT_ROOTS has no duplicate resolved paths');
|
|
123
|
+
loader._reset();
|
|
124
|
+
const snap = loader.loaderSnapshot();
|
|
125
|
+
assert.deepEqual(snap.errors, [], 'default-root discovery yields zero errors (no duplicate-key)');
|
|
126
|
+
loader._reset();
|
|
127
|
+
});
|
|
128
|
+
|
|
87
129
|
test('discoveredRegistry: shapes a loaded manifest like a MODULE_REGISTRY entry', () => {
|
|
88
130
|
loader._reset();
|
|
89
131
|
const root = fixtureRoot({ econ: { manifest: validManifest('econ', { default: true, provides: ['reward'], contributes: { disciplines: ['x'] } }) } });
|
|
@@ -192,3 +192,22 @@ test('an empty-string attribution field is rejected', () => {
|
|
|
192
192
|
assert.equal(r.valid, false);
|
|
193
193
|
assert.ok(r.errors.some((e) => e.startsWith('author')));
|
|
194
194
|
});
|
|
195
|
+
|
|
196
|
+
// --- optional runtime dependencies (ADR 0138 / task 2160) ---
|
|
197
|
+
|
|
198
|
+
test('dependencies: a valid { name: range } object passes', () => {
|
|
199
|
+
const r = validateManifest({ ...baseManifest(), dependencies: { 'discord.js': '^14.26.4' } });
|
|
200
|
+
assert.equal(r.valid, true, r.errors.join('; '));
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('dependencies: omitted is fine (optional)', () => {
|
|
204
|
+
assert.equal(validateManifest(baseManifest()).valid, true);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('dependencies: a non-object, or a non-string range, is rejected', () => {
|
|
208
|
+
for (const bad of [['discord.js'], 'discord.js@14', { 'discord.js': 14 }, { '': '^1' }]) {
|
|
209
|
+
const r = validateManifest({ ...baseManifest(), dependencies: bad });
|
|
210
|
+
assert.equal(r.valid, false, `should reject ${JSON.stringify(bad)}`);
|
|
211
|
+
assert.ok(r.errors.some((e) => e.startsWith('dependencies')), `error should mention dependencies: ${r.errors}`);
|
|
212
|
+
}
|
|
213
|
+
});
|
package/tests/onboard.mjs
CHANGED
|
@@ -117,3 +117,16 @@ test('the web forms (/create + hall wizard) declare EXACTLY the canonical human
|
|
|
117
117
|
assert.deepEqual(uniq, humanKeys, `${name} (${rel}) must tag exactly the canonical human steps [${humanKeys}], found [${uniq}]`);
|
|
118
118
|
}
|
|
119
119
|
});
|
|
120
|
+
|
|
121
|
+
test('the one-click GitHub-App manifest path surfaces the CLI-only Enable-Device-Flow step (task 2129)', () => {
|
|
122
|
+
// GitHub defaults Device Flow OFF and the manifest can't enable it, but `bongos login` needs
|
|
123
|
+
// it — so BOTH manifest surfaces must tell the owner to enable it after the one-click approval.
|
|
124
|
+
// Assert on a phrase DISTINCT to the one-click caveat: the manual OAuth path already mentions
|
|
125
|
+
// Device Flow (task 2127), so a bare "Device Flow" match wouldn't prove the manifest path carries it.
|
|
126
|
+
const { readFileSync } = require('node:fs');
|
|
127
|
+
const path = require('node:path');
|
|
128
|
+
const onboardSrc = readFileSync(path.join(import.meta.dirname, '..', 'scripts/gds/onboard.js'), 'utf8');
|
|
129
|
+
const wizardSrc = readFileSync(path.join(import.meta.dirname, '..', 'modules/hall-ui/public/new-project-wizard.js'), 'utf8');
|
|
130
|
+
assert.match(onboardSrc, /cannot enable Device Flow via the manifest/i, 'bongos onboard one-click caveat missing');
|
|
131
|
+
assert.match(wizardSrc, /Terminal CLI only: after approving, enable/i, 'hall wizard one-click caveat missing');
|
|
132
|
+
});
|
package/tests/package_core.mjs
CHANGED
|
@@ -366,6 +366,14 @@ t('buildPackageJson: prunes to express+pg, keeps the source ranges + the undici
|
|
|
366
366
|
assert.equal(pkg.exports['.'], './src/platform-server.js');
|
|
367
367
|
assert.equal(pkg.exports['./serve-internal'], './src/bongos/serve-internal.js');
|
|
368
368
|
assert.equal(pkg.exports['./module-api'], './src/module-api.js');
|
|
369
|
+
// rehearsal F3 / task 2161 — the game-host kit: the core internals a game-bearing
|
|
370
|
+
// instance's own entrypoint (server.js) wires. The cutover codemod rewrites server.js's
|
|
371
|
+
// relative ./src/* requires to exactly these subpaths, so they MUST be exported.
|
|
372
|
+
assert.equal(pkg.exports['./bongos/api-path-404'], './src/bongos/api-path-404.js');
|
|
373
|
+
assert.equal(pkg.exports['./bongos/uptime-poller'], './src/bongos/uptime-poller.js');
|
|
374
|
+
assert.equal(pkg.exports['./module-loader/loader'], './src/module-loader/loader.js');
|
|
375
|
+
assert.equal(pkg.exports['./modules'], './src/modules.js');
|
|
376
|
+
assert.equal(pkg.exports['./module-seams'], './src/module-seams.js');
|
|
369
377
|
// task 2089 / ADR 0134 — the synthesized package publishes PRIVATE by default.
|
|
370
378
|
assert.deepEqual(pkg.publishConfig, { access: 'restricted' });
|
|
371
379
|
});
|
|
@@ -71,6 +71,21 @@ console.log('POST /provisioning/instances/:id/github-app (mint the one-click lin
|
|
|
71
71
|
t('the attempt row was persisted with the minted state', !!(stored && stored.instanceId === 42 && stored.state === j.state && stored.requestedBy === 7));
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
console.log('\nprovisioningOrigin override (task 2101 — customer App carries the PLATFORM brand, not the game domain):');
|
|
75
|
+
{
|
|
76
|
+
// A control plane that co-hosts a game (OTB = customer 0) has a GAME-branded publicOrigin.
|
|
77
|
+
// provisioningOrigin is the dedicated front door the customer's GitHub App must point at,
|
|
78
|
+
// so mothershipOrigin() must prefer it over publicOrigin. mothershipOrigin() calls
|
|
79
|
+
// api.branding() (module-api captured the fn ref at require-time), so override THAT.
|
|
80
|
+
const prevBranding = api.branding;
|
|
81
|
+
api.branding = () => ({ ...realBranding(), identity: { productName: 'Cloud Bongos' }, domains: { publicOrigin: 'https://amazonprimea.com', provisioningOrigin: 'https://get.cloudbongos.com' } });
|
|
82
|
+
const res = await fetch(`${base}/provisioning/instances/42/github-app`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' });
|
|
83
|
+
const j = await res.json();
|
|
84
|
+
t('redirect_url uses provisioningOrigin, not the game publicOrigin', j.manifest && j.manifest.redirect_url === 'https://get.cloudbongos.com/api/bongos/provisioning/github-app/callback', j.manifest && j.manifest.redirect_url);
|
|
85
|
+
t('webhook hook_attributes.url also uses provisioningOrigin', j.manifest && j.manifest.hook_attributes && j.manifest.hook_attributes.url === 'https://get.cloudbongos.com/api/bongos/provisioning/github-app/webhook', j.manifest && j.manifest.hook_attributes && j.manifest.hook_attributes.url);
|
|
86
|
+
api.branding = prevBranding;
|
|
87
|
+
}
|
|
88
|
+
|
|
74
89
|
console.log('\nGET /provisioning/github-app/callback (the public GitHub redirect):');
|
|
75
90
|
{
|
|
76
91
|
const res = await fetch(`${base}/provisioning/github-app/callback?code=GHCODE123&state=GOODSTATE`);
|
|
@@ -44,13 +44,13 @@ const PUBLISH = [
|
|
|
44
44
|
'scripts/gds/ship.js', // the ship/grade pipeline publishes (sans secrets)
|
|
45
45
|
'scripts/gds/publish-manifest.js',
|
|
46
46
|
'scripts/migrate.sh',
|
|
47
|
-
'server.js',
|
|
48
47
|
'migrations/182_some_change.sql',
|
|
49
48
|
'tests/publish_manifest.mjs',
|
|
50
49
|
'modules/lifecycle/db.js',
|
|
51
50
|
'modules/grading/grader.js',
|
|
52
51
|
'modules/hall-ui/public/work.js',
|
|
53
52
|
'modules/dev-box/boxes.js',
|
|
53
|
+
'modules/provisioning/provisioning.js', // instance-hosting control plane — CORE like its sibling dev-box (task 2135)
|
|
54
54
|
// served-at-runtime surfaces a standalone must carry (task 2078) — their absence
|
|
55
55
|
// from the packaged core 404'd the hall UI + /docs on demo.cloudbongos.com
|
|
56
56
|
'clients/bongos-client/bongos-client.global.js', // /lib/bongos-client.js (window.BongosClient)
|
|
@@ -91,8 +91,19 @@ const EXCLUDE = [
|
|
|
91
91
|
'src/world/terrain.js',
|
|
92
92
|
'src/rooms/WorldRoom.js',
|
|
93
93
|
'src/preview-server.js',
|
|
94
|
+
'server.js', // repo-root game+platform entry (top-level require('colyseus')); the core boots via src/platform-server.js (task 2134)
|
|
94
95
|
'scripts/art/orchestrate.js', // art-feature tooling under scripts/ (legacy location)
|
|
96
|
+
'scripts/gds/art-stage.js', // art sandbox-staging CLI (requires modules/art-pipeline); excluded with the art feature (task 2135)
|
|
95
97
|
'scripts/tile-add.js',
|
|
98
|
+
// host-feature tests that statically import an excluded module's source: shipped in the
|
|
99
|
+
// core package they'd MODULE_NOT_FOUND; excluded with the feature they cover (task 2147)
|
|
100
|
+
'tests/walkability.mjs',
|
|
101
|
+
'tests/terrain_picker.mjs',
|
|
102
|
+
'tests/snapshot_picker.mjs',
|
|
103
|
+
'tests/sprite_facing.mjs',
|
|
104
|
+
'tests/turn_in_place.mjs',
|
|
105
|
+
'tests/day_cycle.mjs',
|
|
106
|
+
'tests/art_key.mjs',
|
|
96
107
|
// instance identity + history
|
|
97
108
|
'config/branding.json',
|
|
98
109
|
'config/modules.json',
|
|
@@ -106,6 +117,7 @@ const EXCLUDE = [
|
|
|
106
117
|
'blockers/blockers.md',
|
|
107
118
|
'limitations/v1-shipped.md',
|
|
108
119
|
'generative-ideas.md',
|
|
120
|
+
'migrations/instance/018_pms_v3_planning_seed.sql', // instance seed migrations = host scope-truth (ADR 0105, task 2140)
|
|
109
121
|
'.env.local',
|
|
110
122
|
// topology / deploy / CI / local gate hooks / secrets — read-sensitive docs
|
|
111
123
|
'infra/Caddyfile',
|
|
@@ -180,5 +192,44 @@ t('allowlist + denylist are non-empty arrays of strings', () => {
|
|
|
180
192
|
for (const g of [...PUBLISH_ALLOWLIST, ...PUBLISH_DENYLIST]) assert.equal(typeof g, 'string');
|
|
181
193
|
});
|
|
182
194
|
|
|
195
|
+
// ── R89 split guard: no PUBLISHABLE test may statically import an EXCLUDED module ─────
|
|
196
|
+
// tests/ is allowlisted, but a test that imports host-module source (game / art-pipeline)
|
|
197
|
+
// would ship inside the @bongos/core package WITHOUT that module → MODULE_NOT_FOUND when a
|
|
198
|
+
// core CI/consumer runs it, and the cutover would delete OTB's own host tests. Every such
|
|
199
|
+
// test must be host-excluded (see CONTENT_EXCLUDE). This DERIVES the set from disk so a NEW
|
|
200
|
+
// host-module test can't slip in unlisted (the enumerated assertions above would miss it).
|
|
201
|
+
// Static relative-literal imports only — temp-dir fixtures (modules/alpha, …) use path.join,
|
|
202
|
+
// so they never match; a module name appearing only as string DATA (this file's own arrays)
|
|
203
|
+
// isn't in a require()/import statement, so it never matches either. (task 2147)
|
|
204
|
+
t('no publishable test statically imports an excluded module (R89 boundary)', () => {
|
|
205
|
+
const fs = require('node:fs');
|
|
206
|
+
const path = require('node:path');
|
|
207
|
+
const { fileURLToPath } = require('node:url');
|
|
208
|
+
const testsDir = path.dirname(fileURLToPath(import.meta.url));
|
|
209
|
+
const IMPORT_RE = /(?:require|import)\s*\(\s*["'](\.\.?\/[^"']*?)modules\/([a-z0-9][a-z0-9-]*)\/[^"']*["']|from\s+["'](\.\.?\/[^"']*?)modules\/([a-z0-9][a-z0-9-]*)\/[^"']*["']/g;
|
|
210
|
+
const detected = []; // test files that import ANY excluded module (non-vacuous check)
|
|
211
|
+
const offenders = []; // …and are still publishable (the actual failure)
|
|
212
|
+
for (const name of fs.readdirSync(testsDir)) {
|
|
213
|
+
if (!/\.(mjs|js|cjs)$/.test(name)) continue;
|
|
214
|
+
const rel = `tests/${name}`;
|
|
215
|
+
const bad = new Set();
|
|
216
|
+
let m; IMPORT_RE.lastIndex = 0;
|
|
217
|
+
const src = fs.readFileSync(path.join(testsDir, name), 'utf8');
|
|
218
|
+
while ((m = IMPORT_RE.exec(src))) {
|
|
219
|
+
const key = m[2] || m[4];
|
|
220
|
+
if (key && !isPublishable(`modules/${key}/module.json`)) bad.add(key);
|
|
221
|
+
}
|
|
222
|
+
if (bad.size === 0) continue;
|
|
223
|
+
detected.push(rel);
|
|
224
|
+
if (isPublishable(rel)) {
|
|
225
|
+
offenders.push(`${rel} imports excluded module(s) [${[...bad].sort().join(', ')}] but is still PUBLISHABLE — add it to CONTENT_EXCLUDE (task 2147)`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// non-vacuous: the scan must actually SEE the known host tests (a broken regex that
|
|
229
|
+
// matched nothing would otherwise pass silently). The 7 game/art tests are the floor.
|
|
230
|
+
assert.ok(detected.length >= 7, `expected ≥7 host-module tests detected, saw ${detected.length} — the import scan may be broken`);
|
|
231
|
+
assert.equal(offenders.length, 0, '\n ' + offenders.join('\n '));
|
|
232
|
+
});
|
|
233
|
+
|
|
183
234
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
184
235
|
process.exit(failed ? 1 : 0);
|