@bongos/core 1.17.1 → 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 +244 -264
- 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/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/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 +71 -4
- package/modules/hall-ui/public/goal-inbox.js +191 -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 +1 -0
- package/modules/hall-ui/public/style.css +43 -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/src/bongos/auth.js +14 -1
- 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/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/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,184 @@
|
|
|
1
|
+
// tests/cutover_codemod.mjs
|
|
2
|
+
//
|
|
3
|
+
// R89 cutover codemod (task 2159 [F1], task 2161 [F3]): rewrite HOST files' core-internal
|
|
4
|
+
// require()s to @bongos/core/* subpaths at cutover. Guards the pure transform (quote-preserving,
|
|
5
|
+
// idempotent, table-driven), the host-only scope oracle, the post-split precondition, and the
|
|
6
|
+
// end-to-end plan over a simulated consumer tree.
|
|
7
|
+
//
|
|
8
|
+
// Run: node tests/cutover_codemod.mjs
|
|
9
|
+
|
|
10
|
+
import { strict as assert } from 'node:assert';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const cm = require('../scripts/gds/cutover-codemod.js');
|
|
15
|
+
const { rewriteCoreRequires, isHostFile, looksLikeConsumer, planCodemod, CORE_INTERNAL_MAP } = cm;
|
|
16
|
+
|
|
17
|
+
let passed = 0;
|
|
18
|
+
let failed = 0;
|
|
19
|
+
function t(name, fn) {
|
|
20
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
21
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.stack || e.message}`); }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---- rewriteCoreRequires: the pure transform (F1 — module-api) --------------
|
|
25
|
+
|
|
26
|
+
t('rewrites ../../src/module-api → @bongos/core/module-api', () => {
|
|
27
|
+
const r = rewriteCoreRequires(`const { pool } = require('../../src/module-api');`);
|
|
28
|
+
assert.equal(r.text, `const { pool } = require('@bongos/core/module-api');`);
|
|
29
|
+
assert.equal(r.changes.length, 1);
|
|
30
|
+
assert.equal(r.changes[0].key, 'module-api');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
t('rewrites deeper ../../../src/module-api (routes depth) too', () => {
|
|
34
|
+
const r = rewriteCoreRequires(`const { registerProvider } = require('../../../src/module-api');`);
|
|
35
|
+
assert.equal(r.text, `const { registerProvider } = require('@bongos/core/module-api');`);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
t('preserves double quotes', () => {
|
|
39
|
+
const r = rewriteCoreRequires(`const api = require("../../src/module-api");`);
|
|
40
|
+
assert.equal(r.text, `const api = require("@bongos/core/module-api");`);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
t('is idempotent — a rewritten @bongos/core require is not matched again', () => {
|
|
44
|
+
const once = rewriteCoreRequires(`const api = require('../../src/module-api');`).text;
|
|
45
|
+
const twice = rewriteCoreRequires(once);
|
|
46
|
+
assert.equal(twice.text, once);
|
|
47
|
+
assert.equal(twice.changes.length, 0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
t('leaves an npm/sibling require untouched', () => {
|
|
51
|
+
const src = `const express = require('express');\nconst helper = require('./helper');\n`;
|
|
52
|
+
const r = rewriteCoreRequires(src);
|
|
53
|
+
assert.equal(r.text, src);
|
|
54
|
+
assert.equal(r.changes.length, 0);
|
|
55
|
+
assert.equal(r.unmapped.length, 0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
t('trailing member access is preserved (only the require() call is replaced)', () => {
|
|
59
|
+
const r = rewriteCoreRequires(`const pool = require('../../src/module-api').pool;`);
|
|
60
|
+
assert.equal(r.text, `const pool = require('@bongos/core/module-api').pool;`);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
t('an UNMAPPED core-internal require is left unchanged AND reported', () => {
|
|
64
|
+
// A core internal NOT in the table (host code should never import it raw) — flagged, not guessed.
|
|
65
|
+
const src = `const brand = require('../../src/branding');`;
|
|
66
|
+
const r = rewriteCoreRequires(src);
|
|
67
|
+
assert.equal(r.text, src, 'must not guess a specifier for an unmapped internal');
|
|
68
|
+
assert.equal(r.changes.length, 0);
|
|
69
|
+
assert.equal(r.unmapped.length, 1);
|
|
70
|
+
assert.equal(r.unmapped[0].key, 'branding');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
t('rewrites every mapped entry in the table', () => {
|
|
74
|
+
for (const [key, mapped] of Object.entries(CORE_INTERNAL_MAP)) {
|
|
75
|
+
const r = rewriteCoreRequires(`require('../../src/${key}')`);
|
|
76
|
+
assert.equal(r.text, `require('@bongos/core/${mapped}')`, `key ${key}`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ---- F3 (task 2161): server.js core internals --------------------------------
|
|
81
|
+
|
|
82
|
+
t('F3: rewrites server.js core internals from ./src/* form', () => {
|
|
83
|
+
assert.equal(rewriteCoreRequires(`const p = require('./src/bongos/api-path-404');`).text,
|
|
84
|
+
`const p = require('@bongos/core/bongos/api-path-404');`);
|
|
85
|
+
assert.equal(rewriteCoreRequires(`const u = require('./src/bongos/uptime-poller');`).text,
|
|
86
|
+
`const u = require('@bongos/core/bongos/uptime-poller');`);
|
|
87
|
+
assert.equal(rewriteCoreRequires(`const l = require('./src/module-loader/loader');`).text,
|
|
88
|
+
`const l = require('@bongos/core/module-loader/loader');`);
|
|
89
|
+
assert.equal(rewriteCoreRequires(`const m = require('./src/modules');`).text,
|
|
90
|
+
`const m = require('@bongos/core/modules');`);
|
|
91
|
+
assert.equal(rewriteCoreRequires(`const s = require('./src/module-seams');`).text,
|
|
92
|
+
`const s = require('@bongos/core/module-seams');`);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
t('F3: serve-internal maps to its curated export name, NOT ./bongos/serve-internal', () => {
|
|
96
|
+
// The file is src/bongos/serve-internal.js but the package exports it as ./serve-internal.
|
|
97
|
+
const r = rewriteCoreRequires(`const { mountInternalSurfaces } = require('./src/bongos/serve-internal');`);
|
|
98
|
+
assert.equal(r.text, `const { mountInternalSurfaces } = require('@bongos/core/serve-internal');`);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ---- scope oracle -----------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
t('isHostFile is true only when isPublishable(rel) === false', () => {
|
|
104
|
+
const fakePub = (rel) => rel.startsWith('src/') || rel.startsWith('modules/lifecycle/');
|
|
105
|
+
assert.equal(isHostFile('modules/game/db.js', fakePub), true);
|
|
106
|
+
assert.equal(isHostFile('server.js', fakePub), true);
|
|
107
|
+
assert.equal(isHostFile('modules/lifecycle/db.js', fakePub), false);
|
|
108
|
+
assert.equal(isHostFile('src/module-api.js', fakePub), false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ---- precondition -----------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
t('looksLikeConsumer: false in the monolith (src/module-api.js present), true post-split', () => {
|
|
114
|
+
const monolith = looksLikeConsumer('/repo', { existsFn: (p) => p.endsWith('/src/module-api.js') });
|
|
115
|
+
const consumer = looksLikeConsumer('/repo', { existsFn: () => false });
|
|
116
|
+
assert.equal(monolith, false);
|
|
117
|
+
assert.equal(consumer, true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ---- planCodemod over a simulated consumer tree -----------------------------
|
|
121
|
+
|
|
122
|
+
t('planCodemod rewrites host files, skips core files, reports unmapped', () => {
|
|
123
|
+
// A fake post-split consumer: host modules + server.js kept; a core module also present
|
|
124
|
+
// (as if partition had not yet run) to prove the scope oracle skips it regardless.
|
|
125
|
+
const tree = {
|
|
126
|
+
'modules/game/db.js': `const { pool } = require('../../src/module-api');`,
|
|
127
|
+
'modules/art-pipeline/routes/art-key.js': `const api = require('../../../src/module-api');`,
|
|
128
|
+
'server.js': `const seams = require('./src/module-seams');`, // F3 — now mapped → rewritten
|
|
129
|
+
'modules/game/experimental.js': `const b = require('../../src/branding');`, // unmapped → reported, not rewritten
|
|
130
|
+
'modules/lifecycle/db.js': `const api = require('../../src/module-api');`, // CORE — must be skipped
|
|
131
|
+
'README.md': 'not a js file',
|
|
132
|
+
};
|
|
133
|
+
const fsImpl = {
|
|
134
|
+
readdirSync: (dir, opts) => {
|
|
135
|
+
// minimal shim: list immediate children of `dir` within the flat `tree`
|
|
136
|
+
const rootPrefix = '/consumer';
|
|
137
|
+
const rel = dir === rootPrefix ? '' : dir.slice(rootPrefix.length + 1);
|
|
138
|
+
const names = new Set();
|
|
139
|
+
for (const p of Object.keys(tree)) {
|
|
140
|
+
if (rel && !p.startsWith(rel + '/')) continue;
|
|
141
|
+
const rest = rel ? p.slice(rel.length + 1) : p;
|
|
142
|
+
const seg = rest.split('/')[0];
|
|
143
|
+
const isDir = rest.includes('/');
|
|
144
|
+
names.add(seg + (isDir ? '/' : ''));
|
|
145
|
+
}
|
|
146
|
+
return [...names].map((n) => {
|
|
147
|
+
const isDir = n.endsWith('/');
|
|
148
|
+
const name = isDir ? n.slice(0, -1) : n;
|
|
149
|
+
return { name, isDirectory: () => isDir, isFile: () => !isDir };
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
readFileSync: (abs) => tree[abs.slice('/consumer/'.length)],
|
|
153
|
+
};
|
|
154
|
+
const isPub = (rel) => rel.startsWith('modules/lifecycle/') || rel.startsWith('src/');
|
|
155
|
+
// Force the filesystem-walk path (no git) so the injected fake fs is exercised.
|
|
156
|
+
const runGit = () => { throw new Error('no git in test'); };
|
|
157
|
+
const plan = planCodemod({ root: '/consumer', fsImpl, runGit, isPublishableFn: isPub });
|
|
158
|
+
|
|
159
|
+
const changed = plan.files.map((f) => f.rel).sort();
|
|
160
|
+
assert.deepEqual(changed, ['modules/art-pipeline/routes/art-key.js', 'modules/game/db.js', 'server.js']);
|
|
161
|
+
// core module skipped even though it has the same relative require
|
|
162
|
+
assert.ok(!changed.includes('modules/lifecycle/db.js'), 'core module must be skipped');
|
|
163
|
+
// a host file with an UNMAPPED core internal is reported, never rewritten
|
|
164
|
+
assert.ok(!changed.includes('modules/game/experimental.js'), 'unmapped internal must not be rewritten');
|
|
165
|
+
assert.ok(plan.unmapped.some((u) => u.rel === 'modules/game/experimental.js' && u.key === 'branding'));
|
|
166
|
+
// the rewritten text is correct for both a host module (F1) and server.js (F3)
|
|
167
|
+
const game = plan.files.find((f) => f.rel === 'modules/game/db.js');
|
|
168
|
+
assert.equal(game.newText, `const { pool } = require('@bongos/core/module-api');`);
|
|
169
|
+
const srv = plan.files.find((f) => f.rel === 'server.js');
|
|
170
|
+
assert.equal(srv.newText, `const seams = require('@bongos/core/module-seams');`);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ---- invariant: every mapped target is an actual @bongos/core export ---------
|
|
174
|
+
|
|
175
|
+
t('every CORE_INTERNAL_MAP target is exported by the core package (a rewrite never outruns an export)', () => {
|
|
176
|
+
const { buildPackageJson } = require('../scripts/gds/package-core.js');
|
|
177
|
+
const pkg = buildPackageJson({ version: '0.0.0-test', sourceDeps: { express: '^4.0.0', pg: '^8.0.0' } });
|
|
178
|
+
for (const mapped of Object.values(CORE_INTERNAL_MAP)) {
|
|
179
|
+
assert.ok(pkg.exports[`./${mapped}`], `@bongos/core must export ./${mapped} (targeted by the codemod)`);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
console.log(`\ncutover_codemod: ${passed} passed, ${failed} failed`);
|
|
184
|
+
if (failed) process.exit(1);
|
|
@@ -37,14 +37,37 @@ async function t(name, fn) {
|
|
|
37
37
|
finally { globalThis.fetch = realFetch; }
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
await t('device_flow_disabled (HTTP 200 + error body) → throws an actionable
|
|
40
|
+
await t('device_flow_disabled (HTTP 200 + error body) → throws an actionable error deep-linking the GitHub-App settings (task 2129)', async () => {
|
|
41
41
|
stubFetch(200, { error: 'device_flow_disabled', error_description: 'Device Flow is not enabled for this app.' });
|
|
42
42
|
await assert.rejects(
|
|
43
43
|
() => auth.startDeviceFlow(),
|
|
44
|
-
(err) => /device_flow_disabled/.test(err.message)
|
|
44
|
+
(err) => /device_flow_disabled/.test(err.message)
|
|
45
|
+
&& /device-flow start/i.test(err.message)
|
|
46
|
+
// creds client_id is Iv1.* (a GitHub App) → point at the Apps listing + the exact toggle.
|
|
47
|
+
&& /github\.com\/settings\/apps/.test(err.message)
|
|
48
|
+
&& /Enable Device Flow/i.test(err.message),
|
|
45
49
|
);
|
|
46
50
|
});
|
|
47
51
|
|
|
52
|
+
await t('device_flow_disabled on an OAuth App (Ov* client_id) → deep-links OAuth developer settings, not /settings/apps (task 2129)', async () => {
|
|
53
|
+
const saved = {};
|
|
54
|
+
for (const p of ['GDS', 'OTB', 'CLOUDBONGOS']) {
|
|
55
|
+
saved[p] = process.env[`${p}_GITHUB_CLIENT_ID`];
|
|
56
|
+
process.env[`${p}_GITHUB_CLIENT_ID`] = 'Ov23liOAuthClientId0';
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
stubFetch(200, { error: 'device_flow_disabled', error_description: 'Device Flow is not enabled for this app.' });
|
|
60
|
+
await assert.rejects(
|
|
61
|
+
() => auth.startDeviceFlow(),
|
|
62
|
+
(err) => /github\.com\/settings\/developers/.test(err.message)
|
|
63
|
+
&& !/settings\/apps/.test(err.message)
|
|
64
|
+
&& /Enable Device Flow/i.test(err.message),
|
|
65
|
+
);
|
|
66
|
+
} finally {
|
|
67
|
+
for (const p of ['GDS', 'OTB', 'CLOUDBONGOS']) process.env[`${p}_GITHUB_CLIENT_ID`] = saved[p];
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
48
71
|
await t('incorrect_client_credentials (HTTP 200 + error) → throws, names the error', async () => {
|
|
49
72
|
stubFetch(200, { error: 'incorrect_client_credentials', error_description: 'The client_id is not valid.' });
|
|
50
73
|
await assert.rejects(() => auth.startDeviceFlow(), (err) => /incorrect_client_credentials/.test(err.message));
|
|
@@ -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 {
|
|
@@ -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
|
});
|