@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,302 @@
|
|
|
1
|
+
// tests/consumer_layout_boot.mjs
|
|
2
|
+
//
|
|
3
|
+
// R89-prereq CI proof (task 2142): boot @bongos/core from a REAL consumer layout —
|
|
4
|
+
// the packaged .tgz installed into node_modules, with the instance's OWN content
|
|
5
|
+
// (config / modules / .claude) sitting at a DIFFERENT root — and prove the two-root
|
|
6
|
+
// module loader (task 2133) discovers modules from BOTH roots.
|
|
7
|
+
//
|
|
8
|
+
// WHY THIS EXISTS. Every existing gate runs the SOURCE tree with all deps present:
|
|
9
|
+
// - tests/platform_boot.mjs imports src/platform-server.js straight from the repo;
|
|
10
|
+
// - tests/fitness_two_root.mjs diverges resolveInstanceRoot() to a temp dir but
|
|
11
|
+
// keeps cwd/core = the real repo (resolveCoreRoot() is still __dirname/..).
|
|
12
|
+
// NONE installs the packaged core into a tree whose instance root is elsewhere —
|
|
13
|
+
// which is exactly why the pre-2133 single-root loader gap was invisible: a
|
|
14
|
+
// core-only scan boots a split instance with none of its host modules, and no test
|
|
15
|
+
// ever exercised that shape. This harness closes the blind spot end to end.
|
|
16
|
+
//
|
|
17
|
+
// WHAT IT PROVES (the two-root checks all go RED against the pre-2133 loader):
|
|
18
|
+
// 1. package — scripts/gds/package-core.js builds an installable .tgz.
|
|
19
|
+
// 2. install — `npm install <tgz>` lands it at node_modules/@bongos/core.
|
|
20
|
+
// 3. roots — resolveCoreRoot() is UNDER node_modules/@bongos/core while
|
|
21
|
+
// resolveInstanceRoot() is the consumer tree: two DIFFERENT dirs, so
|
|
22
|
+
// a green result can never be a same-directory coincidence.
|
|
23
|
+
// 4. boot — `node node_modules/@bongos/core/src/platform-server.js` serves
|
|
24
|
+
// /healthz 200 (the game-less core boots from the package).
|
|
25
|
+
// 5. two-root — discoverModules() loads a module resolved from the CORE modules
|
|
26
|
+
// dir AND the consumer-probe module resolved from the INSTANCE
|
|
27
|
+
// modules dir, with zero errors. A single-root loader physically
|
|
28
|
+
// cannot find consumer-probe → this IS the task-2133 done-when.
|
|
29
|
+
// 6. fitness — the packaged fitness.js discoveredModuleKeys() spans both roots.
|
|
30
|
+
// 7. gen-file-map — reads the INSTANCE root (lists the consumer's .claude/skills).
|
|
31
|
+
// 8. gen-repo-map — maps CORE code (scripts/gds/fitness.js) from node_modules.
|
|
32
|
+
//
|
|
33
|
+
// This is an INTEGRATION harness (a package build + `npm install` + a live boot), so
|
|
34
|
+
// it is EXCLUDED from the DB-free unit gate (scripts/gds/run-unit-tests.js INTEGRATION
|
|
35
|
+
// set) and driven by .github/workflows/consumer-layout-boot.yml. It needs NO Postgres
|
|
36
|
+
// (/healthz, the loader, fitness, and the generators are all DB-free). Re-runnable:
|
|
37
|
+
// node tests/consumer_layout_boot.mjs # builds a fresh core .tgz
|
|
38
|
+
// CORE_TGZ=/abs/path/bongos-core-x.y.z.tgz node tests/consumer_layout_boot.mjs # reuse a prebuilt one
|
|
39
|
+
|
|
40
|
+
import assert from 'node:assert/strict';
|
|
41
|
+
import http from 'node:http';
|
|
42
|
+
import net from 'node:net';
|
|
43
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
44
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, realpathSync } from 'node:fs';
|
|
45
|
+
import { tmpdir } from 'node:os';
|
|
46
|
+
import { join, dirname } from 'node:path';
|
|
47
|
+
import { fileURLToPath } from 'node:url';
|
|
48
|
+
|
|
49
|
+
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
50
|
+
const NPM_BIN = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
51
|
+
const CORE_VERSION_RANGE = '^1.0.0'; // consumer-probe's coreVersion — satisfied by any 1.x core.
|
|
52
|
+
|
|
53
|
+
let passed = 0;
|
|
54
|
+
let failed = 0;
|
|
55
|
+
function check(name, fn) {
|
|
56
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
57
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e && e.message ? e.message : e}`); }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Run a node snippet in `cwd` and parse its stdout as JSON. Used to introspect the
|
|
61
|
+
// INSTALLED core (its loader / fitness / generators) exactly as a consumer process
|
|
62
|
+
// would resolve them — absolute paths are injected so the snippet never depends on
|
|
63
|
+
// the snippet file's own location (mirrors tests/fitness_two_root.mjs).
|
|
64
|
+
function runNodeJSON(snippet, cwd) {
|
|
65
|
+
const r = spawnSync(process.execPath, ['-e', snippet], { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
66
|
+
if (r.status !== 0) {
|
|
67
|
+
throw new Error(`node snippet exited ${r.status}\n--- stdout ---\n${r.stdout}\n--- stderr ---\n${r.stderr}`);
|
|
68
|
+
}
|
|
69
|
+
try { return JSON.parse(r.stdout); }
|
|
70
|
+
catch { throw new Error(`snippet stdout was not JSON:\n${r.stdout}\n--- stderr ---\n${r.stderr}`); }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function freePort() {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const srv = net.createServer();
|
|
76
|
+
srv.on('error', reject);
|
|
77
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
78
|
+
const { port } = srv.address();
|
|
79
|
+
srv.close(() => resolve(port));
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getHealthz(port) {
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
const req = http.get({ host: '127.0.0.1', port, path: '/healthz', timeout: 2000 }, (res) => {
|
|
87
|
+
let body = '';
|
|
88
|
+
res.on('data', (c) => (body += c));
|
|
89
|
+
res.on('end', () => resolve({ status: res.statusCode, body }));
|
|
90
|
+
});
|
|
91
|
+
req.on('error', () => resolve(null));
|
|
92
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function waitForHealthz(port, timeoutMs = 20000) {
|
|
97
|
+
const deadline = Date.now() + timeoutMs;
|
|
98
|
+
while (Date.now() < deadline) {
|
|
99
|
+
const res = await getHealthz(port);
|
|
100
|
+
if (res) return res;
|
|
101
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function main() {
|
|
107
|
+
console.log('=== consumer-layout boot proof (task 2142): install @bongos/core, boot it, assert two-root module load ===');
|
|
108
|
+
|
|
109
|
+
const workRoot = mkdtempSync(join(tmpdir(), 'consumer-layout-'));
|
|
110
|
+
const consumer = join(workRoot, 'consumer');
|
|
111
|
+
let child = null;
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
// --- 1. package -----------------------------------------------------------
|
|
115
|
+
// Build the installable core .tgz from HEAD (the committed tree — so on a PR the
|
|
116
|
+
// job packages exactly the code under review). Reuse a prebuilt one via CORE_TGZ.
|
|
117
|
+
let tgz = process.env.CORE_TGZ || '';
|
|
118
|
+
check('package: scripts/gds/package-core.js builds an installable .tgz', () => {
|
|
119
|
+
if (tgz) { assert.ok(existsSync(tgz), `CORE_TGZ does not exist: ${tgz}`); return; }
|
|
120
|
+
const outDir = join(workRoot, 'dist');
|
|
121
|
+
const r = spawnSync(process.execPath, [join(REPO_ROOT, 'scripts/gds/package-core.js'), '--out-dir', outDir],
|
|
122
|
+
{ cwd: REPO_ROOT, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
|
|
123
|
+
assert.equal(r.status, 0, `package-core exited ${r.status}\n${r.stderr}`);
|
|
124
|
+
const pinLine = r.stdout.trim().split('\n').filter(Boolean).pop();
|
|
125
|
+
const pin = JSON.parse(pinLine);
|
|
126
|
+
tgz = join(outDir, pin.tarball.name);
|
|
127
|
+
assert.ok(existsSync(tgz), `packaged tarball missing: ${tgz}`);
|
|
128
|
+
});
|
|
129
|
+
if (!tgz || !existsSync(tgz)) { throw new Error('cannot continue without a core tarball'); }
|
|
130
|
+
|
|
131
|
+
// --- build the consumer tree: instance content at its OWN root -------------
|
|
132
|
+
// A minimal-but-real Cloud Bongos consumer: a bongos.config.json pointing the
|
|
133
|
+
// instance root at itself, a HOST module (consumer-probe) that is NOT part of
|
|
134
|
+
// @bongos/core, and an instance-owned .claude/skills dir. The host module is the
|
|
135
|
+
// crux: it can only be discovered via resolveInstanceRoot(), so finding it proves
|
|
136
|
+
// the second root is scanned.
|
|
137
|
+
mkdirSync(consumer, { recursive: true });
|
|
138
|
+
writeFileSync(join(consumer, 'package.json'), `${JSON.stringify({
|
|
139
|
+
name: 'consumer-layout-probe',
|
|
140
|
+
version: '0.0.0',
|
|
141
|
+
private: true,
|
|
142
|
+
description: 'Ephemeral @bongos/core consumer used by the R89 two-root CI proof (task 2142).',
|
|
143
|
+
dependencies: { '@bongos/core': `file:${tgz}` },
|
|
144
|
+
}, null, 2)}\n`);
|
|
145
|
+
writeFileSync(join(consumer, 'bongos.config.json'), `${JSON.stringify({ instanceRoot: '.' }, null, 2)}\n`);
|
|
146
|
+
// A tiny instance CLAUDE.md (the charter heading keeps fitness's budget check happy
|
|
147
|
+
// if anything reads it) — makes the tree a realistic instance, not load-bearing here.
|
|
148
|
+
writeFileSync(join(consumer, 'CLAUDE.md'),
|
|
149
|
+
'# Consumer probe instance\n\n## Working with this file (content charter)\n\nEphemeral fixture for the task-2142 two-root CI proof.\n');
|
|
150
|
+
mkdirSync(join(consumer, 'config'), { recursive: true });
|
|
151
|
+
writeFileSync(join(consumer, 'config', 'branding.json'), `${JSON.stringify({
|
|
152
|
+
configDir: 'cloudbongos', envPrefix: 'CLOUDBONGOS',
|
|
153
|
+
identity: { productName: 'Consumer Probe' },
|
|
154
|
+
}, null, 2)}\n`);
|
|
155
|
+
// The host module — a valid manifest, key === dir name, coreVersion any 1.x.
|
|
156
|
+
mkdirSync(join(consumer, 'modules', 'consumer-probe'), { recursive: true });
|
|
157
|
+
writeFileSync(join(consumer, 'modules', 'consumer-probe', 'module.json'), `${JSON.stringify({
|
|
158
|
+
key: 'consumer-probe',
|
|
159
|
+
title: 'Consumer Probe',
|
|
160
|
+
description: 'A host module that ships only in the instance repo, never in @bongos/core.',
|
|
161
|
+
coreVersion: CORE_VERSION_RANGE,
|
|
162
|
+
contributes: {},
|
|
163
|
+
}, null, 2)}\n`);
|
|
164
|
+
// Instance-owned .claude/skills (gen-file-map lists the instance root's skills).
|
|
165
|
+
mkdirSync(join(consumer, '.claude', 'skills', 'consumer-probe-skill'), { recursive: true });
|
|
166
|
+
writeFileSync(join(consumer, '.claude', 'skills', 'consumer-probe-skill', 'SKILL.md'),
|
|
167
|
+
'# consumer-probe-skill\n\nInstance-owned skill fixture for the task-2142 proof.\n');
|
|
168
|
+
mkdirSync(join(consumer, '.claude', 'scheduled-tasks'), { recursive: true });
|
|
169
|
+
|
|
170
|
+
// --- 2. install -----------------------------------------------------------
|
|
171
|
+
const coreDir = join(consumer, 'node_modules', '@bongos', 'core');
|
|
172
|
+
check('install: npm install lands the core at node_modules/@bongos/core', () => {
|
|
173
|
+
const r = spawnSync(NPM_BIN, ['install', '--no-audit', '--no-fund', '--loglevel=error'],
|
|
174
|
+
{ cwd: consumer, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
175
|
+
assert.equal(r.status, 0, `npm install exited ${r.status}\n${r.stderr}`);
|
|
176
|
+
assert.ok(existsSync(join(coreDir, 'package.json')), 'node_modules/@bongos/core/package.json missing');
|
|
177
|
+
assert.ok(existsSync(join(coreDir, 'src', 'platform-server.js')), 'installed core has no src/platform-server.js');
|
|
178
|
+
assert.ok(existsSync(join(coreDir, 'modules')), 'installed core ships no modules/ dir');
|
|
179
|
+
});
|
|
180
|
+
if (!existsSync(coreDir)) { throw new Error('cannot continue without an installed core'); }
|
|
181
|
+
|
|
182
|
+
const A = (p) => JSON.stringify(join(coreDir, p)); // absolute path into the installed core, JSON-quoted
|
|
183
|
+
|
|
184
|
+
// --- 3. roots diverge -----------------------------------------------------
|
|
185
|
+
// The whole proof is meaningless if the two roots secretly coincide, so pin that
|
|
186
|
+
// down first: core resolves UNDER node_modules/@bongos/core, instance is the
|
|
187
|
+
// consumer tree, and they are different directories.
|
|
188
|
+
check('roots: resolveCoreRoot() (node_modules) and resolveInstanceRoot() (consumer) are distinct', () => {
|
|
189
|
+
const r = runNodeJSON(`
|
|
190
|
+
const ic = require(${A('src/instance-config.js')});
|
|
191
|
+
process.stdout.write(JSON.stringify({ core: ic.resolveCoreRoot(), inst: ic.resolveInstanceRoot() }));
|
|
192
|
+
`, consumer);
|
|
193
|
+
// Compare realpaths — the child's process.cwd() resolves symlinks (e.g. macOS
|
|
194
|
+
// /var → /private/var), so match that rather than the raw tmp path we created.
|
|
195
|
+
assert.ok(r.core.includes(join('node_modules', '@bongos', 'core')), `core root not under node_modules: ${r.core}`);
|
|
196
|
+
assert.equal(r.inst, realpathSync(consumer), `instance root should be the consumer tree, got ${r.inst}`);
|
|
197
|
+
assert.notEqual(r.core, r.inst, 'core root and instance root must be different directories');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// --- 4. boot + /healthz ---------------------------------------------------
|
|
201
|
+
const port = await freePort();
|
|
202
|
+
child = spawn(process.execPath, [join(coreDir, 'src', 'platform-server.js')], {
|
|
203
|
+
cwd: consumer,
|
|
204
|
+
env: { ...process.env, PORT: String(port), HOST: '127.0.0.1' },
|
|
205
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
206
|
+
});
|
|
207
|
+
let childErr = '';
|
|
208
|
+
child.stderr.on('data', (c) => { childErr += c; });
|
|
209
|
+
child.stdout.on('data', () => {});
|
|
210
|
+
const health = await waitForHealthz(port);
|
|
211
|
+
check('boot: node node_modules/@bongos/core/src/platform-server.js serves /healthz 200', () => {
|
|
212
|
+
assert.ok(health, `/healthz never came up on port ${port}\n--- server stderr ---\n${childErr.slice(-2000)}`);
|
|
213
|
+
assert.equal(health.status, 200, `/healthz status ${health.status}`);
|
|
214
|
+
assert.equal(health.body.trim(), 'ok', `/healthz body was "${health.body}"`);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// --- 5. two-root module load (the task-2133 done-when) --------------------
|
|
218
|
+
// The real default scan (DEFAULT_ROOTS) PLUS an explicit single-root scan that
|
|
219
|
+
// reproduces the pre-2133 loader shape — so the job doesn't just PASS today, it
|
|
220
|
+
// proves it would go RED if the loader regressed to one root ("done when: green
|
|
221
|
+
// AND would RED against the pre-B1 loader").
|
|
222
|
+
const disc = runNodeJSON(`
|
|
223
|
+
const path = require('node:path');
|
|
224
|
+
const loader = require(${A('src/module-loader/loader.js')});
|
|
225
|
+
const ic = require(${A('src/instance-config.js')});
|
|
226
|
+
const coreModulesDir = path.join(ic.resolveCoreRoot(), 'modules');
|
|
227
|
+
const instModulesDir = path.join(ic.resolveInstanceRoot(), 'modules');
|
|
228
|
+
const dflt = loader.discoverModules(); // real DEFAULT_ROOTS
|
|
229
|
+
const single = loader.discoverModules({ roots: [coreModulesDir] }); // pre-2133: core root only
|
|
230
|
+
const two = loader.discoverModules({ roots: [coreModulesDir, instModulesDir] });
|
|
231
|
+
const hasProbe = (r) => r.loaded.some((m) => m.key === 'consumer-probe');
|
|
232
|
+
process.stdout.write(JSON.stringify({
|
|
233
|
+
coreModulesDir, instModulesDir,
|
|
234
|
+
loaded: dflt.loaded.map((m) => ({ key: m.key, dir: m.dir })),
|
|
235
|
+
errors: dflt.errors,
|
|
236
|
+
singleRootHasProbe: hasProbe(single),
|
|
237
|
+
twoRootHasProbe: hasProbe(two),
|
|
238
|
+
}));
|
|
239
|
+
`, consumer);
|
|
240
|
+
check('two-root: discoverModules() loads modules from BOTH the core AND the instance root, zero errors', () => {
|
|
241
|
+
assert.deepEqual(disc.errors, [], `loader reported errors: ${JSON.stringify(disc.errors)}`);
|
|
242
|
+
const fromCore = disc.loaded.filter((m) => m.dir.startsWith(disc.coreModulesDir));
|
|
243
|
+
const fromInstance = disc.loaded.filter((m) => m.dir.startsWith(disc.instModulesDir));
|
|
244
|
+
assert.ok(fromCore.length > 0,
|
|
245
|
+
`no module resolved from the CORE root (${disc.coreModulesDir}) — the package shipped no modules?`);
|
|
246
|
+
const probe = fromInstance.find((m) => m.key === 'consumer-probe');
|
|
247
|
+
assert.ok(probe,
|
|
248
|
+
`consumer-probe was NOT loaded from the INSTANCE root (${disc.instModulesDir}) — a single-root (pre-2133) loader would miss it. loaded=${JSON.stringify(disc.loaded.map((m) => m.key))}`);
|
|
249
|
+
});
|
|
250
|
+
check('counterfactual: a single-root (pre-2133) scan MISSES consumer-probe — proves this job reds on regression', () => {
|
|
251
|
+
assert.equal(disc.twoRootHasProbe, true, 'two-root scan must find consumer-probe (sanity)');
|
|
252
|
+
assert.equal(disc.singleRootHasProbe, false,
|
|
253
|
+
'a core-only scan found consumer-probe — the instance module must be invisible to a single root, or this job could not RED against the pre-B1 loader');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// --- 6. fitness spans both roots -----------------------------------------
|
|
257
|
+
check('fitness: discoveredModuleKeys() unions the core + instance module roots', () => {
|
|
258
|
+
const r = runNodeJSON(`
|
|
259
|
+
const f = require(${A('scripts/gds/fitness.js')});
|
|
260
|
+
process.stdout.write(JSON.stringify({ keys: f.discoveredModuleKeys() }));
|
|
261
|
+
`, consumer);
|
|
262
|
+
assert.ok(r.keys.includes('consumer-probe'),
|
|
263
|
+
`fitness.discoveredModuleKeys() missing the instance module 'consumer-probe': ${JSON.stringify(r.keys)}`);
|
|
264
|
+
// At least one KNOWN core module must also be present (core root scanned too).
|
|
265
|
+
const coreKeys = disc.loaded.filter((m) => m.dir.startsWith(disc.coreModulesDir)).map((m) => m.key);
|
|
266
|
+
assert.ok(coreKeys.some((k) => r.keys.includes(k)),
|
|
267
|
+
`fitness keys carry no core module (expected some of ${JSON.stringify(coreKeys)}): ${JSON.stringify(r.keys)}`);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// --- 7. gen-file-map reads the INSTANCE root ------------------------------
|
|
271
|
+
check('gen-file-map: buildModel() lists the INSTANCE root .claude/skills (consumer-probe-skill)', () => {
|
|
272
|
+
const model = runNodeJSON(`
|
|
273
|
+
const g = require(${A('scripts/gds/gen-file-map.js')});
|
|
274
|
+
process.stdout.write(JSON.stringify(g.buildModel()));
|
|
275
|
+
`, consumer);
|
|
276
|
+
const skills = model.find((s) => s.id === 'skills');
|
|
277
|
+
assert.ok(skills, 'gen-file-map produced no skills section');
|
|
278
|
+
const names = skills.entries.map((e) => e.name);
|
|
279
|
+
assert.deepEqual(names, ['consumer-probe-skill'],
|
|
280
|
+
`gen-file-map read the wrong .claude/skills (expected only the consumer's): ${JSON.stringify(names)}`);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// --- 8. gen-repo-map reads the CORE root ----------------------------------
|
|
284
|
+
check('gen-repo-map: buildModel() maps CORE code (scripts/gds/fitness.js) from node_modules', () => {
|
|
285
|
+
const model = runNodeJSON(`
|
|
286
|
+
const g = require(${A('scripts/gds/gen-repo-map.js')});
|
|
287
|
+
const m = g.buildModel();
|
|
288
|
+
process.stdout.write(JSON.stringify({ files: m.files.length, hasFitness: m.byFile.some((f) => f.rel === 'scripts/gds/fitness.js') }));
|
|
289
|
+
`, consumer);
|
|
290
|
+
assert.ok(model.files > 50, `core scan found too few files (${model.files}) — core root not resolved to the package?`);
|
|
291
|
+
assert.equal(model.hasFitness, true, 'gen-repo-map did not map the core file scripts/gds/fitness.js');
|
|
292
|
+
});
|
|
293
|
+
} finally {
|
|
294
|
+
if (child && child.exitCode === null) { try { child.kill('SIGTERM'); } catch { /* already gone */ } }
|
|
295
|
+
try { rmSync(workRoot, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
console.log(`\nconsumer_layout_boot: ${passed} passed, ${failed} failed`);
|
|
299
|
+
process.exit(failed ? 1 : 0);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
main().catch((e) => { console.error(e); process.exit(1); });
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// tests/criterion_suggest.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit tests for suggestCriteria() — the pure criterion RECOMMENDER
|
|
4
|
+
// (modules/lifecycle/criterion-suggest.js, task 2165). Given a goal's criteria
|
|
5
|
+
// and a candidate task's title/description, it ranks the criteria by weighted
|
|
6
|
+
// lexical overlap so task-create can suggest which done-when criterion to link
|
|
7
|
+
// (the author confirms — nothing is auto-linked). No DB, so it runs in the
|
|
8
|
+
// DB-free suite.
|
|
9
|
+
//
|
|
10
|
+
// Run: node tests/criterion_suggest.mjs
|
|
11
|
+
|
|
12
|
+
import { strict as assert } from 'node:assert';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const { suggestCriteria, tokenize } = require('../modules/lifecycle/criterion-suggest.js');
|
|
17
|
+
|
|
18
|
+
let passed = 0;
|
|
19
|
+
let failed = 0;
|
|
20
|
+
function test(name, fn) {
|
|
21
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
22
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const crit = (id, criterion_id, criterion_md, satisfied = false) =>
|
|
26
|
+
({ id, criterion_id, criterion_md, satisfied });
|
|
27
|
+
|
|
28
|
+
// A small, realistic goal-25-flavoured criterion set.
|
|
29
|
+
const CRITERIA = [
|
|
30
|
+
crit(138, 'bx-hall', 'Core task actions work in the builders hall without invoking the LLM.'),
|
|
31
|
+
crit(139, 'bx-identity', 'A builder can view and edit their own profile and read a leaderboard.'),
|
|
32
|
+
crit(140, 'bx-onramp', 'Newcomers never dead-end: auth and re-auth are browser-clickable.'),
|
|
33
|
+
crit(141, 'bx-box', 'A builder dev box and sandbox is robust: uncommitted work is not lost.'),
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
test('ranks the most lexically-relevant criterion first', () => {
|
|
37
|
+
const out = suggestCriteria(CRITERIA, {
|
|
38
|
+
title: 'In-hall confirm button for criteria',
|
|
39
|
+
description: 'Let builders confirm a criterion in the hall without the LLM.',
|
|
40
|
+
});
|
|
41
|
+
assert.ok(out.length >= 1, 'expected at least one suggestion');
|
|
42
|
+
assert.equal(out[0].criterion_id, 'bx-hall', `expected bx-hall first, got ${out[0].criterion_id}`);
|
|
43
|
+
assert.ok(out[0].matched_terms.includes('hall'), 'should report the matched term');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('title terms weigh more than description terms', () => {
|
|
47
|
+
// "profile" only in the title → should beat a body-only "hall" match.
|
|
48
|
+
const out = suggestCriteria(CRITERIA, {
|
|
49
|
+
title: 'Edit your profile',
|
|
50
|
+
description: 'Shown in the hall.',
|
|
51
|
+
});
|
|
52
|
+
assert.equal(out[0].criterion_id, 'bx-identity', `title term should win, got ${out[0].criterion_id}`);
|
|
53
|
+
const identity = out.find((c) => c.criterion_id === 'bx-identity');
|
|
54
|
+
const hall = out.find((c) => c.criterion_id === 'bx-hall');
|
|
55
|
+
assert.ok(hall === undefined || identity.score > hall.score, 'title match must outscore body match');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('returns [] when nothing overlaps (no low-confidence noise)', () => {
|
|
59
|
+
const out = suggestCriteria(CRITERIA, {
|
|
60
|
+
title: 'Migrate the Postgres backup vendor to object storage',
|
|
61
|
+
description: 'Nightly encrypted dumps to a bucket.',
|
|
62
|
+
});
|
|
63
|
+
assert.deepEqual(out, [], 'no shared vocabulary → no suggestion');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('never suggests an already-satisfied criterion', () => {
|
|
67
|
+
const satisfiedHall = [crit(138, 'bx-hall', 'Core task actions work in the builders hall.', true)];
|
|
68
|
+
const out = suggestCriteria(satisfiedHall, { title: 'hall task actions' });
|
|
69
|
+
assert.deepEqual(out, [], 'satisfied criteria are excluded');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('respects the limit and is stable (id tiebreak on equal scores)', () => {
|
|
73
|
+
const tied = [
|
|
74
|
+
crit(20, 'b', 'builder profile'),
|
|
75
|
+
crit(10, 'a', 'builder profile'),
|
|
76
|
+
crit(30, 'c', 'builder profile'),
|
|
77
|
+
];
|
|
78
|
+
const out = suggestCriteria(tied, { title: 'builder profile' }, { limit: 2 });
|
|
79
|
+
assert.equal(out.length, 2, 'limit caps the result');
|
|
80
|
+
assert.deepEqual(out.map((c) => c.id), [10, 20], 'equal scores break ties by ascending id');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('includeZero returns all unsatisfied criteria in score order', () => {
|
|
84
|
+
const out = suggestCriteria(CRITERIA, { title: 'profile' }, { includeZero: true, limit: 10 });
|
|
85
|
+
assert.equal(out.length, 4, 'includeZero keeps the zero-overlap criteria');
|
|
86
|
+
assert.equal(out[0].criterion_id, 'bx-identity', 'best match still leads');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('empty / malformed inputs are safe', () => {
|
|
90
|
+
assert.deepEqual(suggestCriteria([], { title: 'x' }), []);
|
|
91
|
+
assert.deepEqual(suggestCriteria(null, { title: 'x' }), []);
|
|
92
|
+
assert.deepEqual(suggestCriteria(CRITERIA, {}), [], 'no candidate terms → no suggestion');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('tokenize drops stopwords, short tokens, and singularizes', () => {
|
|
96
|
+
const toks = tokenize('The builders can edit profiles and tasks.');
|
|
97
|
+
assert.ok(!toks.includes('the'), 'stopword dropped');
|
|
98
|
+
assert.ok(!toks.includes('can'), 'stopword dropped');
|
|
99
|
+
assert.ok(toks.includes('builder'), 'builders → builder');
|
|
100
|
+
assert.ok(toks.includes('profile'), 'profiles → profile');
|
|
101
|
+
assert.ok(toks.includes('task'), 'tasks → task');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log(`criterion-suggest recommender tests: ${passed} passed, ${failed} failed`);
|
|
106
|
+
if (failed > 0) process.exit(1);
|
|
@@ -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));
|