@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/fitness.mjs
CHANGED
|
@@ -4,11 +4,13 @@ import { test } from 'node:test';
|
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { onDiskModuleKeys } from './helpers.mjs';
|
|
7
8
|
|
|
8
9
|
const require = createRequire(import.meta.url);
|
|
9
10
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
11
|
const fitness = require(path.join(ROOT, 'scripts', 'gds', 'fitness.js'));
|
|
11
12
|
|
|
13
|
+
|
|
12
14
|
test('all fitness checks pass with no hard failures (the CI gate)', () => {
|
|
13
15
|
const results = fitness.runAll();
|
|
14
16
|
const failed = results.filter((r) => r.hardFail);
|
|
@@ -158,12 +160,26 @@ test('module boundary actually CATCHES violations when enforcing (ratchet flip)'
|
|
|
158
160
|
test('core-carries-no-module-code: the live kernel machinery is clean', () => {
|
|
159
161
|
const r = fitness.checkCoreCarriesNoModuleCode();
|
|
160
162
|
assert.equal(r.hardFail, false, r.violations.join('\n'));
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
|
|
163
|
+
// The check is live, not vacuous — it discovered the real module keys. This used to
|
|
164
|
+
// be a hardcoded 19-key roster that went stale at the CB→OTB split (it still named
|
|
165
|
+
// art-pipeline / character-anim / demo / game, and had never learned governance or
|
|
166
|
+
// platform-identity), so it asserted a topology that no longer existed — task 1002470.
|
|
167
|
+
// Derived from an INDEPENDENT readdir of modules/, so a discovery regression in
|
|
168
|
+
// fitness.js still diverges from disk and fails here.
|
|
169
|
+
assert.deepEqual(fitness.discoveredModuleKeys().sort(), onDiskModuleKeys());
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('the discovered-module roster is derived from a NON-EMPTY disk read (mutation check)', () => {
|
|
173
|
+
// Guards the check above against going vacuous: if modules/ read back empty, the
|
|
174
|
+
// deepEqual would compare [] to [] and pass while proving nothing.
|
|
175
|
+
const keys = onDiskModuleKeys();
|
|
176
|
+
assert.ok(keys.length >= 5, `expected a real on-disk module roster, found ${keys.length}: ${keys.join(', ')}`);
|
|
177
|
+
// And it still FAILS when discovery actually drops a module.
|
|
178
|
+
assert.throws(
|
|
179
|
+
() => assert.deepEqual(keys.slice(1), keys),
|
|
180
|
+
/Expected values to be strictly deep-equal/,
|
|
181
|
+
'a dropped module must still fail the roster comparison',
|
|
182
|
+
);
|
|
167
183
|
});
|
|
168
184
|
|
|
169
185
|
test('core-carries-no-module-code actually CATCHES a hardcoded key (and ignores comments)', () => {
|
|
@@ -407,6 +423,10 @@ test('hall zone budgets check actually CATCHES a breach (not a no-op)', () => {
|
|
|
407
423
|
// (a sibling-module import, and an ungated module write route).
|
|
408
424
|
|
|
409
425
|
test('demo proof module is live + clean (boundary + rank + migration green)', () => {
|
|
426
|
+
// modules/demo/ is the acceptance-proof module; an instance that does not install it
|
|
427
|
+
// has nothing to prove here (task 1002470). The RED-LINE tests below are pure-function
|
|
428
|
+
// checks on path strings and still run either way, so the one-way rules stay covered.
|
|
429
|
+
if (!onDiskModuleKeys().includes('demo')) return;
|
|
410
430
|
// discovered with the other modules (asserted above) and clean on every gate.
|
|
411
431
|
assert.ok(fitness.discoveredModuleKeys().includes('demo'), 'demo is a discovered module');
|
|
412
432
|
assert.equal(fitness.checkModulesImportOnlyApi().hardFail, false, 'demo imports only the doorway');
|
package/tests/gate_approvals.mjs
CHANGED
|
@@ -438,11 +438,24 @@ await t('both routes are requireBuilder-gated and registered', async () => {
|
|
|
438
438
|
}
|
|
439
439
|
});
|
|
440
440
|
|
|
441
|
-
|
|
441
|
+
// ADR 0157 SPLIT these two apart: reading the held-PR list is `gate_approval.read`
|
|
442
|
+
// (metic — the Gate page opens to the working rank, and a page whose only data endpoint
|
|
443
|
+
// 403s is a page that renders an error), while APPROVING still clears the gate-review
|
|
444
|
+
// hard floor and stays `gate_approval.decide` (archon, system:true). Asserting the split
|
|
445
|
+
// explicitly is the point — a future edit that re-merged them onto one atom would either
|
|
446
|
+
// close the page again or hand out the approval, and both are caught here.
|
|
447
|
+
await t('route-rank auditor: gate-approval READ is metic+, APPROVE stays archon, no findings', async () => {
|
|
442
448
|
const res = checkRouteRanks();
|
|
443
449
|
const mine = res.routes.filter((r) => r.route.includes('gate-approvals'));
|
|
444
450
|
assert.equal(mine.length, 2, 'both routes seen by the auditor');
|
|
445
|
-
|
|
451
|
+
const byKey = new Map(mine.map((r) => [r.key, r]));
|
|
452
|
+
const read = byKey.get('GET /gate-approvals');
|
|
453
|
+
const approve = byKey.get('POST /gate-approvals/:pr/approve');
|
|
454
|
+
assert.ok(read && approve, 'both the list and the approve route must be present');
|
|
455
|
+
assert.deepEqual(read.perms, ['gate_approval.read'], 'the list route gates on the read atom');
|
|
456
|
+
assert.equal(read.rank, 'metic+archon', 'the list route is metic+ (ADR 0157)');
|
|
457
|
+
assert.deepEqual(approve.perms, ['gate_approval.decide'], 'approve gates on the decide atom');
|
|
458
|
+
assert.equal(approve.rank, 'archon', 'approving a held PR must stay archon');
|
|
446
459
|
const myFindings = res.findings.filter((f) => (f.route || '').includes('gate-approvals'));
|
|
447
460
|
assert.deepEqual(myFindings, [], 'no auditor findings on the gate-approval routes');
|
|
448
461
|
});
|
|
@@ -77,10 +77,12 @@ test('landProvenForTask: published tip NOT in main → NOT proven (the false-shi
|
|
|
77
77
|
assert.match(r.reason, /not in main/);
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
-
test('landProvenForTask: no published tip but matched PR references this task → proven (legacy fallback)', async () => {
|
|
80
|
+
test('landProvenForTask: no published tip but matched MERGED PR references this task → proven (legacy fallback)', async () => {
|
|
81
81
|
const r = await gh.landProvenForTask(
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
// merged:true mirrors how real callers reach this fallback (post-landConfirmed);
|
|
83
|
+
// task 1002615 made it a hard requirement (see the open-PR test below).
|
|
84
|
+
{ taskId: 999, publishedSha: null, status: { merged: true, pr_body: 'for task 999' } },
|
|
85
|
+
depsWithCompare('behind') // must NOT be consulted — no sha anywhere
|
|
84
86
|
);
|
|
85
87
|
assert.equal(r.proven, true);
|
|
86
88
|
assert.match(r.reason, /references task 999/);
|
|
@@ -88,12 +90,56 @@ test('landProvenForTask: no published tip but matched PR references this task
|
|
|
88
90
|
|
|
89
91
|
test('landProvenForTask: no published tip and matched PR references a DIFFERENT task → NOT proven', async () => {
|
|
90
92
|
const r = await gh.landProvenForTask(
|
|
91
|
-
{ taskId: 1299, publishedSha: null, status: { pr_title: 'task 1297: overlay', pr_body: 'for task 1297' } },
|
|
93
|
+
{ taskId: 1299, publishedSha: null, status: { merged: true, pr_title: 'task 1297: overlay', pr_body: 'for task 1297' } },
|
|
92
94
|
depsWithCompare('behind')
|
|
93
95
|
);
|
|
94
96
|
assert.equal(r.proven, false);
|
|
95
97
|
});
|
|
96
98
|
|
|
99
|
+
// --- task 1002615: the out-of-band / rewritten-history land proofs -----------
|
|
100
|
+
|
|
101
|
+
// Per-sha compare responses, so the tip and the merge commit can answer differently.
|
|
102
|
+
function depsWithCompareBySha(map) {
|
|
103
|
+
return {
|
|
104
|
+
token: 'x',
|
|
105
|
+
repoInfo: { owner: 'o', repo: 'r' },
|
|
106
|
+
fetchImpl: async (url) => {
|
|
107
|
+
const m = /compare\/[^.]+\.\.\.([0-9a-f]{7,64})/.exec(String(url));
|
|
108
|
+
return { ok: true, status: 200, json: async () => ({ status: (m && map[m[1]]) || 'ahead' }) };
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const MERGE_SHA = 'f'.repeat(40);
|
|
114
|
+
|
|
115
|
+
test('landProvenForTask (1002615): squash merge — tip NOT in main but the merged PR\'s merge_sha is → proven', async () => {
|
|
116
|
+
const r = await gh.landProvenForTask(
|
|
117
|
+
{ taskId: 7, publishedSha: SHA,
|
|
118
|
+
status: { merged: true, merge_sha: MERGE_SHA, pr_title: 'task 7: thing' } },
|
|
119
|
+
depsWithCompareBySha({ [SHA]: 'ahead', [MERGE_SHA]: 'behind' })
|
|
120
|
+
);
|
|
121
|
+
assert.equal(r.proven, true, 'a squash/rebase land must not strand on the tip check');
|
|
122
|
+
assert.match(r.reason, /merged PR for task 7/);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('landProvenForTask (1002615): an OPEN PR that references the task is NOT a land (merged:true required)', async () => {
|
|
126
|
+
const r = await gh.landProvenForTask(
|
|
127
|
+
{ taskId: 8, publishedSha: null, status: { merged: false, pr_state: 'open', pr_body: 'for task 8' } },
|
|
128
|
+
depsWithCompare('behind')
|
|
129
|
+
);
|
|
130
|
+
assert.equal(r.proven, false, 'reference alone on an un-merged PR must never prove');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('landProvenForTask (1002615): merged PR for a DIFFERENT task + tip not in main → NOT proven (reuse guard intact)', async () => {
|
|
134
|
+
const r = await gh.landProvenForTask(
|
|
135
|
+
{ taskId: 7, publishedSha: SHA,
|
|
136
|
+
status: { merged: true, merge_sha: MERGE_SHA, pr_title: 'task 999: other' } },
|
|
137
|
+
depsWithCompareBySha({ [SHA]: 'ahead', [MERGE_SHA]: 'behind' })
|
|
138
|
+
);
|
|
139
|
+
assert.equal(r.proven, false);
|
|
140
|
+
assert.match(r.reason, /not in main/);
|
|
141
|
+
});
|
|
142
|
+
|
|
97
143
|
test('landProvenForTask: a non-sha published tip ("HEAD") falls back to the PR check, never the compare API', async () => {
|
|
98
144
|
let compareCalled = false;
|
|
99
145
|
const deps = {
|
|
@@ -17,12 +17,34 @@ const auth = require(path.join(ROOT, 'src/bongos/auth.js'));
|
|
|
17
17
|
const routesAuth = require(path.join(ROOT, 'src/bongos/routes/auth.js'));
|
|
18
18
|
const gh = require(path.join(ROOT, 'src/bongos/github-repos.js'));
|
|
19
19
|
|
|
20
|
-
test('scopeForWebFlow: identity-only by default, adds
|
|
20
|
+
test('scopeForWebFlow: identity-only by default, adds repo ONLY when repos=true', () => {
|
|
21
21
|
assert.equal(auth.scopeForWebFlow(), 'read:user');
|
|
22
22
|
assert.equal(auth.scopeForWebFlow({ repos: false }), 'read:user');
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
assert.
|
|
23
|
+
// ADR 0155 (task 1002561): `repo`, not `public_repo` — public_repo grants write to
|
|
24
|
+
// PUBLIC repos only, so adopting an existing PRIVATE repo 403'd at the scaffold push.
|
|
25
|
+
assert.equal(auth.scopeForWebFlow({ repos: true }), 'read:user repo');
|
|
26
|
+
// The widening is opt-in and MUST stay opt-in: an ordinary hall sign-in / the CLI
|
|
27
|
+
// device flow must never carry a repo-acting scope.
|
|
28
|
+
assert.ok(!auth.scopeForWebFlow().split(' ').includes('repo'));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('grantedScopeCoversRepos: a stale pre-0155 public_repo token is NOT sufficient', () => {
|
|
32
|
+
// The exact shape GitHub stored for the live failure (github_oauth_tokens.scope):
|
|
33
|
+
// comma-separated, GitHub's own order, and `public_repo` CONTAINS the substring
|
|
34
|
+
// "repo" — a naive match would wrongly accept it and reintroduce the silent
|
|
35
|
+
// "private repos missing from the picker" degrade.
|
|
36
|
+
assert.equal(auth.grantedScopeCoversRepos('public_repo,read:user'), false);
|
|
37
|
+
assert.equal(auth.grantedScopeCoversRepos('read:user'), false);
|
|
38
|
+
assert.equal(auth.grantedScopeCoversRepos(''), false);
|
|
39
|
+
assert.equal(auth.grantedScopeCoversRepos(null), false);
|
|
40
|
+
assert.equal(auth.grantedScopeCoversRepos(undefined), false);
|
|
41
|
+
// Sufficient: `repo` present, in either order / separator GitHub might use.
|
|
42
|
+
assert.equal(auth.grantedScopeCoversRepos('read:user,repo'), true);
|
|
43
|
+
assert.equal(auth.grantedScopeCoversRepos('repo,read:user'), true);
|
|
44
|
+
assert.equal(auth.grantedScopeCoversRepos('read:user repo'), true);
|
|
45
|
+
assert.equal(auth.grantedScopeCoversRepos('repo'), true);
|
|
46
|
+
// `repo` implies public_repo, so a token carrying both is fine too.
|
|
47
|
+
assert.equal(auth.grantedScopeCoversRepos('public_repo,repo,read:user'), true);
|
|
26
48
|
});
|
|
27
49
|
|
|
28
50
|
test('safeReturnTo still guards after the ?repos change: relative ok, cross-origin/protocol-relative blocked', () => {
|
|
@@ -77,7 +99,10 @@ test('createUserRepo: sends auto_init:false + private:false, returns full_name/h
|
|
|
77
99
|
};
|
|
78
100
|
const out = await gh.createUserRepo('tok', { name: 'mercury', fetchImpl });
|
|
79
101
|
assert.equal(sentBody.auto_init, false, 'empty repo — no README/first commit');
|
|
80
|
-
|
|
102
|
+
// Still public AFTER the ADR 0155 scope widen: `repo` COULD create a private repo,
|
|
103
|
+
// but a repo Cloud Bongos creates stays public by product choice (open-source /
|
|
104
|
+
// capture-resistant by design). Guards against the widen silently changing what we make.
|
|
105
|
+
assert.equal(sentBody.private, false, 'a repo WE create is public by product choice');
|
|
81
106
|
assert.equal(sentBody.name, 'mercury');
|
|
82
107
|
assert.deepEqual(out, { full_name: 'me/mercury', html_url: 'https://github.com/me/mercury' });
|
|
83
108
|
});
|
|
@@ -119,3 +119,26 @@ test('GET /github/repos → 403 repo_scope_required when configured but no store
|
|
|
119
119
|
db.getGithubToken = orig;
|
|
120
120
|
}
|
|
121
121
|
});
|
|
122
|
+
|
|
123
|
+
// ADR 0155 / task 1002561. Widening scopeForWebFlow to `repo` does NOT upgrade rows
|
|
124
|
+
// already in github_oauth_tokens. A surviving pre-0155 `public_repo` token can still
|
|
125
|
+
// list + create PUBLIC repos, so WITHOUT the scope check the route 200s while silently
|
|
126
|
+
// omitting the user's private repos — and the real failure resurfaces minutes later as
|
|
127
|
+
// an opaque 403 on the provisioner's push. It must take the reconnect path instead.
|
|
128
|
+
// (No 200-path test here: `repo`-scoped tokens fall through to a real GitHub call, and
|
|
129
|
+
// this file is deliberately network-free. The positive direction is covered exhaustively
|
|
130
|
+
// by grantedScopeCoversRepos in tests/github_repos_helpers.mjs.)
|
|
131
|
+
test('GET /github/repos → 403 repo_scope_required for a STALE pre-0155 public_repo token', async () => {
|
|
132
|
+
const orig = db.getGithubToken;
|
|
133
|
+
db.getGithubToken = async () => ({ token: 'gho_stale', scope: 'public_repo,read:user' });
|
|
134
|
+
try {
|
|
135
|
+
const handler = finalHandler(buildGithubRouter(), 'get', '/github/repos');
|
|
136
|
+
const res = fakeRes();
|
|
137
|
+
await handler({ builder: { id: 1 }, query: {} }, res);
|
|
138
|
+
assert.equal(res._status, 403, 'a public_repo-only token must not be treated as sufficient');
|
|
139
|
+
assert.equal(res._body.error.code, 'repo_scope_required');
|
|
140
|
+
assert.match(res._body.error.message, /private/i, 'tells the user WHY reconnecting is needed');
|
|
141
|
+
} finally {
|
|
142
|
+
db.getGithubToken = orig;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
@@ -51,7 +51,7 @@ const buildGoalsRouter = require('../modules/lifecycle/routes/goals.js');
|
|
|
51
51
|
// The pure, exported, ordered create gate — the one place the decision is made.
|
|
52
52
|
const { authorizeGoalTaskCreate } = buildGoalsRouter;
|
|
53
53
|
const { deriveRequiredRank, highestRank } = require('../modules/lifecycle/db.js');
|
|
54
|
-
const { MODULE_EXPECTED_RANKS, checkRouteRanks } = require('../src/bongos/route-rank-check.js');
|
|
54
|
+
const { MODULE_EXPECTED_RANKS, checkRouteRanks, pinMismatch } = require('../src/bongos/route-rank-check.js');
|
|
55
55
|
const { scopeIncludesProtected, protectedModules, isModuleKey } = require('../src/bongos/module-scope-map.js');
|
|
56
56
|
|
|
57
57
|
const goalsSrc = readFileSync(new URL('../modules/lifecycle/routes/goals.js', import.meta.url), 'utf8');
|
|
@@ -169,7 +169,12 @@ await test('[pin] every goal route is pinned in MODULE_EXPECTED_RANKS.lifecycle'
|
|
|
169
169
|
assert.equal(pins['POST /goals'], 'metic+archon');
|
|
170
170
|
assert.equal(pins['POST /goals/:id/members'], 'any-builder', 'task 1762: widened to owner|lead|archon, in-handler gate');
|
|
171
171
|
assert.equal(pins['POST /goals/:id/scope'], 'metic+archon');
|
|
172
|
-
|
|
172
|
+
// ADR 0157 lowered `goal.reorder` archon→metic and re-pinned it by atom. Ordering the
|
|
173
|
+
// queue is prioritisation, not an authority grant — the "never self-granted" property
|
|
174
|
+
// that matters here is that reorder is NOT reachable through a goal's own
|
|
175
|
+
// owner/manager authority, and that still holds: it needs a global permission no
|
|
176
|
+
// member role confers.
|
|
177
|
+
assert.equal(pins['POST /goals/reorder'], 'perm:goal.reorder', 'reorder needs a global permission, never a goal-member grant');
|
|
173
178
|
});
|
|
174
179
|
await test('[pin] LIVE: every pinned goal route classifies to its expected rank (a drift would mismatch)', () => {
|
|
175
180
|
const res = checkRouteRanks();
|
|
@@ -180,7 +185,12 @@ await test('[pin] LIVE: every pinned goal route classifies to its expected rank
|
|
|
180
185
|
if (!key.includes('/goals')) continue; // this suite owns the goal-route pins
|
|
181
186
|
const r = byKey.get(key);
|
|
182
187
|
assert.ok(r, `pinned goal route ${key} must exist in the live route set`);
|
|
183
|
-
|
|
188
|
+
// A pin is either a rank label or `perm:<atom>[,<atom>]` (ADR 0151 / ADR 0157). For an
|
|
189
|
+
// atom pin the live row carries the ATOMS, and its rank label is derived from the
|
|
190
|
+
// catalog floor — so compare atoms to atoms, ranks to ranks. Reusing the checker's own
|
|
191
|
+
// pinMismatch keeps this suite from re-implementing (and drifting from) that rule.
|
|
192
|
+
const mismatch = pinMismatch(expected, r);
|
|
193
|
+
assert.equal(mismatch, null, `goal route ${key} ${mismatch}`);
|
|
184
194
|
}
|
|
185
195
|
});
|
|
186
196
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// tests/goal_cli.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit test for the PURE helpers exported from scripts/gds/goal.js (the goal
|
|
4
|
+
// board CLI): rollup(), criterionGlyph(), criterionState(), bucketTasks(),
|
|
5
|
+
// liveTasks(). All are deterministic with no DB, network, or live server —
|
|
6
|
+
// requiring the script must NOT run the CLI (main is require.main-guarded).
|
|
7
|
+
// Run: node tests/goal_cli.mjs
|
|
8
|
+
|
|
9
|
+
import { strict as assert } from 'node:assert';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const goal = require('../scripts/gds/goal.js');
|
|
14
|
+
|
|
15
|
+
let passed = 0;
|
|
16
|
+
let failed = 0;
|
|
17
|
+
function test(name, fn) {
|
|
18
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
19
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// All EIGHT statuses of migration 012's CHECK constraint.
|
|
23
|
+
const ALL_STATUSES = ['backlog', 'ready', 'active', 'blocked', 'completed', 'confirmed', 'shipped', 'abandoned'];
|
|
24
|
+
const t = (status) => ({ status });
|
|
25
|
+
|
|
26
|
+
test('rollup: counts shipped over total', () => {
|
|
27
|
+
assert.equal(goal.rollup([t('shipped'), t('shipped'), t('ready')]), '2/3 shipped · 1 ready');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('rollup: in-flight covers active/completed/confirmed', () => {
|
|
31
|
+
assert.equal(goal.rollup([t('active'), t('completed'), t('confirmed')]), '0/3 shipped · 3 in flight');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('rollup: omits zero segments (backlog-only goal shows only shipped fraction)', () => {
|
|
35
|
+
assert.equal(goal.rollup([t('backlog'), t('backlog')]), '0/2 shipped');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('rollup: abandoned is excluded from the denominator (migration 017 rule)', () => {
|
|
39
|
+
// Without the exclusion this would read "2/3 shipped" for a goal with no live work left.
|
|
40
|
+
assert.equal(goal.rollup([t('shipped'), t('shipped'), t('abandoned')]), '2/2 shipped');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('liveTasks: filters only abandoned', () => {
|
|
44
|
+
const live = goal.liveTasks(ALL_STATUSES.map(t));
|
|
45
|
+
assert.equal(live.length, ALL_STATUSES.length - 1);
|
|
46
|
+
assert.ok(live.every((x) => x.status !== 'abandoned'));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('bucketTasks: partitions all 8 lifecycle statuses into exactly one bucket', () => {
|
|
50
|
+
const tasks = ALL_STATUSES.map(t);
|
|
51
|
+
const b = goal.bucketTasks(tasks);
|
|
52
|
+
assert.equal(b.shipped.length, 1);
|
|
53
|
+
assert.equal(b.inFlight.length, 3);
|
|
54
|
+
assert.equal(b.ready.length, 1);
|
|
55
|
+
assert.equal(b.gated.length, 2); // backlog + blocked
|
|
56
|
+
assert.equal(b.abandoned.length, 1);
|
|
57
|
+
const total = b.shipped.length + b.inFlight.length + b.ready.length + b.gated.length + b.abandoned.length;
|
|
58
|
+
assert.equal(total, tasks.length, 'no status may fall through the buckets');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('criterionGlyph: satisfied wins over pending_review', () => {
|
|
62
|
+
assert.equal(goal.criterionGlyph({ satisfied: true, pending_review: true }), '✓');
|
|
63
|
+
assert.equal(goal.criterionGlyph({ satisfied: false, pending_review: true }), '◇');
|
|
64
|
+
assert.equal(goal.criterionGlyph({ satisfied: false, pending_review: false }), '○');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('criterionState: pending review names the /goal-review queue', () => {
|
|
68
|
+
assert.match(goal.criterionState({ satisfied: false, pending_review: true }), /pending review/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('criterionState: open criterion reports unshipped-of-linked counts (string API ids)', () => {
|
|
72
|
+
// The API returns counts as strings — the formatter must not NaN on them.
|
|
73
|
+
const s = goal.criterionState({ satisfied: false, pending_review: false, unshipped_tasks: '3', linked_tasks: '3' });
|
|
74
|
+
assert.equal(s, '3 of 3 linked tasks unshipped');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('criterionState: singular linked task reads grammatically', () => {
|
|
78
|
+
const s = goal.criterionState({ satisfied: false, pending_review: false, unshipped_tasks: '1', linked_tasks: '1' });
|
|
79
|
+
assert.equal(s, '1 of 1 linked task unshipped');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('criterionState: zero linked tasks must NOT read as complete', () => {
|
|
83
|
+
const s = goal.criterionState({ satisfied: false, pending_review: false, unshipped_tasks: '0', linked_tasks: '0' });
|
|
84
|
+
assert.equal(s, 'no linked tasks yet');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
console.log('');
|
|
88
|
+
console.log(`${passed} passed, ${failed} failed`);
|
|
89
|
+
if (failed > 0) process.exit(1);
|