@bongos/core 1.18.19 → 1.19.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 +514 -194
- package/.claude/skills/builder-box/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +1 -1
- package/.claude/skills/idea-triage/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +5 -5
- package/.claude/skills/scan-before-install/SKILL.md +165 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +63 -41
- package/clients/bongos-client/index.cjs +63 -41
- package/clients/bongos-client/index.d.ts +78 -44
- package/clients/bongos-client/index.mjs +63 -41
- package/docs/adr/0020-security-model-public-stub.md +1 -1
- package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +2 -0
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +2 -1
- package/docs/adr/0155-adopt-private-repo-widen-oauth-scope.md +51 -0
- package/docs/adr/0156-user-set-display-name-wins.md +62 -0
- package/docs/adr/0157-archon-is-rank-and-identity-only.md +93 -0
- package/docs/adr/0158-choke-point-verdict-taxonomy.md +174 -0
- package/docs/adr/0159-merge-gate-reconciliation.md +88 -0
- package/docs/adr/0160-main-audit-repo-verifiable-caller.md +77 -0
- package/docs/adr/README.md +8 -0
- package/docs/api/openapi.json +1209 -233
- package/docs/api-reference.md +61 -46
- package/docs/architecture.md +1 -1
- package/docs/canonical-permissions.md +61 -16
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +4 -1
- package/docs/onboarding/diagrams/assertions.json +3 -3
- package/docs/recipes/core-release-pipeline.md +179 -0
- package/docs/recipes/packaging-the-core.md +2 -0
- package/docs/recipes/parallel-goal-waves.md +1 -3
- package/docs/recipes/private-npm-distribution.md +2 -0
- package/docs/recipes/upgrading-the-core.md +2 -0
- package/migrations/core_197_display_name_source.sql +56 -0
- package/migrations/core_204_rekey_active_claim_per_session_worktree.sql +77 -0
- package/modules/autonomy/routes/autonomy.js +1 -1
- package/modules/builder-settings/db.js +7 -1
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +63 -41
- package/modules/dev-box/routes/box.js +11 -9
- package/modules/discord/auth.js +3 -1
- package/modules/discord/routes/discord.js +8 -2
- package/modules/discord/ship-broadcast.js +48 -1
- package/modules/governance/catalog.js +70 -20
- package/modules/governance/db.js +102 -0
- package/modules/governance/migrations/governance_006_version_create.sql +38 -0
- package/modules/governance/migrations/governance_007_delegate_archon_ops_to_metic.sql +72 -0
- package/modules/governance/routes/governance.js +235 -6
- package/modules/grading/grader-workers/index.js +18 -9
- package/modules/hall-ui/public/gate.js +6 -2
- package/modules/hall-ui/public/goals-page.js +6 -2
- package/modules/hall-ui/public/governance.css +100 -0
- package/modules/hall-ui/public/governance.html +77 -0
- package/modules/hall-ui/public/governance.js +353 -0
- package/modules/hall-ui/public/harbor.js +14 -8
- package/modules/hall-ui/public/instances-panel.js +3 -1
- package/modules/hall-ui/public/new-project-wizard.js +6 -0
- package/modules/hall-ui/public/people.html +51 -0
- package/modules/hall-ui/public/people.js +100 -0
- package/modules/hall-ui/public/profile.html +2 -1
- package/modules/hall-ui/public/profile.js +9 -3
- package/modules/hall-ui/public/sessions.js +4 -2
- package/modules/hall-ui/public/shell.js +28 -7
- package/modules/hall-ui/public/task-detail.js +6 -3
- package/modules/hall-ui/public/watch.js +17 -9
- package/modules/ideas/blockers.js +44 -7
- package/modules/ideas/routes/blockers.js +12 -7
- package/modules/ideas/routes/inbox.js +2 -2
- package/modules/lifecycle/db.js +84 -17
- package/modules/lifecycle/github-push.js +37 -10
- package/modules/lifecycle/publish-reconciler.js +128 -31
- package/modules/lifecycle/routes/claims.js +10 -4
- package/modules/lifecycle/routes/dependencies.js +2 -2
- package/modules/lifecycle/routes/done-when.js +4 -4
- package/modules/lifecycle/routes/gate-approvals.js +9 -3
- package/modules/lifecycle/routes/goals.js +5 -5
- package/modules/lifecycle/routes/tasks.js +16 -16
- package/modules/lifecycle/routes/versions.js +13 -2
- package/modules/lifecycle/ship-card.js +38 -7
- package/modules/memory/routes/memory.js +2 -2
- package/modules/onboarding/routes/access-requests.js +3 -3
- package/modules/platform-identity/routes/projects.js +10 -6
- package/modules/platform-identity/routes/scouting.js +2 -2
- package/modules/provisioning/migrations/provisioning_004_onboard_mode.sql +30 -0
- package/modules/provisioning/provisioning.js +24 -6
- package/modules/provisioning/routes/provisioning.js +24 -6
- package/modules/security/artifact-scan/aggregate.js +133 -0
- package/modules/security/artifact-scan/detectors/capabilities.js +53 -0
- package/modules/security/artifact-scan/detectors/config-obfuscation.js +41 -0
- package/modules/security/artifact-scan/detectors/credentials.js +112 -0
- package/modules/security/artifact-scan/detectors/dependencies.js +115 -0
- package/modules/security/artifact-scan/detectors/hooks.js +116 -0
- package/modules/security/artifact-scan/detectors/injection.js +118 -0
- package/modules/security/artifact-scan/detectors/mcp.js +102 -0
- package/modules/security/artifact-scan/detectors/network.js +67 -0
- package/modules/security/artifact-scan/detectors/npm-lifecycle.js +67 -0
- package/modules/security/artifact-scan/detectors/permissions.js +91 -0
- package/modules/security/artifact-scan/detectors/shared.js +219 -0
- package/modules/security/artifact-scan/fence-local.js +99 -0
- package/modules/security/artifact-scan/fetch.js +330 -0
- package/modules/security/artifact-scan/floor.js +319 -0
- package/modules/security/artifact-scan/limits.js +66 -0
- package/modules/security/artifact-scan/manifest.js +203 -0
- package/modules/security/artifact-scan/mirror.js +158 -0
- package/modules/security/artifact-scan/quarantine.js +149 -0
- package/modules/security/artifact-scan/rebuild-spec.js +120 -0
- package/modules/security/artifact-scan/report.js +155 -0
- package/modules/security/artifact-scan/sanitize.js +317 -0
- package/modules/security/migrations/security_001_tool_scans.sql +145 -0
- package/modules/security/module.json +2 -2
- package/modules/security/routes/reports.js +11 -7
- package/modules/security/routes/scans.js +115 -0
- package/modules/security/scan-store.js +214 -0
- package/modules/security/tests/aggregate.mjs +283 -0
- package/modules/security/tests/artifact_scan.mjs +591 -0
- package/modules/security/tests/floor.mjs +549 -0
- package/modules/security/tests/mirror.mjs +350 -0
- package/modules/security/tests/sanitize_rebuild.mjs +499 -0
- package/modules/security/tests/scan_store.mjs +250 -0
- package/modules/security/tests/tool_scans_migration.mjs +116 -0
- package/modules/sessions/routes/sessions.js +6 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +13 -3
- package/scripts/gds/abandon.js +1 -1
- package/scripts/gds/audit-authorship.sh +6 -0
- package/scripts/gds/box.js +1 -1
- package/scripts/gds/claim.js +15 -0
- package/scripts/gds/cli-lib.js +16 -4
- package/scripts/gds/gating.js +28 -13
- package/scripts/gds/gen-api-docs.js +1 -1
- package/scripts/gds/goal.js +234 -0
- package/scripts/gds/init.js +70 -15
- package/scripts/gds/main-audit.js +80 -11
- package/scripts/gds/mirror-redact.js +6 -0
- package/scripts/gds/newcomer-floor.js +2 -2
- package/scripts/gds/provision.js +230 -94
- package/scripts/gds/run-unit-tests.js +9 -1
- package/scripts/gds/sandbox-stage.js +66 -9
- package/scripts/gds/search-ingest-md.js +1 -0
- package/scripts/gds/sequence.js +67 -8
- package/scripts/gds/ship.js +183 -50
- package/scripts/hall-preview/server.js +1 -1
- package/scripts/migrate.sh +5 -5
- package/src/bongos/auth.js +129 -27
- package/src/bongos/db-kernel.js +66 -5
- package/src/bongos/db.js +6 -7
- package/src/bongos/github-repos.js +19 -7
- package/src/bongos/goal-scope-check.js +1 -1
- package/src/bongos/module-scope-map.js +3 -1
- package/src/bongos/permission-path-check.js +1 -1
- package/src/bongos/route-rank-check.js +73 -30
- package/src/bongos/routes/audit-log.js +4 -2
- package/src/bongos/routes/auth.js +8 -4
- package/src/bongos/routes/backup.js +7 -4
- package/src/bongos/routes/builders.js +23 -11
- package/src/bongos/routes/github.js +15 -4
- package/src/bongos/routes/llm-cache.js +5 -2
- package/src/bongos/routes/modules.js +9 -5
- package/src/bongos/routes/security.js +21 -12
- package/src/bongos/serve-internal.js +22 -7
- package/src/module-api.js +86 -13
- package/src/platform-server.js +14 -0
- package/tests/api_client.mjs +18 -2
- package/tests/api_path_404.mjs +3 -0
- package/tests/art_stage.mjs +2 -1
- package/tests/atlas.mjs +32 -4
- package/tests/auth_admission_gate.mjs +17 -5
- package/tests/auth_error_brand.mjs +41 -21
- package/tests/auth_resolve_failure.mjs +155 -0
- package/tests/blocker_link.mjs +88 -0
- package/tests/bongos_downloads.mjs +2 -1
- package/tests/box_dns_repoint.mjs +2 -0
- package/tests/box_ship_permissions.mjs +2 -0
- package/tests/box_terminal_ssrf.mjs +23 -17
- package/tests/claim_error_surface.mjs +23 -0
- package/tests/claude_materialize.mjs +4 -0
- package/tests/cli_box_scope.mjs +40 -0
- package/tests/cloud_bongos_pack.mjs +34 -4
- package/tests/conductor.mjs +2 -1
- package/tests/criterion_authoring_gate.mjs +28 -5
- package/tests/currency_label.mjs +10 -1
- package/tests/deploy_instance.mjs +2 -0
- package/tests/devbox_downloads.mjs +2 -1
- package/tests/display_name.mjs +95 -6
- package/tests/eviction_policy.mjs +2 -1
- package/tests/file_map.mjs +10 -0
- package/tests/first_admin_bootstrap.mjs +10 -2
- package/tests/first_run_engine_check.mjs +2 -1
- package/tests/fitness.mjs +26 -6
- package/tests/gate_approvals.mjs +15 -2
- package/tests/github_push_land_proof.mjs +50 -4
- package/tests/github_repos_helpers.mjs +30 -5
- package/tests/github_token_store.mjs +23 -0
- package/tests/goal_abuse_matrix.mjs +13 -3
- package/tests/goal_cli.mjs +89 -0
- package/tests/governance_abuse_matrix.mjs +386 -0
- package/tests/governance_fold.mjs +156 -0
- package/tests/governance_fold_bench.mjs +50 -0
- package/tests/governance_live_proof.mjs +201 -0
- package/tests/governance_page_gates.mjs +21 -9
- package/tests/governance_parity.mjs +11 -2
- package/tests/governance_require_permission.mjs +125 -1
- package/tests/governance_routes.mjs +334 -0
- package/tests/grader.mjs +7 -3
- package/tests/hall_nav.mjs +14 -4
- package/tests/helpers.mjs +73 -0
- package/tests/hook_core_resolve.mjs +2 -1
- package/tests/host_topology_skips.mjs +97 -0
- package/tests/infra_exec_paths.mjs +14 -4
- package/tests/init.mjs +128 -0
- package/tests/known_failing_ratchet.mjs +89 -0
- package/tests/lifecycle-card.mjs +2 -1
- package/tests/main_audit.mjs +72 -1
- package/tests/memory_isolation.mjs +56 -9
- package/tests/migrate_root_resolution.mjs +1 -1
- package/tests/mirror_redact.mjs +19 -0
- package/tests/module_api.mjs +3 -1
- package/tests/module_loader.mjs +4 -0
- package/tests/modules.mjs +35 -7
- package/tests/modules_route.mjs +29 -5
- package/tests/onboarding_observability.mjs +49 -11
- package/tests/onboarding_route_signals.mjs +1 -0
- package/tests/one_active_claim_per_session.mjs +80 -0
- package/tests/pre_compact.mjs +2 -1
- package/tests/preview_server.mjs +2 -1
- package/tests/profile_route.mjs +1 -0
- package/tests/provision.mjs +170 -0
- package/tests/provisioning_onboard_mode.mjs +180 -0
- package/tests/publish_manifest.mjs +16 -3
- package/tests/publish_reconciler.mjs +181 -10
- package/tests/rank_tier_single_source.mjs +205 -0
- package/tests/recall_hints.mjs +2 -1
- package/tests/repo_map.mjs +4 -0
- package/tests/route_permission_check.mjs +14 -5
- package/tests/rubric_fixtures.mjs +3 -0
- package/tests/run_routine_two_root.mjs +2 -0
- package/tests/sandbox_stage.mjs +69 -7
- package/tests/search_ingest.mjs +7 -0
- package/tests/security_docs.mjs +28 -6
- package/tests/sequence.mjs +64 -0
- package/tests/session-cards.mjs +2 -1
- package/tests/session_start_freshness.mjs +2 -1
- package/tests/ship_broadcast_server.mjs +63 -1
- package/tests/ship_card.mjs +66 -0
- package/tests/ship_ci_deploy.mjs +66 -1
- package/tests/ship_smoke_gate.mjs +76 -0
- package/tests/versions_route.mjs +15 -0
- package/tests/workflow_exec_paths.mjs +16 -4
- package/tests/worktree_guard.mjs +2 -1
package/tests/init.mjs
CHANGED
|
@@ -114,6 +114,33 @@ test('kickoffTasks: includes the load-bearing owner-manual steps', () => {
|
|
|
114
114
|
assert.ok(t.every((x) => x.title && x.description), 'every step has a title + description');
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
// Every kickoff task must satisfy the DB CHECK constraints (tasks_kind_check /
|
|
118
|
+
// tasks_discipline_check) or the insert is rejected — which is how the branding
|
|
119
|
+
// step vanished on 1.18.18: kind:'ideator' (a discipline, not a kind) violated
|
|
120
|
+
// tasks_kind_check and postTask absorbed the failure. Pin the vocabulary at the
|
|
121
|
+
// source so this class of bug can't reach a user again.
|
|
122
|
+
test('kickoffTasks: every kind/discipline satisfies the DB CHECK vocabulary', () => {
|
|
123
|
+
const { VALID_KINDS, VALID_DISCIPLINES } = require('../modules/lifecycle/task-classifier.js');
|
|
124
|
+
const all = [
|
|
125
|
+
...init.kickoffTasks(init.normalizeSpec({ modules: ['art-pipeline', 'discord', 'dev-box', 'game'],
|
|
126
|
+
firstAdmin: 'x', repo: { owner: 'o', name: 'n' } })),
|
|
127
|
+
init.productionTopologyKickoff(),
|
|
128
|
+
];
|
|
129
|
+
for (const t of all) {
|
|
130
|
+
assert.ok(VALID_KINDS.has(t.kind), `"${t.title}": kind '${t.kind}' violates tasks_kind_check`);
|
|
131
|
+
if (t.discipline !== undefined) {
|
|
132
|
+
assert.ok(VALID_DISCIPLINES.has(t.discipline), `"${t.title}": discipline '${t.discipline}' violates tasks_discipline_check`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('kickoffTasks: the branding step is a decision BY an ideator (kind vs discipline)', () => {
|
|
138
|
+
const look = init.kickoffTasks(fullSpec()).find((t) => /instance look/i.test(t.title));
|
|
139
|
+
assert.ok(look, 'the branding kickoff step exists');
|
|
140
|
+
assert.equal(look.kind, 'decision');
|
|
141
|
+
assert.equal(look.discipline, 'ideator');
|
|
142
|
+
});
|
|
143
|
+
|
|
117
144
|
// ---- validateSpec ----
|
|
118
145
|
test('validateSpec: complete spec → no errors', () => {
|
|
119
146
|
assert.deepEqual(init.validateSpec(fullSpec()), []);
|
|
@@ -158,6 +185,18 @@ test('versionPayload: explicit criterion_id is slugified', () => {
|
|
|
158
185
|
assert.equal(init.versionPayload(spec).criteria[0].criterion_id, 'prove-the-loop');
|
|
159
186
|
});
|
|
160
187
|
|
|
188
|
+
// versions.track is NOT NULL with no DB default — leaving it off the payload is
|
|
189
|
+
// exactly the 1.18.18 bug (every POST /versions 500ed, blocking init outright).
|
|
190
|
+
test('versionPayload: always sends an explicit, valid track', () => {
|
|
191
|
+
assert.equal(init.versionPayload(fullSpec()).track, 'product'); // instance's first version = product
|
|
192
|
+
const internal = fullSpec();
|
|
193
|
+
internal.version.track = 'internal';
|
|
194
|
+
assert.equal(init.versionPayload(internal).track, 'internal'); // explicit internal respected
|
|
195
|
+
const junk = fullSpec();
|
|
196
|
+
junk.version.track = 'bogus';
|
|
197
|
+
assert.equal(init.versionPayload(junk).track, 'product'); // junk coerced, never forwarded
|
|
198
|
+
});
|
|
199
|
+
|
|
161
200
|
// ---- provisioning arm (ADR 0111 §3, task 1946) ----
|
|
162
201
|
const provSpec = (over = {}) => init.normalizeSpec({
|
|
163
202
|
identity: { productName: 'Cloud Bongos' },
|
|
@@ -543,6 +582,44 @@ async function runSeedTests() {
|
|
|
543
582
|
assert.ok(findTaskPost(api, /Production topology/), 'filed the manual fallback task on error');
|
|
544
583
|
});
|
|
545
584
|
|
|
585
|
+
await at('seedViaApi: version POST carries the explicit track (the NOT NULL column)', async () => {
|
|
586
|
+
const api = fakeApi();
|
|
587
|
+
await init.seedViaApi(provSpec({}), { log: () => {}, api });
|
|
588
|
+
const vpost = api.calls.find((c) => c.path.endsWith('/versions'));
|
|
589
|
+
assert.ok(vpost, 'POSTed the version');
|
|
590
|
+
assert.equal(vpost.body.track, 'product');
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
await at('seedViaApi: the ideator kickoff task POSTs kind=decision + discipline=ideator', async () => {
|
|
594
|
+
const api = fakeApi();
|
|
595
|
+
await init.seedViaApi(provSpec({}), { log: () => {}, api });
|
|
596
|
+
const look = findTaskPost(api, /instance look/i);
|
|
597
|
+
assert.ok(look, 'the branding kickoff task was POSTed');
|
|
598
|
+
assert.equal(look.body.kind, 'decision');
|
|
599
|
+
assert.equal(look.body.discipline, 'ideator');
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
await at('seedViaApi: a rejected task POST is LOUD — failures returned, counts reflect actual vs expected', async () => {
|
|
603
|
+
// Reject exactly one kickoff task (the way tasks_kind_check used to), leave the rest 200.
|
|
604
|
+
const calls = [];
|
|
605
|
+
const api = async (method, path, body) => {
|
|
606
|
+
calls.push({ method, path, body });
|
|
607
|
+
if (path.endsWith('/tasks') && /instance look/i.test((body && body.title) || '')) {
|
|
608
|
+
return { ok: false, status: 400, data: { error: 'validation_failed' } };
|
|
609
|
+
}
|
|
610
|
+
return { ok: true, status: 200, data: {} };
|
|
611
|
+
};
|
|
612
|
+
api.calls = calls;
|
|
613
|
+
const lines = [];
|
|
614
|
+
const out = await init.seedViaApi(provSpec({}), { log: (m) => lines.push(m), api });
|
|
615
|
+
assert.equal(out.failures.length, 1, 'the dropped task is reported, not absorbed');
|
|
616
|
+
assert.match(out.failures[0].title, /instance look/i);
|
|
617
|
+
assert.equal(out.created, out.attempted - 1, 'created reflects the real count');
|
|
618
|
+
const text = lines.join('\n');
|
|
619
|
+
assert.match(text, /FAILED to file/, 'per-failure report is printed');
|
|
620
|
+
assert.match(text, new RegExp(`⚠ filed ${out.created} of ${out.attempted}`), 'summary is a warning with actual-vs-expected counts, not a green check');
|
|
621
|
+
});
|
|
622
|
+
|
|
546
623
|
await at('seedViaApi: no provisioning block → no provisioning POST, topo stays a normal kickoff task', async () => {
|
|
547
624
|
const spec = init.normalizeSpec({ identity: { productName: 'X Co' }, repo: { owner: 'o', name: 'n' },
|
|
548
625
|
firstAdmin: 'x', version: { id: 'V1', name: 'V1', done_when: 'ship' } });
|
|
@@ -604,6 +681,57 @@ async function runRegistryCoreTests() {
|
|
|
604
681
|
assert.match(out, /mutually exclusive/);
|
|
605
682
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
606
683
|
});
|
|
684
|
+
|
|
685
|
+
// ---- --adopt now honors --vendor-core (task 1002562) --------------------------
|
|
686
|
+
// Adopt used to ALWAYS write the caret range, which resolves NOWHERE on npm — so an
|
|
687
|
+
// adopted repo could not `npm ci --omit=dev`, the exact command the provisioner runs on
|
|
688
|
+
// the box. The vendor-plan resolution moved ABOVE the adopt branch in main() so both
|
|
689
|
+
// paths share it.
|
|
690
|
+
await at('main --adopt --vendor-core --dry-run: pins the file: tarball, not the caret', async () => {
|
|
691
|
+
const { dir, p } = specFile();
|
|
692
|
+
const tgz = join(dir, 'bongos-core-1.17.0.tgz');
|
|
693
|
+
writeFileSync(tgz, 'x');
|
|
694
|
+
const target = join(dir, 'existing');
|
|
695
|
+
mkdirSync(target, { recursive: true });
|
|
696
|
+
writeFileSync(join(target, 'package.json'), JSON.stringify({ name: 'their-app', dependencies: { react: '^18' } }));
|
|
697
|
+
try {
|
|
698
|
+
const { code, out } = await capture(['--from', p, '--dir', target, '--no-seed', '--adopt', '--accept', '--vendor-core', tgz, '--dry-run']);
|
|
699
|
+
assert.equal(code, 0, out);
|
|
700
|
+
assert.match(out, /file:vendor\/bongos-core-1\.17\.0\.tgz/, 'the adopt layer pins the vendored tarball');
|
|
701
|
+
assert.match(out, /would vendor .* and run `npm install` to generate package-lock\.json/, 'plans the vendor + lockfile steps adopt previously skipped entirely');
|
|
702
|
+
assert.ok(!/\^1\./.test(out.split('would add').pop() || ''), 'not the caret range for the core dep');
|
|
703
|
+
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
await at('main --adopt (no --vendor-core): unchanged — still the caret range, no vendor step', async () => {
|
|
707
|
+
const { dir, p } = specFile();
|
|
708
|
+
const target = join(dir, 'existing');
|
|
709
|
+
mkdirSync(target, { recursive: true });
|
|
710
|
+
writeFileSync(join(target, 'package.json'), JSON.stringify({ name: 'their-app' }));
|
|
711
|
+
try {
|
|
712
|
+
const { code, out } = await capture(['--from', p, '--dir', target, '--no-seed', '--adopt', '--accept', '--dry-run']);
|
|
713
|
+
assert.equal(code, 0, out);
|
|
714
|
+
assert.match(out, /would add @bongos\/core@\^/, 'a plain laptop adopt keeps the declared caret');
|
|
715
|
+
assert.ok(!/would vendor /.test(out), 'no vendoring without --vendor-core');
|
|
716
|
+
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
await at('main --adopt: an ALREADY-declared core dep is never re-pinned over (adopt never overwrites)', async () => {
|
|
720
|
+
const { dir, p } = specFile();
|
|
721
|
+
const tgz = join(dir, 'bongos-core-1.17.0.tgz');
|
|
722
|
+
writeFileSync(tgz, 'x');
|
|
723
|
+
const target = join(dir, 'existing');
|
|
724
|
+
mkdirSync(target, { recursive: true });
|
|
725
|
+
// The owner already pins their own core range — re-pinning it to our tarball would be
|
|
726
|
+
// exactly the overwrite adopt promises never to do.
|
|
727
|
+
writeFileSync(join(target, 'package.json'), JSON.stringify({ dependencies: { '@bongos/core': '^1.16.0' } }));
|
|
728
|
+
try {
|
|
729
|
+
const { code, out } = await capture(['--from', p, '--dir', target, '--no-seed', '--adopt', '--accept', '--vendor-core', tgz, '--dry-run']);
|
|
730
|
+
assert.equal(code, 0, out);
|
|
731
|
+
assert.match(out, /already a dependency — package\.json left untouched/);
|
|
732
|
+
assert.ok(!/would vendor /.test(out), 'and the tarball is NOT pinned over their declaration');
|
|
733
|
+
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
734
|
+
});
|
|
607
735
|
}
|
|
608
736
|
|
|
609
737
|
await runSeedTests();
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// tests/known_failing_ratchet.mjs — the explicit, justified allowlist for the unit
|
|
2
|
+
// gate's remaining failures, with the count ratcheted so it cannot grow (task 1002470).
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS, AND WHY IT IS NOT A MUTE
|
|
5
|
+
//
|
|
6
|
+
// `scripts/gds/run-unit-tests.js` already has a KNOWN_FAILING set, but gate-review.js
|
|
7
|
+
// HARD-FLOORS that file (ADR 0058): adding an entry there is classified as muting a
|
|
8
|
+
// test and escalates to the owner. It also genuinely hides the failure — a muted test
|
|
9
|
+
// stops running. This file does the opposite:
|
|
10
|
+
//
|
|
11
|
+
// - it does NOT change which tests run, and does NOT make the gate exit 0. The three
|
|
12
|
+
// files below still execute, still fail, and run-unit-tests.js still exits 1;
|
|
13
|
+
// - it RUNS each declared file and asserts it STILL fails. Fix one and this test goes
|
|
14
|
+
// red until the entry is removed — so the list cannot rot into a graveyard;
|
|
15
|
+
// - it caps the count, so a new failure cannot be quietly parked here.
|
|
16
|
+
//
|
|
17
|
+
// The value is the JUSTIFICATION: each entry names why it is not fixed and who owns it,
|
|
18
|
+
// so "3 failing" is a reviewed position rather than unexplained red.
|
|
19
|
+
//
|
|
20
|
+
// DB-free, no network. Run: node tests/known_failing_ratchet.mjs
|
|
21
|
+
|
|
22
|
+
import { strict as assert } from 'node:assert';
|
|
23
|
+
import { test } from 'node:test';
|
|
24
|
+
import { execFileSync } from 'node:child_process';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
const TESTS_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
|
|
30
|
+
// The ceiling. Raising it is a deliberate, reviewable act — and should be rare.
|
|
31
|
+
// (Ratcheted 3 → 2 when publish_manifest was FIXED under task 1002611: its
|
|
32
|
+
// ">=7 host tests" non-vacuity floor had gone stale after ADR 0107 moved the
|
|
33
|
+
// host-module tests to modules/<key>/tests/; it is a regex specimen self-test
|
|
34
|
+
// now, and idea 1000542 — which owned it — is superseded.)
|
|
35
|
+
const MAX_KNOWN_FAILING = 2;
|
|
36
|
+
|
|
37
|
+
const KNOWN_FAILING = {
|
|
38
|
+
platform_boot: {
|
|
39
|
+
owner: 'task 1002617 (parked in run-unit-tests KNOWN_FAILING by task 1002611)',
|
|
40
|
+
why: 'GET / on the apex returns 302, not the 200 landing page the test expects. '
|
|
41
|
+
+ 'Possibly a REAL product bug (the apex redirecting instead of serving the '
|
|
42
|
+
+ 'landing) rather than a test problem, so it was deliberately not "fixed" by '
|
|
43
|
+
+ 'relaxing the assertion. Diagnose the redirect first.',
|
|
44
|
+
},
|
|
45
|
+
served_paths_shipped: {
|
|
46
|
+
owner: 'task 1002617; collides with the ACTIVE claim "make the /docs/openapi.json endpoint self-healing"',
|
|
47
|
+
why: 'Asserts serve-internal.js reads docs/api/openapi.json. That exact path is what '
|
|
48
|
+
+ 'the in-flight claim is changing, so touching it here would collide. Whoever lands '
|
|
49
|
+
+ 'that claim should re-check this assertion and remove this entry.',
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function runsRed(name) {
|
|
54
|
+
try {
|
|
55
|
+
execFileSync(process.execPath, [path.join(TESTS_DIR, `${name}.mjs`)], {
|
|
56
|
+
stdio: ['ignore', 'ignore', 'ignore'], timeout: 60_000,
|
|
57
|
+
env: { ...process.env, NODE_ENV: 'test' },
|
|
58
|
+
});
|
|
59
|
+
return false; // exited 0 — it PASSES now
|
|
60
|
+
} catch {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
test('every known-failing entry still actually fails — a fixed one must be removed', () => {
|
|
66
|
+
const nowPassing = Object.keys(KNOWN_FAILING).sort().filter((name) => !runsRed(name));
|
|
67
|
+
assert.deepEqual(nowPassing, [],
|
|
68
|
+
'these are on the known-failing allowlist but now PASS. Delete their entries from '
|
|
69
|
+
+ 'tests/known_failing_ratchet.mjs — an allowlist that outlives the failures it '
|
|
70
|
+
+ 'documents is how a graveyard starts.');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('the known-failing count is ratcheted and cannot grow silently', () => {
|
|
74
|
+
const n = Object.keys(KNOWN_FAILING).length;
|
|
75
|
+
assert.ok(n <= MAX_KNOWN_FAILING,
|
|
76
|
+
`${n} known-failing tests exceeds the ratchet of ${MAX_KNOWN_FAILING}. Fix the new `
|
|
77
|
+
+ 'failure rather than parking it here; raising MAX_KNOWN_FAILING is a deliberate act.');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('every known-failing entry carries a real justification and an owner', () => {
|
|
81
|
+
// A bare list is what this file exists to prevent — an entry with no reason and no
|
|
82
|
+
// owner is indistinguishable from a mute.
|
|
83
|
+
for (const [name, entry] of Object.entries(KNOWN_FAILING)) {
|
|
84
|
+
assert.ok(entry && typeof entry.why === 'string' && entry.why.trim().length >= 60,
|
|
85
|
+
`${name}: needs a substantive 'why', not a placeholder`);
|
|
86
|
+
assert.ok(entry && typeof entry.owner === 'string' && entry.owner.trim().length > 0,
|
|
87
|
+
`${name}: needs an owner (a task, an idea, or an explicit "unassigned")`);
|
|
88
|
+
}
|
|
89
|
+
});
|
package/tests/lifecycle-card.mjs
CHANGED
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
|
|
18
18
|
import { strict as assert } from 'node:assert';
|
|
19
19
|
import { createRequire } from 'node:module';
|
|
20
|
+
import { requireHostModule } from './helpers.mjs';
|
|
20
21
|
|
|
21
22
|
const require = createRequire(import.meta.url);
|
|
22
|
-
const hook = require
|
|
23
|
+
const hook = requireHostModule(require, '../.claude/hooks/lifecycle-card.js');
|
|
23
24
|
|
|
24
25
|
let passed = 0;
|
|
25
26
|
let failed = 0;
|
package/tests/main_audit.mjs
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import { strict as assert } from 'node:assert';
|
|
9
9
|
import { createRequire } from 'node:module';
|
|
10
|
+
import { execFileSync } from 'node:child_process';
|
|
10
11
|
|
|
11
12
|
const require = createRequire(import.meta.url);
|
|
12
|
-
const { analyzeCommits } = require('../scripts/gds/main-audit.js');
|
|
13
|
+
const { analyzeCommits, clampWindow, defaultRange, firstParentDepth, rowsToMap } = require('../scripts/gds/main-audit.js');
|
|
13
14
|
|
|
14
15
|
let passed = 0, failed = 0;
|
|
15
16
|
function t(name, fn) {
|
|
@@ -133,5 +134,75 @@ t('#863-followup (finding #6): a sub-Metic author NAME still resolves and BLOCKS
|
|
|
133
134
|
assert.equal(r.findings[0].rank, 'thetes');
|
|
134
135
|
});
|
|
135
136
|
|
|
137
|
+
// -- the default range window (task 1002566) -------------------------------
|
|
138
|
+
//
|
|
139
|
+
// Everything above tests the pure ANALYZER. Nothing tested what FEEDS it, which
|
|
140
|
+
// is how the audit ran for months without producing a single verdict: the
|
|
141
|
+
// default range was `origin/main~200..origin/main`, `~200` walks FIRST parents
|
|
142
|
+
// 200 times, and this repo's first-parent chain is 186 deep (526 commits, but
|
|
143
|
+
// nearly every land is a --no-ff merge). git rejected the range outright and the
|
|
144
|
+
// failure surfaced as a generic "git log failed".
|
|
145
|
+
|
|
146
|
+
t('a deep history uses the full window', () => {
|
|
147
|
+
assert.equal(clampWindow(500, 200), 200);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
t('the regression: a 186-deep first-parent chain clamps instead of overrunning', () => {
|
|
151
|
+
assert.equal(clampWindow(186, 200), 185);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
t('a two-commit history still audits its one reachable commit', () => {
|
|
155
|
+
assert.equal(clampWindow(2, 200), 1);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
t('a single-commit history is not auditable and says so rather than guessing', () => {
|
|
159
|
+
assert.equal(clampWindow(1, 200), null);
|
|
160
|
+
assert.equal(clampWindow(0, 200), null);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
t('a non-numeric depth is refused, not coerced into a bogus range', () => {
|
|
164
|
+
for (const bad of [NaN, undefined, null, 'twelve']) {
|
|
165
|
+
assert.equal(clampWindow(bad, 200), null, `${String(bad)} must not produce a range`);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
t('defaultRange clamps to the ref it is given', () => {
|
|
170
|
+
assert.equal(defaultRange('origin/main', 186, 200), 'origin/main~185..origin/main');
|
|
171
|
+
assert.equal(defaultRange('HEAD', 500, 200), 'HEAD~200..HEAD');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
t('defaultRange returns null (not a broken range string) when nothing is auditable', () => {
|
|
175
|
+
assert.equal(defaultRange('HEAD', 1, 200), null);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// The proof the unit assertions alone cannot give: resolve the range against
|
|
179
|
+
// THIS repo's real history and confirm git accepts it. This is the assertion
|
|
180
|
+
// that would have caught the original bug.
|
|
181
|
+
t('the default range actually resolves against this repo', () => {
|
|
182
|
+
const ref = 'HEAD';
|
|
183
|
+
const depth = firstParentDepth(ref);
|
|
184
|
+
assert.ok(depth > 1, `expected a real history, got depth ${depth}`);
|
|
185
|
+
const range = defaultRange(ref, depth);
|
|
186
|
+
assert.ok(range, 'no range produced for a repo with real history');
|
|
187
|
+
// Throws if git cannot resolve the range — exactly the original failure.
|
|
188
|
+
execFileSync('git', ['rev-list', '--count', range], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// rowsToMap feeds every roster source (authenticated roster, --db, and the
|
|
192
|
+
// task-1002612 public-leaderboard fallback the CI caller relies on) — the
|
|
193
|
+
// login-only keying is the #863-followup anti-aliasing rule.
|
|
194
|
+
t('rowsToMap keys ONLY github_login — display_name is builder-controlled', () => {
|
|
195
|
+
const map = rowsToMap([
|
|
196
|
+
{ github_login: 'SomeBuilder', display_name: 'somearchon', rank: 'metic' },
|
|
197
|
+
{ display_name: 'no-login-row', rank: 'archon' },
|
|
198
|
+
]);
|
|
199
|
+
assert.deepEqual(map, { somebuilder: 'metic' });
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
t('rowsToMap: empty or missing rows → empty map, never a throw', () => {
|
|
203
|
+
assert.deepEqual(rowsToMap([]), {});
|
|
204
|
+
assert.deepEqual(rowsToMap(null), {});
|
|
205
|
+
});
|
|
206
|
+
|
|
136
207
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
137
208
|
process.exit(failed ? 1 : 0);
|
|
@@ -185,9 +185,18 @@ await t('the two Archon forensic endpoints are both rank-gated to archon', () =>
|
|
|
185
185
|
const idx = src.indexOf(route);
|
|
186
186
|
assert.ok(idx >= 0, `route ${route} should be declared`);
|
|
187
187
|
const window = src.slice(idx, idx + 200);
|
|
188
|
+
// BV1.R105 migrated these two onto `requirePermission('memory.read.any')`, and
|
|
189
|
+
// ADR 0157 lowered that atom's floor archon→metic. So neither the old literal
|
|
190
|
+
// `requireRank('archon')` nor any rank name is the invariant any more.
|
|
191
|
+
//
|
|
192
|
+
// What this test protects is UNCHANGED in substance: a cross-builder read must
|
|
193
|
+
// require an EXPLICIT privileged gate that no ordinary builder passes. Asserting
|
|
194
|
+
// the atom (rather than a rank) states exactly that, and keeps stating it if the
|
|
195
|
+
// floor moves again — while still failing loudly if the gate is dropped, widened
|
|
196
|
+
// to bare `requireBuilder`, or swapped for an own-scoped permission.
|
|
188
197
|
assert.ok(
|
|
189
|
-
/
|
|
190
|
-
`route ${route} must be gated by
|
|
198
|
+
/requirePermission\('memory\.read\.any'\)/.test(window),
|
|
199
|
+
`route ${route} must be gated by requirePermission('memory.read.any')`
|
|
191
200
|
);
|
|
192
201
|
}
|
|
193
202
|
});
|
|
@@ -208,16 +217,20 @@ await t('the SELF read endpoints never derive the owner from request input', ()
|
|
|
208
217
|
assert.ok(!/req\.body\.builder/i.test(codeOnly), 'no owner should come from req.body.builder*');
|
|
209
218
|
assert.ok(!/req\.body\.owner/i.test(codeOnly), 'no owner should come from req.body.owner');
|
|
210
219
|
// An input-supplied owner (req.params.builderId) may ONLY appear inside a
|
|
211
|
-
// PRIVILEGED cross-builder endpoint: either the
|
|
212
|
-
//
|
|
213
|
-
// (requireBfgPrincipal — an identity gate stricter than
|
|
214
|
-
// ADR 0026 §6C). Both are audited. No SELF endpoint reads it.
|
|
220
|
+
// PRIVILEGED cross-builder endpoint: either the permission-gated forensic READS
|
|
221
|
+
// (`memory.read.any` — floor metic since ADR 0157, archon before it) or the
|
|
222
|
+
// BFG-principal-gated WRITE (requireBfgPrincipal — an identity gate stricter than
|
|
223
|
+
// any rank; 6C.1 / ADR 0026 §6C). Both are audited. No SELF endpoint reads it.
|
|
224
|
+
//
|
|
225
|
+
// Matched on the ATOM, not a rank, for the same reason as the gate check above: the
|
|
226
|
+
// isolation invariant is "an explicit cross-builder permission was required", which
|
|
227
|
+
// is exactly what the atom names.
|
|
215
228
|
const paramUses = [...codeOnly.matchAll(/req\.params\.builderId/g)].map((m) => m.index);
|
|
216
229
|
for (const i of paramUses) {
|
|
217
230
|
const window = codeOnly.slice(Math.max(0, i - 700), i);
|
|
218
231
|
assert.ok(
|
|
219
|
-
/
|
|
220
|
-
|
|
232
|
+
/requirePermission\('memory\.read\.any'\)/.test(window) || /requireBfgPrincipal/.test(window),
|
|
233
|
+
"req.params.builderId may only be read inside a memory.read.any-gated forensic read OR the BFG-principal-gated write"
|
|
221
234
|
);
|
|
222
235
|
}
|
|
223
236
|
});
|
|
@@ -309,6 +322,27 @@ db.insertAuditLog = async function stubAudit(row) {
|
|
|
309
322
|
const express = require('express');
|
|
310
323
|
const app = express();
|
|
311
324
|
app.use(express.json());
|
|
325
|
+
// Install res.fail() as the real app does (the tests/search_isolation.mjs idiom, task 1991).
|
|
326
|
+
// Required since BV1.R105 swapped these routes' gate to requirePermission, whose denial
|
|
327
|
+
// path calls res.fail() — without it a 403 threw "res.fail is not a function" and killed
|
|
328
|
+
// the whole HTTP layer below, so the cross-builder isolation assertions never ran.
|
|
329
|
+
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
330
|
+
app.use(attachFail);
|
|
331
|
+
|
|
332
|
+
// requirePermission resolves authority through the `governance` kernel port and fails
|
|
333
|
+
// CLOSED when it is absent — correct in production (a missing authority system must not
|
|
334
|
+
// admit), but it means an unstubbed port 403s every rank including archon. Register a
|
|
335
|
+
// provider that answers from the real catalog seed for whatever rank the auth stub is
|
|
336
|
+
// currently impersonating, so the cases below exercise the PERMISSION decision rather
|
|
337
|
+
// than the port-missing fallback.
|
|
338
|
+
const seams = require('../src/module-seams.js');
|
|
339
|
+
const catalog = require('../modules/governance/catalog.js');
|
|
340
|
+
seams.registerProvider('governance', {
|
|
341
|
+
resolveBuilderPermissions: async () => catalog.seedForRank(
|
|
342
|
+
(fixture.session && fixture.session.rank) || 'xenos',
|
|
343
|
+
),
|
|
344
|
+
});
|
|
345
|
+
|
|
312
346
|
app.use('/api/gds', buildMemoryRouter());
|
|
313
347
|
|
|
314
348
|
const server = http.createServer(app);
|
|
@@ -333,10 +367,23 @@ try {
|
|
|
333
367
|
assert.equal(fixture.storeCalls.length, 0, 'no store call may happen when the rank gate refuses');
|
|
334
368
|
});
|
|
335
369
|
|
|
336
|
-
|
|
370
|
+
// ADR 0157 lowered `memory.read.any` archon→metic, so a Metic is now ADMITTED here
|
|
371
|
+
// where it was previously refused. The isolation invariant this file protects is
|
|
372
|
+
// unchanged: a cross-builder read still requires an explicit privileged permission and
|
|
373
|
+
// is still audited — it is the set of ranks holding that permission that the owner
|
|
374
|
+
// widened. The sub-Metic refusal in (a) is the assertion that still guards the wall.
|
|
375
|
+
await t('(a2) a metic is now ADMITTED to the forensic route (ADR 0157) and is audited', async () => {
|
|
337
376
|
reset();
|
|
338
377
|
fixture.session = { id: 5, rank: 'metic' };
|
|
339
378
|
const res = await fetch(`${base}/memory/builders/99/file?path=MEMORY.md`);
|
|
379
|
+
assert.notEqual(res.status, 403);
|
|
380
|
+
assert.equal(fixture.auditCalls.length, 1, 'a metic forensic read must still be audited');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
await t('(a3) a thetes is STILL refused the forensic route (the wall held below metic)', async () => {
|
|
384
|
+
reset();
|
|
385
|
+
fixture.session = { id: 6, rank: 'thetes' };
|
|
386
|
+
const res = await fetch(`${base}/memory/builders/99/file?path=MEMORY.md`);
|
|
340
387
|
assert.equal(res.status, 403);
|
|
341
388
|
assert.equal(fixture.storeCalls.length, 0);
|
|
342
389
|
});
|
|
@@ -96,7 +96,7 @@ t('CORE_ROOT is always the script-relative repo root, independent of the invocat
|
|
|
96
96
|
});
|
|
97
97
|
|
|
98
98
|
// ==========================================================================
|
|
99
|
-
// A CORE ROOT WHOSE PATH CONTAINS A SPACE (
|
|
99
|
+
// A CORE ROOT WHOSE PATH CONTAINS A SPACE (fixed in 1.18.19)
|
|
100
100
|
// ==========================================================================
|
|
101
101
|
// `for f in $MIGRATIONS` word-split the newline-separated migration list on IFS — which includes
|
|
102
102
|
// space — so a checkout at "/Users/.../Hermeslines Marketing/..." fragmented every path, the
|
package/tests/mirror_redact.mjs
CHANGED
|
@@ -31,6 +31,7 @@ const R = require('../scripts/gds/mirror-redact.js');
|
|
|
31
31
|
const {
|
|
32
32
|
buildRedactionRules, collectSensitiveLiterals, redactText, findLeaks, findSecretLeak,
|
|
33
33
|
deriveInstanceLiterals, isPublicRoutableIPv4, isPublicRoutableIPv6, parseIPv6, registrableLabel,
|
|
34
|
+
firstPublicIP,
|
|
34
35
|
} = R;
|
|
35
36
|
|
|
36
37
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -116,6 +117,24 @@ t('isPublicRoutableIPv6 + a "12:34:56" timestamp is NOT treated as an IP', () =>
|
|
|
116
117
|
assert.equal(parseIPv6('2001:db8::1')[0], 0x2001);
|
|
117
118
|
});
|
|
118
119
|
|
|
120
|
+
t('reserved ::/8 is NOT a public IP — degenerate shorthand in binary is not a leak', () => {
|
|
121
|
+
// Two-to-three-character forms like `5::` parse as valid IPv6 and used to classify
|
|
122
|
+
// public, so random bytes inside committed .png/.webp assets tripped the C9 full-tree
|
|
123
|
+
// mirror gate with 7 phantom "public-ip" leaks (task 1002470). ::/8 is IETF-reserved
|
|
124
|
+
// (RFC 4291 §2.4) — nothing is reachable there, so it can never be a real leak.
|
|
125
|
+
for (const s of ['5::', 'b::', 'c9::', 'd::', '4::', '8::', 'a::', '00ff::1']) {
|
|
126
|
+
assert.equal(isPublicRoutableIPv6(s), false, `${s} is in reserved ::/8 and must not be flagged`);
|
|
127
|
+
}
|
|
128
|
+
// ...and the narrowing did NOT blunt the gate: real global unicast still fires, in
|
|
129
|
+
// both the detector and the redactor.
|
|
130
|
+
for (const s of ['2000::1', '2001:db8::1', '2606:4700:4700::1111', '3fff::1']) {
|
|
131
|
+
assert.equal(isPublicRoutableIPv6(s), true, `${s} is a real public address and must still be flagged`);
|
|
132
|
+
}
|
|
133
|
+
assert.equal(firstPublicIP('sprite bytes :: more bytes'), null, 'binary-ish shorthand is not reported');
|
|
134
|
+
assert.equal(firstPublicIP('origin 2606:4700:4700::1111'), '2606:4700:4700::1111');
|
|
135
|
+
assert.ok(!redactText('host 2606:4700:4700::1111', []).includes('2606:4700'), 'a real public IPv6 is still redacted');
|
|
136
|
+
});
|
|
137
|
+
|
|
119
138
|
// ── the DETECTOR is stricter than redaction ───────────────────────────────────────────
|
|
120
139
|
t('findLeaks catches a raw literal substring a bounded rule would miss', () => {
|
|
121
140
|
// "azoltanb" has no word boundary around zoltan; a \bzoltan\b rule misses it, the
|
package/tests/module_api.mjs
CHANGED
|
@@ -21,12 +21,14 @@ const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
|
|
|
21
21
|
const PUBLISHED_SURFACE = [
|
|
22
22
|
'CORE_VERSION',
|
|
23
23
|
'requireBuilder', 'requireRank', 'rankMeetsThreshold', 'requireBfgPrincipal', 'allowBoxScope', 'resolveSession',
|
|
24
|
+
'requirePermission', // BV1.R105: the authority-atom gate module routes migrate onto (1.19.0; ADR 0151)
|
|
24
25
|
'pool', 'getPool', 'instanceDbName', 'withTx', 'getBuilderById',
|
|
25
26
|
'secretBox', // Gemini-key storage relocated to modules/art-pipeline + its 'art.getBuilderGeminiKeyRow' port (ADR 0102)
|
|
26
27
|
'routeRankCheck', 'permissionPathCheck', // BV1.R73: kernel trust classifiers (1.6.0)
|
|
27
28
|
'repoInfo', 'moduleScopeMap', 'pathMatch', // BV1.R86: kernel repo-identity + scope-wall + touches-overlap for the carved lifecycle (1.12.0)
|
|
28
29
|
'goalScopeCheck', // BV1.R60: kernel scope wall for the goal-authored-task create gate (1.13.0; ADR 0086 §3)
|
|
29
30
|
'insertAuditLog', 'LIVE_RANK_LADDER', // createTask + VOTABLE_TASK_STATUSES + tallyPeerVotes RETIRED at 1.12.0 (BV1.R86 → lifecycle port); captureIdea retired at 1.8.0 (BV1.R78 → ideas.capture port)
|
|
31
|
+
'RANK_TIER', // BV1.R108: the canonical 1-based rank→tier map, derived from LIVE_RANK_LADDER (tests/rank_tier_single_source.mjs)
|
|
30
32
|
'llmCache', // BV1.R73: shared LLM-cost cache (1.6.0; tracked domain leak)
|
|
31
33
|
'llmPricing', // BV1.R77: shared token→USD pricing (1.7.0; tracked domain leak — economy carve)
|
|
32
34
|
'branding', 'clientBranding', 'userAgent', 'resolveEnv', 'readConfigFileSync', 'configHome', 'configPath', // userAgent added 1.12.0 (BV1.R86; github-push UA)
|
|
@@ -51,7 +53,7 @@ test('CORE_VERSION is a semver string', () => {
|
|
|
51
53
|
});
|
|
52
54
|
|
|
53
55
|
test('auth gates are re-exported as functions', () => {
|
|
54
|
-
for (const k of ['requireBuilder', 'requireRank', 'rankMeetsThreshold', 'requireBfgPrincipal']) {
|
|
56
|
+
for (const k of ['requireBuilder', 'requireRank', 'requirePermission', 'rankMeetsThreshold', 'requireBfgPrincipal']) {
|
|
55
57
|
assert.equal(typeof api[k], 'function', `${k} should be a function`);
|
|
56
58
|
}
|
|
57
59
|
});
|
package/tests/module_loader.mjs
CHANGED
|
@@ -315,6 +315,10 @@ test('the demo proof module mounts + serves through the loader (BV1.R52 / task 1
|
|
|
315
315
|
// BV1.R52 acceptance: the net-new scaffolded module is discovered, is enabled
|
|
316
316
|
// by config/modules.json, and its route factory builds a real router with the
|
|
317
317
|
// health read route + the rank-gated ping write route — all with NO core edit.
|
|
318
|
+
// modules/demo/ is content-excluded from the published core (ADR 0098), exactly as
|
|
319
|
+
// the roster comment above already notes — so on a core checkout there is nothing
|
|
320
|
+
// to mount and this acceptance proof does not apply (task 1002470).
|
|
321
|
+
if (!fs.existsSync(path.join(ROOT, 'modules', 'demo'))) return;
|
|
318
322
|
loader._reset();
|
|
319
323
|
const { isModuleEnabled } = require(path.join(ROOT, 'src', 'modules.js'));
|
|
320
324
|
assert.equal(isModuleEnabled('demo'), true, 'config/modules.json enables demo');
|
package/tests/modules.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// tests/modules.mjs — the feature-module selection resolver (task 1193 / V4.R54, ADR 0062 §4).
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { test } from 'node:test';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
4
5
|
import modulesMod from '../src/modules.js';
|
|
5
6
|
|
|
6
7
|
const {
|
|
@@ -17,6 +18,14 @@ const OTB = { modules: { game: true, 'art-pipeline': true, discord: true, 'dev-b
|
|
|
17
18
|
// loadModules() does in production. Membership across the move is preserved here.
|
|
18
19
|
const EFFECTIVE = modulesMod.effectiveRegistry();
|
|
19
20
|
|
|
21
|
+
// Sample feature-module keys for the PURE resolver tests below. `game` and
|
|
22
|
+
// `art-pipeline` left the core at the CB→OTB split, so the effective registry no
|
|
23
|
+
// longer knows them and passing them made resolveModules throw `cannot enable
|
|
24
|
+
// unknown module` — the tests were asserting a topology, not the resolver (task
|
|
25
|
+
// 1002470). Synthesized here so the precedence/merge/freeze semantics stay under
|
|
26
|
+
// test on any instance, whatever it happens to have installed.
|
|
27
|
+
const SAMPLE_REG = { ...EFFECTIVE, game: { default: false }, 'art-pipeline': { default: false } };
|
|
28
|
+
|
|
20
29
|
test('vanilla (no config) resolves every module OFF — fail-to-vanilla, never fail-to-OTB', () => {
|
|
21
30
|
const m = resolveModules({});
|
|
22
31
|
for (const k of KNOWN_MODULES) assert.equal(m[k], false, `${k} should default off`);
|
|
@@ -28,7 +37,7 @@ test('every registry module has an explicit default of false', () => {
|
|
|
28
37
|
});
|
|
29
38
|
|
|
30
39
|
test('instance config turns modules on', () => {
|
|
31
|
-
const m = resolveModules({ neutral: VANILLA, instance: OTB, registry:
|
|
40
|
+
const m = resolveModules({ neutral: VANILLA, instance: OTB, registry: SAMPLE_REG });
|
|
32
41
|
assert.equal(m.game, true);
|
|
33
42
|
assert.equal(m['dev-box'], true);
|
|
34
43
|
assert.equal(isModuleEnabled('discord', m), true);
|
|
@@ -41,19 +50,31 @@ test('instance config turns modules on', () => {
|
|
|
41
50
|
// gate + builder economy + idea inbox + red-team intake + prefs + onboarding +
|
|
42
51
|
// session corpus + the MAS autonomy machinery + the status dashboard + the
|
|
43
52
|
// builders' hall ship to every instance — see each module.json).
|
|
44
|
-
|
|
53
|
+
// Asserted as a PROPERTY rather than a frozen 16-key list (which named modules the
|
|
54
|
+
// core no longer carries — task 1002470).
|
|
55
|
+
// Everything the instance switched on resolves on...
|
|
56
|
+
for (const k of Object.keys(OTB.modules)) assert.equal(m[k], true, `instance-enabled '${k}' must resolve on`);
|
|
57
|
+
// ...every `default: true` core-domain module resolves on WITHOUT being listed...
|
|
58
|
+
const coreDefaults = Object.keys(SAMPLE_REG).filter((k) => SAMPLE_REG[k].default);
|
|
59
|
+
assert.ok(coreDefaults.length >= 5, `expected a real core-domain roster, found ${coreDefaults.length}`);
|
|
60
|
+
for (const k of coreDefaults) assert.equal(m[k], true, `default-on core module '${k}' must resolve on unlisted`);
|
|
61
|
+
// ...and a registry key that is neither listed nor default-on stays off.
|
|
62
|
+
for (const k of Object.keys(SAMPLE_REG)) {
|
|
63
|
+
if (SAMPLE_REG[k].default || k in OTB.modules) continue;
|
|
64
|
+
assert.equal(m[k], false, `unlisted opt-in module '${k}' must stay off`);
|
|
65
|
+
}
|
|
45
66
|
});
|
|
46
67
|
|
|
47
68
|
test('instance config overrides the neutral starter per-flag (deep merge of the modules map)', () => {
|
|
48
69
|
const neutral = { modules: { game: false, discord: false, 'dev-box': false, 'art-pipeline': false } };
|
|
49
70
|
const instance = { modules: { discord: true } }; // only flip one
|
|
50
|
-
const m = resolveModules({ neutral, instance, registry:
|
|
71
|
+
const m = resolveModules({ neutral, instance, registry: SAMPLE_REG });
|
|
51
72
|
assert.equal(m.discord, true);
|
|
52
73
|
assert.equal(m.game, false); // neutral value survives
|
|
53
74
|
});
|
|
54
75
|
|
|
55
76
|
test('env override beats instance config (precedence), with prefix + snake-casing', () => {
|
|
56
|
-
const m = resolveModules({ instance: OTB, env: { OTB_MODULE_DEV_BOX: '0' }, envPrefix: 'OTB', registry:
|
|
77
|
+
const m = resolveModules({ instance: OTB, env: { OTB_MODULE_DEV_BOX: '0' }, envPrefix: 'OTB', registry: SAMPLE_REG });
|
|
57
78
|
assert.equal(m['dev-box'], false); // env off beat instance on
|
|
58
79
|
assert.equal(m.game, true); // others untouched
|
|
59
80
|
});
|
|
@@ -111,19 +132,23 @@ test('disciplines: vanilla offers engineer + ideator (core); art-pipeline adds a
|
|
|
111
132
|
// instance), so it is offered with no modules on; only 'artist' is module-gated.
|
|
112
133
|
// art-pipeline is now a DISCOVERED module (BV1.R48), so pass EFFECTIVE registry.
|
|
113
134
|
assert.deepEqual(enabledDisciplines(resolveModules({})), ['engineer', 'ideator']);
|
|
114
|
-
|
|
135
|
+
// The 'artist' leg reads art-pipeline's OWN module.json contributes, so it only
|
|
136
|
+
// means anything where that module is installed (task 1002470). The core half
|
|
137
|
+
// above — ideation ships to every instance — is asserted unconditionally.
|
|
138
|
+
if (!EFFECTIVE['art-pipeline']) return;
|
|
139
|
+
const m = resolveModules({ instance: { modules: { 'art-pipeline': true } }, registry: SAMPLE_REG });
|
|
115
140
|
assert.deepEqual(enabledDisciplines(m), ['engineer', 'ideator', 'artist']);
|
|
116
141
|
});
|
|
117
142
|
|
|
118
143
|
test('clientModules is a safe boolean projection covering every known module + disciplines', () => {
|
|
119
|
-
const c = clientModules(resolveModules({ instance: OTB, registry:
|
|
144
|
+
const c = clientModules(resolveModules({ instance: OTB, registry: SAMPLE_REG }));
|
|
120
145
|
for (const k of KNOWN_MODULES) assert.equal(typeof c.enabled[k], 'boolean');
|
|
121
146
|
assert.ok(Array.isArray(c.disciplines));
|
|
122
147
|
assert.ok(c.disciplines.includes('engineer'));
|
|
123
148
|
});
|
|
124
149
|
|
|
125
150
|
test('the resolved set is frozen (callers cannot mutate the singleton)', () => {
|
|
126
|
-
const m = resolveModules({ instance: OTB, registry:
|
|
151
|
+
const m = resolveModules({ instance: OTB, registry: SAMPLE_REG });
|
|
127
152
|
assert.throws(() => { m.game = false; }, TypeError);
|
|
128
153
|
});
|
|
129
154
|
|
|
@@ -131,6 +156,9 @@ test('the committed config files resolve: OTB enables all four, on disk', () =>
|
|
|
131
156
|
// loadModules reads config/modules.json over config/modules.neutral.json.
|
|
132
157
|
// No GDS_BRANDING_FILE → still the default OTB instance pack (regression guard
|
|
133
158
|
// for the GDS_MODULES_FILE selector below).
|
|
159
|
+
// config/modules.json is INSTANCE content — the neutral core ships only
|
|
160
|
+
// config/modules.neutral.json, so there is no instance pack to assert (task 1002470).
|
|
161
|
+
if (!existsSync(new URL('../config/modules.json', import.meta.url))) return;
|
|
134
162
|
const m = loadModules({ env: {} });
|
|
135
163
|
assert.equal(m.game, true);
|
|
136
164
|
assert.equal(m['art-pipeline'], true);
|