@bongos/core 1.19.1072 → 1.19.1074
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 +320 -465
- package/.claude/skills/feedback/SKILL.md +2 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -60
- package/clients/bongos-client/index.cjs +4 -60
- package/clients/bongos-client/index.d.ts +5 -90
- package/clients/bongos-client/index.mjs +4 -60
- package/config/branding.neutral.json +1 -5
- package/config/modules.neutral.json +2 -3
- package/config/scheduled-routines.json +0 -2
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -0
- package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -0
- package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
- package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +1 -1
- package/docs/adr/0341-the-page-is-the-unit-of-tweak-mode.md +6 -2
- package/docs/adr/0345-we-host-every-projects-hall-and-its-app-deploys-where-the-owner-chooses.md +94 -0
- package/docs/adr/README.md +2 -1
- package/docs/api/openapi.json +171 -1661
- package/docs/api-reference.md +9 -41
- package/docs/architecture.md +16 -1
- package/docs/branding-contract.md +0 -3
- package/docs/copy-inventory.md +454 -479
- package/docs/copy-registry.json +746 -978
- package/docs/file-map.md +5 -34
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +13 -1
- package/docs/onboarding/diagrams/04-architecture.mmd +11 -22
- package/docs/onboarding/diagrams/README.md +3 -3
- package/docs/onboarding/diagrams/assertions.json +2 -22
- package/docs/onboarding/primer.md +9 -28
- package/docs/page-inventory.json +4 -1
- package/docs/page-readings.json +1032 -1077
- package/docs/recipes/render-pilot.md +29 -10
- package/migrations/core_257_module_store_registry.sql +87 -0
- package/migrations/core_258_drop_dev_box_tables.sql +55 -0
- package/modules/agents/module.json +1 -0
- package/modules/autonomy/module.json +1 -0
- package/modules/builder-settings/module.json +1 -0
- package/modules/builder-settings/render-prefs.js +4 -5
- package/modules/copy-desk/docx.js +78 -5
- package/modules/copy-desk/module.json +1 -0
- package/modules/copy-desk/routes/copy-desk.js +9 -9
- package/modules/copy-desk/tests/copy_docx.mjs +107 -1
- package/modules/copy-desk/tests/fixtures/word-docx.mjs +30 -0
- package/modules/discord/module.json +1 -0
- package/modules/economy/module.json +1 -0
- package/modules/economy/routes/credits.js +1 -2
- package/modules/government/catalog.js +13 -17
- package/modules/government/migrations/government_019_retire_box_permissions.sql +33 -0
- package/modules/government/module.json +1 -0
- package/modules/government/protected-surfaces.json +3 -11
- package/modules/government/resolver.js +3 -46
- package/modules/government/routes/government.js +0 -2
- package/modules/government/session-scopes.js +43 -83
- package/modules/government/session-scopes.json +8 -18
- package/modules/grading/module.json +1 -0
- package/modules/hall-ui/module.json +1 -0
- package/modules/hall-ui/public/board-lib.js +2 -2
- package/modules/hall-ui/public/builders.js +29 -40
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/collab.js +1 -1
- package/modules/hall-ui/public/diagrams.html +3 -3
- package/modules/hall-ui/public/dom-utils.js +1 -1
- package/modules/hall-ui/public/gate.js +2 -2
- package/modules/hall-ui/public/gate.states.json +1 -1
- package/modules/hall-ui/public/hall-render.js +43 -79
- package/modules/hall-ui/public/index.html +5 -1
- package/modules/hall-ui/public/modules.js +1 -0
- package/modules/hall-ui/public/oversight.css +5 -5
- package/modules/hall-ui/public/palette.js +0 -11
- package/modules/hall-ui/public/primer.js +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings-sessions.js +1 -1
- package/modules/hall-ui/public/settings.css +0 -1
- package/modules/hall-ui/public/settings.html +4 -33
- package/modules/hall-ui/public/settings.js +3 -33
- package/modules/hall-ui/public/settings.states.json +2 -2
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +10 -13
- package/modules/hall-ui/public/task.html +4 -1
- package/modules/hall-ui/public/task.js +36 -1
- package/modules/hall-ui/public/tweak-editor-lib.js +18 -1
- package/modules/hall-ui/public/tweak-editor.css +16 -0
- package/modules/hall-ui/public/tweak-editor.html +26 -3
- package/modules/hall-ui/public/tweak-editor.js +32 -1
- package/modules/hall-ui/public/work.js +1 -1
- package/modules/hall-ui/records/tweak-editor.md +1 -0
- package/modules/ideas/module.json +1 -0
- package/modules/lifecycle/db-claim-reads.js +1 -58
- package/modules/lifecycle/db-ship.js +0 -5
- package/modules/lifecycle/db.js +0 -4
- package/modules/lifecycle/kickoff-checklist.js +0 -4
- package/modules/lifecycle/lifecycle.js +0 -8
- package/modules/lifecycle/module.json +1 -0
- package/modules/lifecycle/routes/tasks.js +5 -7
- package/modules/memory/module.json +1 -0
- package/modules/npm-release/module.json +1 -0
- package/modules/onboarding/module.json +1 -0
- package/modules/onboarding/onboarding-state.js +17 -36
- package/modules/onboarding/routes/access-requests.js +3 -3
- package/modules/platform-identity/module.json +1 -0
- package/modules/platform-identity/platform-identity.js +13 -5
- package/modules/platform-identity/routes/sso.js +2 -2
- package/modules/platform-identity/tests/platform-identity.mjs +17 -3
- package/modules/provisioning/module.json +1 -0
- package/modules/provisioning/provisioning.js +2 -2
- package/modules/provisioning/starter-bundles.js +5 -25
- package/modules/public-landing/module.json +1 -0
- package/modules/public-landing/public/assets/cosmos.css +1 -1
- package/modules/public-landing/public/contact.html +1 -1
- package/modules/public-landing/public/privacy.html +1 -1
- package/modules/public-landing/public/projects.html +101 -32
- package/modules/public-landing/public/projects.probes.json +2 -2
- package/modules/public-landing/public/projects.states.json +4 -3
- package/modules/public-landing/public/terms.html +1 -1
- package/modules/security/module.json +1 -0
- package/modules/sessions/module.json +1 -0
- package/modules/specialities/module.json +1 -0
- package/modules/status-ui/module.json +1 -0
- package/modules/ui-design/module.json +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +84 -0
- package/scripts/gds/artifact-format.js +203 -0
- package/scripts/gds/audit-authorship.sh +1 -13
- package/scripts/gds/claim.js +0 -15
- package/scripts/gds/cli-lib.js +8 -8
- package/scripts/gds/diagram-facts.js +12 -23
- package/scripts/gds/do-api.js +13 -80
- package/scripts/gds/doc-cli-guard.js +2 -0
- package/scripts/gds/doctor.js +1 -1
- package/scripts/gds/feedback-latest.js +139 -9
- package/scripts/gds/fitness-checks-identity.js +2 -7
- package/scripts/gds/fitness-checks-write-validation.js +4 -0
- package/scripts/gds/fitness-ratchets.js +4 -4
- package/scripts/gds/fitness.js +10 -10
- package/scripts/gds/gen-diagrams.js +49 -71
- package/scripts/gds/http-api-client.js +12 -19
- package/scripts/gds/init.js +1 -1
- package/scripts/gds/lint-infra-exec.js +1 -1
- package/scripts/gds/local-preview-lib.js +8 -39
- package/scripts/gds/local-preview.js +2 -15
- package/scripts/gds/migration-namespace.js +3 -3
- package/scripts/gds/module-artifact.js +212 -0
- package/scripts/gds/module.js +95 -6
- package/scripts/gds/package-core.js +5 -116
- package/scripts/gds/provision-config.js +2 -2
- package/scripts/gds/provision-net.js +2 -3
- package/scripts/gds/provision.js +9 -9
- package/scripts/gds/publish-manifest.js +1 -2
- package/scripts/gds/regen-instance-docs.js +1 -1
- package/scripts/gds/run-unit-tests.js +2 -16
- package/scripts/gds/sandbox-stage.js +39 -341
- package/scripts/gds/ship-flow.js +6 -6
- package/scripts/gds/ship-land.js +3 -4
- package/scripts/gds/ship-regen.js +10 -41
- package/scripts/gds/ship.js +4 -24
- package/scripts/gds/start.js +0 -15
- package/scripts/hall-preview/README.md +5 -5
- package/scripts/hall-preview/server.js +6 -6
- package/scripts/render-diagrams.sh +1 -1
- package/src/bongos/api-errors.js +1 -1
- package/src/bongos/auth-github.js +2 -3
- package/src/bongos/auth.js +51 -126
- package/src/bongos/db-kernel.js +2 -3
- package/src/bongos/db.js +5 -28
- package/src/bongos/module-scope-map.js +14 -29
- package/src/bongos/module-store.js +141 -0
- package/src/bongos/route-rank-check.js +3 -16
- package/src/bongos/routes/auth.js +0 -116
- package/src/bongos/routes/builders.js +5 -15
- package/src/bongos/routes/instance.js +8 -14
- package/src/bongos/routes/me.js +6 -22
- package/src/bongos/routes/modules.js +83 -0
- package/src/bongos/routes.js +5 -0
- package/src/module-api.js +13 -6
- package/src/module-loader/catalog.js +3 -0
- package/src/module-loader/loader.js +4 -4
- package/src/module-loader/manifest-schema.js +15 -7
- package/src/module-seams.js +2 -3
- package/src/modules.js +42 -14
- package/tests/agents_spend_guard.mjs +1 -1
- package/tests/api_client.mjs +0 -15
- package/tests/auth_page_gate.mjs +10 -10
- package/tests/canonical_profile_url.mjs +1 -3
- package/tests/claim_action.mjs +4 -1
- package/tests/claim_from_task_and_home.mjs +127 -0
- package/tests/consumer_layout_boot.mjs +2 -0
- package/tests/copy_desk_page_docx.mjs +33 -2
- package/tests/core_upgrade_runner.mjs +6 -6
- package/tests/deploy_divergence_line.mjs +11 -11
- package/tests/diagram_facts_offset.mjs +1 -1
- package/tests/do_api.mjs +106 -0
- package/tests/feedback_latest.mjs +122 -0
- package/tests/fitness.mjs +44 -42
- package/tests/gate_approvals.mjs +0 -1
- package/tests/go_live.mjs +3 -3
- package/tests/government_abuse_matrix.mjs +20 -19
- package/tests/government_ownership_scope.mjs +33 -35
- package/tests/government_parity.mjs +2 -2
- package/tests/government_protected_surfaces.mjs +56 -37
- package/tests/government_require_permission.mjs +3 -3
- package/tests/government_seed.mjs +37 -4
- package/tests/government_session_scope.mjs +92 -366
- package/tests/hall_error_envelope.mjs +4 -0
- package/tests/hall_landing_boot.mjs +4 -1
- package/tests/hall_palette.mjs +1 -16
- package/tests/hall_record_world.mjs +6 -0
- package/tests/hall_tweak_editor.mjs +52 -1
- package/tests/host_topology_skips.mjs +1 -2
- package/tests/html_comment_nesting.mjs +137 -0
- package/tests/infra_exec_paths.mjs +9 -10
- package/tests/init.mjs +4 -5
- package/tests/instance_manifest.mjs +18 -25
- package/tests/kickoff_checklist.mjs +1 -1
- package/tests/lib_sh_resolution.mjs +0 -221
- package/tests/local_preview.mjs +0 -12
- package/tests/module-scope-map.mjs +9 -9
- package/tests/module_catalog.mjs +1 -1
- package/tests/module_cli.mjs +15 -15
- package/tests/module_contributions.mjs +6 -6
- package/tests/module_loader.mjs +6 -6
- package/tests/module_manifest.mjs +13 -4
- package/tests/module_store_publish.mjs +307 -0
- package/tests/module_store_publish_route.mjs +121 -0
- package/tests/module_store_registry_migration.mjs +70 -0
- package/tests/modules.mjs +56 -26
- package/tests/onboarding_route_signals.mjs +21 -31
- package/tests/onboarding_state.mjs +35 -49
- package/tests/permission_path.mjs +0 -3
- package/tests/platform_boot.mjs +1 -2
- package/tests/profile_route.mjs +1 -6
- package/tests/projects_hub.mjs +5 -5
- package/tests/projects_hub_app_step.mjs +138 -0
- package/tests/projects_hub_module_picker.mjs +2 -2
- package/tests/projects_hub_pre_uat.mjs +4 -5
- package/tests/provision.mjs +7 -7
- package/tests/provisioning_starter_bundles.mjs +0 -22
- package/tests/publish_branch_route.mjs +10 -18
- package/tests/publish_manifest.mjs +1 -2
- package/tests/rank_tier_single_source.mjs +1 -13
- package/tests/repo_map.mjs +3 -4
- package/tests/runner_drift.mjs +3 -21
- package/tests/sandbox_stage.mjs +57 -390
- package/tests/session_rename_fallback.mjs +0 -21
- package/tests/ship_error_shape.mjs +1 -1
- package/tests/task_visual_slots.mjs +31 -0
- package/tests/terms_acceptance.mjs +1 -1
- package/tests/upgrade.mjs +4 -4
- package/tests/watch_sealed_floor.mjs +0 -1
- package/tests/wizard_draft_resume.mjs +2 -2
- package/tests/wizard_intent_resume.mjs +21 -10
- package/tests/wizard_preselect_why.mjs +18 -18
- package/docs/onboarding/browser-terminal-guide.md +0 -73
- package/docs/recipes/managed-settings-remote-control.md +0 -416
- package/modules/dev-box/CLAUDE.md +0 -15
- package/modules/dev-box/box-access.js +0 -623
- package/modules/dev-box/box-credential.js +0 -94
- package/modules/dev-box/box-onboard.js +0 -486
- package/modules/dev-box/boxes.js +0 -1038
- package/modules/dev-box/db.js +0 -39
- package/modules/dev-box/module.json +0 -19
- package/modules/dev-box/routes/box.js +0 -1223
- package/scripts/gds/box-auth-check.js +0 -160
- package/scripts/gds/box-infra.js +0 -321
- package/scripts/gds/box-sync.js +0 -216
- package/scripts/gds/box.js +0 -1332
- package/scripts/gds/cf-tunnel.js +0 -558
- package/scripts/gds/smoke-box.sh +0 -150
- package/scripts/gds/tree-preflight.js +0 -241
- package/src/bongos/app-pair.js +0 -172
- package/tests/app_pair.mjs +0 -168
- package/tests/box_access.mjs +0 -578
- package/tests/box_auth_check.mjs +0 -114
- package/tests/box_code_staleness.mjs +0 -129
- package/tests/box_connect_e2e.mjs +0 -196
- package/tests/box_cost.mjs +0 -70
- package/tests/box_credential.mjs +0 -125
- package/tests/box_dns_repoint.mjs +0 -153
- package/tests/box_env.mjs +0 -64
- package/tests/box_host_keys.mjs +0 -126
- package/tests/box_infra_resolve.mjs +0 -246
- package/tests/box_onboard.mjs +0 -317
- package/tests/box_scope_session.mjs +0 -119
- package/tests/box_sweep_docs.mjs +0 -119
- package/tests/box_sync.mjs +0 -55
- package/tests/box_sync_scope_report.mjs +0 -126
- package/tests/box_task_scope.mjs +0 -169
- package/tests/box_task_scope_pause.mjs +0 -66
- package/tests/box_terminal_ssrf.mjs +0 -96
- package/tests/boxes.mjs +0 -1585
- package/tests/cf_ruleset_preflight.mjs +0 -142
- package/tests/cf_tunnel.mjs +0 -496
- package/tests/docs_scrubber_damage.mjs +0 -111
- package/tests/module_scope_active_claims.mjs +0 -94
- package/tests/ship_api_client_pathspec.mjs +0 -100
- package/tests/tree_preflight.mjs +0 -243
|
@@ -26,7 +26,6 @@ import { test } from 'node:test';
|
|
|
26
26
|
import { createRequire } from 'node:module';
|
|
27
27
|
import { spawnSync } from 'node:child_process';
|
|
28
28
|
import fs from 'node:fs';
|
|
29
|
-
import os from 'node:os';
|
|
30
29
|
import path from 'node:path';
|
|
31
30
|
import { fileURLToPath } from 'node:url';
|
|
32
31
|
import { fitnessSource } from './helpers.mjs';
|
|
@@ -35,12 +34,6 @@ const require = createRequire(import.meta.url);
|
|
|
35
34
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
36
35
|
const GDS = path.join(ROOT, 'scripts', 'gds');
|
|
37
36
|
const fitness = require(path.join(GDS, 'fitness.js'));
|
|
38
|
-
// The CURRENT session filename, so the expectations below follow the rename chain
|
|
39
|
-
// (task 1003704 renamed the file; 1003706 retires the old names) instead of being
|
|
40
|
-
// re-edited at each step. SESSION_FILENAMES[0] is what the CLI WRITES; the tail is
|
|
41
|
-
// what it still accepts on read.
|
|
42
|
-
const ic = require(path.join(ROOT, 'src', 'instance-config.js'));
|
|
43
|
-
const SESSION_FILE = ic.SESSION_FILENAMES[0];
|
|
44
37
|
|
|
45
38
|
// Every bash script that resolves a GDS token or API base.
|
|
46
39
|
const SMOKES = [
|
|
@@ -159,217 +152,3 @@ test('the whole host-identity check is green on this tree', () => {
|
|
|
159
152
|
const r = fitness.checkKernelPromptsNoHardcodedIdentity();
|
|
160
153
|
assert.equal(r.ok, true, `violations:\n${r.violations.join('\n')}`);
|
|
161
154
|
});
|
|
162
|
-
|
|
163
|
-
// ---- 4. the SAME rule applies to the baked box scripts (task 1003252) ------
|
|
164
|
-
//
|
|
165
|
-
// THE SECOND BUG, same class, different file group. The sweep above fixed
|
|
166
|
-
// scripts/gds/*.sh but never looked at infra/box-*.sh — the scripts baked onto a
|
|
167
|
-
// dev box. Those kept reading a hardcoded ~/.config/otb/gds-session.json while
|
|
168
|
-
// paste-token.js writes to ~/.config/<branding.configDir>/. On this instance
|
|
169
|
-
// (configDir "cloudbongos") that meant every /builder-reauth silently stranded the
|
|
170
|
-
// box: the 5-min heartbeat, the terminal/host-key/version reporters and the
|
|
171
|
-
// GDS-backed git credential helper all kept presenting a DEAD token and failing
|
|
172
|
-
// with a misleading "session expired — re-auth on the box", which is exactly what
|
|
173
|
-
// the builder had just done. Observed live: four reporters 401ing every tick and a
|
|
174
|
-
// checkout 311 commits behind origin/main.
|
|
175
|
-
//
|
|
176
|
-
// The fix mirrors instance-config.js's read-side fallback in bash: prefer the most
|
|
177
|
-
// recently written gds-session.json across ~/.config/*/, since branding is not
|
|
178
|
-
// readable from a script that must also run before any clone exists.
|
|
179
|
-
|
|
180
|
-
const BOX_SCRIPTS = [
|
|
181
|
-
'box-heartbeat.sh',
|
|
182
|
-
'box-report-terminal.sh',
|
|
183
|
-
'box-report-host-keys.sh',
|
|
184
|
-
'box-report-version.sh',
|
|
185
|
-
];
|
|
186
|
-
|
|
187
|
-
const INFRA = path.join(ROOT, 'infra');
|
|
188
|
-
const boxCode = (f) => fitness.stripCommentsForIdentity(fs.readFileSync(path.join(INFRA, f), 'utf8'), '.sh');
|
|
189
|
-
|
|
190
|
-
test('every baked box script resolves the session across config dirs, not just the legacy one', () => {
|
|
191
|
-
for (const f of BOX_SCRIPTS) {
|
|
192
|
-
const src = boxCode(f);
|
|
193
|
-
for (const name of ic.SESSION_FILENAMES.slice(0, 2)) {
|
|
194
|
-
assert.ok(src.includes(`.config/*/${name}`),
|
|
195
|
-
`${f} must scan ~/.config/*/${name} — without the glob a /builder-reauth into a `
|
|
196
|
-
+ 'non-"otb" configDir strands the box on a dead token, and without BOTH names '
|
|
197
|
-
+ 'half the fleet is unreadable: a box provisioned before the rename has only the '
|
|
198
|
-
+ 'old name, one provisioned after has only the new (task 1003704)');
|
|
199
|
-
}
|
|
200
|
-
// The legacy literal may survive ONLY as the last-resort default, never as the
|
|
201
|
-
// sole source — i.e. it must be preceded by the glob scan.
|
|
202
|
-
// The scan widens what these scripts will read, so each candidate must be owned
|
|
203
|
-
// by the box user -- otherwise a file dropped by another local account could win
|
|
204
|
-
// simply by being newest.
|
|
205
|
-
assert.match(src, /\[ -O "\$_sess" \]/,
|
|
206
|
-
`${f} must require candidate session files to be owned by the current user`);
|
|
207
|
-
const globAt = src.indexOf('.config/*/');
|
|
208
|
-
const legacyAt = src.indexOf(`.config/otb/${SESSION_FILE}`);
|
|
209
|
-
if (legacyAt !== -1) {
|
|
210
|
-
assert.ok(globAt < legacyAt,
|
|
211
|
-
`${f} reaches the legacy path before scanning ~/.config/*/ — the fallback must come first`);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
test('every baked box script passes bash -n', (t) => {
|
|
217
|
-
const probe = bash(['-n', 'infra/box-heartbeat.sh']);
|
|
218
|
-
if (!probe) return t.skip('bash not available on this machine');
|
|
219
|
-
for (const f of BOX_SCRIPTS) {
|
|
220
|
-
const r = bash(['-n', `infra/${f}`]);
|
|
221
|
-
assert.equal(r.status, 0, `bash -n infra/${f} failed:\n${r.stderr}`);
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// Pull the resolver straight out of the committed file so this exercises the real
|
|
226
|
-
// code, not a copy that could drift away from it.
|
|
227
|
-
function resolverBlock(f) {
|
|
228
|
-
const src = fs.readFileSync(path.join(INFRA, f), 'utf8');
|
|
229
|
-
const start = src.indexOf('if [ -z "${GDS_SESSION:-}" ]; then');
|
|
230
|
-
// Matched by PREFIX, up to the closing quote — never by the full line including the
|
|
231
|
-
// filename. The filename is the one thing a rename changes, so spelling it here made
|
|
232
|
-
// this test fail with "could not locate the resolver block" on a perfectly good
|
|
233
|
-
// rename, and on Windows (where it already failed for its own reason) that looked
|
|
234
|
-
// like no new failure at all (task 1003704).
|
|
235
|
-
const endPrefix = 'GDS_SESSION="${GDS_SESSION:-$HOME/.config/otb/';
|
|
236
|
-
const at = src.indexOf(endPrefix, start);
|
|
237
|
-
const close = at === -1 ? -1 : src.indexOf('"', at + endPrefix.length);
|
|
238
|
-
assert.ok(start !== -1 && at !== -1 && close !== -1,
|
|
239
|
-
`could not locate the resolver block in ${f}`);
|
|
240
|
-
return src.slice(start, close + 1);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// These two assert on the PATH STRING the shell resolver prints. Everything above
|
|
244
|
-
// checks content and syntax and runs everywhere; only these two compare spellings,
|
|
245
|
-
// and Git Bash answers in its own mount namespace — a Win32 HOME comes back as
|
|
246
|
-
// '/tmp/boxsess-…', which maps to a Git-install mount this side cannot reconstruct
|
|
247
|
-
// without reading Git's mount table (task 1003529). The box scripts these cover run
|
|
248
|
-
// on the Linux dev box, so the claim is verified where it is true rather than
|
|
249
|
-
// left permanently red here — a red nobody can act on is how a suite stops being read.
|
|
250
|
-
const SHELL_PATH_SPELLING_SKIP =
|
|
251
|
-
'shell path spellings: Git Bash resolves HOME into its own mount namespace (these scripts run on the Linux box)';
|
|
252
|
-
|
|
253
|
-
test('the resolver prefers the most recently written session across config dirs', (t) => {
|
|
254
|
-
if (process.platform === 'win32') return t.skip(SHELL_PATH_SPELLING_SKIP);
|
|
255
|
-
const probe = bash(['-n', 'infra/box-report-version.sh']);
|
|
256
|
-
if (!probe) return t.skip('bash not available on this machine');
|
|
257
|
-
|
|
258
|
-
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'boxsess-'));
|
|
259
|
-
const legacy = path.join(home, '.config', 'otb');
|
|
260
|
-
const configured = path.join(home, '.config', 'cloudbongos');
|
|
261
|
-
fs.mkdirSync(legacy, { recursive: true });
|
|
262
|
-
fs.mkdirSync(configured, { recursive: true });
|
|
263
|
-
fs.writeFileSync(path.join(legacy, 'gds-session.json'), '{"token":"stale"}');
|
|
264
|
-
fs.writeFileSync(path.join(configured, 'gds-session.json'), '{"token":"fresh"}');
|
|
265
|
-
// Make the legacy copy unambiguously older than the freshly re-authed one.
|
|
266
|
-
const old = new Date(Date.now() - 60_000);
|
|
267
|
-
fs.utimesSync(path.join(legacy, 'gds-session.json'), old, old);
|
|
268
|
-
|
|
269
|
-
const block = resolverBlock('box-report-version.sh');
|
|
270
|
-
const run = (env) => spawnSync('bash', ['-c', `set -uo pipefail\n${block}\nprintf '%s' "$GDS_SESSION"`],
|
|
271
|
-
{ encoding: 'utf8', env: { PATH: process.env.PATH, HOME: home, ...env } });
|
|
272
|
-
|
|
273
|
-
// 1. the freshly written session wins over the stale legacy one
|
|
274
|
-
assert.equal(run({}).stdout, path.join(configured, 'gds-session.json'));
|
|
275
|
-
|
|
276
|
-
// 2. an explicit override still beats the scan
|
|
277
|
-
assert.equal(run({ GDS_SESSION: '/explicit/path.json' }).stdout, '/explicit/path.json');
|
|
278
|
-
|
|
279
|
-
// 3. with nothing on disk it falls back to the historical path, so the caller's
|
|
280
|
-
// "no session at ..." message still names somewhere sensible
|
|
281
|
-
const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'boxsess-empty-'));
|
|
282
|
-
const r3 = spawnSync('bash', ['-c', `set -uo pipefail\n${block}\nprintf '%s' "$GDS_SESSION"`],
|
|
283
|
-
{ encoding: 'utf8', env: { PATH: process.env.PATH, HOME: empty } });
|
|
284
|
-
assert.equal(r3.stdout, path.join(empty, '.config', 'otb', SESSION_FILE));
|
|
285
|
-
|
|
286
|
-
fs.rmSync(home, { recursive: true, force: true });
|
|
287
|
-
fs.rmSync(empty, { recursive: true, force: true });
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
// box-heartbeat.sh is the odd one out: its override variable (OTB_GDS_SESSION) is
|
|
291
|
-
// NOT the variable it resolves into (SF). A first cut of the fix guarded on the
|
|
292
|
-
// override but assigned to SF, so a caller that SET the override had it silently
|
|
293
|
-
// dropped and fell through to the legacy default — the exact bug being fixed,
|
|
294
|
-
// reintroduced through the back door. Pin all three branches.
|
|
295
|
-
// box-heartbeat.sh's resolver, pulled straight out of the committed file. Shared by the
|
|
296
|
-
// two tests below so there is a single extraction to fix when the names next move.
|
|
297
|
-
function heartbeatSfBlock() {
|
|
298
|
-
const src = fs.readFileSync(path.join(INFRA, 'box-heartbeat.sh'), 'utf8');
|
|
299
|
-
const start = src.indexOf('SF="${OTB_GDS_SESSION:-}"');
|
|
300
|
-
// Prefix, not the whole line — see resolverBlock above.
|
|
301
|
-
const endPrefix = 'SF="${SF:-$HOME/.config/otb/';
|
|
302
|
-
const at = src.indexOf(endPrefix, start);
|
|
303
|
-
const close = at === -1 ? -1 : src.indexOf('"', at + endPrefix.length);
|
|
304
|
-
assert.ok(start !== -1 && at !== -1 && close !== -1,
|
|
305
|
-
'could not locate the SF resolver in box-heartbeat.sh');
|
|
306
|
-
return src.slice(start, close + 1);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
test('box-heartbeat.sh honors OTB_GDS_SESSION, scans, then falls back', (t) => {
|
|
310
|
-
if (process.platform === 'win32') return t.skip(SHELL_PATH_SPELLING_SKIP);
|
|
311
|
-
const probe = bash(['-n', 'infra/box-heartbeat.sh']);
|
|
312
|
-
if (!probe) return t.skip('bash not available on this machine');
|
|
313
|
-
|
|
314
|
-
const block = heartbeatSfBlock();
|
|
315
|
-
|
|
316
|
-
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'hbsess-'));
|
|
317
|
-
const configured = path.join(home, '.config', 'cloudbongos');
|
|
318
|
-
fs.mkdirSync(configured, { recursive: true });
|
|
319
|
-
fs.writeFileSync(path.join(configured, 'gds-session.json'), '{"token":"fresh"}');
|
|
320
|
-
|
|
321
|
-
const run = (env, h = home) => spawnSync('bash',
|
|
322
|
-
['-c', `set -euo pipefail\n${block}\nprintf '%s' "$SF"`],
|
|
323
|
-
{ encoding: 'utf8', env: { PATH: process.env.PATH, HOME: h, ...env } }).stdout;
|
|
324
|
-
|
|
325
|
-
assert.equal(run({ OTB_GDS_SESSION: '/explicit/override.json' }), '/explicit/override.json',
|
|
326
|
-
'the OTB_GDS_SESSION override must win — guarding on it but assigning elsewhere drops it');
|
|
327
|
-
assert.equal(run({}), path.join(configured, 'gds-session.json'));
|
|
328
|
-
|
|
329
|
-
const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'hbsess-empty-'));
|
|
330
|
-
assert.equal(run({}, empty), path.join(empty, '.config', 'otb', SESSION_FILE));
|
|
331
|
-
|
|
332
|
-
fs.rmSync(home, { recursive: true, force: true });
|
|
333
|
-
fs.rmSync(empty, { recursive: true, force: true });
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
// The rename leaves the fleet in FOUR states at once, and the two tests above only cover
|
|
337
|
-
// the pre-rename half (their fixtures write the old name). These are the other two, and
|
|
338
|
-
// they are the ones a rename actually breaks (task 1003704). Same extracted resolver — one
|
|
339
|
-
// harness for this block, deliberately, so part 4 (task 1003706) has one place to change.
|
|
340
|
-
test('the box resolver handles a post-rename box and a migrated one', (t) => {
|
|
341
|
-
const probe = bash(['-n', 'infra/box-heartbeat.sh']);
|
|
342
|
-
if (!probe) return t.skip('bash not available on this machine');
|
|
343
|
-
|
|
344
|
-
const block = heartbeatSfBlock();
|
|
345
|
-
const run = (h) => spawnSync('bash', ['-c', `set -euo pipefail\n${block}\nprintf '%s' "$SF"`],
|
|
346
|
-
{ encoding: 'utf8', env: { PATH: process.env.PATH, HOME: h } }).stdout;
|
|
347
|
-
|
|
348
|
-
// A box provisioned AFTER the rename has ONLY the new name. Before the glob was widened
|
|
349
|
-
// this fell through to the historical default, which does not exist, so every heartbeat
|
|
350
|
-
// tick failed with a misleading "no session".
|
|
351
|
-
const fresh = fs.mkdtempSync(path.join(os.tmpdir(), 'hbsess-new-'));
|
|
352
|
-
const freshCfg = path.join(fresh, '.config', 'cloudbongos');
|
|
353
|
-
fs.mkdirSync(freshCfg, { recursive: true });
|
|
354
|
-
fs.writeFileSync(path.join(freshCfg, SESSION_FILE), '{"token":"fresh"}');
|
|
355
|
-
const tail = (p) => p.replace(/\\/g, '/').split('/').slice(-2).join('/');
|
|
356
|
-
assert.equal(tail(run(fresh)), `cloudbongos/${SESSION_FILE}`,
|
|
357
|
-
'a box provisioned after the rename must resolve the new name');
|
|
358
|
-
|
|
359
|
-
// A MIGRATED box carries both, the new one written later by the CLI's migrate-on-read.
|
|
360
|
-
// Newest-wins must take the new one; taking the older would read a stale token and report
|
|
361
|
-
// the misleading "session expired" of task 1003252.
|
|
362
|
-
const both = fs.mkdtempSync(path.join(os.tmpdir(), 'hbsess-both-'));
|
|
363
|
-
const bothCfg = path.join(both, '.config', 'cloudbongos');
|
|
364
|
-
fs.mkdirSync(bothCfg, { recursive: true });
|
|
365
|
-
const legacyName = ic.SESSION_FILENAMES[1];
|
|
366
|
-
fs.writeFileSync(path.join(bothCfg, legacyName), '{"token":"stale"}');
|
|
367
|
-
fs.writeFileSync(path.join(bothCfg, SESSION_FILE), '{"token":"fresh"}');
|
|
368
|
-
const older = new Date(Date.now() - 60_000);
|
|
369
|
-
fs.utimesSync(path.join(bothCfg, legacyName), older, older);
|
|
370
|
-
assert.equal(tail(run(both)), `cloudbongos/${SESSION_FILE}`,
|
|
371
|
-
'a migrated box must prefer the newly written file, not the superseded one');
|
|
372
|
-
|
|
373
|
-
fs.rmSync(fresh, { recursive: true, force: true });
|
|
374
|
-
fs.rmSync(both, { recursive: true, force: true });
|
|
375
|
-
});
|
package/tests/local_preview.mjs
CHANGED
|
@@ -41,18 +41,6 @@ t('hasGameEntry: keyed on the preview entry file', () => {
|
|
|
41
41
|
assert.equal(lib.hasGameEntry({ fs: { existsSync: () => false } }), false);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
t('onBox: any box marker ⇒ true; none ⇒ false (keeps the localhost path off a box, task 1443)', () => {
|
|
45
|
-
const only = (target) => ({ fs: { existsSync: (p) => p === target } });
|
|
46
|
-
assert.equal(lib.onBox(only('/etc/otb/box.env')), true, 'box.env');
|
|
47
|
-
assert.equal(lib.onBox(only('/usr/local/bin/box-preview')), true, 'box-preview wrapper');
|
|
48
|
-
assert.equal(lib.onBox(only('/etc/systemd/system/box-game-preview.service')), true, 'box-game-preview unit');
|
|
49
|
-
assert.equal(lib.onBox({ fs: { existsSync: () => false } }), false, 'a laptop has none of them');
|
|
50
|
-
// honors the dep names sandbox-stage.js injects, and a throwing existsSync is swallowed → false
|
|
51
|
-
assert.equal(lib.onBox({ boxEnvPath: '/custom/box.env', fs: { existsSync: (p) => p === '/custom/box.env' } }), true);
|
|
52
|
-
assert.equal(lib.onBox({ previewWrapper: '/custom/box-preview', fs: { existsSync: (p) => p === '/custom/box-preview' } }), true);
|
|
53
|
-
assert.equal(lib.onBox({ fs: { existsSync: () => { throw new Error('EACCES'); } } }), false, 'throwing existsSync → false, not a crash');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
44
|
t('readPid: parses a pidfile; null on garbage/missing', () => {
|
|
57
45
|
assert.equal(lib.readPid({ fs: { readFileSync: () => '12345\n' } }), 12345);
|
|
58
46
|
assert.equal(lib.readPid({ fs: { readFileSync: () => 'garbage' } }), null);
|
|
@@ -22,7 +22,7 @@ test('roster covers the decided ~16 modules + kernel, each with non-empty globs'
|
|
|
22
22
|
// the decided roster from docs/design/modular-architecture/02-module-map.md §6 + kernel
|
|
23
23
|
for (const k of ['kernel', 'lifecycle', 'economy', 'grading', 'memory', 'onboarding',
|
|
24
24
|
'ideas', 'builder-settings', 'sessions', 'autonomy', 'security', 'hall-ui',
|
|
25
|
-
'status-ui', 'game', 'art-pipeline', 'discord', '
|
|
25
|
+
'status-ui', 'game', 'art-pipeline', 'discord', 'control-plane']) {
|
|
26
26
|
assert.ok(keys.includes(k), `roster missing module "${k}"`);
|
|
27
27
|
const e = scope.scopeForModule(k);
|
|
28
28
|
assert.ok(e && Array.isArray(e.globs) && e.globs.length > 0, `module "${k}" has no globs`);
|
|
@@ -97,7 +97,7 @@ test('`protected` is DERIVED from the imported PROTECTED_GLOBS, never hand-set',
|
|
|
97
97
|
// inside a module, which is the core->module edge ADR 0083 rule 1 forbids.
|
|
98
98
|
const PROTECTED_MODULES = [
|
|
99
99
|
'agents',
|
|
100
|
-
'
|
|
100
|
+
'control-plane',
|
|
101
101
|
'devsecops',
|
|
102
102
|
'government',
|
|
103
103
|
'grading',
|
|
@@ -119,7 +119,7 @@ test('the seven trust-core modules can never leave the protected set', () => {
|
|
|
119
119
|
// with the rest: their territory IS the trust / ship / authz core, so an edit
|
|
120
120
|
// that unprotects one is a security regression rather than a roster change.
|
|
121
121
|
// ADR 0314 narrows which MODULES derive protected; it must never reach these.
|
|
122
|
-
for (const k of ['kernel', 'lifecycle', 'government', 'grading', '
|
|
122
|
+
for (const k of ['kernel', 'lifecycle', 'government', 'grading', 'control-plane', 'devsecops', 'methodology']) {
|
|
123
123
|
assert.equal(scope.scopeForModule(k).protected, true, `${k} must stay protected`);
|
|
124
124
|
}
|
|
125
125
|
});
|
|
@@ -524,10 +524,10 @@ test('a cross-cutting module owns its paths everywhere EXCEPT the drift advisory
|
|
|
524
524
|
});
|
|
525
525
|
|
|
526
526
|
test('real cross-module contact is still flagged — the signal the advisory exists for', () => {
|
|
527
|
-
// "You said lifecycle, you touched
|
|
527
|
+
// "You said lifecycle, you touched discord."
|
|
528
528
|
assert.deepEqual(
|
|
529
|
-
scope.moduleDriftFiles('lifecycle', ['modules/
|
|
530
|
-
['modules/
|
|
529
|
+
scope.moduleDriftFiles('lifecycle', ['modules/discord/discord-bot.js']),
|
|
530
|
+
['modules/discord/discord-bot.js'],
|
|
531
531
|
);
|
|
532
532
|
// Its own territory, and territory no module owns, both stay quiet.
|
|
533
533
|
assert.deepEqual(scope.moduleDriftFiles('lifecycle', ['modules/lifecycle/rot.js']), []);
|
|
@@ -557,11 +557,11 @@ test('devsecops is deliberately NOT cross-cutting (task 1003770)', () => {
|
|
|
557
557
|
|
|
558
558
|
test('moduleDriftFiles degrades safely on junk input', () => {
|
|
559
559
|
// ship-flow calls this with whatever the claim carries; it must never throw.
|
|
560
|
-
assert.deepEqual(scope.moduleDriftFiles('not-a-module', ['modules/
|
|
560
|
+
assert.deepEqual(scope.moduleDriftFiles('not-a-module', ['modules/discord/app.js']), [],
|
|
561
561
|
'an unknown key has no declared scope to stray from');
|
|
562
562
|
assert.deepEqual(scope.moduleDriftFiles('lifecycle', []), []);
|
|
563
563
|
assert.deepEqual(scope.moduleDriftFiles('lifecycle', null), []);
|
|
564
|
-
assert.deepEqual(scope.moduleDriftFiles(null, ['modules/
|
|
564
|
+
assert.deepEqual(scope.moduleDriftFiles(null, ['modules/discord/app.js']), []);
|
|
565
565
|
});
|
|
566
566
|
|
|
567
567
|
// EVERY PROTECTED PATH HAS AN OWNING MODULE (task 1004111).
|
|
@@ -642,7 +642,7 @@ test('adopting the unowned paths did NOT change which modules are protected', ()
|
|
|
642
642
|
// The property that made assignment safe rather than a re-coupling: every
|
|
643
643
|
// receiving module was ALREADY protected, so no goal became Archon-only that
|
|
644
644
|
// was not before. If this fails, an adoption widened the trust boundary.
|
|
645
|
-
for (const k of ['kernel', 'lifecycle', '
|
|
645
|
+
for (const k of ['kernel', 'lifecycle', 'control-plane', 'devsecops', 'methodology']) {
|
|
646
646
|
assert.equal(scope.scopeForModule(k).protected, true, `${k} was already protected and must stay so`);
|
|
647
647
|
}
|
|
648
648
|
// and the adoption changed no COUNT either. Pinned against PROTECTED_MODULES
|
package/tests/module_catalog.mjs
CHANGED
|
@@ -25,7 +25,7 @@ const CORE = path.join(path.sep, 'srv', 'app', 'node_modules', '@bongos', 'core'
|
|
|
25
25
|
const INST = path.join(path.sep, 'srv', 'app');
|
|
26
26
|
|
|
27
27
|
const manifest = (over = {}) => ({
|
|
28
|
-
key: 'x', title: 'X', description: 'a module', coreVersion: '^1.0.0', contributes: {}, ...over,
|
|
28
|
+
key: 'x', title: 'X', description: 'a module', version: '1.0.0', coreVersion: '^1.0.0', contributes: {}, ...over,
|
|
29
29
|
});
|
|
30
30
|
const loadedIn = (root, key, over = {}) => ({
|
|
31
31
|
key, dir: path.join(root, 'modules', key), manifest: manifest({ key, title: key, ...over }),
|
package/tests/module_cli.mjs
CHANGED
|
@@ -102,8 +102,8 @@ test('evaluateUpgrade: all enabled modules satisfied → ok', () => {
|
|
|
102
102
|
const v = mod.evaluateUpgrade({
|
|
103
103
|
core: '1.0.0',
|
|
104
104
|
rows: [
|
|
105
|
-
{ key: 'a', coreVersion: '^1.0.0', enabled: true, satisfied: true },
|
|
106
|
-
{ key: 'b', coreVersion: '^1.0.0', enabled: false, satisfied: true },
|
|
105
|
+
{ key: 'a', version: '1.0.0', coreVersion: '^1.0.0', enabled: true, satisfied: true },
|
|
106
|
+
{ key: 'b', version: '1.0.0', coreVersion: '^1.0.0', enabled: false, satisfied: true },
|
|
107
107
|
],
|
|
108
108
|
});
|
|
109
109
|
assert.equal(v.ok, true);
|
|
@@ -114,8 +114,8 @@ test('evaluateUpgrade: an ENABLED mismatch refuses; a DISABLED mismatch only war
|
|
|
114
114
|
const v = mod.evaluateUpgrade({
|
|
115
115
|
core: '1.0.0',
|
|
116
116
|
rows: [
|
|
117
|
-
{ key: 'future', coreVersion: '^2.0.0', enabled: true, satisfied: false },
|
|
118
|
-
{ key: 'old-wip', coreVersion: '^2.0.0', enabled: false, satisfied: false },
|
|
117
|
+
{ key: 'future', version: '1.0.0', coreVersion: '^2.0.0', enabled: true, satisfied: false },
|
|
118
|
+
{ key: 'old-wip', version: '1.0.0', coreVersion: '^2.0.0', enabled: false, satisfied: false },
|
|
119
119
|
],
|
|
120
120
|
});
|
|
121
121
|
assert.equal(v.ok, false, 'an enabled mismatch must block (fail-closed)');
|
|
@@ -165,7 +165,7 @@ test('cmdUpgrade: exit 0 with no modules, exit 1 when an enabled module is incom
|
|
|
165
165
|
|
|
166
166
|
const bad = mod.cmdUpgrade([], {
|
|
167
167
|
...sink,
|
|
168
|
-
gather: () => ({ core: '1.0.0', errors: [], rows: [{ key: 'future', coreVersion: '^2.0.0', enabled: true, satisfied: false }] }),
|
|
168
|
+
gather: () => ({ core: '1.0.0', errors: [], rows: [{ key: 'future', version: '1.0.0', coreVersion: '^2.0.0', enabled: true, satisfied: false }] }),
|
|
169
169
|
});
|
|
170
170
|
assert.equal(bad, 1);
|
|
171
171
|
assert.ok(lines.some((l) => /REFUSING/.test(l)), 'refusal is reported');
|
|
@@ -204,7 +204,7 @@ test('checkModulePublishability: fails a module containing denied content (game
|
|
|
204
204
|
const dir = path.join(root, 'game');
|
|
205
205
|
fs.mkdirSync(path.join(dir, 'public', 'assets'), { recursive: true });
|
|
206
206
|
fs.writeFileSync(path.join(dir, 'module.json'), JSON.stringify({
|
|
207
|
-
key: 'game', title: 'Game', description: 'x', coreVersion: '^1.0.0', license: 'AGPL-3.0', contributes: {},
|
|
207
|
+
key: 'game', title: 'Game', description: 'x', version: '1.0.0', coreVersion: '^1.0.0', license: 'AGPL-3.0', contributes: {},
|
|
208
208
|
}, null, 2));
|
|
209
209
|
fs.writeFileSync(path.join(dir, 'public', 'assets', 'reference-map.png'), 'not-a-real-png');
|
|
210
210
|
const r = mod.checkModulePublishability('game', { modulesDir: root });
|
|
@@ -427,10 +427,10 @@ test('evaluateUpgrade: sorts self-declared deprecations, and flags the ones the
|
|
|
427
427
|
const v = mod.evaluateUpgrade({
|
|
428
428
|
core: '1.13.0',
|
|
429
429
|
rows: [
|
|
430
|
-
{ key: 'healthy', coreVersion: '^1.0.0', enabled: true, satisfied: true, maintenance: { status: 'core-maintained' } },
|
|
431
|
-
{ key: 'sunset', coreVersion: '^1.0.0', enabled: false, satisfied: true, maintenance: { status: 'deprecated', successor: 'newer', removeAfter: '1.12.0' } },
|
|
432
|
-
{ key: 'sunset-later', coreVersion: '^1.0.0', enabled: false, satisfied: true, maintenance: { status: 'deprecated', note: 'going', removeAfter: '1.20.0' } },
|
|
433
|
-
{ key: 'nobody', coreVersion: '^1.0.0', enabled: false, satisfied: true, maintenance: { status: 'orphaned', note: 'unowned' } },
|
|
430
|
+
{ key: 'healthy', version: '1.0.0', coreVersion: '^1.0.0', enabled: true, satisfied: true, maintenance: { status: 'core-maintained' } },
|
|
431
|
+
{ key: 'sunset', version: '1.0.0', coreVersion: '^1.0.0', enabled: false, satisfied: true, maintenance: { status: 'deprecated', successor: 'newer', removeAfter: '1.12.0' } },
|
|
432
|
+
{ key: 'sunset-later', version: '1.0.0', coreVersion: '^1.0.0', enabled: false, satisfied: true, maintenance: { status: 'deprecated', note: 'going', removeAfter: '1.20.0' } },
|
|
433
|
+
{ key: 'nobody', version: '1.0.0', coreVersion: '^1.0.0', enabled: false, satisfied: true, maintenance: { status: 'orphaned', note: 'unowned' } },
|
|
434
434
|
],
|
|
435
435
|
});
|
|
436
436
|
assert.equal(v.ok, true, 'a deprecation is not a compatibility failure — it must not block a bump');
|
|
@@ -443,8 +443,8 @@ test('evaluateUpgrade: a module with no maintenance block is reported as undecla
|
|
|
443
443
|
const v = mod.evaluateUpgrade({
|
|
444
444
|
core: '1.0.0',
|
|
445
445
|
rows: [
|
|
446
|
-
{ key: 'owned', coreVersion: '^1.0.0', enabled: true, satisfied: true, maintenance: { status: 'core-maintained' } },
|
|
447
|
-
{ key: 'orphan-risk', coreVersion: '^1.0.0', enabled: false, satisfied: true },
|
|
446
|
+
{ key: 'owned', version: '1.0.0', coreVersion: '^1.0.0', enabled: true, satisfied: true, maintenance: { status: 'core-maintained' } },
|
|
447
|
+
{ key: 'orphan-risk', version: '1.0.0', coreVersion: '^1.0.0', enabled: false, satisfied: true },
|
|
448
448
|
],
|
|
449
449
|
});
|
|
450
450
|
assert.deepEqual(v.undeclared.map((r) => r.key), ['orphan-risk']);
|
|
@@ -486,7 +486,7 @@ test('cmdUpgrade: a DISABLED incompatible module is reported with its owner, not
|
|
|
486
486
|
core: '2.0.0',
|
|
487
487
|
errors: [],
|
|
488
488
|
rows: [{
|
|
489
|
-
key: 'stale-catalog-entry', coreVersion: '^1.0.0', enabled: false, satisfied: false,
|
|
489
|
+
key: 'stale-catalog-entry', version: '1.0.0', coreVersion: '^1.0.0', enabled: false, satisfied: false,
|
|
490
490
|
maintainer: 'Ada <ada@example.com>', maintenance: { status: 'maintained' },
|
|
491
491
|
}],
|
|
492
492
|
}),
|
|
@@ -507,8 +507,8 @@ test('cmdUpgrade: reports an expired deprecation and modules with no declared up
|
|
|
507
507
|
core: '1.13.0',
|
|
508
508
|
errors: [],
|
|
509
509
|
rows: [
|
|
510
|
-
{ key: 'sunset', coreVersion: '^1.0.0', enabled: false, satisfied: true, maintenance: { status: 'deprecated', successor: 'newer', removeAfter: '1.12.0' } },
|
|
511
|
-
{ key: 'unowned', coreVersion: '^1.0.0', enabled: false, satisfied: true },
|
|
510
|
+
{ key: 'sunset', version: '1.0.0', coreVersion: '^1.0.0', enabled: false, satisfied: true, maintenance: { status: 'deprecated', successor: 'newer', removeAfter: '1.12.0' } },
|
|
511
|
+
{ key: 'unowned', version: '1.0.0', coreVersion: '^1.0.0', enabled: false, satisfied: true },
|
|
512
512
|
],
|
|
513
513
|
}),
|
|
514
514
|
});
|
|
@@ -54,10 +54,10 @@ test('collectContributions: ABSENT → empty; REGISTERED → each invoked with c
|
|
|
54
54
|
assert.deepEqual(await seams.collectContributions('profile.fields', { builderId: 7 }), []);
|
|
55
55
|
// registered — both function-contributors get the ctx and return their slice
|
|
56
56
|
const seen = [];
|
|
57
|
-
seams.contribute('profile.fields', (ctx) => { seen.push(ctx.builderId); return {
|
|
57
|
+
seams.contribute('profile.fields', (ctx) => { seen.push(ctx.builderId); return { gallery: { x: 1 } }; });
|
|
58
58
|
seams.contribute('profile.fields', async (ctx) => { seen.push(ctx.builderId); return { discord: { linked: true } }; });
|
|
59
59
|
const out = await seams.collectContributions('profile.fields', { builderId: 7 });
|
|
60
|
-
assert.deepEqual(out, [{
|
|
60
|
+
assert.deepEqual(out, [{ gallery: { x: 1 } }, { discord: { linked: true } }]);
|
|
61
61
|
assert.deepEqual(seen, [7, 7]);
|
|
62
62
|
seams._reset();
|
|
63
63
|
});
|
|
@@ -86,13 +86,13 @@ test('collectContributions: a non-function contribution is returned as-is', asyn
|
|
|
86
86
|
test('mergeContributions: adds new keys, refuses to overwrite a core key', () => {
|
|
87
87
|
const base = { builder: { id: 1 }, discord: { linked: false } };
|
|
88
88
|
const { base: merged, collisions } = seams.mergeContributions(base, [
|
|
89
|
-
{
|
|
89
|
+
{ gallery: { connected: true } }, // new key → added
|
|
90
90
|
{ discord: { linked: true } }, // collides with a core key → ignored
|
|
91
91
|
null, // ignored
|
|
92
92
|
{ art: { keySet: false } }, // new key → added
|
|
93
93
|
]);
|
|
94
94
|
assert.equal(merged, base); // mutates + returns the same object
|
|
95
|
-
assert.deepEqual(merged.
|
|
95
|
+
assert.deepEqual(merged.gallery, { connected: true });
|
|
96
96
|
assert.deepEqual(merged.art, { keySet: false });
|
|
97
97
|
assert.deepEqual(merged.discord, { linked: false }); // core value preserved
|
|
98
98
|
assert.deepEqual(collisions, ['discord']);
|
|
@@ -139,9 +139,9 @@ test('settings-panel seam: ABSENT → empty; REGISTERED → present, sorted by o
|
|
|
139
139
|
assert.deepEqual(OTB.settingsPanels(), []);
|
|
140
140
|
// registered — registration order is by `order`, not insertion order
|
|
141
141
|
OTB.contributeSettingsPanel({ id: 'discord', title: 'Connections', order: 30, mount() {} });
|
|
142
|
-
OTB.contributeSettingsPanel({ id: '
|
|
142
|
+
OTB.contributeSettingsPanel({ id: 'art', title: 'Art keys', order: 10, mount() {} });
|
|
143
143
|
const ids = OTB.settingsPanels().map((p) => p.id);
|
|
144
|
-
assert.deepEqual(ids, ['
|
|
144
|
+
assert.deepEqual(ids, ['art', 'discord']);
|
|
145
145
|
// a panel without id or mount() is rejected loudly
|
|
146
146
|
assert.throws(() => OTB.contributeSettingsPanel({ title: 'no id' }), /requires id \+ mount/);
|
|
147
147
|
assert.throws(() => OTB.contributeSettingsPanel({ id: 'x' }), /requires id \+ mount/);
|
package/tests/module_loader.mjs
CHANGED
|
@@ -33,17 +33,17 @@ function fixtureRoot(spec) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const validManifest = (key, extra = {}) => ({
|
|
36
|
-
key, title: key, description: 'd', coreVersion: '^1.0.0', contributes: {}, ...extra,
|
|
36
|
+
key, title: key, description: 'd', version: '1.0.0', coreVersion: '^1.0.0', contributes: {}, ...extra,
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
test('discoverModules: loads a valid module, ignores a dir without module.json', () => {
|
|
40
40
|
const root = fixtureRoot({
|
|
41
|
-
'
|
|
41
|
+
'art-pipeline': { manifest: validManifest('art-pipeline', { contributes: { routes: ['stage'] } }) },
|
|
42
42
|
'not-a-module': { files: { 'readme.txt': 'hi' } },
|
|
43
43
|
});
|
|
44
44
|
const { loaded, errors, skipped } = loader.discoverModules({ roots: [root], core: '1.0.0' });
|
|
45
45
|
assert.equal(loaded.length, 1);
|
|
46
|
-
assert.equal(loaded[0].key, '
|
|
46
|
+
assert.equal(loaded[0].key, 'art-pipeline');
|
|
47
47
|
assert.deepEqual(errors, []);
|
|
48
48
|
assert.deepEqual(skipped, []);
|
|
49
49
|
});
|
|
@@ -71,7 +71,7 @@ test('discoverModules: key must match the directory name', () => {
|
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
test('discoverModules: a coreVersion mismatch is SKIPPED (not loaded, not an error)', () => {
|
|
74
|
-
const root = fixtureRoot({ future: { manifest: validManifest('future', { coreVersion: '^2.0.0' }) } });
|
|
74
|
+
const root = fixtureRoot({ future: { manifest: validManifest('future', { version: '1.0.0', coreVersion: '^2.0.0' }) } });
|
|
75
75
|
const { loaded, skipped, errors } = loader.discoverModules({ roots: [root], core: '1.0.0' });
|
|
76
76
|
assert.equal(loaded.length, 0);
|
|
77
77
|
assert.equal(errors.length, 0);
|
|
@@ -226,7 +226,7 @@ test('task 1003820: mountModuleRoutes refuses two enabled providers of one port'
|
|
|
226
226
|
});
|
|
227
227
|
|
|
228
228
|
test('the live tree discovers the extracted modules cleanly (BV1.R43/R48/R47/R49/R72)', () => {
|
|
229
|
-
//
|
|
229
|
+
// the first module graduated in BV1.R43 (since retired), art-pipeline next
|
|
230
230
|
// (BV1.R48 / task 1417), discord next (BV1.R47 / task 1416), game next
|
|
231
231
|
// (BV1.R49 / c9cb5ea1 — public/ + src/world|rooms → modules/game/), then the
|
|
232
232
|
// declaration-only character-anim module (ADR 0088 / task 1558), then memory —
|
|
@@ -276,7 +276,7 @@ test('the live tree discovers the extracted modules cleanly (BV1.R43/R48/R47/R49
|
|
|
276
276
|
// That is precisely how the agents regression reached an open PR (task 1002485).
|
|
277
277
|
loader._reset();
|
|
278
278
|
const snap = loader.loaderSnapshot();
|
|
279
|
-
assert.deepEqual(snap.loaded, ['agents', 'autonomy', 'builder-settings', 'copy-desk', '
|
|
279
|
+
assert.deepEqual(snap.loaded, ['agents', 'autonomy', 'builder-settings', 'copy-desk', 'discord', 'economy', 'government', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'npm-release', 'onboarding', 'platform-identity', 'provisioning', 'public-landing', 'security', 'sessions', 'specialities', 'status-ui', 'ui-design']);
|
|
280
280
|
assert.deepEqual(snap.errors, []);
|
|
281
281
|
assert.deepEqual(snap.skipped, []);
|
|
282
282
|
loader._reset();
|
|
@@ -15,7 +15,7 @@ const baseManifest = () => ({
|
|
|
15
15
|
key: 'dev-box',
|
|
16
16
|
title: 'Dev boxes',
|
|
17
17
|
description: 'Per-builder cloud dev environments.',
|
|
18
|
-
coreVersion: '^1.0.0',
|
|
18
|
+
version: '1.0.0', coreVersion: '^1.0.0',
|
|
19
19
|
contributes: { routes: ['box'], pollers: ['box-lifecycle'], uiSections: ['boxes'] },
|
|
20
20
|
});
|
|
21
21
|
|
|
@@ -62,7 +62,7 @@ test('isValidRange', () => {
|
|
|
62
62
|
// --- manifest validation ---
|
|
63
63
|
|
|
64
64
|
test('a minimal valid manifest passes', () => {
|
|
65
|
-
const r = validateManifest({ key: 'x', title: 'X', description: 'd', coreVersion: '^1.0.0', contributes: {} });
|
|
65
|
+
const r = validateManifest({ key: 'x', title: 'X', description: 'd', version: '1.0.0', coreVersion: '^1.0.0', contributes: {} });
|
|
66
66
|
assert.equal(r.valid, true, r.errors.join('; '));
|
|
67
67
|
});
|
|
68
68
|
|
|
@@ -81,7 +81,7 @@ test('a full valid manifest passes (provides/consumes/prerequisites/migrations)'
|
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
test('missing required fields each produce an error', () => {
|
|
84
|
-
for (const field of ['key', 'title', 'description', 'coreVersion']) {
|
|
84
|
+
for (const field of ['key', 'title', 'description', 'version', 'coreVersion']) {
|
|
85
85
|
const m = baseManifest();
|
|
86
86
|
delete m[field];
|
|
87
87
|
const r = validateManifest(m);
|
|
@@ -97,11 +97,20 @@ test('bad key format is rejected', () => {
|
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
test('invalid coreVersion range is rejected', () => {
|
|
100
|
-
const r = validateManifest({ ...baseManifest(), coreVersion: 'whenever' });
|
|
100
|
+
const r = validateManifest({ ...baseManifest(), version: '1.0.0', coreVersion: 'whenever' });
|
|
101
101
|
assert.equal(r.valid, false);
|
|
102
102
|
assert.ok(r.errors.some((e) => e.startsWith('coreVersion')));
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
+
test('version must be an exact X.Y.Z — a range or junk is rejected (task 1003781)', () => {
|
|
106
|
+
for (const bad of ['^1.0.0', '1.0', 'v1.0.0', '1.0.0-beta', '']) {
|
|
107
|
+
const r = validateManifest({ ...baseManifest(), version: bad });
|
|
108
|
+
assert.equal(r.valid, false, `"${bad}" should be rejected`);
|
|
109
|
+
assert.ok(r.errors.some((e) => e.startsWith('version')));
|
|
110
|
+
}
|
|
111
|
+
assert.equal(validateManifest({ ...baseManifest(), version: '2.10.3' }).valid, true);
|
|
112
|
+
});
|
|
113
|
+
|
|
105
114
|
test('unknown contributes key is rejected (typo guard)', () => {
|
|
106
115
|
const r = validateManifest({ ...baseManifest(), contributes: { routs: ['box'] } });
|
|
107
116
|
assert.equal(r.valid, false);
|