@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
package/tests/onboard.mjs
CHANGED
|
@@ -37,12 +37,30 @@ test('onboardPlan(adopt): same sequence + human steps, but the scaffold-leg labe
|
|
|
37
37
|
assert.doesNotMatch(label(green, 'init'), /--adopt/);
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
+
test('onboardPlan lists seed-first-version as an explicit AUTO step right after provision (task 2119 anti-drift)', () => {
|
|
41
|
+
// The first-version seed used to be an unlisted afterthought bolted to the local
|
|
42
|
+
// `bongos init` leg (run --no-seed in every hosted flow, so it never fired). Making
|
|
43
|
+
// it a named step in the ONE canonical sequence is what stops it silently drifting.
|
|
44
|
+
const plan = ob.onboardPlan();
|
|
45
|
+
const keys = plan.map((s) => s.key);
|
|
46
|
+
const seed = plan.find((s) => s.key === 'seed-version');
|
|
47
|
+
assert.ok(seed, 'the plan carries a seed-version step');
|
|
48
|
+
assert.equal(seed.kind, 'auto', 'seeding is automated (the runner does it, no owner action)');
|
|
49
|
+
assert.equal(keys.indexOf('seed-version'), keys.indexOf('provision') + 1, 'seed immediately follows provision (the DB now exists)');
|
|
50
|
+
assert.ok(keys.indexOf('seed-version') < keys.indexOf('first-sign-in'), 'a version exists before the owner ever signs in');
|
|
51
|
+
// present in BOTH modes (not a greenfield/adopt branch).
|
|
52
|
+
assert.ok(ob.onboardPlan('adopt').some((s) => s.key === 'seed-version'), 'adopt gets it too');
|
|
53
|
+
});
|
|
54
|
+
|
|
40
55
|
test('oauthAppInstructions pins the apex callback + homepage and points at ACCOUNT Developer settings', () => {
|
|
41
56
|
const oa = ob.oauthAppInstructions('mercury.example.com');
|
|
42
57
|
assert.equal(oa.homepageUrl, 'https://mercury.example.com');
|
|
43
58
|
assert.equal(oa.callbackUrl, 'https://mercury.example.com/api/gds/auth/web/callback');
|
|
44
59
|
assert.match(oa.location, /ACCOUNT/);
|
|
45
60
|
assert.match(oa.location, /Developer settings/);
|
|
61
|
+
// task 2127 — must tell the owner to enable Device Flow (off by default; the CLI login
|
|
62
|
+
// uses it). Its absence was the 2026-07-08 demo login-500 root cause.
|
|
63
|
+
assert.match(oa.enableDeviceFlow, /Device Flow/i);
|
|
46
64
|
});
|
|
47
65
|
|
|
48
66
|
test('buildInitSpec maps answers to a STANDALONE init spec (domain → origin + provisioning shape)', () => {
|
|
@@ -68,3 +86,47 @@ test('buildInitSpec without a domain still defaults a standalone provisioning sh
|
|
|
68
86
|
test('healthzUrl', () => {
|
|
69
87
|
assert.equal(ob.healthzUrl('mercury.example.com'), 'https://mercury.example.com/healthz');
|
|
70
88
|
});
|
|
89
|
+
|
|
90
|
+
// ── task 2085: ONE source for the onboarding sequence (anti-drift) ────────────
|
|
91
|
+
|
|
92
|
+
test('onboardPlan is the SAME function the provisioning module owns (single source, not a copy)', () => {
|
|
93
|
+
// The CLI must CONSUME modules/provisioning/onboard-plan.js, not carry its own copy —
|
|
94
|
+
// that is the whole point of task 2085. A re-declared duplicate would pass every other
|
|
95
|
+
// test yet silently drift; identity of the function reference proves there is one source.
|
|
96
|
+
const canonical = require('../modules/provisioning/onboard-plan.js').onboardPlan;
|
|
97
|
+
assert.equal(ob.onboardPlan, canonical, 'scripts/gds/onboard.js must re-export the module’s onboardPlan');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('the web forms (/create + hall wizard) declare EXACTLY the canonical human steps — drift guard', () => {
|
|
101
|
+
// goal 35 / criterion 165: "the canonical step sequence is the source of truth;
|
|
102
|
+
// skill/CLI/hall are forms over it." Each web form tags its human-step <li>s with
|
|
103
|
+
// data-onboard-step="<key>"; this asserts that set equals the plan's human-step keys,
|
|
104
|
+
// so adding/removing/renaming a human step in the single source fails CI until every
|
|
105
|
+
// form is updated (and vice-versa) — the sequence can no longer drift between forms.
|
|
106
|
+
const { readFileSync } = require('node:fs');
|
|
107
|
+
const path = require('node:path');
|
|
108
|
+
const humanKeys = ob.onboardPlan().filter((s) => s.kind === 'human').map((s) => s.key).sort();
|
|
109
|
+
const surfaces = {
|
|
110
|
+
'/create landing page': 'public-landing/create.html',
|
|
111
|
+
'hall new-project wizard': 'modules/hall-ui/public/new-project-wizard.js',
|
|
112
|
+
};
|
|
113
|
+
for (const [name, rel] of Object.entries(surfaces)) {
|
|
114
|
+
const src = readFileSync(path.join(import.meta.dirname, '..', rel), 'utf8');
|
|
115
|
+
const found = [...src.matchAll(/data-onboard-step="([^"]+)"/g)].map((m) => m[1]);
|
|
116
|
+
const uniq = [...new Set(found)].sort();
|
|
117
|
+
assert.deepEqual(uniq, humanKeys, `${name} (${rel}) must tag exactly the canonical human steps [${humanKeys}], found [${uniq}]`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('the one-click GitHub-App manifest path surfaces the CLI-only Enable-Device-Flow step (task 2129)', () => {
|
|
122
|
+
// GitHub defaults Device Flow OFF and the manifest can't enable it, but `bongos login` needs
|
|
123
|
+
// it — so BOTH manifest surfaces must tell the owner to enable it after the one-click approval.
|
|
124
|
+
// Assert on a phrase DISTINCT to the one-click caveat: the manual OAuth path already mentions
|
|
125
|
+
// Device Flow (task 2127), so a bare "Device Flow" match wouldn't prove the manifest path carries it.
|
|
126
|
+
const { readFileSync } = require('node:fs');
|
|
127
|
+
const path = require('node:path');
|
|
128
|
+
const onboardSrc = readFileSync(path.join(import.meta.dirname, '..', 'scripts/gds/onboard.js'), 'utf8');
|
|
129
|
+
const wizardSrc = readFileSync(path.join(import.meta.dirname, '..', 'modules/hall-ui/public/new-project-wizard.js'), 'utf8');
|
|
130
|
+
assert.match(onboardSrc, /cannot enable Device Flow via the manifest/i, 'bongos onboard one-click caveat missing');
|
|
131
|
+
assert.match(wizardSrc, /Terminal CLI only: after approving, enable/i, 'hall wizard one-click caveat missing');
|
|
132
|
+
});
|
package/tests/package_core.mjs
CHANGED
|
@@ -25,6 +25,8 @@ const {
|
|
|
25
25
|
ARTIFACT_NAME, MANIFEST_SCHEMA, TAR_ROOT,
|
|
26
26
|
// task 1824 — the consumer-core file-classification layer
|
|
27
27
|
isDocPath, functionalSecretLeaks, processCoreFile,
|
|
28
|
+
// task 2121 — agent-doc (CLAUDE.md/AGENTS.md) stub
|
|
29
|
+
isAgentDoc, CORE_AGENT_DOC_STUB,
|
|
28
30
|
// task 1882 (ADR 0108 §2) — the real installable npm package
|
|
29
31
|
CORE_DEP_NAMES, buildPackageJson, resolveLockDep, lockfileClosure, prunePackageLock,
|
|
30
32
|
// task 2089 (ADR 0134) — private-by-default npm publish
|
|
@@ -141,27 +143,27 @@ t('withTarballInfo adds the exact-bytes coordinate for the side-car pin', () =>
|
|
|
141
143
|
version: '1.13.0', coreContract: '1.13.0', sourceCommit: 'abc', sourceRef: 'HEAD',
|
|
142
144
|
builtAtIso: '2026-07-01T00:00:00.000Z', files: [], treeHash: 'x', redaction: null,
|
|
143
145
|
});
|
|
144
|
-
const side = withTarballInfo(inner, { name: '
|
|
146
|
+
const side = withTarballInfo(inner, { name: 'bongos-core-1.13.0.tgz', sha256: 'cafe', bytes: 42 });
|
|
145
147
|
assert.equal(side.tarball.sha256, 'cafe');
|
|
146
148
|
assert.equal(side.tarball.bytes, 42);
|
|
147
149
|
assert.equal(inner.tarball, undefined); // withTarballInfo does not mutate the inner
|
|
148
150
|
});
|
|
149
151
|
|
|
150
152
|
t('artifact filenames', () => {
|
|
151
|
-
assert.equal(tarballName('1.13.0'), '
|
|
152
|
-
assert.equal(manifestName('1.13.0'), '
|
|
153
|
-
assert.equal(stageDirName('1.13.0'), '
|
|
153
|
+
assert.equal(tarballName('1.13.0'), 'bongos-core-1.13.0.tgz');
|
|
154
|
+
assert.equal(manifestName('1.13.0'), 'bongos-core-1.13.0.manifest.json');
|
|
155
|
+
assert.equal(stageDirName('1.13.0'), 'bongos-core-1.13.0');
|
|
154
156
|
});
|
|
155
157
|
|
|
156
158
|
// ── ustar name/prefix split ──────────────────────────────────────────────────────────
|
|
157
159
|
t('splitUstarName: short path → name only', () => {
|
|
158
|
-
const r = splitUstarName('
|
|
160
|
+
const r = splitUstarName('bongos-core/a.js');
|
|
159
161
|
assert.equal(r.prefix, '');
|
|
160
|
-
assert.equal(r.name, '
|
|
162
|
+
assert.equal(r.name, 'bongos-core/a.js');
|
|
161
163
|
});
|
|
162
164
|
|
|
163
165
|
t('splitUstarName: long path splits at a / into name<=100 + prefix<=155', () => {
|
|
164
|
-
const long = '
|
|
166
|
+
const long = 'bongos-core/' + 'dir/'.repeat(24) + 'file.js'; // > 100 bytes → forces a name/prefix split
|
|
165
167
|
const r = splitUstarName(long);
|
|
166
168
|
assert.ok(r, 'expected a split');
|
|
167
169
|
assert.ok(Buffer.byteLength(r.name) <= 100);
|
|
@@ -177,18 +179,18 @@ t('splitUstarName: unsplittable (>100-byte tail) → null (longlink fallback)',
|
|
|
177
179
|
// ── buildTar — produces a real, readable, deterministic archive ──────────────────────
|
|
178
180
|
t('buildTar round-trips a small set (content + mode preserved)', () => {
|
|
179
181
|
const entries = [
|
|
180
|
-
{ name: '
|
|
181
|
-
{ name: '
|
|
182
|
+
{ name: 'bongos-core/a.txt', mode: 0o644, buf: Buffer.from('hello\n') },
|
|
183
|
+
{ name: 'bongos-core/bin/run.sh', mode: 0o755, buf: Buffer.from('#!/bin/sh\necho hi\n') },
|
|
182
184
|
];
|
|
183
185
|
const files = readTar(buildTar(entries));
|
|
184
|
-
assert.equal(files.get('
|
|
185
|
-
assert.equal(files.get('
|
|
186
|
-
assert.equal(files.get('
|
|
187
|
-
assert.equal(files.get('
|
|
186
|
+
assert.equal(files.get('bongos-core/a.txt').content.toString(), 'hello\n');
|
|
187
|
+
assert.equal(files.get('bongos-core/bin/run.sh').content.toString(), '#!/bin/sh\necho hi\n');
|
|
188
|
+
assert.equal(files.get('bongos-core/bin/run.sh').mode & 0o777, 0o755);
|
|
189
|
+
assert.equal(files.get('bongos-core/a.txt').mode & 0o777, 0o644);
|
|
188
190
|
});
|
|
189
191
|
|
|
190
192
|
t('buildTar round-trips a >100-byte path via the GNU longlink path', () => {
|
|
191
|
-
const longName = '
|
|
193
|
+
const longName = 'bongos-core/' + 'deep/'.repeat(25) + 'leaf.js'; // ~150 bytes, but splittable
|
|
192
194
|
const unsplittable = 'z'.repeat(130); // forces the longlink branch (no valid split)
|
|
193
195
|
const entries = [
|
|
194
196
|
{ name: longName, mode: 0o644, buf: Buffer.from('deep\n') },
|
|
@@ -201,15 +203,15 @@ t('buildTar round-trips a >100-byte path via the GNU longlink path', () => {
|
|
|
201
203
|
|
|
202
204
|
t('buildTar is byte-deterministic (same input → identical bytes; entry order irrelevant)', () => {
|
|
203
205
|
const e1 = [
|
|
204
|
-
{ name: '
|
|
205
|
-
{ name: '
|
|
206
|
+
{ name: 'bongos-core/b.txt', mode: 0o644, buf: Buffer.from('B') },
|
|
207
|
+
{ name: 'bongos-core/a.txt', mode: 0o644, buf: Buffer.from('A') },
|
|
206
208
|
];
|
|
207
209
|
const e2 = [...e1].reverse();
|
|
208
210
|
assert.equal(sha256hex(buildTar(e1)), sha256hex(buildTar(e2)));
|
|
209
211
|
});
|
|
210
212
|
|
|
211
213
|
t('buildTar output gunzips and ends with two zero blocks', () => {
|
|
212
|
-
const gz = buildTar([{ name: '
|
|
214
|
+
const gz = buildTar([{ name: 'bongos-core/a', mode: 0o644, buf: Buffer.from('x') }]);
|
|
213
215
|
const tar = zlib.gunzipSync(gz);
|
|
214
216
|
assert.equal(tar.length % 512, 0);
|
|
215
217
|
const tail = tar.subarray(tar.length - 1024);
|
|
@@ -298,6 +300,50 @@ t('DOCS are genericized: instance brand/owner redacted, output is clean', () =>
|
|
|
298
300
|
assert.equal(r.leaks.length, 0); // clean after redaction → no abort
|
|
299
301
|
});
|
|
300
302
|
|
|
303
|
+
// ── task 2121 — CLAUDE.md/AGENTS.md ship as a brand-free stub (demo-audit fix) ──────
|
|
304
|
+
t('isAgentDoc: matches CLAUDE.md/AGENTS.md at root + any depth, nothing else', () => {
|
|
305
|
+
assert.equal(isAgentDoc('CLAUDE.md'), true);
|
|
306
|
+
assert.equal(isAgentDoc('scripts/gds/CLAUDE.md'), true);
|
|
307
|
+
assert.equal(isAgentDoc('src/bongos/routes/CLAUDE.md'), true);
|
|
308
|
+
assert.equal(isAgentDoc('modules/lifecycle/CLAUDE.md'), true);
|
|
309
|
+
assert.equal(isAgentDoc('AGENTS.md'), true);
|
|
310
|
+
assert.equal(isAgentDoc('docs/CLAUDE.md'), true);
|
|
311
|
+
// NOT an agent doc: other markdown, or a filename that merely contains the word
|
|
312
|
+
assert.equal(isAgentDoc('README.md'), false);
|
|
313
|
+
assert.equal(isAgentDoc('docs/adr/0100.md'), false);
|
|
314
|
+
assert.equal(isAgentDoc('docs/CLAUDE-notes.md'), false);
|
|
315
|
+
assert.equal(isAgentDoc('src/claude.js'), false);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
t('processCoreFile STUBS a CLAUDE.md — the core memory never ships (no auto-load pollution)', () => {
|
|
319
|
+
// A realistic core CLAUDE.md: the wrong project title + instance brand + a long body.
|
|
320
|
+
const coreClaude = '# Off the Boats — Project Memory\n' +
|
|
321
|
+
'Visit https://acme.example (world Xanadu), owner zoltan. ' + 'lorem ipsum '.repeat(200);
|
|
322
|
+
const r = proc('CLAUDE.md', coreClaude);
|
|
323
|
+
assert.equal(r.kind, 'stub'); // classified as a stub, not a doc
|
|
324
|
+
assert.equal(r.leaks.length, 0); // brand-free by construction → never a leak
|
|
325
|
+
const out = r.buf.toString();
|
|
326
|
+
assert.equal(out, CORE_AGENT_DOC_STUB); // exactly the stub, verbatim
|
|
327
|
+
assert.ok(!/Off the Boats/i.test(out), 'wrong project title gone');
|
|
328
|
+
assert.ok(!/acme/i.test(out) && !/zoltan/i.test(out) && !/Xanadu/i.test(out), 'no instance brand');
|
|
329
|
+
assert.ok(!/lorem ipsum/.test(out), 'original 200-line body gone');
|
|
330
|
+
assert.ok(out.length < coreClaude.length, 'stub is far shorter than the real memory');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
t('processCoreFile STUBS nested + module CLAUDE.md too (all auto-load sites)', () => {
|
|
334
|
+
for (const p of ['scripts/gds/CLAUDE.md', 'src/bongos/CLAUDE.md', 'modules/economy/CLAUDE.md']) {
|
|
335
|
+
const r = proc(p, '# nested map\nsecret https://acme.example detail');
|
|
336
|
+
assert.equal(r.kind, 'stub', `${p} stubbed`);
|
|
337
|
+
assert.equal(r.buf.toString(), CORE_AGENT_DOC_STUB, `${p} → stub body`);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
t('the stub itself carries no instance brand and states it does not govern', () => {
|
|
342
|
+
assert.ok(!/acme|zoltan|Xanadu|Off the Boats/i.test(CORE_AGENT_DOC_STUB));
|
|
343
|
+
assert.ok(/does not govern/i.test(CORE_AGENT_DOC_STUB));
|
|
344
|
+
assert.ok(/@bongos\/core/.test(CORE_AGENT_DOC_STUB));
|
|
345
|
+
});
|
|
346
|
+
|
|
301
347
|
// ── task 1882 (ADR 0108 §2) — the real installable npm package ─────────────────────
|
|
302
348
|
t('TAR_ROOT is the npm-required "package/" (not the ARTIFACT_NAME dir)', () => {
|
|
303
349
|
assert.equal(TAR_ROOT, 'package/');
|
|
@@ -309,7 +355,7 @@ t('buildPackageJson: prunes to express+pg, keeps the source ranges + the undici
|
|
|
309
355
|
sourceDeps: { express: '^4.21.2', pg: '^8.20.0', phaser: '^3.87.0', 'discord.js': '^14.26.4' },
|
|
310
356
|
sourceOverrides: { undici: '^6.27.0' },
|
|
311
357
|
});
|
|
312
|
-
assert.equal(pkg.name, '@
|
|
358
|
+
assert.equal(pkg.name, '@bongos/core');
|
|
313
359
|
assert.equal(pkg.version, '1.14.0');
|
|
314
360
|
assert.deepEqual(pkg.dependencies, { express: '^4.21.2', pg: '^8.20.0' });
|
|
315
361
|
assert.equal(pkg.dependencies.phaser, undefined);
|
|
@@ -320,12 +366,20 @@ t('buildPackageJson: prunes to express+pg, keeps the source ranges + the undici
|
|
|
320
366
|
assert.equal(pkg.exports['.'], './src/platform-server.js');
|
|
321
367
|
assert.equal(pkg.exports['./serve-internal'], './src/bongos/serve-internal.js');
|
|
322
368
|
assert.equal(pkg.exports['./module-api'], './src/module-api.js');
|
|
369
|
+
// rehearsal F3 / task 2161 — the game-host kit: the core internals a game-bearing
|
|
370
|
+
// instance's own entrypoint (server.js) wires. The cutover codemod rewrites server.js's
|
|
371
|
+
// relative ./src/* requires to exactly these subpaths, so they MUST be exported.
|
|
372
|
+
assert.equal(pkg.exports['./bongos/api-path-404'], './src/bongos/api-path-404.js');
|
|
373
|
+
assert.equal(pkg.exports['./bongos/uptime-poller'], './src/bongos/uptime-poller.js');
|
|
374
|
+
assert.equal(pkg.exports['./module-loader/loader'], './src/module-loader/loader.js');
|
|
375
|
+
assert.equal(pkg.exports['./modules'], './src/modules.js');
|
|
376
|
+
assert.equal(pkg.exports['./module-seams'], './src/module-seams.js');
|
|
323
377
|
// task 2089 / ADR 0134 — the synthesized package publishes PRIVATE by default.
|
|
324
378
|
assert.deepEqual(pkg.publishConfig, { access: 'restricted' });
|
|
325
379
|
});
|
|
326
380
|
|
|
327
381
|
t('publishArgs: private (restricted) publish, dry-run unless live (task 2089 / ADR 0134)', () => {
|
|
328
|
-
const tgz = 'dist/
|
|
382
|
+
const tgz = 'dist/bongos-core-1.14.0.tgz';
|
|
329
383
|
// Default: dry-run, restricted (never public, never a real upload).
|
|
330
384
|
assert.deepEqual(publishArgs(tgz, false), ['publish', tgz, '--access', 'restricted', '--dry-run']);
|
|
331
385
|
// Live: still restricted (private) — the permanent publish is never public here.
|
|
@@ -408,8 +462,8 @@ t('lockfileClosure: matches the real repo lockfile — express+pg close over the
|
|
|
408
462
|
});
|
|
409
463
|
|
|
410
464
|
t('prunePackageLock: emits a self-consistent pruned lockfile (root ranges + only the kept packages)', () => {
|
|
411
|
-
const pruned = prunePackageLock(FIXTURE_LOCK, { name: '@
|
|
412
|
-
assert.equal(pruned.name, '@
|
|
465
|
+
const pruned = prunePackageLock(FIXTURE_LOCK, { name: '@bongos/core', version: '1.14.0', rootRanges: { a: '^1.0.0' }, rootDepNames: ['a'] });
|
|
466
|
+
assert.equal(pruned.name, '@bongos/core');
|
|
413
467
|
assert.equal(pruned.version, '1.14.0');
|
|
414
468
|
assert.equal(pruned.lockfileVersion, 3);
|
|
415
469
|
assert.equal(pruned.requires, true);
|
|
@@ -423,7 +477,7 @@ t('prunePackageLock: emits a self-consistent pruned lockfile (root ranges + only
|
|
|
423
477
|
t('prunePackageLock: on the real repo lockfile, drops the vast majority of packages', () => {
|
|
424
478
|
const realLock = require('../package-lock.json');
|
|
425
479
|
const pruned = prunePackageLock(realLock, {
|
|
426
|
-
name: '@
|
|
480
|
+
name: '@bongos/core', version: '1.14.0', rootRanges: { express: '^4.21.2', pg: '^8.20.0' },
|
|
427
481
|
});
|
|
428
482
|
const keptCount = Object.keys(pruned.packages).length - 1; // exclude the root '' entry
|
|
429
483
|
const sourceCount = Object.keys(realLock.packages).length - 1;
|
package/tests/platform_boot.mjs
CHANGED
|
@@ -107,8 +107,11 @@ test('the apex host serves the landing page — not the game — on a game-less
|
|
|
107
107
|
// it IS the landing, with the branding pack injected (look + identity from config)
|
|
108
108
|
assert.match(res.body, /window\.__BRANDING__=/);
|
|
109
109
|
assert.match(res.body, /Enter the Hall/);
|
|
110
|
-
// the
|
|
111
|
-
|
|
110
|
+
// the per-project landing carries the top-right identity chip + hall entry
|
|
111
|
+
// (goal 35, task 2094). "Create a project" is an ORG-level action that lives
|
|
112
|
+
// only on the gate now — it must NOT appear on a project's own landing.
|
|
113
|
+
assert.match(res.body, /id="who"/);
|
|
114
|
+
assert.doesNotMatch(res.body, /Create a project/);
|
|
112
115
|
});
|
|
113
116
|
|
|
114
117
|
test('the apex host serves the branded /create page (goal 35, task 2081)', async () => {
|
package/tests/provision.mjs
CHANGED
|
@@ -46,6 +46,33 @@ t('dbCreateCmd / migrateCmd use the slug db + GDS-only migrate', () => {
|
|
|
46
46
|
assert.match(P.migrateCmd(coTenant), /PGDATABASE=mercury MEDUSA_GDS_ONLY=1 \.\/scripts\/migrate\.sh/);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
t('seedFirstVersionCmd: idempotent (only-if-empty) V1 seed against the slug db, track explicit, one criterion — the fix for the empty-versions bug (task 2119)', () => {
|
|
50
|
+
const cmd = P.seedFirstVersionCmd(coTenant);
|
|
51
|
+
// targets the instance's OWN db, matching createdb/migrate
|
|
52
|
+
assert.match(cmd, /psql .*-d mercury /, 'seeds the instance db');
|
|
53
|
+
assert.match(cmd, /ON_ERROR_STOP=1/, 'aborts on any error (atomic, no half-seed)');
|
|
54
|
+
// the version the greenfield init spec defines — ONE source of truth
|
|
55
|
+
const v = P.instanceInitSpec(coTenant).version;
|
|
56
|
+
assert.match(cmd, /INSERT INTO versions \(id, name, track, status, done_when\)/);
|
|
57
|
+
assert.match(cmd, new RegExp(`\\$v\\$${reEsc(v.id)}\\$v\\$`), 'seeds the spec version id (V1)');
|
|
58
|
+
assert.match(cmd, /\$v\$product\$v\$/, 'track is set EXPLICITLY (versions.track is NOT NULL, no default since mig 112)');
|
|
59
|
+
assert.match(cmd, /\$v\$building\$v\$/, 'status building, mirroring seedViaApi');
|
|
60
|
+
assert.ok(cmd.includes(v.done_when), 'carries the spec done_when verbatim (incl. the → arrows)');
|
|
61
|
+
// the IDEMPOTENT guard — only seeds when there is NO version yet, so it never disturbs an existing project
|
|
62
|
+
assert.match(cmd, /WHERE NOT EXISTS \(SELECT 1 FROM versions\)/, 'only-if-empty guard');
|
|
63
|
+
// one done-when criterion so /status has a finish line (matches seedViaApi's 'first-goal')
|
|
64
|
+
assert.match(cmd, /INSERT INTO done_when_criteria/);
|
|
65
|
+
assert.match(cmd, /\$v\$first-goal\$v\$/);
|
|
66
|
+
// dollar-quoted literals ⇒ the whole -c arg is single-quoted (safe inline / SSH / cloud-init)
|
|
67
|
+
assert.match(cmd, /-c '.*'/, 'the SQL is a single-quoted -c argument');
|
|
68
|
+
assert.doesNotMatch(cmd, /-c ".*"/, 'never double-quoted (would break cloud-init bash -lc "…")');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
t('seedFirstVersionCmd: privileged runs psql as the postgres role; default is bare (peer auth) — mirrors createdb/migrate', () => {
|
|
72
|
+
assert.match(P.seedFirstVersionCmd(coTenant, { privileged: false }), /^psql /);
|
|
73
|
+
assert.match(P.seedFirstVersionCmd(coTenant, { privileged: true }), /^sudo -u postgres psql /);
|
|
74
|
+
});
|
|
75
|
+
|
|
49
76
|
t('serviceUnit binds the instance port + DB + per-instance env file, runs platform-server', () => {
|
|
50
77
|
const u = P.serviceUnit(coTenant, { etcBase: '/etc' });
|
|
51
78
|
assert.match(u, /Environment=PORT=3003/);
|
|
@@ -60,7 +87,7 @@ t('serviceUnit(standalone) runs the CORE PACKAGE server from the instance\'s OWN
|
|
|
60
87
|
const u = P.serviceUnit(inst, { etcBase: '/etc' });
|
|
61
88
|
const root = P.standaloneRoot(inst);
|
|
62
89
|
assert.match(u, new RegExp('WorkingDirectory=' + reEsc(root)), 'runs from the instance\'s own repo dir');
|
|
63
|
-
assert.match(u, /ExecStart=\/usr\/bin\/node node_modules\/@
|
|
90
|
+
assert.match(u, /ExecStart=\/usr\/bin\/node node_modules\/@bongos\/core\/src\/platform-server\.js/, 'runs the pinned core package server');
|
|
64
91
|
assert.match(u, /Environment=PORT=3003/);
|
|
65
92
|
assert.match(u, /EnvironmentFile=\/etc\/mercury\/web\.env/);
|
|
66
93
|
});
|
|
@@ -70,7 +97,7 @@ t('standalone step generators: root under standaloneBase + pull / npm-ci / insta
|
|
|
70
97
|
assert.match(P.standaloneRoot(inst), /\/mercury$/);
|
|
71
98
|
assert.equal(P.standalonePullCmd(), 'git pull --ff-only origin main');
|
|
72
99
|
assert.equal(P.standaloneInstallCmd(), 'npm ci --omit=dev');
|
|
73
|
-
assert.match(P.standaloneMigrateCmd(inst), /PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules\/@
|
|
100
|
+
assert.match(P.standaloneMigrateCmd(inst), /PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules\/@bongos\/core\/scripts\/migrate\.sh/);
|
|
74
101
|
});
|
|
75
102
|
|
|
76
103
|
t('webEnvBody: OAuth creds use the LOADED pack\'s envPrefix (not the slug) + brand/port/db', () => {
|
|
@@ -98,6 +125,18 @@ t('webEnvBody: a domain writes the single-host origin overrides so the hall load
|
|
|
98
125
|
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_OAUTH_ORIGIN=${O}`));
|
|
99
126
|
});
|
|
100
127
|
|
|
128
|
+
t('webEnvBody: the repo target_ref seats the founding owner via <prefix>_FIRST_ADMIN (goal 35 / criterion 165)', () => {
|
|
129
|
+
// Regression (caught live on demo.cloudbongos.com, 2026-07-07): a provisioned instance
|
|
130
|
+
// MUST convey its founding owner, or src/bongos/auth.js maybeSeatFirstAdmin can never
|
|
131
|
+
// seat an Archon on first sign-in and the owner can't claim their own instance. Derived
|
|
132
|
+
// from the repo owner in target_ref — the SAME source instanceInitSpec uses for the
|
|
133
|
+
// scaffold's branding.firstAdmin, so the env override and config/branding.json agree.
|
|
134
|
+
const b = P.webEnvBody({ ...coTenant, hosting_shape: 'standalone', domain: 'mercury.example.com', target_ref: 'octocat/mercury' });
|
|
135
|
+
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_FIRST_ADMIN=octocat`));
|
|
136
|
+
// No target_ref ⇒ the line is omitted (org-owned or unknown owner sets it by hand).
|
|
137
|
+
assert.doesNotMatch(P.webEnvBody(coTenant), /_FIRST_ADMIN=/);
|
|
138
|
+
});
|
|
139
|
+
|
|
101
140
|
t('caddyBlock: a domain → on-demand-TLS reverse_proxy to the local port; no domain → a comment', () => {
|
|
102
141
|
const withDomain = P.caddyBlock({ ...coTenant, domain: 'mercury.example.com' });
|
|
103
142
|
assert.match(withDomain, /mercury\.example\.com \{/);
|
|
@@ -142,11 +181,22 @@ t('healthzCmd hits the instance port /healthz', () => {
|
|
|
142
181
|
assert.match(P.healthzCmd(coTenant), /127\.0\.0\.1:3003\/healthz/);
|
|
143
182
|
});
|
|
144
183
|
|
|
145
|
-
t('dedicatedUserData bakes createdb + migrate into cloud-init', () => {
|
|
184
|
+
t('dedicatedUserData bakes createdb + migrate + first-version seed into cloud-init (task 2119: dedicated must not diverge)', () => {
|
|
146
185
|
const ud = P.dedicatedUserData(coTenant);
|
|
147
186
|
assert.match(ud, /^#cloud-config/);
|
|
148
187
|
assert.match(ud, /createdb mercury/);
|
|
149
188
|
assert.match(ud, /MEDUSA_GDS_ONLY=1/);
|
|
189
|
+
// the seed rides cloud-init too, so a dedicated droplet also comes up able to take tasks
|
|
190
|
+
assert.match(ud, /INSERT INTO versions/, 'seed baked into cloud-init');
|
|
191
|
+
// it must be its OWN runcmd line AFTER migrate (the schema must exist first)
|
|
192
|
+
const lines = ud.split('\n');
|
|
193
|
+
const iMig = lines.findIndex((l) => /migrate\.sh/.test(l));
|
|
194
|
+
const iSeed = lines.findIndex((l) => /INSERT INTO versions/.test(l));
|
|
195
|
+
assert.ok(iMig >= 0 && iSeed > iMig, 'seed line comes after the migrate line');
|
|
196
|
+
// the seed line is a valid bash -lc "…" item: single-quoted -c arg, NO double quotes to break the YAML scalar
|
|
197
|
+
const seedLine = lines[iSeed];
|
|
198
|
+
assert.match(seedLine, /- \[ bash, -lc, "psql .*" \]/, 'well-formed cloud-init runcmd item');
|
|
199
|
+
assert.doesNotMatch(seedLine.replace(/^.*-lc, "/, '').replace(/" \]$/, ''), /"/, 'the seed command carries no inner double quotes');
|
|
150
200
|
});
|
|
151
201
|
|
|
152
202
|
console.log('\nport allocation:');
|
|
@@ -215,12 +265,15 @@ await ta('provisionInstance(co-tenant) composes db → migrate → service → h
|
|
|
215
265
|
const joined = cmds.join('\n');
|
|
216
266
|
assert.match(joined, /createdb mercury/);
|
|
217
267
|
assert.match(joined, /MEDUSA_GDS_ONLY=1/);
|
|
268
|
+
assert.match(joined, /INSERT INTO versions/, 'seeds the first version (task 2119)');
|
|
218
269
|
assert.match(joined, /systemctl daemon-reload/);
|
|
219
270
|
assert.match(joined, /systemctl enable --now mercury/);
|
|
220
271
|
assert.match(joined, /healthz/);
|
|
221
|
-
// ordering: createdb before migrate before enable before healthz
|
|
272
|
+
// ordering: createdb before migrate before SEED before enable before healthz
|
|
222
273
|
const idx = (re) => cmds.findIndex((c) => re.test(c));
|
|
223
274
|
assert.ok(idx(/createdb/) < idx(/migrate\.sh/), 'createdb before migrate');
|
|
275
|
+
assert.ok(idx(/migrate\.sh/) < idx(/INSERT INTO versions/), 'migrate before the version seed (schema must exist)');
|
|
276
|
+
assert.ok(idx(/INSERT INTO versions/) < idx(/enable --now/), 'seed before service enable');
|
|
224
277
|
assert.ok(idx(/migrate\.sh/) < idx(/enable --now/), 'migrate before service enable');
|
|
225
278
|
assert.ok(idx(/enable --now/) < idx(/healthz/), 'service before healthz');
|
|
226
279
|
// writes: the per-instance env file + the systemd unit
|
|
@@ -243,8 +296,9 @@ await ta('provisionInstance(standalone) SCAFFOLDS its repo, then syncs (pull + n
|
|
|
243
296
|
assert.match(joined, /git pull --ff-only origin main/, 'pulls the instance repo');
|
|
244
297
|
assert.match(joined, /npm ci --omit=dev/, 'installs the pinned core');
|
|
245
298
|
assert.match(joined, /createdb mercury/);
|
|
246
|
-
assert.match(joined, /node_modules\/@
|
|
299
|
+
assert.match(joined, /node_modules\/@bongos\/core\/scripts\/migrate\.sh/, 'migrates from the INSTANCE root\'s core');
|
|
247
300
|
assert.ok(!cmds.some((c) => /\.\/scripts\/migrate\.sh/.test(c)), 'standalone does NOT use the control-plane checkout migrate');
|
|
301
|
+
assert.match(joined, /INSERT INTO versions/, 'standalone seeds its first version too (task 2119)');
|
|
248
302
|
// ordering: clone → package-core → init → push → pull → npm ci → migrate → service
|
|
249
303
|
const idx = (re) => cmds.findIndex((c) => re.test(c));
|
|
250
304
|
assert.ok(idx(/git clone/) < idx(/package-core/), 'clone before tarball build');
|
|
@@ -253,6 +307,8 @@ await ta('provisionInstance(standalone) SCAFFOLDS its repo, then syncs (pull + n
|
|
|
253
307
|
assert.ok(idx(/push -u origin main/) < idx(/git pull/), 'scaffold push before the deploy pull');
|
|
254
308
|
assert.ok(idx(/git pull/) < idx(/npm ci/), 'pull before npm ci');
|
|
255
309
|
assert.ok(idx(/npm ci/) < idx(/migrate\.sh/), 'npm ci before migrate');
|
|
310
|
+
assert.ok(idx(/migrate\.sh/) < idx(/INSERT INTO versions/), 'migrate before the version seed');
|
|
311
|
+
assert.ok(idx(/INSERT INTO versions/) < idx(/enable --now/), 'seed before service enable');
|
|
256
312
|
assert.ok(idx(/migrate\.sh/) < idx(/enable --now/), 'migrate before service enable');
|
|
257
313
|
assert.ok(writes.some((w) => /\/mercury\.service$/.test(w)), 'wrote the (standalone) systemd unit');
|
|
258
314
|
});
|
|
@@ -316,6 +372,47 @@ await ta('scaffoldStandaloneRepo: idempotent — a repo that already carries pac
|
|
|
316
372
|
}
|
|
317
373
|
});
|
|
318
374
|
|
|
375
|
+
// ── ADR 0134 (task 2090): registry core-install path + fail-soft to vendored ──
|
|
376
|
+
// Runs the scaffold against a FRESH standaloneBase (no package.json → it proceeds) with
|
|
377
|
+
// an injected env, and asserts which core-resolution shape it chose from the commands.
|
|
378
|
+
async function scaffoldWithEnv(env) {
|
|
379
|
+
const tmp = mkdtempSync(join(tmpdir(), 'scaffold-'));
|
|
380
|
+
const origBase = P.CONFIG.standaloneBase;
|
|
381
|
+
P.CONFIG.standaloneBase = tmp;
|
|
382
|
+
try {
|
|
383
|
+
const { deps, cmds } = recorder({ apply: true });
|
|
384
|
+
await P.scaffoldStandaloneRepo(
|
|
385
|
+
{ slug: 'demo', hosting_shape: 'standalone', target_ref: 'lars589/demo' },
|
|
386
|
+
{ ...deps, env },
|
|
387
|
+
);
|
|
388
|
+
return cmds.join('\n');
|
|
389
|
+
} finally {
|
|
390
|
+
P.CONFIG.standaloneBase = origBase;
|
|
391
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
await ta('scaffoldStandaloneRepo: PROVISION_CORE_FROM_REGISTRY=1 + NPM_TOKEN → registry path (init --registry-core, no tarball build)', async () => {
|
|
396
|
+
const joined = await scaffoldWithEnv({ PROVISION_CORE_FROM_REGISTRY: '1', NPM_TOKEN: 'npm_readonly' });
|
|
397
|
+
assert.match(joined, /init\.js --from .* --dir .* --no-seed --force --registry-core/, 'materializes via --registry-core');
|
|
398
|
+
assert.ok(!/package-core\.js/.test(joined), 'does NOT build a core tarball on the registry path');
|
|
399
|
+
assert.ok(!/--vendor-core/.test(joined), 'does NOT vendor a tarball on the registry path');
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
await ta('scaffoldStandaloneRepo: registry requested but NO NPM_TOKEN → FAILS SOFT to the vendored tarball', async () => {
|
|
403
|
+
const joined = await scaffoldWithEnv({ PROVISION_CORE_FROM_REGISTRY: '1' });
|
|
404
|
+
assert.match(joined, /node scripts\/gds\/package-core\.js/, 'builds the core tarball (vendored fallback)');
|
|
405
|
+
assert.match(joined, /init\.js --from .* --dir .* --no-seed --force --vendor-core /, 'file:-pins the vendored tarball');
|
|
406
|
+
assert.ok(!/--registry-core/.test(joined), 'does NOT take the registry path without a token');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
await ta('scaffoldStandaloneRepo: default (no registry env) stays on the vendored tarball path', async () => {
|
|
410
|
+
const joined = await scaffoldWithEnv({});
|
|
411
|
+
assert.match(joined, /node scripts\/gds\/package-core\.js/, 'vendored is the default');
|
|
412
|
+
assert.match(joined, /--vendor-core /);
|
|
413
|
+
assert.ok(!/--registry-core/.test(joined));
|
|
414
|
+
});
|
|
415
|
+
|
|
319
416
|
await ta('provisionInstance is idempotent: an already-active instance is a no-op', async () => {
|
|
320
417
|
const { deps, cmds } = recorder();
|
|
321
418
|
const r = await P.provisionInstance({ ...coTenant, status: 'active' }, deps);
|
|
@@ -323,10 +420,11 @@ await ta('provisionInstance is idempotent: an already-active instance is a no-op
|
|
|
323
420
|
assert.equal(cmds.length, 0, 'no commands run for an active instance');
|
|
324
421
|
});
|
|
325
422
|
|
|
326
|
-
await ta('provisionInstance(dedicated) does NOT run local createdb/migrate (they ride cloud-init)', async () => {
|
|
423
|
+
await ta('provisionInstance(dedicated) does NOT run local createdb/migrate/seed (they ride cloud-init)', async () => {
|
|
327
424
|
const { deps, cmds } = recorder();
|
|
328
425
|
await P.provisionInstance({ ...coTenant, hosting_shape: 'dedicated', tier: 's-2vcpu-4gb' }, deps);
|
|
329
426
|
assert.ok(!cmds.some((c) => /createdb/.test(c)), 'dedicated does not run createdb locally');
|
|
427
|
+
assert.ok(!cmds.some((c) => /INSERT INTO versions/.test(c)), 'dedicated does not run the version seed locally (it rides cloud-init)');
|
|
330
428
|
});
|
|
331
429
|
|
|
332
430
|
console.log('\nDNS UPSERT composition (P4, CONFIG override):');
|
|
@@ -552,8 +650,8 @@ t('migrateCmd: privileged runs migrate as postgres via env+bash (survives sudo e
|
|
|
552
650
|
|
|
553
651
|
t('standaloneMigrateCmd: privileged runs the instance-root migrate as postgres; default unchanged', () => {
|
|
554
652
|
const inst = { ...coTenant, hosting_shape: 'standalone' };
|
|
555
|
-
assert.equal(P.standaloneMigrateCmd(inst), 'PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules/@
|
|
556
|
-
assert.equal(P.standaloneMigrateCmd(inst, { privileged: true }), 'sudo -u postgres env PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules/@
|
|
653
|
+
assert.equal(P.standaloneMigrateCmd(inst), 'PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules/@bongos/core/scripts/migrate.sh');
|
|
654
|
+
assert.equal(P.standaloneMigrateCmd(inst, { privileged: true }), 'sudo -u postgres env PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules/@bongos/core/scripts/migrate.sh');
|
|
557
655
|
});
|
|
558
656
|
|
|
559
657
|
t('makeWriteFile(privileged): a { sudo:true } write stages a temp file + `sudo install -D -m <mode>` (never writes /etc directly)', () => {
|
|
@@ -615,6 +713,7 @@ await ta('provisionInstance(co-tenant, privileged): createdb/migrate run as post
|
|
|
615
713
|
const joined = cmds.join('\n');
|
|
616
714
|
assert.match(joined, /sudo -u postgres createdb mercury/, 'createdb as postgres');
|
|
617
715
|
assert.match(joined, /sudo -u postgres env PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash \.\/scripts\/migrate\.sh/, 'migrate as postgres');
|
|
716
|
+
assert.match(joined, /sudo -u postgres psql .*INSERT INTO versions/, 'first-version seed runs as postgres too (task 2119)');
|
|
618
717
|
assert.match(joined, /sudo systemctl daemon-reload/, 'daemon-reload sudo-prefixed');
|
|
619
718
|
assert.match(joined, /sudo systemctl enable --now mercury/, 'enable sudo-prefixed');
|
|
620
719
|
assert.match(joined, /sudo systemctl reload caddy/, 'reload caddy sudo-prefixed');
|
|
@@ -30,7 +30,7 @@ provisioning.attachManifestCode = async (_db, id, code) => ({ id, status: 'code_
|
|
|
30
30
|
// A branding with a known publicOrigin so mothershipOrigin() resolves deterministically.
|
|
31
31
|
const branding = require(`${ROOT}/src/branding.js`);
|
|
32
32
|
const realBranding = branding.branding;
|
|
33
|
-
branding.branding = () => ({ ...realBranding(), domains: { publicOrigin: 'https://cloudbongos.com' } });
|
|
33
|
+
branding.branding = () => ({ ...realBranding(), identity: { ...(realBranding().identity || {}), productName: 'Cloud Bongos' }, domains: { publicOrigin: 'https://cloudbongos.com' } });
|
|
34
34
|
|
|
35
35
|
// Inject the signed-in owner (the real requireBuilder needs a session).
|
|
36
36
|
const api = require(`${ROOT}/src/module-api.js`);
|
|
@@ -66,15 +66,31 @@ console.log('POST /provisioning/instances/:id/github-app (mint the one-click lin
|
|
|
66
66
|
t('post_url targets GitHub personal apps/new carrying the state', j.post_url === `https://github.com/settings/apps/new?state=${j.state}`, j.post_url);
|
|
67
67
|
t('manifest.redirect_url is the MOTHERSHIP callback', j.manifest && j.manifest.redirect_url === 'https://cloudbongos.com/api/bongos/provisioning/github-app/callback', j.manifest && j.manifest.redirect_url);
|
|
68
68
|
t('manifest.callback_urls are the INSTANCE sign-in (both prefixes)', j.manifest && j.manifest.callback_urls[0] === 'https://mercury.example.com/api/gds/auth/web/callback' && j.manifest.callback_urls[1] === 'https://mercury.example.com/api/bongos/auth/web/callback');
|
|
69
|
+
t('manifest.name is the instance domain (unique + matches the consent-screen redirect) within 34 chars', j.manifest && j.manifest.name === 'mercury.example.com' && j.manifest.name.length <= 34, j.manifest && j.manifest.name);
|
|
69
70
|
t('org_url derived from the target_ref owner (acme)', j.org_url === `https://github.com/organizations/acme/settings/apps/new?state=${j.state}`, j.org_url);
|
|
70
71
|
t('the attempt row was persisted with the minted state', !!(stored && stored.instanceId === 42 && stored.state === j.state && stored.requestedBy === 7));
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
console.log('\nprovisioningOrigin override (task 2101 — customer App carries the PLATFORM brand, not the game domain):');
|
|
75
|
+
{
|
|
76
|
+
// A control plane that co-hosts a game (OTB = customer 0) has a GAME-branded publicOrigin.
|
|
77
|
+
// provisioningOrigin is the dedicated front door the customer's GitHub App must point at,
|
|
78
|
+
// so mothershipOrigin() must prefer it over publicOrigin. mothershipOrigin() calls
|
|
79
|
+
// api.branding() (module-api captured the fn ref at require-time), so override THAT.
|
|
80
|
+
const prevBranding = api.branding;
|
|
81
|
+
api.branding = () => ({ ...realBranding(), identity: { productName: 'Cloud Bongos' }, domains: { publicOrigin: 'https://amazonprimea.com', provisioningOrigin: 'https://get.cloudbongos.com' } });
|
|
82
|
+
const res = await fetch(`${base}/provisioning/instances/42/github-app`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' });
|
|
83
|
+
const j = await res.json();
|
|
84
|
+
t('redirect_url uses provisioningOrigin, not the game publicOrigin', j.manifest && j.manifest.redirect_url === 'https://get.cloudbongos.com/api/bongos/provisioning/github-app/callback', j.manifest && j.manifest.redirect_url);
|
|
85
|
+
t('webhook hook_attributes.url also uses provisioningOrigin', j.manifest && j.manifest.hook_attributes && j.manifest.hook_attributes.url === 'https://get.cloudbongos.com/api/bongos/provisioning/github-app/webhook', j.manifest && j.manifest.hook_attributes && j.manifest.hook_attributes.url);
|
|
86
|
+
api.branding = prevBranding;
|
|
87
|
+
}
|
|
88
|
+
|
|
73
89
|
console.log('\nGET /provisioning/github-app/callback (the public GitHub redirect):');
|
|
74
90
|
{
|
|
75
91
|
const res = await fetch(`${base}/provisioning/github-app/callback?code=GHCODE123&state=GOODSTATE`);
|
|
76
92
|
const html = await res.text();
|
|
77
|
-
t('good state → 200 text/html
|
|
93
|
+
t('good state → 200 text/html progress page wired to auto-open', res.status === 200 && /text\/html/.test(res.headers.get('content-type') || '') && /Setting up your instance/.test(html) && /Placing credentials/.test(html) && /__CB_SETUP__/.test(html) && html.includes('GOODSTATE'), `status=${res.status}`);
|
|
78
94
|
t('the page names the instance host', /mercury\.example\.com/.test(html));
|
|
79
95
|
}
|
|
80
96
|
{
|
|
@@ -87,6 +103,32 @@ console.log('\nGET /provisioning/github-app/callback (the public GitHub redirect
|
|
|
87
103
|
t('missing code/state → 400', res.status === 400, `status=${res.status}`);
|
|
88
104
|
}
|
|
89
105
|
|
|
106
|
+
console.log('\nGET /provisioning/github-app/status (the page poll → auto-open signal):');
|
|
107
|
+
{
|
|
108
|
+
const res = await fetch(`${base}/provisioning/github-app/status?state=GOODSTATE`);
|
|
109
|
+
const j = await res.json();
|
|
110
|
+
t('pending → 200 ok, not ready, names the domain', res.status === 200 && j.ok === true && j.status === 'pending' && j.ready === false && j.domain === 'mercury.example.com', `status=${res.status} ${JSON.stringify(j)}`);
|
|
111
|
+
}
|
|
112
|
+
{
|
|
113
|
+
// Swap the stub to a placed-credentials ('done') attempt to prove ready flips true.
|
|
114
|
+
const prev = provisioning.getOAuthManifestByState;
|
|
115
|
+
provisioning.getOAuthManifestByState = async (_db, state) => (state === 'GOODSTATE'
|
|
116
|
+
? { id: 99, instance_id: 42, requested_by: 7, status: 'done', expires_at: new Date(Date.now() + 600000).toISOString() }
|
|
117
|
+
: null);
|
|
118
|
+
const res = await fetch(`${base}/provisioning/github-app/status?state=GOODSTATE`);
|
|
119
|
+
const j = await res.json();
|
|
120
|
+
t("status 'done' → ready:true + terminal:true", res.status === 200 && j.ready === true && j.terminal === true, JSON.stringify(j));
|
|
121
|
+
provisioning.getOAuthManifestByState = prev;
|
|
122
|
+
}
|
|
123
|
+
{
|
|
124
|
+
const res = await fetch(`${base}/provisioning/github-app/status?state=NOPE`);
|
|
125
|
+
t('unknown state → 404 (no oracle)', res.status === 404, `status=${res.status}`);
|
|
126
|
+
}
|
|
127
|
+
{
|
|
128
|
+
const res = await fetch(`${base}/provisioning/github-app/status`);
|
|
129
|
+
t('missing state → 400', res.status === 400, `status=${res.status}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
90
132
|
console.log('\nparseId contract (the (req,res) signature — a bad id is a clean 400, not a 500):');
|
|
91
133
|
{
|
|
92
134
|
const res = await fetch(`${base}/provisioning/instances/not-a-number/github-app`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' });
|