@bongos/core 1.17.0 → 1.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +363 -312
- package/.claude/skills/blocker-review/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +2 -0
- package/.claude/skills/builder-exit/SKILL.md +2 -2
- package/.claude/skills/builder-redteam/SKILL.md +3 -3
- package/.claude/skills/builder-setup/SKILL.md +2 -2
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/dev/SKILL.md +1 -1
- package/.claude/skills/idea-triage/SKILL.md +2 -2
- package/.claude/skills/ideate/SKILL.md +1 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/.claude/skills/new-project/SKILL.md +3 -0
- package/.claude/skills/paint/SKILL.md +1 -1
- package/CLAUDE.md +11 -238
- package/GOVERNANCE.md +1 -1
- package/README.md +1 -1
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +4 -4
- package/clients/bongos-client/bongos-client.global.js +14 -1
- package/clients/bongos-client/index.cjs +14 -1
- package/clients/bongos-client/index.d.ts +19 -2
- package/clients/bongos-client/index.mjs +11 -1
- package/clients/bongos-client/package.json +1 -1
- package/docs/adr/0001-stack-choice-single-vps.md +1 -1
- package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
- package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
- package/docs/adr/0020-security-model-public-stub.md +1 -1
- package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
- package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
- package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
- package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
- package/docs/adr/0030-builders-hall-live-updates.md +1 -1
- package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
- package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
- package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
- package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
- package/docs/adr/0044-per-box-live-game-preview.md +1 -1
- package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
- package/docs/adr/0057-container-cost-ledger.md +3 -3
- package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
- package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
- package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
- package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
- package/docs/adr/0096-require-reward-before-workable.md +9 -9
- package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
- package/docs/adr/0100-two-repo-instance-model.md +1 -1
- package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
- package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
- package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
- package/docs/adr/0133-github-app-manifest-onboarding-signin.md +2 -0
- package/docs/adr/0134-private-first-npm-distribution.md +20 -6
- package/docs/adr/0135-module-upstream-submission-interim-queue.md +34 -0
- package/docs/adr/0136-update-channel-subscription-policy.md +51 -0
- package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
- package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
- package/docs/adr/README.md +14 -10
- package/docs/api/openapi.json +315 -6
- package/docs/api-reference.md +10 -5
- package/docs/architecture.md +3 -3
- package/docs/branding-contract.md +5 -4
- package/docs/canonical-permissions.md +1 -1
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +20 -19
- package/docs/handoff-template.md +2 -2
- package/docs/modules-contract.md +8 -1
- package/docs/onboarding/diagrams/README.md +7 -7
- package/docs/onboarding/drachmae-and-karma.md +18 -18
- package/docs/onboarding/primer.md +9 -9
- package/docs/recipes/audit-replay.md +2 -2
- package/docs/recipes/builder-box-lifecycle.md +2 -2
- package/docs/recipes/claude-design-loop.md +1 -1
- package/docs/recipes/instance-deploy.md +4 -4
- package/docs/recipes/instance-first-run.md +51 -0
- package/docs/recipes/ops-gotchas.md +1 -1
- package/docs/recipes/packaging-the-core.md +5 -5
- package/docs/recipes/private-npm-distribution.md +39 -12
- package/docs/recipes/self-host.md +1 -1
- package/docs/recipes/staging-environment.md +1 -1
- package/docs/recipes/upgrading-the-core.md +63 -8
- package/docs/research/cursor-competitive-analysis.md +1 -1
- package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
- package/migrations/core_191_module_submissions.sql +45 -0
- package/modules/autonomy/CLAUDE.md +11 -45
- package/modules/builder-settings/CLAUDE.md +11 -54
- package/modules/dev-box/CLAUDE.md +11 -34
- package/modules/discord/CLAUDE.md +11 -46
- package/modules/discord/module.json +2 -1
- package/modules/economy/CLAUDE.md +11 -44
- package/modules/economy/achievements.js +9 -1
- package/modules/economy/credits.js +10 -1
- package/modules/grading/CLAUDE.md +11 -38
- package/modules/hall-ui/CLAUDE.md +11 -57
- package/modules/hall-ui/public/builders.js +71 -4
- package/modules/hall-ui/public/goal-inbox.js +191 -0
- package/modules/hall-ui/public/goals-page.js +227 -22
- package/modules/hall-ui/public/goals.html +16 -0
- package/modules/hall-ui/public/hall-widgets.js +14 -1
- package/modules/hall-ui/public/index.html +3 -0
- package/modules/hall-ui/public/new-project-wizard.js +6 -4
- package/modules/hall-ui/public/style.css +43 -0
- package/modules/ideas/CLAUDE.md +11 -33
- package/modules/lifecycle/CLAUDE.md +11 -59
- package/modules/lifecycle/routes/goals.js +80 -0
- package/modules/memory/CLAUDE.md +11 -36
- package/modules/onboarding/CLAUDE.md +11 -51
- package/modules/provisioning/CLAUDE.md +15 -0
- package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
- package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
- package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
- package/modules/provisioning/module.json +17 -0
- package/modules/provisioning/onboard-plan.js +37 -0
- package/modules/provisioning/provisioning.js +539 -0
- package/modules/provisioning/rate-limit.js +35 -0
- package/modules/provisioning/routes/provisioning.js +607 -0
- package/modules/provisioning/tests/provisioning.mjs +232 -0
- package/modules/provisioning/ui/.gitkeep +0 -0
- package/modules/security/CLAUDE.md +11 -40
- package/modules/security/secret-scrub.js +10 -0
- package/modules/sessions/CLAUDE.md +11 -35
- package/modules/status-ui/CLAUDE.md +11 -43
- package/package-lock.json +2 -2
- package/package.json +7 -2
- package/public-landing/create.html +58 -12
- package/public-landing/index.html +40 -21
- package/scripts/discord/README.md +1 -1
- package/scripts/gds/CLAUDE.md +11 -140
- package/scripts/gds/claim.js +98 -48
- package/scripts/gds/claude-materialize.js +174 -20
- package/scripts/gds/claude-md-template.js +84 -0
- package/scripts/gds/cli-lib.js +112 -2
- package/scripts/gds/cutover-codemod.js +241 -0
- package/scripts/gds/fitness.js +54 -0
- package/scripts/gds/gen-api-client.js +10 -10
- package/scripts/gds/init.js +112 -16
- package/scripts/gds/lint-infra-exec.js +70 -0
- package/scripts/gds/lint-workflow-exec.js +63 -0
- package/scripts/gds/migration-namespace.js +1 -1
- package/scripts/gds/mirror-lineage-check.js +14 -5
- package/scripts/gds/mirror-redact.js +8 -0
- package/scripts/gds/module.js +118 -4
- package/scripts/gds/npmrc.js +93 -0
- package/scripts/gds/onboard.js +17 -24
- package/scripts/gds/package-core.js +74 -17
- package/scripts/gds/provision.js +102 -17
- package/scripts/gds/publish-manifest.js +60 -2
- package/scripts/gds/run-routine.js +51 -13
- package/scripts/gds/run-unit-tests.js +4 -0
- package/scripts/gds/seed-adopt-tasks.js +1 -1
- package/scripts/gds/status.js +6 -1
- package/scripts/gds/task.js +166 -0
- package/scripts/gds/update-channel.js +186 -0
- package/scripts/gds/upgrade.js +172 -25
- package/src/bongos/CLAUDE.md +11 -60
- package/src/bongos/auth.js +24 -0
- package/src/bongos/module-submissions.js +56 -0
- package/src/bongos/route-rank-check.js +6 -0
- package/src/bongos/routes/CLAUDE.md +11 -39
- package/src/bongos/routes/auth.js +6 -1
- package/src/bongos/routes/modules.js +123 -21
- package/src/bongos/serve-internal.js +10 -1
- package/src/branding.js +1 -0
- package/src/instance-config.js +1 -1
- package/src/module-api.js +24 -1
- package/src/module-loader/loader.js +36 -4
- package/src/module-loader/manifest-schema.js +16 -1
- package/tests/branding.mjs +11 -0
- package/tests/claim_error_surface.mjs +107 -0
- package/tests/claude_materialize.mjs +125 -0
- package/tests/consumer_layout_boot.mjs +302 -0
- package/tests/cutover_codemod.mjs +184 -0
- package/tests/device_flow_start.mjs +96 -0
- package/tests/first_run_engine_check.mjs +79 -0
- package/tests/goal_membership_requests.mjs +33 -3
- package/tests/hook_core_resolve.mjs +125 -0
- package/tests/hook_user_agents.mjs +62 -0
- package/tests/infra_exec_paths.mjs +104 -0
- package/tests/init.mjs +106 -6
- package/tests/instance_mismatch_guard.mjs +101 -0
- package/tests/mirror_lineage_proof.mjs +14 -0
- package/tests/module_cli.mjs +130 -2
- package/tests/module_loader.mjs +42 -0
- package/tests/module_manifest.mjs +19 -0
- package/tests/module_submissions.mjs +135 -0
- package/tests/npmrc.mjs +105 -0
- package/tests/onboard.mjs +62 -0
- package/tests/package_core.mjs +76 -22
- package/tests/platform_boot.mjs +5 -2
- package/tests/provision.mjs +107 -8
- package/tests/provisioning_github_app_routes.mjs +44 -2
- package/tests/publish_manifest.mjs +52 -1
- package/tests/run_routine.mjs +26 -1
- package/tests/run_routine_two_root.mjs +77 -0
- package/tests/scrubber_corpus.mjs +17 -0
- package/tests/served_paths_shipped.mjs +2 -2
- package/tests/task_cli.mjs +99 -0
- package/tests/update_channel.mjs +186 -0
- package/tests/upgrade.mjs +220 -25
- package/tests/workflow_exec_paths.mjs +98 -0
- package/docs/design/container-exterior-tiles.md +0 -49
- package/docs/design/container-exterior.md +0 -81
- package/docs/design/container-interior.md +0 -77
- package/docs/design/world-map-scroll.md +0 -41
- package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
- package/docs/recipes/cloudbongos-staging-gate.md +0 -139
- package/docs/recipes/cloudbongos-standup.md +0 -111
- package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
- package/docs/recipes/parallel-load-test.md +0 -32
- package/docs/recipes/pixel-art-pipeline.md +0 -411
- package/docs/recipes/staging-data.md +0 -50
- package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
- package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
- package/migrations/instance/110_medusa_planning_seed.sql +0 -70
- package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
- package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
- package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
- package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
- package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
- package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
- package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
- package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
- package/scripts/gds/art-stage.js +0 -152
- package/server.js +0 -212
- package/tests/art_key.mjs +0 -121
- package/tests/day_cycle.mjs +0 -197
- package/tests/snapshot_picker.mjs +0 -233
- package/tests/sprite_facing.mjs +0 -35
- package/tests/terrain_picker.mjs +0 -298
- package/tests/turn_in_place.mjs +0 -280
- package/tests/walkability.mjs +0 -188
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// tests/instance_mismatch_guard.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit test for the PURE fail-closed instance guard exported from
|
|
4
|
+
// scripts/gds/cli-lib.js (task 2102):
|
|
5
|
+
// - instanceOriginHost(origin) → host string, or null if unparseable
|
|
6
|
+
// - detectInstanceMismatch({ session, brand, env }) → { mismatch, expected, actual }
|
|
7
|
+
//
|
|
8
|
+
// The guard stops the CLI from silently borrowing a DIFFERENT instance's session
|
|
9
|
+
// (the 2026-07-08 demo-session failure, where a fresh demo clone used the
|
|
10
|
+
// founder's amazonprimea.com session and offered to write into the wrong live
|
|
11
|
+
// backlog). Both helpers are deterministic with no DB/network/fs, so they
|
|
12
|
+
// unit-test standalone. Run: node tests/instance_mismatch_guard.mjs
|
|
13
|
+
|
|
14
|
+
import { strict as assert } from 'node:assert';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const cli = require('../scripts/gds/cli-lib.js');
|
|
19
|
+
|
|
20
|
+
let passed = 0;
|
|
21
|
+
let failed = 0;
|
|
22
|
+
function test(name, fn) {
|
|
23
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
24
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const DEMO = { domains: { publicOrigin: 'https://demo.cloudbongos.com' } };
|
|
28
|
+
const OTB = { domains: { publicOrigin: 'https://amazonprimea.com' } };
|
|
29
|
+
const noEnv = {}; // no <PREFIX>_API_BASE override in scope
|
|
30
|
+
|
|
31
|
+
test('instanceOriginHost: extracts host, tolerates trailing slash', () => {
|
|
32
|
+
assert.equal(cli.instanceOriginHost('https://amazonprimea.com'), 'amazonprimea.com');
|
|
33
|
+
assert.equal(cli.instanceOriginHost('https://amazonprimea.com/'), 'amazonprimea.com');
|
|
34
|
+
assert.equal(cli.instanceOriginHost('https://demo.cloudbongos.com/api/gds'), 'demo.cloudbongos.com');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('instanceOriginHost: unparseable input → null (edge)', () => {
|
|
38
|
+
assert.equal(cli.instanceOriginHost('not a url'), null);
|
|
39
|
+
assert.equal(cli.instanceOriginHost(''), null);
|
|
40
|
+
assert.equal(cli.instanceOriginHost(null), null);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('match: same-host session + branding → no mismatch (the OTB/normal case)', () => {
|
|
44
|
+
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com' }, brand: OTB, env: noEnv });
|
|
45
|
+
assert.equal(r.mismatch, false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('match: scheme/trailing-slash differences still match on host', () => {
|
|
49
|
+
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com/' }, brand: OTB, env: noEnv });
|
|
50
|
+
assert.equal(r.mismatch, false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('mismatch: demo repo + founder amazonprimea session → REFUSE (the reported failure)', () => {
|
|
54
|
+
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com' }, brand: DEMO, env: noEnv });
|
|
55
|
+
assert.equal(r.mismatch, true);
|
|
56
|
+
assert.equal(r.expected, 'https://demo.cloudbongos.com');
|
|
57
|
+
assert.equal(r.actual, 'https://amazonprimea.com');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('mismatch: demo repo + cloudbongos.com session (sibling instance) → REFUSE', () => {
|
|
61
|
+
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://cloudbongos.com' }, brand: DEMO, env: noEnv });
|
|
62
|
+
assert.equal(r.mismatch, true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('env-override: an explicit *_API_BASE override wins (dev/staging) → no mismatch', () => {
|
|
66
|
+
const r = cli.detectInstanceMismatch({
|
|
67
|
+
session: { api_base: 'https://amazonprimea.com' }, brand: DEMO,
|
|
68
|
+
env: { GDS_API_BASE: 'http://localhost:3000' },
|
|
69
|
+
});
|
|
70
|
+
assert.equal(r.mismatch, false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('no-branding: repo declares no publicOrigin → cannot assert a mismatch → no mismatch', () => {
|
|
74
|
+
assert.equal(cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com' }, brand: {}, env: noEnv }).mismatch, false);
|
|
75
|
+
assert.equal(cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com' }, brand: { domains: {} }, env: noEnv }).mismatch, false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('no-session: nothing to borrow → no mismatch (the no-session/401 path handles it)', () => {
|
|
79
|
+
assert.equal(cli.detectInstanceMismatch({ session: null, brand: DEMO, env: noEnv }).mismatch, false);
|
|
80
|
+
assert.equal(cli.detectInstanceMismatch({ session: {}, brand: DEMO, env: noEnv }).mismatch, false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// task 2124 / demo-audit C2 — assertInstanceMatch is the exit-wrapper that RAW-fetch read
|
|
84
|
+
// commands (status.js) call so they honor the same guard apiCall/cliClient already enforce.
|
|
85
|
+
// Its decision reuses detectInstanceMismatch (covered above); here we lock the wrapper's
|
|
86
|
+
// contract + its deterministic opt-out branch (the exiting branch would process.exit).
|
|
87
|
+
test('assertInstanceMatch is exported (the guard raw-fetch reads must call)', () => {
|
|
88
|
+
assert.equal(typeof cli.assertInstanceMatch, 'function');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Async opt-out branch — the sync test() helper can't await, so run it at top level.
|
|
92
|
+
// { allowInstanceMismatch: true } returns BEFORE loadSession/detect (no fs, no exit).
|
|
93
|
+
try {
|
|
94
|
+
await cli.assertInstanceMatch({ allowInstanceMismatch: true });
|
|
95
|
+
passed++; console.log(' ok assertInstanceMatch({allowInstanceMismatch:true}) resolves without exiting');
|
|
96
|
+
} catch (err) {
|
|
97
|
+
failed++; console.log(' FAIL assertInstanceMatch opt-out resolves without exiting'); console.log(` ${err.message}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(`\ninstance-mismatch guard (task 2102 + C2 raw-read wrapper) tests: ${passed} passed, ${failed} failed`);
|
|
101
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
@@ -22,6 +22,20 @@ test('migrationNumbersFromPaths picks CORE migrations only (not module/ADR numbe
|
|
|
22
22
|
assert.deepEqual([...set].sort((a, b) => a - b), [3, 44, 60]);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
+
test('migrationNumbersFromPaths excludes migrations/instance/ seeds (task 2152)', () => {
|
|
26
|
+
// migrations/instance/*.sql are this instance's host seeds (ADR 0105), excluded from
|
|
27
|
+
// the publishable mirror (task 2140). Counting them as core put every instance seed in
|
|
28
|
+
// the PRIVATE set but not the MIRROR set → a false private⇄mirror divergence that
|
|
29
|
+
// red-lined the unit gate. Only the bare top-level core migration counts.
|
|
30
|
+
const set = migrationNumbersFromPaths([
|
|
31
|
+
'migrations/080_core_thing.sql', // bare core → counted
|
|
32
|
+
'migrations/instance/080_gds_v4_planning_seed.sql', // instance seed → NOT core
|
|
33
|
+
'migrations/instance/186_governance_seed.sql', // instance seed → NOT core
|
|
34
|
+
'migrations/core_192_namespaced.sql', // core_ prefix ≠ bare NNN_ form → not counted either
|
|
35
|
+
]);
|
|
36
|
+
assert.deepEqual([...set].sort((a, b) => a - b), [80]);
|
|
37
|
+
});
|
|
38
|
+
|
|
25
39
|
test('parseEnumChecks extracts the quoted value-lists from CHECK (... IN (...))', () => {
|
|
26
40
|
const sql = `
|
|
27
41
|
CREATE TABLE tasks (
|
package/tests/module_cli.mjs
CHANGED
|
@@ -268,6 +268,126 @@ test('cmdCheck: usage error (exit 2) when no key is given', () => {
|
|
|
268
268
|
assert.equal(code, 2);
|
|
269
269
|
});
|
|
270
270
|
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// packageModuleForSubmission / readSignOff — the fs half of `module submit`
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
test('readSignOff: null when never signed off; the recorded record once it is', () => {
|
|
276
|
+
const root = tmpModulesRoot();
|
|
277
|
+
mod.scaffoldModule({ key: 'widget', modulesDir: root, core: '1.0.0' });
|
|
278
|
+
assert.equal(mod.readSignOff('widget', { modulesDir: root }), null);
|
|
279
|
+
mod.recordSignOff('widget', 'Ada Lovelace <ada@example.com>', { modulesDir: root });
|
|
280
|
+
const signOff = mod.readSignOff('widget', { modulesDir: root });
|
|
281
|
+
assert.equal(signOff.signOff.signedBy, 'Ada Lovelace <ada@example.com>');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('packageModuleForSubmission: snapshots the manifest + every file (base64) + the sign-off', () => {
|
|
285
|
+
const root = tmpModulesRoot();
|
|
286
|
+
mod.scaffoldModule({ key: 'widget', modulesDir: root, core: '1.0.0' });
|
|
287
|
+
mod.recordSignOff('widget', 'Ada Lovelace <ada@example.com>', { modulesDir: root });
|
|
288
|
+
|
|
289
|
+
const pkg = mod.packageModuleForSubmission('widget', { modulesDir: root });
|
|
290
|
+
assert.equal(pkg.key, 'widget');
|
|
291
|
+
assert.equal(pkg.manifest.key, 'widget');
|
|
292
|
+
assert.equal(pkg.signOff.signOff.signedBy, 'Ada Lovelace <ada@example.com>');
|
|
293
|
+
assert.ok(pkg.totalBytes > 0);
|
|
294
|
+
|
|
295
|
+
const paths = pkg.files.map((f) => f.path).sort();
|
|
296
|
+
assert.deepEqual(paths, [...paths].sort(), 'files are sorted by path');
|
|
297
|
+
assert.ok(paths.includes('module.json'));
|
|
298
|
+
assert.ok(paths.includes('routes/widget.js'));
|
|
299
|
+
// the sign-off file itself travels in the snapshot too — it's part of the module tree
|
|
300
|
+
assert.ok(paths.includes('.upstream-signoff.json'));
|
|
301
|
+
|
|
302
|
+
const manifestFile = pkg.files.find((f) => f.path === 'module.json');
|
|
303
|
+
const roundTripped = JSON.parse(Buffer.from(manifestFile.contentBase64, 'base64').toString('utf8'));
|
|
304
|
+
assert.deepEqual(roundTripped, pkg.manifest);
|
|
305
|
+
assert.equal(manifestFile.bytes, Buffer.byteLength(JSON.stringify(pkg.manifest, null, 2) + '\n'));
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// bongos module submit <key> — ADR 0107 §1 (network call is always injected —
|
|
310
|
+
// never hits cli-lib.js / the real API in a unit test)
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
test('cmdSubmit: refuses (exit 1) when the pre-check fails, without ever attempting the network call', async () => {
|
|
314
|
+
const lines = [];
|
|
315
|
+
const sink = { log: (s) => lines.push(String(s)), errlog: (s) => lines.push(String(s)) };
|
|
316
|
+
let submitCalled = false;
|
|
317
|
+
const check = () => ({ key: 'game', ok: false, findings: [{ kind: 'content', detail: 'contains denied content' }], files: [], denied: [] });
|
|
318
|
+
const submit = async () => { submitCalled = true; return { ok: true, status: 201, data: { submission: { id: 1, status: 'pending' } } }; };
|
|
319
|
+
|
|
320
|
+
const code = await mod.cmdSubmit(['game'], { ...sink, check, submit });
|
|
321
|
+
assert.equal(code, 1);
|
|
322
|
+
assert.equal(submitCalled, false, 'a failing pre-check must never reach the network');
|
|
323
|
+
assert.ok(lines.some((l) => /REFUSING/.test(l)));
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('cmdSubmit: refuses (exit 1) when the pre-check passes but no sign-off is on file and none was given', async () => {
|
|
327
|
+
const lines = [];
|
|
328
|
+
const sink = { log: (s) => lines.push(String(s)), errlog: (s) => lines.push(String(s)) };
|
|
329
|
+
let submitCalled = false;
|
|
330
|
+
const check = () => ({ key: 'widget', ok: true, findings: [], files: [], denied: [] });
|
|
331
|
+
const readSign = () => null;
|
|
332
|
+
const submit = async () => { submitCalled = true; return { ok: true, status: 201, data: {} }; };
|
|
333
|
+
|
|
334
|
+
const code = await mod.cmdSubmit(['widget'], { ...sink, check, readSign, submit });
|
|
335
|
+
assert.equal(code, 1);
|
|
336
|
+
assert.equal(submitCalled, false);
|
|
337
|
+
assert.ok(lines.some((l) => /REFUSING/.test(l) && /sign-off/.test(l)));
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('cmdSubmit: --sign-off records a NEW sign-off when none is on file, then submits', async () => {
|
|
341
|
+
const lines = [];
|
|
342
|
+
const sink = { log: (s) => lines.push(String(s)), errlog: (s) => lines.push(String(s)) };
|
|
343
|
+
const check = () => ({ key: 'widget', ok: true, findings: [], files: [], denied: [] });
|
|
344
|
+
let signedCall = null;
|
|
345
|
+
const recorded = { signOff: { signedBy: 'Ada Lovelace <ada@example.com>', date: '2026-07-08T00:00:00.000Z' }, path: 'modules/widget/.upstream-signoff.json' };
|
|
346
|
+
const sign = (key, by) => { signedCall = { key, by }; return recorded; };
|
|
347
|
+
let readSignCalls = 0;
|
|
348
|
+
const readSign = () => { readSignCalls += 1; return readSignCalls === 1 ? null : recorded; };
|
|
349
|
+
let submitArgs = null;
|
|
350
|
+
const submit = async (key, body) => { submitArgs = { key, body }; return { ok: true, status: 201, data: { submission: { id: 7, status: 'pending' } } }; };
|
|
351
|
+
|
|
352
|
+
const code = await mod.cmdSubmit(['widget', '--sign-off', 'Ada Lovelace <ada@example.com>'], { ...sink, check, sign, readSign, submit });
|
|
353
|
+
assert.equal(code, 0);
|
|
354
|
+
assert.deepEqual(signedCall, { key: 'widget', by: 'Ada Lovelace <ada@example.com>' });
|
|
355
|
+
assert.deepEqual(submitArgs, { key: 'widget', body: { signed_by: 'Ada Lovelace <ada@example.com>' } });
|
|
356
|
+
assert.ok(lines.some((l) => /Filed — submission id 7/.test(l)));
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test('cmdSubmit: an existing sign-off on file is reused — --sign-off is not required again', async () => {
|
|
360
|
+
const lines = [];
|
|
361
|
+
const sink = { log: (s) => lines.push(String(s)), errlog: (s) => lines.push(String(s)) };
|
|
362
|
+
const check = () => ({ key: 'widget', ok: true, findings: [], files: [], denied: [] });
|
|
363
|
+
let signCalled = false;
|
|
364
|
+
const sign = () => { signCalled = true; };
|
|
365
|
+
const readSign = () => ({ signOff: { signedBy: 'Ada Lovelace <ada@example.com>', date: 'now' } });
|
|
366
|
+
const submit = async () => ({ ok: true, status: 201, data: { submission: { id: 9, status: 'pending' } } });
|
|
367
|
+
|
|
368
|
+
const code = await mod.cmdSubmit(['widget'], { ...sink, check, sign, readSign, submit });
|
|
369
|
+
assert.equal(code, 0);
|
|
370
|
+
assert.equal(signCalled, false, 'an existing sign-off is never re-recorded');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test('cmdSubmit: surfaces a rejected submission (e.g. the server-side pre-check disagrees) as exit 1', async () => {
|
|
374
|
+
const lines = [];
|
|
375
|
+
const sink = { log: (s) => lines.push(String(s)), errlog: (s) => lines.push(String(s)) };
|
|
376
|
+
const check = () => ({ key: 'widget', ok: true, findings: [], files: [], denied: [] });
|
|
377
|
+
const readSign = () => ({ signOff: { signedBy: 'Ada Lovelace <ada@example.com>', date: 'now' } });
|
|
378
|
+
const submit = async () => ({ ok: false, status: 422, data: { error: 'precheck_failed', message: 'server disagrees' } });
|
|
379
|
+
|
|
380
|
+
const code = await mod.cmdSubmit(['widget'], { ...sink, check, readSign, submit });
|
|
381
|
+
assert.equal(code, 1);
|
|
382
|
+
assert.ok(lines.some((l) => /REFUSING/.test(l) && /server disagrees/.test(l)));
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('cmdSubmit: usage error (exit 2) when no key is given', async () => {
|
|
386
|
+
const lines = [];
|
|
387
|
+
const code = await mod.cmdSubmit([], { log: (s) => lines.push(s), errlog: (s) => lines.push(s) });
|
|
388
|
+
assert.equal(code, 2);
|
|
389
|
+
});
|
|
390
|
+
|
|
271
391
|
// ---------------------------------------------------------------------------
|
|
272
392
|
// dispatch wiring (bin/bongos.js)
|
|
273
393
|
// ---------------------------------------------------------------------------
|
|
@@ -283,10 +403,18 @@ test('the `module` verb dispatches to module.js, `upgrade` to upgrade.js', () =>
|
|
|
283
403
|
assert.ok(h.includes('upgrade'));
|
|
284
404
|
});
|
|
285
405
|
|
|
286
|
-
test('`bongos module check` is documented in help / dispatches to module.js check subcommand', () => {
|
|
406
|
+
test('`bongos module check` is documented in help / dispatches to module.js check subcommand', async () => {
|
|
287
407
|
const lines = [];
|
|
288
408
|
mod.printHelp((s) => lines.push(s));
|
|
289
409
|
assert.ok(lines.some((l) => /module check/.test(l)));
|
|
290
410
|
// module.js itself dispatches the "check" subcommand (bongos.js forwards args[1:] verbatim to module.js)
|
|
291
|
-
|
|
411
|
+
// main() is async (bongos module submit makes a network call) — every subcommand's exit code resolves via the same Promise.
|
|
412
|
+
assert.equal(await mod.main(['node', 'module.js', 'check']), 2, 'no key => usage error');
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test('`bongos module submit` is documented in help / dispatches to module.js submit subcommand', async () => {
|
|
416
|
+
const lines = [];
|
|
417
|
+
mod.printHelp((s) => lines.push(s));
|
|
418
|
+
assert.ok(lines.some((l) => /module submit/.test(l)));
|
|
419
|
+
assert.equal(await mod.main(['node', 'module.js', 'submit']), 2, 'no key => usage error');
|
|
292
420
|
});
|
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
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// tests/module_submissions.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit smoke for the ADR 0107 §1 upstreaming review queue (task 1770,
|
|
4
|
+
// migration core_191_module_submissions). Proves, with NO live Postgres:
|
|
5
|
+
//
|
|
6
|
+
// DB helpers (against a stubbed pool seam):
|
|
7
|
+
// - createModuleSubmission() INSERTs, jsonb-casting + JSON.stringify'ing
|
|
8
|
+
// manifest/files/sign_off, parameterized
|
|
9
|
+
// - listModuleSubmissions() SELECTs metadata only (no `files` — the file
|
|
10
|
+
// snapshot can run to hundreds of KB and no list consumer needs it),
|
|
11
|
+
// newest-first, optionally filtered by status and/or module_key
|
|
12
|
+
//
|
|
13
|
+
// We stub the pool by pre-seeding the require cache for src/bongos/pool.js
|
|
14
|
+
// BEFORE requiring src/bongos/module-submissions.js — the same monkeypatch
|
|
15
|
+
// seam tests/security_docs.mjs uses.
|
|
16
|
+
//
|
|
17
|
+
// Run: node tests/module_submissions.mjs
|
|
18
|
+
|
|
19
|
+
import { strict as assert } from 'node:assert';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
27
|
+
|
|
28
|
+
let passed = 0;
|
|
29
|
+
let failed = 0;
|
|
30
|
+
function test(name, fn) {
|
|
31
|
+
return Promise.resolve()
|
|
32
|
+
.then(fn)
|
|
33
|
+
.then(() => { passed++; console.log(` ok ${name}`); })
|
|
34
|
+
.catch((e) => { failed++; console.error(` FAIL ${name}\n ${e.message}`); });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const calls = [];
|
|
38
|
+
let nextRows = [];
|
|
39
|
+
const fakePool = {
|
|
40
|
+
query: async (text, params) => {
|
|
41
|
+
calls.push({ text, params });
|
|
42
|
+
return { rows: nextRows };
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
const poolPath = require.resolve(path.join(ROOT, 'src', 'bongos', 'pool.js'));
|
|
46
|
+
require.cache[poolPath] = { id: poolPath, filename: poolPath, loaded: true, exports: { pool: fakePool, instanceDbName: () => undefined } };
|
|
47
|
+
|
|
48
|
+
const moduleSubmissions = require(path.join(ROOT, 'src', 'bongos', 'module-submissions.js'));
|
|
49
|
+
|
|
50
|
+
function lastCall() { return calls[calls.length - 1]; }
|
|
51
|
+
|
|
52
|
+
console.log('module_submissions — DB helpers (stubbed pool):');
|
|
53
|
+
|
|
54
|
+
await test('createModuleSubmission() INSERTs, jsonb-casts manifest/files/sign_off, parameterized', async () => {
|
|
55
|
+
calls.length = 0;
|
|
56
|
+
const manifest = { key: 'widget', title: 'Widget', license: 'AGPL-3.0' };
|
|
57
|
+
const files = [{ path: 'module.json', contentBase64: 'e30=', bytes: 2 }];
|
|
58
|
+
const signOff = { module: 'widget', signOff: { signedBy: 'Ada Lovelace <ada@example.com>', date: '2026-07-08T00:00:00.000Z' } };
|
|
59
|
+
const precheckPassedAt = new Date('2026-07-08T00:00:00.000Z');
|
|
60
|
+
nextRows = [{
|
|
61
|
+
id: 1, module_key: 'widget', submitted_by: 42, manifest, sign_off: signOff,
|
|
62
|
+
precheck_passed_at: precheckPassedAt, source_repo: 'lars589/amazonprimea',
|
|
63
|
+
status: 'pending', reviewed_by: null, reviewed_at: null, review_notes: null,
|
|
64
|
+
submitted_at: '2026-07-08T00:00:01.000Z',
|
|
65
|
+
}];
|
|
66
|
+
|
|
67
|
+
const row = await moduleSubmissions.createModuleSubmission({
|
|
68
|
+
moduleKey: 'widget', submittedBy: 42, manifest, files, signOff, precheckPassedAt,
|
|
69
|
+
sourceRepo: 'lars589/amazonprimea',
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const { text, params } = lastCall();
|
|
73
|
+
assert.ok(/INSERT INTO module_submissions/i.test(text));
|
|
74
|
+
assert.ok(/RETURNING/i.test(text), 'must return the inserted row');
|
|
75
|
+
// files IS written (params[3] below) but is NOT in the RETURNING list — the
|
|
76
|
+
// write path never echoes the (possibly large) snapshot back.
|
|
77
|
+
const returningClause = text.slice(text.search(/RETURNING/i));
|
|
78
|
+
assert.ok(!/\bfiles\b/i.test(returningClause), 'RETURNING must not include files');
|
|
79
|
+
|
|
80
|
+
assert.equal(params[0], 'widget');
|
|
81
|
+
assert.equal(params[1], 42);
|
|
82
|
+
assert.equal(params[2], JSON.stringify(manifest));
|
|
83
|
+
assert.equal(params[3], JSON.stringify(files));
|
|
84
|
+
assert.equal(params[4], JSON.stringify(signOff));
|
|
85
|
+
assert.equal(params[5], precheckPassedAt);
|
|
86
|
+
assert.equal(params[6], 'lars589/amazonprimea');
|
|
87
|
+
|
|
88
|
+
assert.equal(row.id, 1);
|
|
89
|
+
assert.equal(row.status, 'pending');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await test('createModuleSubmission() defaults a missing sourceRepo to null (parameterized, not the string "undefined")', async () => {
|
|
93
|
+
calls.length = 0;
|
|
94
|
+
nextRows = [{ id: 2, module_key: 'widget', status: 'pending' }];
|
|
95
|
+
await moduleSubmissions.createModuleSubmission({
|
|
96
|
+
moduleKey: 'widget', submittedBy: 1, manifest: {}, files: [], signOff: {}, precheckPassedAt: new Date(),
|
|
97
|
+
});
|
|
98
|
+
const { params } = lastCall();
|
|
99
|
+
assert.equal(params[6], null);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await test('listModuleSubmissions(): no filters => SELECT with no WHERE, newest-first, no files column', async () => {
|
|
103
|
+
calls.length = 0;
|
|
104
|
+
nextRows = [{ id: 2, module_key: 'b', status: 'pending' }, { id: 1, module_key: 'a', status: 'pending' }];
|
|
105
|
+
const rows = await moduleSubmissions.listModuleSubmissions();
|
|
106
|
+
const { text, params } = lastCall();
|
|
107
|
+
assert.ok(/SELECT/i.test(text));
|
|
108
|
+
assert.ok(!/WHERE/i.test(text));
|
|
109
|
+
assert.ok(/ORDER BY\s+submitted_at\s+DESC/i.test(text));
|
|
110
|
+
const selectClause = text.slice(0, text.search(/FROM/i));
|
|
111
|
+
assert.ok(!/\bfiles\b/i.test(selectClause), 'list SELECT must not include the files column');
|
|
112
|
+
assert.deepEqual(params, []);
|
|
113
|
+
assert.deepEqual(rows, nextRows);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await test('listModuleSubmissions({status}): filters by status, parameterized', async () => {
|
|
117
|
+
calls.length = 0;
|
|
118
|
+
nextRows = [];
|
|
119
|
+
await moduleSubmissions.listModuleSubmissions({ status: 'pending' });
|
|
120
|
+
const { text, params } = lastCall();
|
|
121
|
+
assert.ok(/WHERE\s+status\s*=\s*\$1/i.test(text));
|
|
122
|
+
assert.deepEqual(params, ['pending']);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await test('listModuleSubmissions({status, moduleKey}): both filters AND together, parameterized in order', async () => {
|
|
126
|
+
calls.length = 0;
|
|
127
|
+
nextRows = [];
|
|
128
|
+
await moduleSubmissions.listModuleSubmissions({ status: 'accepted', moduleKey: 'widget' });
|
|
129
|
+
const { text, params } = lastCall();
|
|
130
|
+
assert.ok(/WHERE\s+status\s*=\s*\$1\s+AND\s+module_key\s*=\s*\$2/i.test(text));
|
|
131
|
+
assert.deepEqual(params, ['accepted', 'widget']);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
135
|
+
if (failed > 0) process.exit(1);
|
package/tests/npmrc.mjs
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// tests/npmrc.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit tests for scripts/gds/npmrc.js (ADR 0134, task 2090) — the single source of the
|
|
4
|
+
// env-fed `.npmrc` used to install the PRIVATE @bongos/core from the npm registry, plus
|
|
5
|
+
// the opt-in / token gates that keep the vendored-tarball path the default/fallback.
|
|
6
|
+
//
|
|
7
|
+
// Run: node tests/npmrc.mjs
|
|
8
|
+
|
|
9
|
+
import { strict as assert } from 'node:assert';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const n = require('../scripts/gds/npmrc.js');
|
|
17
|
+
|
|
18
|
+
let passed = 0, failed = 0;
|
|
19
|
+
function test(name, fn) {
|
|
20
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
21
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---- npmrcContent ----------------------------------------------------------
|
|
25
|
+
test('npmrcContent: scopes @bongos to the registry + auth token EXPANDS from the env (literal ${NPM_TOKEN})', () => {
|
|
26
|
+
const body = n.npmrcContent();
|
|
27
|
+
assert.match(body, /@bongos:registry=https:\/\/registry\.npmjs\.org\//);
|
|
28
|
+
assert.match(body, /\/\/registry\.npmjs\.org\/:_authToken=\$\{NPM_TOKEN\}/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('npmrcContent: carries NO real secret — only the ${NPM_TOKEN} placeholder', () => {
|
|
32
|
+
const body = n.npmrcContent();
|
|
33
|
+
// the only _authToken= assignment must be the env placeholder, never a concrete value
|
|
34
|
+
const tokenLines = body.split('\n').filter((l) => /_authToken=/.test(l));
|
|
35
|
+
assert.equal(tokenLines.length, 1);
|
|
36
|
+
assert.equal(tokenLines[0].trim(), '//registry.npmjs.org/:_authToken=${NPM_TOKEN}');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('npmrcContent: comment lines use the ini `;` comment char', () => {
|
|
40
|
+
const comments = n.npmrcContent().split('\n').filter((l) => l.startsWith(';'));
|
|
41
|
+
assert.ok(comments.length >= 3, 'has explanatory comments');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('npmrcContent: honors custom scope/host/tokenEnv', () => {
|
|
45
|
+
const body = n.npmrcContent({ scope: '@acme', host: 'npm.acme.dev', tokenEnv: 'ACME_TOKEN' });
|
|
46
|
+
assert.match(body, /@acme:registry=https:\/\/npm\.acme\.dev\//);
|
|
47
|
+
assert.match(body, /\/\/npm\.acme\.dev\/:_authToken=\$\{ACME_TOKEN\}/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ---- gates -----------------------------------------------------------------
|
|
51
|
+
test('hasNpmToken: true only when NPM_TOKEN is a non-empty string', () => {
|
|
52
|
+
assert.equal(n.hasNpmToken({ NPM_TOKEN: 'npm_abc' }), true);
|
|
53
|
+
assert.equal(n.hasNpmToken({ NPM_TOKEN: '' }), false);
|
|
54
|
+
assert.equal(n.hasNpmToken({ NPM_TOKEN: ' ' }), false);
|
|
55
|
+
assert.equal(n.hasNpmToken({}), false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('registryCoreRequested: 1/true/yes/on → true; everything else → false', () => {
|
|
59
|
+
for (const v of ['1', 'true', 'TRUE', 'yes', 'on']) assert.equal(n.registryCoreRequested({ PROVISION_CORE_FROM_REGISTRY: v }), true, v);
|
|
60
|
+
for (const v of ['0', 'false', 'no', '', 'off', 'garbage']) assert.equal(n.registryCoreRequested({ PROVISION_CORE_FROM_REGISTRY: v }), false, v);
|
|
61
|
+
assert.equal(n.registryCoreRequested({}), false, 'unset');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ---- writeNpmrc ------------------------------------------------------------
|
|
65
|
+
test('writeNpmrc: writes the env-fed .npmrc + returns its path', () => {
|
|
66
|
+
const dir = mkdtempSync(join(tmpdir(), 'npmrc-'));
|
|
67
|
+
try {
|
|
68
|
+
const p = n.writeNpmrc(dir, { log: () => {} });
|
|
69
|
+
assert.equal(p, join(dir, '.npmrc'));
|
|
70
|
+
assert.equal(readFileSync(p, 'utf8'), n.npmrcContent());
|
|
71
|
+
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('writeNpmrc: dry-run writes nothing but returns the intended path', () => {
|
|
75
|
+
const dir = mkdtempSync(join(tmpdir(), 'npmrc-'));
|
|
76
|
+
try {
|
|
77
|
+
const p = n.writeNpmrc(dir, { dryRun: true, log: () => {} });
|
|
78
|
+
assert.equal(p, join(dir, '.npmrc'));
|
|
79
|
+
assert.equal(existsSync(join(dir, '.npmrc')), false, 'dry-run must not write');
|
|
80
|
+
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('writeNpmrc: overwrite:false leaves a pre-existing .npmrc untouched (returns null)', () => {
|
|
84
|
+
const dir = mkdtempSync(join(tmpdir(), 'npmrc-'));
|
|
85
|
+
try {
|
|
86
|
+
const existing = '@other:registry=https://npm.other.dev/\n';
|
|
87
|
+
writeFileSync(join(dir, '.npmrc'), existing);
|
|
88
|
+
const p = n.writeNpmrc(dir, { overwrite: false, log: () => {} });
|
|
89
|
+
assert.equal(p, null, 'skipped');
|
|
90
|
+
assert.equal(readFileSync(join(dir, '.npmrc'), 'utf8'), existing, 'existing content preserved');
|
|
91
|
+
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('writeNpmrc: overwrite:true (default) replaces an existing .npmrc', () => {
|
|
95
|
+
const dir = mkdtempSync(join(tmpdir(), 'npmrc-'));
|
|
96
|
+
try {
|
|
97
|
+
writeFileSync(join(dir, '.npmrc'), 'stale\n');
|
|
98
|
+
n.writeNpmrc(dir, { log: () => {} });
|
|
99
|
+
assert.equal(readFileSync(join(dir, '.npmrc'), 'utf8'), n.npmrcContent());
|
|
100
|
+
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
console.log('');
|
|
104
|
+
console.log(`npmrc tests: ${passed} passed, ${failed} failed`);
|
|
105
|
+
process.exit(failed ? 1 : 0);
|