@bongos/core 1.18.19 → 1.19.3
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 +547 -197
- 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/bin/bongos.js +33 -7
- 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/0137-upstream-publish-policy-manual-owner-gated.md +1 -1
- 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/0161-publish-on-merge.md +34 -0
- package/docs/adr/README.md +9 -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 +181 -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 +6 -2
- 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/migrations/core_205_versions_track_default.sql +28 -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 +99 -69
- 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/bump-version.js +76 -0
- 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 +77 -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 +96 -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_cli.mjs +91 -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/bump_version.mjs +71 -0
- 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 +147 -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/patch_gate_ordering.mjs +102 -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/currency_label.mjs
CHANGED
|
@@ -55,10 +55,19 @@ test('clientBranding is a SMALL allow-list — no server-only field leaks to the
|
|
|
55
55
|
db: { database: 'amazonprimea' },
|
|
56
56
|
envPrefix: 'OTB',
|
|
57
57
|
});
|
|
58
|
-
|
|
58
|
+
// `reward` joined the projection in task 1002469 (ADR 0146 follow-up) — the resolved
|
|
59
|
+
// reward POLICY, so a builder on a cost-plus-only instance isn't shown a per-task
|
|
60
|
+
// estimate that will never pay. The allow-list was not updated with it, so this guard
|
|
61
|
+
// had been failing since that landed (task 1002470).
|
|
62
|
+
assert.deepEqual(Object.keys(projected).sort(), ['copy', 'currency', 'identity', 'rankLabels', 'reward', 'theme']);
|
|
59
63
|
for (const leak of ['domains', 'repo', 'db', 'envPrefix', 'llmProjectName']) {
|
|
60
64
|
assert.equal(projected[leak], undefined, `${leak} must not reach the client`);
|
|
61
65
|
}
|
|
66
|
+
// The new member is itself held to the same small-surface rule — the point of this
|
|
67
|
+
// test is that every projected key is a reviewed one, not merely that it is listed.
|
|
68
|
+
assert.deepEqual(Object.keys(projected.reward).sort(), ['mode', 'perTaskCredit']);
|
|
69
|
+
assert.equal(typeof projected.reward.mode, 'string');
|
|
70
|
+
assert.equal(typeof projected.reward.perTaskCredit, 'boolean');
|
|
62
71
|
});
|
|
63
72
|
|
|
64
73
|
test('R56: injectBrandingGlobal inserts a synchronous window.__BRANDING__ before </head>', () => {
|
|
@@ -14,9 +14,11 @@ import { mkdtempSync, rmSync, existsSync, statSync } from 'node:fs';
|
|
|
14
14
|
import { tmpdir } from 'node:os';
|
|
15
15
|
import { join, dirname } from 'node:path';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { skipUnlessPresent } from './helpers.mjs';
|
|
17
18
|
|
|
18
19
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
20
|
const SCRIPT = join(ROOT, 'scripts', 'deploy', 'deploy-instance.sh');
|
|
21
|
+
skipUnlessPresent('scripts/deploy/deploy-instance.sh');
|
|
20
22
|
|
|
21
23
|
let passed = 0;
|
|
22
24
|
let failed = 0;
|
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
import { strict as assert } from 'node:assert';
|
|
11
11
|
import { createRequire } from 'node:module';
|
|
12
|
+
import { requireHostModule } from './helpers.mjs';
|
|
12
13
|
|
|
13
14
|
const require = createRequire(import.meta.url);
|
|
14
15
|
const { buildDevboxDownloads } = require('../src/devbox-downloads.js');
|
|
15
|
-
const committedConfig = require
|
|
16
|
+
const committedConfig = requireHostModule(require, '../config/devbox-app.json');
|
|
16
17
|
|
|
17
18
|
let passed = 0;
|
|
18
19
|
let failed = 0;
|
package/tests/display_name.mjs
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
// tests/display_name.mjs
|
|
2
2
|
//
|
|
3
|
-
// Unit smoke for
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// Unit smoke for the self-service display-name editor (GET/PATCH
|
|
4
|
+
// /me/display-name), in two parts:
|
|
5
|
+
//
|
|
6
|
+
// 1. modules/builder-settings/display-name.js (task 1669) — the pure
|
|
7
|
+
// validator. Trim, reject empty / whitespace-only, the length ceiling, and
|
|
8
|
+
// control-character rejection.
|
|
9
|
+
// 2. The PERSISTENCE guard (task 1002568, ADR 0156) — "your name wins,
|
|
10
|
+
// always". Only covering the validator is why the bug shipped: the save
|
|
11
|
+
// worked and then upsertBuilder, which runs on EVERY sign-in, re-set
|
|
12
|
+
// display_name from the GitHub name. These tests read the REAL SQL both
|
|
13
|
+
// halves emit and check the conflict branch's behaviour, so deleting the
|
|
14
|
+
// guard fails here instead of in production.
|
|
15
|
+
//
|
|
16
|
+
// No pg: the shared pool's query() is stubbed to record the SQL.
|
|
9
17
|
//
|
|
10
18
|
// Run: node tests/display_name.mjs
|
|
11
19
|
|
|
@@ -119,5 +127,86 @@ t('hasControlChar detects control chars and clears normal text', () => {
|
|
|
119
127
|
assert.equal(d.hasControlChar(`with${BEL}bell`), true);
|
|
120
128
|
});
|
|
121
129
|
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Persistence: a user-set name survives the next sign-in (task 1002568)
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
// Stub the SHARED pool object before requiring anything that queries it — both
|
|
135
|
+
// db-kernel.js and the module's db.js capture this exact singleton at load.
|
|
136
|
+
const { pool } = require('../src/bongos/pool.js');
|
|
137
|
+
let lastSql = null;
|
|
138
|
+
pool.query = async (sql) => { lastSql = sql; return { rows: [{}] }; };
|
|
139
|
+
|
|
140
|
+
const kernel = require('../src/bongos/db-kernel.js');
|
|
141
|
+
const settingsDb = require('../modules/builder-settings/db.js');
|
|
142
|
+
|
|
143
|
+
// Pull one assignment out of an ON CONFLICT ... DO UPDATE SET clause. None of
|
|
144
|
+
// the assignments contain a comma, so splitting on commas is exact.
|
|
145
|
+
function conflictAssignment(sql, column) {
|
|
146
|
+
const setClause = sql.slice(sql.indexOf('DO UPDATE'), sql.indexOf('RETURNING'));
|
|
147
|
+
for (const part of setClause.replace(/^[\s\S]*?SET/, '').split(',')) {
|
|
148
|
+
const [lhs, ...rhs] = part.split('=');
|
|
149
|
+
if (lhs.trim() === column) return rhs.join('=').trim();
|
|
150
|
+
}
|
|
151
|
+
throw new Error(`no "${column} = …" assignment in the conflict branch`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Evaluate the display_name assignment for one row, so the tests below read as
|
|
155
|
+
// behaviour ("whose name ends up stored?") rather than string matching. An
|
|
156
|
+
// unguarded assignment — the shape that shipped the bug — is a bare column
|
|
157
|
+
// reference and evaluates to whatever GitHub just sent.
|
|
158
|
+
function resolvedDisplayName(expr, { source, stored, incoming }) {
|
|
159
|
+
const ref = (token) => {
|
|
160
|
+
if (/^EXCLUDED\.display_name$/i.test(token)) return incoming;
|
|
161
|
+
if (/^builders\.display_name$/i.test(token)) return stored;
|
|
162
|
+
throw new Error(`unrecognised display_name reference: ${token}`);
|
|
163
|
+
};
|
|
164
|
+
const norm = expr.replace(/\s+/g, ' ').trim();
|
|
165
|
+
const m = /^CASE WHEN builders\.display_name_source = '(\w+)' THEN (\S+) ELSE (\S+) END$/i.exec(norm);
|
|
166
|
+
if (!m) return ref(norm);
|
|
167
|
+
return source === m[1] ? ref(m[2]) : ref(m[3]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Drive both halves once (they're async), then assert on the captured SQL with
|
|
171
|
+
// the same synchronous `t` helper the validator tests use.
|
|
172
|
+
await kernel.upsertBuilder({
|
|
173
|
+
githubId: 64553469, githubLogin: 'jaxri', displayName: 'GitHub Name', avatarUrl: null,
|
|
174
|
+
});
|
|
175
|
+
const upsertExpr = conflictAssignment(lastSql, 'display_name');
|
|
176
|
+
|
|
177
|
+
await settingsDb.setBuilderDisplayName(1000095, 'Renée the Builder');
|
|
178
|
+
const setNameSql = lastSql.replace(/\s+/g, ' ');
|
|
179
|
+
|
|
180
|
+
console.log('\nsign-in persistence (upsertBuilder conflict branch):');
|
|
181
|
+
|
|
182
|
+
t('the conflict branch keys display_name off display_name_source', () => {
|
|
183
|
+
assert.ok(
|
|
184
|
+
/display_name_source/.test(upsertExpr),
|
|
185
|
+
`upsertBuilder must not re-set display_name unconditionally; got: ${upsertExpr}`
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
t('a builder who set their own name keeps it when GitHub sends a different one', () => {
|
|
190
|
+
const kept = resolvedDisplayName(upsertExpr, {
|
|
191
|
+
source: 'user', stored: 'Renée the Builder', incoming: 'renee-gh',
|
|
192
|
+
});
|
|
193
|
+
assert.equal(kept, 'Renée the Builder');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
t('a builder who never renamed still picks up a GitHub rename', () => {
|
|
197
|
+
const synced = resolvedDisplayName(upsertExpr, {
|
|
198
|
+
source: 'github', stored: 'Old GitHub Name', incoming: 'New GitHub Name',
|
|
199
|
+
});
|
|
200
|
+
assert.equal(synced, 'New GitHub Name');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
t('setBuilderDisplayName stamps the name as user-set', () => {
|
|
204
|
+
assert.match(
|
|
205
|
+
setNameSql,
|
|
206
|
+
/display_name_source = 'user'/,
|
|
207
|
+
'without the stamp the saved name reverts at the next sign-in'
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
122
211
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
123
212
|
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import { strict as assert } from 'node:assert';
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
|
+
import { requireHostModule } from './helpers.mjs';
|
|
7
8
|
const require = createRequire(import.meta.url);
|
|
8
|
-
const { classifyEviction, estTokens, blockText, stubFor, normalize, readRange, isVolatileBash, salientTokens } = require
|
|
9
|
+
const { classifyEviction, estTokens, blockText, stubFor, normalize, readRange, isVolatileBash, salientTokens } = requireHostModule(require, '../.claude/hooks/eviction-policy.js');
|
|
9
10
|
|
|
10
11
|
let passed = 0, failed = 0;
|
|
11
12
|
function test(name, fn) {
|
package/tests/file_map.mjs
CHANGED
|
@@ -31,7 +31,12 @@ test('committed file-map sections are current (the Path-C CI gate)', () => {
|
|
|
31
31
|
test('EVERY skill + scheduled-task dir on disk is listed AND has a note — no silent omission, no ghost', () => {
|
|
32
32
|
const model = gen.buildModel();
|
|
33
33
|
assert.ok(model.length === 2, 'two generated sections');
|
|
34
|
+
let checked = 0;
|
|
34
35
|
for (const section of model) {
|
|
36
|
+
// .claude/skills + .claude/scheduled-tasks are INSTANCE-materialized; a neutral
|
|
37
|
+
// core checkout carries neither (task 1002470). Check the sections that exist.
|
|
38
|
+
if (!existsSync(path.join(ROOT, section.dir))) continue;
|
|
39
|
+
checked += 1;
|
|
35
40
|
const onDisk = readdirSync(path.join(ROOT, section.dir), { withFileTypes: true })
|
|
36
41
|
.filter((e) => e.isDirectory() && !e.name.startsWith('.'))
|
|
37
42
|
.map((e) => e.name)
|
|
@@ -41,6 +46,11 @@ test('EVERY skill + scheduled-task dir on disk is listed AND has a note — no s
|
|
|
41
46
|
assert.equal(section.missing.length, 0, `${section.dir}: dirs without a note: ${section.missing.join(', ')}`);
|
|
42
47
|
assert.equal(section.stale.length, 0, `${section.dir}: notes with no dir (ghosts): ${section.stale.join(', ')}`);
|
|
43
48
|
}
|
|
49
|
+
// Non-vacuity: on an instance that materializes .claude/, this must have checked
|
|
50
|
+
// something — a skipped-everything run would otherwise pass while asserting nothing.
|
|
51
|
+
if (existsSync(path.join(ROOT, '.claude'))) {
|
|
52
|
+
assert.ok(checked > 0, '.claude/ exists but no section was checked — the exhaustiveness guard went vacuous');
|
|
53
|
+
}
|
|
44
54
|
});
|
|
45
55
|
|
|
46
56
|
test('the generated blocks live between markers and carry one ← note per entry', () => {
|
|
@@ -6,16 +6,24 @@
|
|
|
6
6
|
import assert from 'node:assert/strict';
|
|
7
7
|
import { test } from 'node:test';
|
|
8
8
|
import auth from '../src/bongos/auth.js';
|
|
9
|
+
import brandingMod from '../src/branding.js';
|
|
9
10
|
|
|
10
11
|
const { firstAdminLogin, maybeSeatFirstAdmin } = auth;
|
|
12
|
+
const { branding } = brandingMod;
|
|
11
13
|
|
|
12
14
|
function clearEnv() {
|
|
13
15
|
for (const k of ['OTB_FIRST_ADMIN', 'GDS_FIRST_ADMIN', 'PMS_FIRST_ADMIN', 'CLOUDBONGOS_FIRST_ADMIN']) delete process.env[k];
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
test('firstAdminLogin: resolves from branding
|
|
18
|
+
test('firstAdminLogin: resolves from the branding pack when no env override', () => {
|
|
19
|
+
// Asserts the RESOLUTION, not one instance's owner. This used to hardcode the OTB
|
|
20
|
+
// founder's login, which both broke on any other instance (the neutral pack sets
|
|
21
|
+
// firstAdmin: "") and put a founder identity in a test — the thing
|
|
22
|
+
// scripts/gds/audit-authorship.sh exists to keep out (task 1002470).
|
|
17
23
|
clearEnv();
|
|
18
|
-
|
|
24
|
+
const configured = (branding().firstAdmin || '').trim() || null;
|
|
25
|
+
assert.equal(firstAdminLogin(), configured,
|
|
26
|
+
'with no env override, firstAdminLogin must be exactly what the branding pack configures');
|
|
19
27
|
});
|
|
20
28
|
|
|
21
29
|
test('firstAdminLogin: env override wins over branding (configured prefix)', () => {
|
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
|
|
11
11
|
import { strict as assert } from 'node:assert';
|
|
12
12
|
import { createRequire } from 'node:module';
|
|
13
|
+
import { requireHostModule } from './helpers.mjs';
|
|
13
14
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
14
15
|
import { tmpdir } from 'node:os';
|
|
15
16
|
import { join } from 'node:path';
|
|
16
17
|
|
|
17
18
|
const require = createRequire(import.meta.url);
|
|
18
|
-
const { firstRunEngineCheck } = require
|
|
19
|
+
const { firstRunEngineCheck } = requireHostModule(require, '../.claude/hooks/session-start.js');
|
|
19
20
|
|
|
20
21
|
let passed = 0, failed = 0;
|
|
21
22
|
function t(name, fn) {
|
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);
|