@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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// tests/blocker_link.mjs
|
|
2
|
+
//
|
|
3
|
+
// Task 1002559 — linking a blocker to a task must actually put that task in
|
|
4
|
+
// 'blocked', so the resolve-side trigger (migration 009) has something to
|
|
5
|
+
// promote. Before this, `linkBlockerToTask` was a bare INSERT and the whole
|
|
6
|
+
// link → blocked → auto-ready loop was dead end-to-end.
|
|
7
|
+
//
|
|
8
|
+
// The SQL itself is exercised by the post-deploy smoke run (same convention as
|
|
9
|
+
// tests/criterion_proposal.mjs). What is unit-tested here is the eligibility
|
|
10
|
+
// predicate the SQL is built from — `blocksOnLink` and LINK_BLOCKABLE_STATUSES
|
|
11
|
+
// are the single source of truth for the UPDATE's status guard, so a drift
|
|
12
|
+
// between the two is not expressible.
|
|
13
|
+
//
|
|
14
|
+
// Run: node tests/blocker_link.mjs
|
|
15
|
+
|
|
16
|
+
import { strict as assert } from 'node:assert';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const blockers = require('../modules/ideas/blockers.js');
|
|
21
|
+
|
|
22
|
+
let passed = 0;
|
|
23
|
+
let failed = 0;
|
|
24
|
+
|
|
25
|
+
function test(name, fn) {
|
|
26
|
+
try {
|
|
27
|
+
fn();
|
|
28
|
+
passed += 1;
|
|
29
|
+
console.log(` ok ${name}`);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
failed += 1;
|
|
32
|
+
console.log(` FAIL ${name}\n ${err.message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// -- the eligibility predicate --------------------------------------------
|
|
37
|
+
|
|
38
|
+
test('a ready task is blocked on link — the live trap this fixes', () => {
|
|
39
|
+
assert.equal(blockers.blocksOnLink('ready'), true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('backlog is NOT blocked on link — blocked→ready would launder the promote gate', () => {
|
|
43
|
+
// backlog is pre-workable and already unclaimable; migration 176 keeps a
|
|
44
|
+
// dep-ship from promoting it past an open blocker. Flipping it to 'blocked'
|
|
45
|
+
// would make the resolve trigger (009) promote it to 'ready', turning a
|
|
46
|
+
// blocker resolution into a promotion nobody authorised (promote is
|
|
47
|
+
// metic+archon).
|
|
48
|
+
assert.equal(blockers.blocksOnLink('backlog'), false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('an active task is not yanked out from under its claim holder', () => {
|
|
52
|
+
assert.equal(blockers.blocksOnLink('active'), false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('terminal states are never rewritten', () => {
|
|
56
|
+
for (const s of ['shipped', 'abandoned', 'completed', 'confirmed']) {
|
|
57
|
+
assert.equal(blockers.blocksOnLink(s), false, `${s} must not be re-blocked`);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('re-linking an already-blocked task is a no-op, not a double-write', () => {
|
|
62
|
+
assert.equal(blockers.blocksOnLink('blocked'), false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('an unknown/null status is refused rather than assumed blockable', () => {
|
|
66
|
+
assert.equal(blockers.blocksOnLink('nonsense'), false);
|
|
67
|
+
assert.equal(blockers.blocksOnLink(null), false);
|
|
68
|
+
assert.equal(blockers.blocksOnLink(undefined), false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// -- the constant the SQL is built from ------------------------------------
|
|
72
|
+
|
|
73
|
+
test('LINK_BLOCKABLE_STATUSES is exported so the UPDATE guard cannot drift', () => {
|
|
74
|
+
assert.ok(Array.isArray(blockers.LINK_BLOCKABLE_STATUSES));
|
|
75
|
+
assert.deepEqual(blockers.LINK_BLOCKABLE_STATUSES, ['ready']);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('every blockable status round-trips through the resolve trigger', () => {
|
|
79
|
+
// Migration 009 promotes `status='blocked'` → 'ready'. So every status we
|
|
80
|
+
// flip INTO blocked must be one that 'ready' is a correct destination for,
|
|
81
|
+
// or the link/resolve pair silently changes a task's standing.
|
|
82
|
+
for (const s of blockers.LINK_BLOCKABLE_STATUSES) {
|
|
83
|
+
assert.equal(s, 'ready', `${s} does not round-trip: resolve returns it to 'ready'`);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
88
|
+
process.exit(failed ? 1 : 0);
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// tests/bongos_cli.mjs
|
|
2
|
+
//
|
|
3
|
+
// The `bongos` dispatcher's --help contract (task 1002625). The regression that
|
|
4
|
+
// motivates this file: `bongos init --help` used to FORWARD the flag to init.js,
|
|
5
|
+
// which doesn't recognise it and dropped into its interactive interview —
|
|
6
|
+
// printing "Project name?" and blocking forever (fatal in CI). The dispatcher
|
|
7
|
+
// now answers --help/-h itself for every verb whose script has no usage printer
|
|
8
|
+
// of its own; the five that do (selfHelp) keep their richer output, and `exec`
|
|
9
|
+
// always forwards (the flag belongs to the child script).
|
|
10
|
+
//
|
|
11
|
+
// Run: node tests/bongos_cli.mjs
|
|
12
|
+
|
|
13
|
+
import { strict as assert } from 'node:assert';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const cli = require('../bin/bongos.js');
|
|
21
|
+
|
|
22
|
+
const BIN = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'bongos.js');
|
|
23
|
+
|
|
24
|
+
let passed = 0;
|
|
25
|
+
let failed = 0;
|
|
26
|
+
function test(name, fn) {
|
|
27
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
28
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Every spawn gets a hard timeout — the exact failure this file guards against
|
|
32
|
+
// is a verb BLOCKING on interactive input, and a hung test is that bug escaping.
|
|
33
|
+
const run = (...args) => spawnSync(process.execPath, [BIN, ...args], {
|
|
34
|
+
encoding: 'utf8', timeout: 30_000, input: '',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ---- pure: the help card + the selfHelp roster ----
|
|
38
|
+
|
|
39
|
+
test('verbHelpText: names the verb, its summary, and the full-help pointer', () => {
|
|
40
|
+
const out = cli.verbHelpText('claim', cli.VERBS.claim);
|
|
41
|
+
assert.match(out, /^bongos claim — /);
|
|
42
|
+
assert.match(out, /Claim a task by id/);
|
|
43
|
+
assert.match(out, /bongos help/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('VERBS: selfHelp is only claimed by scripts that really print their own usage', () => {
|
|
47
|
+
// Verified by reading each script (they all branch on --help/-h to a usage()
|
|
48
|
+
// printer). If a verb is added here, verify its script first — a wrong
|
|
49
|
+
// selfHelp:true recreates the blocking-interview bug for that verb.
|
|
50
|
+
const selfHelp = Object.entries(cli.VERBS).filter(([, d]) => d.selfHelp).map(([v]) => v).sort();
|
|
51
|
+
assert.deepEqual(selfHelp, ['dev', 'module', 'status', 'task', 'upgrade']);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ---- spawned: the dispatcher answers --help without touching the script ----
|
|
55
|
+
|
|
56
|
+
test('bongos --help (first arg) prints the dispatcher help, exit 0', () => {
|
|
57
|
+
const r = run('--help');
|
|
58
|
+
assert.equal(r.status, 0, r.stderr);
|
|
59
|
+
assert.match(r.stdout, /bongos — the Cloud Bongos CLI/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('bongos init --help answers instantly — no interview, no blocking (the 1002625 regression)', () => {
|
|
63
|
+
const r = run('init', '--help');
|
|
64
|
+
assert.equal(r.signal, null, 'must not die on the spawn timeout — that means it blocked on input');
|
|
65
|
+
assert.equal(r.status, 0, r.stderr);
|
|
66
|
+
assert.match(r.stdout, /^bongos init — /);
|
|
67
|
+
assert.ok(!/Project name\?/.test(r.stdout + r.stderr), 'the interactive interview must not start');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('bongos claim -h is answered by the dispatcher too (short flag, non-selfHelp verb)', () => {
|
|
71
|
+
const r = run('claim', '-h');
|
|
72
|
+
assert.equal(r.status, 0, r.stderr);
|
|
73
|
+
assert.match(r.stdout, /^bongos claim — /);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('bongos task --help forwards to task.js (selfHelp) and gets its richer usage', () => {
|
|
77
|
+
const r = run('task', '--help');
|
|
78
|
+
assert.equal(r.status, 0, r.stderr);
|
|
79
|
+
assert.ok(!/^bongos task — .*\n\nForwards to/.test(r.stdout), 'must NOT be the dispatcher card');
|
|
80
|
+
assert.match(r.stdout + r.stderr, /new|usage/i, 'task.js prints its own usage');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('bongos exec still refuses a missing path with exit 2 (--help interception must not swallow exec)', () => {
|
|
84
|
+
const r = run('exec');
|
|
85
|
+
assert.equal(r.status, 2);
|
|
86
|
+
assert.match(r.stderr, /package-relative script path/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log(`bongos CLI tests: ${passed} passed, ${failed} failed`);
|
|
91
|
+
if (failed > 0) process.exit(1);
|
|
@@ -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 { buildBongosDownloads } = require('../src/bongos-downloads.js');
|
|
15
|
-
const committedConfig = require
|
|
16
|
+
const committedConfig = requireHostModule(require, '../config/bongos-cli.json');
|
|
16
17
|
|
|
17
18
|
let passed = 0;
|
|
18
19
|
let failed = 0;
|
|
@@ -20,6 +20,7 @@ import { strict as assert } from 'node:assert';
|
|
|
20
20
|
import { createRequire } from 'node:module';
|
|
21
21
|
import { spawnSync } from 'node:child_process';
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { skipUnlessPresent } from './helpers.mjs';
|
|
23
24
|
import fs from 'node:fs';
|
|
24
25
|
import os from 'node:os';
|
|
25
26
|
import path from 'node:path';
|
|
@@ -28,6 +29,7 @@ const require = createRequire(import.meta.url);
|
|
|
28
29
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
29
30
|
const repo = path.resolve(here, '..');
|
|
30
31
|
const HOOK = path.join(repo, 'infra', 'box-dns-cloudflare.sh');
|
|
32
|
+
skipUnlessPresent('infra/box-dns-cloudflare.sh');
|
|
31
33
|
|
|
32
34
|
// ── a fake `curl` that emulates the Cloudflare API per FAKE_CF_SCENARIO ─────────
|
|
33
35
|
const FAKE_CURL = `#!/usr/bin/env node
|
|
@@ -12,9 +12,11 @@ import { strict as assert } from 'node:assert';
|
|
|
12
12
|
import fs from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { skipUnlessPresent } from './helpers.mjs';
|
|
15
16
|
|
|
16
17
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
18
|
const settingsPath = path.join(__dirname, '..', '.claude', 'settings.json');
|
|
19
|
+
skipUnlessPresent('.claude/settings.json');
|
|
18
20
|
|
|
19
21
|
let passed = 0, failed = 0;
|
|
20
22
|
function test(name, fn) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// the WEB TIER fetch it. The old guard (/^https:\/\/[^\s]+$/) accepted ANY https
|
|
6
6
|
// host, so a builder could make the prod server probe internal/metadata
|
|
7
7
|
// endpoints (SSRF). isSafeTerminalUrl(url, login) must allow ONLY the two real
|
|
8
|
-
// tunnel shapes — `term-<login
|
|
8
|
+
// tunnel shapes — `term-<login>.<apex>` (bound to the caller's login)
|
|
9
9
|
// and `*.trycloudflare.com` — and reject everything else.
|
|
10
10
|
//
|
|
11
11
|
// Pure function, no server/DB/network.
|
|
@@ -25,19 +25,25 @@ const test = (name, fn) => {
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
const LOGIN = 'lars589';
|
|
28
|
+
// The dev-box apex is INJECTED rather than taken from the live instance branding
|
|
29
|
+
// (task 1002470). Two reasons: the neutral core has no amazonprimea.com apex, so the
|
|
30
|
+
// three allow-cases could not pass; and every reject-case below was passing for the
|
|
31
|
+
// WRONG reason — an apex mismatch, not the rule it names. With a pinned apex,
|
|
32
|
+
// 'rejects another builder's term- host' really does prove the login binding.
|
|
33
|
+
const APEX = 'apex.example';
|
|
28
34
|
|
|
29
35
|
// ---- allowed: the two legitimate tunnel shapes -------------------------------
|
|
30
36
|
test('allows the named-tunnel apex host bound to the caller login', () => {
|
|
31
|
-
assert.equal(isSafeTerminalUrl(
|
|
37
|
+
assert.equal(isSafeTerminalUrl(`https://term-lars589.${APEX}`, LOGIN, APEX), true);
|
|
32
38
|
});
|
|
33
|
-
test(
|
|
34
|
-
assert.equal(isSafeTerminalUrl(
|
|
39
|
+
test(`allows the named-tunnel dev-zone host (term-<login>.dev.${APEX})`, () => {
|
|
40
|
+
assert.equal(isSafeTerminalUrl(`https://term-lars589.dev.${APEX}/`, LOGIN, APEX), true);
|
|
35
41
|
});
|
|
36
42
|
test('allows a Cloudflare quick-tunnel host (*.trycloudflare.com)', () => {
|
|
37
|
-
assert.equal(isSafeTerminalUrl('https://random-words-1234.trycloudflare.com', LOGIN), true);
|
|
43
|
+
assert.equal(isSafeTerminalUrl('https://random-words-1234.trycloudflare.com', LOGIN, APEX), true);
|
|
38
44
|
});
|
|
39
45
|
test('login match is case-insensitive (DNS is case-insensitive)', () => {
|
|
40
|
-
assert.equal(isSafeTerminalUrl(
|
|
46
|
+
assert.equal(isSafeTerminalUrl(`https://term-LarsCode.${APEX}`, 'larscode', APEX), true);
|
|
41
47
|
});
|
|
42
48
|
|
|
43
49
|
// ---- rejected: SSRF targets --------------------------------------------------
|
|
@@ -54,36 +60,36 @@ for (const bad of [
|
|
|
54
60
|
'https://printer.local/',
|
|
55
61
|
]) {
|
|
56
62
|
test(`rejects SSRF target ${bad}`, () => {
|
|
57
|
-
assert.equal(isSafeTerminalUrl(bad, LOGIN), false);
|
|
63
|
+
assert.equal(isSafeTerminalUrl(bad, LOGIN, APEX), false);
|
|
58
64
|
});
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
// ---- rejected: wrong scheme / shape / cross-login ----------------------------
|
|
62
68
|
test('rejects http (must be https)', () => {
|
|
63
|
-
assert.equal(isSafeTerminalUrl(
|
|
69
|
+
assert.equal(isSafeTerminalUrl(`http://term-lars589.${APEX}`, LOGIN, APEX), false);
|
|
64
70
|
});
|
|
65
71
|
test('rejects embedded credentials (user:pass@host)', () => {
|
|
66
|
-
assert.equal(isSafeTerminalUrl(
|
|
72
|
+
assert.equal(isSafeTerminalUrl(`https://user:pass@term-lars589.${APEX}`, LOGIN, APEX), false);
|
|
67
73
|
});
|
|
68
74
|
test('rejects another builder\'s term- host (login binding)', () => {
|
|
69
|
-
assert.equal(isSafeTerminalUrl(
|
|
75
|
+
assert.equal(isSafeTerminalUrl(`https://term-someoneelse.${APEX}`, LOGIN, APEX), false);
|
|
70
76
|
});
|
|
71
77
|
test('rejects a non-tunnel host on the project domain', () => {
|
|
72
|
-
assert.equal(isSafeTerminalUrl(
|
|
78
|
+
assert.equal(isSafeTerminalUrl(`https://api.${APEX}`, LOGIN, APEX), false);
|
|
73
79
|
});
|
|
74
80
|
test('rejects an arbitrary external host', () => {
|
|
75
|
-
assert.equal(isSafeTerminalUrl('https://evil.example.com', LOGIN), false);
|
|
81
|
+
assert.equal(isSafeTerminalUrl('https://evil.example.com', LOGIN, APEX), false);
|
|
76
82
|
});
|
|
77
83
|
test('rejects a lookalike suffix (…trycloudflare.com.evil.com)', () => {
|
|
78
|
-
assert.equal(isSafeTerminalUrl('https://x.trycloudflare.com.evil.com', LOGIN), false);
|
|
84
|
+
assert.equal(isSafeTerminalUrl('https://x.trycloudflare.com.evil.com', LOGIN, APEX), false);
|
|
79
85
|
});
|
|
80
86
|
test('rejects garbage / unparseable', () => {
|
|
81
|
-
assert.equal(isSafeTerminalUrl('not a url', LOGIN), false);
|
|
82
|
-
assert.equal(isSafeTerminalUrl('', LOGIN), false);
|
|
83
|
-
assert.equal(isSafeTerminalUrl(null, LOGIN), false);
|
|
87
|
+
assert.equal(isSafeTerminalUrl('not a url', LOGIN, APEX), false);
|
|
88
|
+
assert.equal(isSafeTerminalUrl('', LOGIN, APEX), false);
|
|
89
|
+
assert.equal(isSafeTerminalUrl(null, LOGIN, APEX), false);
|
|
84
90
|
});
|
|
85
91
|
test('rejects when login is empty (cannot bind the term- form)', () => {
|
|
86
|
-
assert.equal(isSafeTerminalUrl(
|
|
92
|
+
assert.equal(isSafeTerminalUrl(`https://term-lars589.${APEX}`, '', APEX), false);
|
|
87
93
|
});
|
|
88
94
|
|
|
89
95
|
console.log(`\n${pass} passed, ${fail} failed`);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// tests/bump_version.mjs — the CI auto-patch bump is shape-preserving (task 1002620).
|
|
2
|
+
//
|
|
3
|
+
// bump-version.js rewrites the single most load-bearing literal in the repo.
|
|
4
|
+
// These tests pin the three properties that must survive any refactor:
|
|
5
|
+
// 1. the bumped literal keeps the exact `^const CORE_VERSION = '…';` shape
|
|
6
|
+
// deploy-divergence.js greps and regex-reads;
|
|
7
|
+
// 2. a changelog entry lands inside the comment block, not after the const;
|
|
8
|
+
// 3. unrecognizable input throws instead of silently mis-editing.
|
|
9
|
+
|
|
10
|
+
import test from 'node:test';
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
const { bumpPatch, CONST_RE } = require(path.join(ROOT, 'scripts', 'gds', 'bump-version.js'));
|
|
20
|
+
|
|
21
|
+
const SAMPLE = [
|
|
22
|
+
'// prologue',
|
|
23
|
+
'// 1.19.0 — some entry. (task 1)',
|
|
24
|
+
'// 1.19.1 — another entry. (task 2)',
|
|
25
|
+
'// ---------------------------------------------------------------------------',
|
|
26
|
+
"const CORE_VERSION = '1.19.1'; // carrier; see changelog above",
|
|
27
|
+
'',
|
|
28
|
+
'module.exports = { CORE_VERSION };',
|
|
29
|
+
'',
|
|
30
|
+
].join('\n');
|
|
31
|
+
|
|
32
|
+
test('bumps patch and keeps the exact const line shape', () => {
|
|
33
|
+
const { from, to, text } = bumpPatch(SAMPLE);
|
|
34
|
+
assert.equal(from, '1.19.1');
|
|
35
|
+
assert.equal(to, '1.19.2');
|
|
36
|
+
const m = text.match(CONST_RE);
|
|
37
|
+
assert.ok(m, 'bumped text must still match the canonical const regex');
|
|
38
|
+
assert.equal(m[0].startsWith("const CORE_VERSION = '1.19.2';"), true);
|
|
39
|
+
// exactly one const line survives
|
|
40
|
+
assert.equal([...text.matchAll(/^const CORE_VERSION = /gm)].length, 1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('inserts the changelog entry above the divider, mentioning both versions', () => {
|
|
44
|
+
const { text } = bumpPatch(SAMPLE, { note: 'run 42' });
|
|
45
|
+
const dividerAt = text.indexOf('// ------');
|
|
46
|
+
const entryAt = text.indexOf('1.19.2 — CI auto-patch');
|
|
47
|
+
assert.ok(entryAt !== -1, 'entry exists');
|
|
48
|
+
assert.ok(entryAt < dividerAt, 'entry sits inside the changelog block, above the divider');
|
|
49
|
+
assert.match(text, /landed since 1\.19\.1 with no explicit bump\. run 42\./);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('a 9→10 patch rollover carries, not truncates', () => {
|
|
53
|
+
const src = SAMPLE.replace(/1\.19\.1/g, '1.19.9');
|
|
54
|
+
const { to } = bumpPatch(src);
|
|
55
|
+
assert.equal(to, '1.19.10');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('unrecognizable input throws loudly instead of mis-editing', () => {
|
|
59
|
+
assert.throws(() => bumpPatch('nothing here'), /could not find the CORE_VERSION literal/);
|
|
60
|
+
assert.throws(
|
|
61
|
+
() => bumpPatch("const CORE_VERSION = '1.2.3'; // no divider above"),
|
|
62
|
+
/changelog closing divider/
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('the REAL src/module-api.js is bumpable right now (shape not drifted)', () => {
|
|
67
|
+
const real = fs.readFileSync(path.join(ROOT, 'src', 'module-api.js'), 'utf8');
|
|
68
|
+
const { from, to, text } = bumpPatch(real);
|
|
69
|
+
assert.match(from, /^\d+\.\d+\.\d+$/);
|
|
70
|
+
assert.ok(text.includes(`const CORE_VERSION = '${to}';`));
|
|
71
|
+
});
|
|
@@ -33,6 +33,7 @@ const text = (r) => r.lines.join('\n');
|
|
|
33
33
|
t('never dumps [object Object] for any known code', () => {
|
|
34
34
|
const codes = [
|
|
35
35
|
'stage_required', 'session_already_has_active_claim', 'session_claim_check_failed',
|
|
36
|
+
'session_already_has_bound_claim',
|
|
36
37
|
'newcomer_must_ship_first', 'ALREADY_CLAIMED', 'MODULE_CONFLICT',
|
|
37
38
|
'TOUCHES_CONFLICT', 'TASK_NOT_READY', 'TASK_NOT_FOUND', 'DEPS_NOT_SHIPPED',
|
|
38
39
|
'some_unmapped_future_code',
|
|
@@ -96,6 +97,28 @@ t('MODULE_CONFLICT + TOUCHES_CONFLICT: list conflicting task titles', () => {
|
|
|
96
97
|
assert.match(tc, /Baz/);
|
|
97
98
|
});
|
|
98
99
|
|
|
100
|
+
// task 1002589: migration 180's one-bound-claim-per-session DB index tripped
|
|
101
|
+
// (claimTask now maps that 23505 by err.constraint instead of collapsing it
|
|
102
|
+
// onto ALREADY_CLAIMED). The honest render: the TARGET task is still free —
|
|
103
|
+
// the SESSION holds a bound claim elsewhere. Never blame a phantom rival.
|
|
104
|
+
t('session_already_has_bound_claim: names the in-flight claim, not "another session"', () => {
|
|
105
|
+
const out = text(formatClaimFailure(
|
|
106
|
+
envelope('session_already_has_bound_claim', {
|
|
107
|
+
active_task_id: 77, active_task_title: 'Held elsewhere', what_to_do: 'ship 77 or claim unbound',
|
|
108
|
+
}), 12));
|
|
109
|
+
assert.match(out, /#77/, 'should name the in-flight task id');
|
|
110
|
+
assert.match(out, /Held elsewhere/, 'should name the in-flight task title');
|
|
111
|
+
assert.match(out, /ship 77 or claim unbound/, 'should relay the server what_to_do');
|
|
112
|
+
assert.match(out, /still free/i, 'should say the target task is not actually taken');
|
|
113
|
+
assert.ok(!/claimed by another session/i.test(out), 'must not blame a phantom builder');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
t('session_already_has_bound_claim without details still renders honest guidance', () => {
|
|
117
|
+
const out = text(formatClaimFailure(envelope('session_already_has_bound_claim', {}), 12));
|
|
118
|
+
assert.match(out, /bound claim/i);
|
|
119
|
+
assert.match(out, /bind_session|builder-ship|builder-release/, 'fallback should name a way out');
|
|
120
|
+
});
|
|
121
|
+
|
|
99
122
|
t('unknown code: fallback carries code + message, exit 1', () => {
|
|
100
123
|
const r = formatClaimFailure(envelope('weird_thing', {}, 'boom', 500), 1);
|
|
101
124
|
assert.equal(r.exit, 1);
|
|
@@ -109,6 +109,9 @@ t('materializeClaude: copies + rewrites skills into a fresh instance dir (no lef
|
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
t('materializeClaude: materializes scheduled-tasks — bodies→bongos, companions keep _core-resolve (task 2138)', () => {
|
|
112
|
+
// There is nothing to materialize FROM when the core checkout carries no
|
|
113
|
+
// .claude/ source tree (package-excluded — task 1002470).
|
|
114
|
+
if (!existsSync(join(ROOT, '.claude', 'scheduled-tasks'))) return;
|
|
112
115
|
const dir = mkdtempSync(join(tmpdir(), 'w3-rt-'));
|
|
113
116
|
const res = m.materializeClaude({ coreRoot: ROOT, instanceDir: dir, dryRun: false });
|
|
114
117
|
assert.ok(res.scheduledTasks > 0, 'at least one scheduled-task file materialized');
|
|
@@ -204,6 +207,7 @@ t('disabledModuleSkills: only skills of DISABLED modules are collected (default
|
|
|
204
207
|
|
|
205
208
|
// ---- materializeClaude end-to-end: rebrand + skill-exclude (from the REAL repo) --
|
|
206
209
|
t('materializeClaude: copied hooks carry NO founder origin/config-dir; a disabled-module skill is excluded', () => {
|
|
210
|
+
if (!existsSync(join(ROOT, '.claude', 'hooks'))) return;
|
|
207
211
|
const dir = mkdtempSync(join(tmpdir(), 'rb-e2e-'));
|
|
208
212
|
// Target instance: demo-branded, art-pipeline OFF (so its otb-* skills must be dropped).
|
|
209
213
|
mkdirSync(join(dir, 'config'), { recursive: true });
|
package/tests/cli_box_scope.mjs
CHANGED
|
@@ -9,12 +9,20 @@
|
|
|
9
9
|
//
|
|
10
10
|
// Run: node tests/cli_box_scope.mjs
|
|
11
11
|
|
|
12
|
+
// task 1002162: the detector originally compared `res.data.error` to the STRING
|
|
13
|
+
// 'box_scope'. The live server emits the ADR 0116 (R01) envelope, so that field
|
|
14
|
+
// is an OBJECT and the test never matched — both call sites in cli-lib were dead
|
|
15
|
+
// code. The envelope cases below import the server's REAL `envelope()` formatter
|
|
16
|
+
// rather than hand-typing the shape, so if the wire contract changes again this
|
|
17
|
+
// test fails instead of silently drifting back into a no-op.
|
|
18
|
+
|
|
12
19
|
import { strict as assert } from 'node:assert';
|
|
13
20
|
import { createRequire } from 'node:module';
|
|
14
21
|
|
|
15
22
|
const require = createRequire(import.meta.url);
|
|
16
23
|
const lib = require('../scripts/gds/cli-lib.js');
|
|
17
24
|
const { isBoxScope } = lib;
|
|
25
|
+
const { envelope } = require('../src/bongos/api-errors.js');
|
|
18
26
|
|
|
19
27
|
let passed = 0, failed = 0;
|
|
20
28
|
function test(name, fn) {
|
|
@@ -47,6 +55,38 @@ test('isBoxScope: false on missing/empty data (no crash)', () => {
|
|
|
47
55
|
assert.equal(isBoxScope(undefined), false);
|
|
48
56
|
});
|
|
49
57
|
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// task 1002162 — the ADR 0116 envelope. THE REGRESSION: these are the shapes the
|
|
60
|
+
// live server actually sends, built by the server's own formatter.
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
test('isBoxScope: true for the REAL server envelope (the shape that was missed)', () => {
|
|
64
|
+
assert.equal(isBoxScope({ status: 403, data: envelope('box_scope') }), true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('isBoxScope: true for an envelope carrying message + details', () => {
|
|
68
|
+
const body = envelope('box_scope', 'box-scoped session', { what_to_do: 're-auth' });
|
|
69
|
+
assert.equal(body.error.code, 'box_scope', 'guard: envelope() still nests under .error.code');
|
|
70
|
+
assert.equal(isBoxScope({ status: 403, data: body }), true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('isBoxScope: false for a DIFFERENT code in envelope form — must not hijack', () => {
|
|
74
|
+
assert.equal(isBoxScope({ status: 403, data: envelope('not_your_task') }), false);
|
|
75
|
+
assert.equal(isBoxScope({ status: 403, data: envelope('forbidden') }), false);
|
|
76
|
+
assert.equal(isBoxScope({ status: 403, data: envelope('rank_forbidden') }), false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('isBoxScope: false for the envelope on the WRONG status', () => {
|
|
80
|
+
assert.equal(isBoxScope({ status: 401, data: envelope('box_scope') }), false);
|
|
81
|
+
assert.equal(isBoxScope({ status: 200, data: envelope('box_scope') }), false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('isBoxScope: no crash on a null/odd error field', () => {
|
|
85
|
+
assert.equal(isBoxScope({ status: 403, data: { error: null } }), false);
|
|
86
|
+
assert.equal(isBoxScope({ status: 403, data: { error: {} } }), false);
|
|
87
|
+
assert.equal(isBoxScope({ status: 403, data: {} }), false);
|
|
88
|
+
});
|
|
89
|
+
|
|
50
90
|
console.log('');
|
|
51
91
|
console.log(`cli_box_scope: ${passed} passed, ${failed} failed`);
|
|
52
92
|
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -5,11 +5,15 @@
|
|
|
5
5
|
// the customer-0 pack Cloud Bongos itself runs on.
|
|
6
6
|
import assert from 'node:assert/strict';
|
|
7
7
|
import { test } from 'node:test';
|
|
8
|
+
import { existsSync, writeFileSync, rmSync } from 'node:fs';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
8
11
|
import brandingMod from '../src/branding.js';
|
|
9
12
|
import serveInternal from '../src/bongos/serve-internal.js';
|
|
10
13
|
|
|
11
|
-
const { loadBranding, clientBranding } = brandingMod;
|
|
14
|
+
const { loadBranding, clientBranding, INSTANCE_PATH } = brandingMod;
|
|
12
15
|
const { themeMarkup, injectBrandingGlobal } = serveInternal;
|
|
16
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
17
|
|
|
14
18
|
// A standalone/vanilla boot has NO instance config — point the loader at a
|
|
15
19
|
// nonexistent instance path so it resolves the neutral starter alone (the
|
|
@@ -65,6 +69,10 @@ test('customer-0: GDS_BRANDING_FILE selects the cloudbongos.com instance pack (t
|
|
|
65
69
|
// The live dogfood instance runs from the SAME checkout as OTB; the env knob
|
|
66
70
|
// points this process at its own pack (deltas over neutral) instead of OTB's
|
|
67
71
|
// config/branding.json. Proves the brand-switch + the cloudbongos pack content.
|
|
72
|
+
// Both packs are INSTANCE content — the neutral core ships neither, so this
|
|
73
|
+
// asserts one deployment's config (task 1002470). The selector MECHANISM is
|
|
74
|
+
// proven unconditionally by the fixture test below.
|
|
75
|
+
if (!existsSync(join(ROOT, 'config/branding.cloudbongos.json'))) return;
|
|
68
76
|
const b = loadBranding({ env: { GDS_BRANDING_FILE: 'config/branding.cloudbongos.json' } });
|
|
69
77
|
assert.equal(b.identity.productName, 'Cloud Bongos'); // from neutral
|
|
70
78
|
assert.equal(b.currency.label, 'credits'); // from neutral, not "drachmae"
|
|
@@ -77,9 +85,31 @@ test('customer-0: GDS_BRANDING_FILE selects the cloudbongos.com instance pack (t
|
|
|
77
85
|
assert.equal(b.firstAdmin, 'lars589');
|
|
78
86
|
});
|
|
79
87
|
|
|
80
|
-
test('customer-0: no GDS_BRANDING_FILE still resolves
|
|
88
|
+
test('customer-0: no GDS_BRANDING_FILE still resolves the instance pack (default unchanged)', () => {
|
|
81
89
|
// The OTB process (server.js) sets no GDS_BRANDING_FILE → config/branding.json.
|
|
90
|
+
// Same instance-content caveat as above: with no instance pack the correct default
|
|
91
|
+
// IS the neutral starter, which is what the vanilla tests in this file already pin.
|
|
92
|
+
if (!existsSync(INSTANCE_PATH)) return;
|
|
82
93
|
const b = loadBranding({ env: {} });
|
|
83
|
-
assert.
|
|
84
|
-
|
|
94
|
+
assert.notDeepEqual(b.identity, vanilla().identity, 'the instance pack must win over neutral by default');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('GDS_BRANDING_FILE selects a named pack over the default (the selector mechanism)', () => {
|
|
98
|
+
// The half of the two tests above that must hold on EVERY instance, proven against a
|
|
99
|
+
// throwaway fixture instead of one deployment's committed pack (task 1002470).
|
|
100
|
+
const rel = 'config/branding.__fixture-selector-test.json';
|
|
101
|
+
const abs = join(ROOT, rel);
|
|
102
|
+
writeFileSync(abs, JSON.stringify({ identity: { productName: 'Fixture Product' }, currency: { label: 'zorkmids' } }));
|
|
103
|
+
try {
|
|
104
|
+
const picked = loadBranding({ env: { GDS_BRANDING_FILE: rel } });
|
|
105
|
+
assert.equal(picked.identity.productName, 'Fixture Product', 'the named pack was selected');
|
|
106
|
+
assert.equal(picked.currency.label, 'zorkmids', 'its deltas won over neutral');
|
|
107
|
+
// ...and unspecified keys still fall through to the neutral starter (it is a MERGE,
|
|
108
|
+
// not a replace) — so a partial pack can never blank out the platform defaults.
|
|
109
|
+
assert.equal(picked.copy.hallName, vanilla().copy.hallName, 'unspecified keys fall through to neutral');
|
|
110
|
+
// Counterfactual: without the env knob the fixture must NOT be in play.
|
|
111
|
+
assert.notEqual(loadBranding({ env: {} }).currency.label, 'zorkmids', 'the selector is what chose it');
|
|
112
|
+
} finally {
|
|
113
|
+
rmSync(abs, { force: true });
|
|
114
|
+
}
|
|
85
115
|
});
|
package/tests/conductor.mjs
CHANGED
|
@@ -10,9 +10,10 @@
|
|
|
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
|
|
|
14
15
|
const require = createRequire(import.meta.url);
|
|
15
|
-
const conductor = require
|
|
16
|
+
const conductor = requireHostModule(require, '../.claude/hooks/conductor.js');
|
|
16
17
|
|
|
17
18
|
let passed = 0;
|
|
18
19
|
let failed = 0;
|
|
@@ -29,6 +29,7 @@ const buildDoneWhenRouter = require('../modules/lifecycle/routes/done-when.js');
|
|
|
29
29
|
const { authorizeCriterionCreate } = buildDoneWhenRouter;
|
|
30
30
|
const { authorizeGoalManagement, isGoalOwnerOrManager } = require('../modules/lifecycle/goal-authz.js');
|
|
31
31
|
const { MODULE_EXPECTED_RANKS } = require('../src/bongos/route-rank-check.js');
|
|
32
|
+
const catalog = require('../modules/governance/catalog.js');
|
|
32
33
|
|
|
33
34
|
const doneWhenSrc = readFileSync(new URL('../modules/lifecycle/routes/done-when.js', import.meta.url), 'utf8');
|
|
34
35
|
const { test, summary } = makeRunner();
|
|
@@ -239,12 +240,19 @@ await test('the gate reuses the SAME owner|manager predicate as routes/goals.js
|
|
|
239
240
|
|
|
240
241
|
// ---- route-source + pin contracts (drift catchers) ----
|
|
241
242
|
|
|
243
|
+
// BV1.R105 migrated this middleware from requireRank('metic','archon') to the
|
|
244
|
+
// `criterion.create` ATOM. The audience assertion did not weaken — it got stricter:
|
|
245
|
+
// the spelling check now runs THROUGH the catalog, so a floor change on the atom
|
|
246
|
+
// reds this test even though the route source is untouched. (Pinning the atom name
|
|
247
|
+
// alone would let `criterion.create` quietly drop to xenos and still pass.)
|
|
242
248
|
await test('the route is gated metic+archon and calls the pure gate', () => {
|
|
243
249
|
assert.match(
|
|
244
250
|
doneWhenSrc,
|
|
245
|
-
/router\.post\('\/versions\/:id\/done-when',\s*auth\.requireBuilder,\s*auth\.
|
|
246
|
-
'the middleware must be
|
|
251
|
+
/router\.post\('\/versions\/:id\/done-when',\s*auth\.requireBuilder,\s*auth\.requirePermission\('criterion\.create'\)/,
|
|
252
|
+
'the middleware must be requirePermission(criterion.create)'
|
|
247
253
|
);
|
|
254
|
+
assert.equal(catalog.byKey('criterion.create').floor, 'metic',
|
|
255
|
+
'criterion.create must floor at metic — that floor IS what makes this route metic+archon');
|
|
248
256
|
assert.match(doneWhenSrc, /authorizeCriterionCreate\(\{/, 'the handler must call the pure gate, not inline the decision');
|
|
249
257
|
});
|
|
250
258
|
|
|
@@ -256,13 +264,28 @@ await test('the `// rank:` annotation matches the middleware (gen-api-docs reads
|
|
|
256
264
|
);
|
|
257
265
|
});
|
|
258
266
|
|
|
259
|
-
|
|
267
|
+
// ADR 0154 made "unsatisfy stays ARCHON" its safety argument for delegating criterion
|
|
268
|
+
// CREATE to Metic: the forward ratchet moved, the backward one did not. ADR 0157 (owner
|
|
269
|
+
// decision, 2026-08-05) overrides that specific carve-out — `criterion.unsatisfy` is
|
|
270
|
+
// neither rank/identity nor one of the four escalation keys, so it delegated with the rest
|
|
271
|
+
// of the operational block.
|
|
272
|
+
//
|
|
273
|
+
// What this test still guards is the part that never depended on the rank: unsatisfy must
|
|
274
|
+
// remain gated on its OWN dedicated atom, distinct from `criterion.satisfy`. If the two
|
|
275
|
+
// were ever merged, confirming a criterion would silently confer the power to un-confirm
|
|
276
|
+
// one — and THAT is the asymmetry ADR 0154 actually cared about.
|
|
277
|
+
await test('unsatisfy is gated on its OWN atom — the backward ratchet is a separate grant', () => {
|
|
260
278
|
assert.match(
|
|
261
279
|
doneWhenSrc,
|
|
262
|
-
/router\.post\('\/done-when\/:criterionId\/unsatisfy',\s*auth\.requireBuilder,\s*auth\.
|
|
280
|
+
/router\.post\('\/done-when\/:criterionId\/unsatisfy',\s*auth\.requireBuilder,\s*auth\.requirePermission\('criterion\.unsatisfy'\)/,
|
|
263
281
|
'un-flipping a confirmed criterion rewrites shipped scope-truth'
|
|
264
282
|
);
|
|
265
|
-
assert.equal(
|
|
283
|
+
assert.equal(catalog.byKey('criterion.unsatisfy').floor, 'metic',
|
|
284
|
+
'ADR 0157 delegated the backward ratchet to the working rank');
|
|
285
|
+
assert.notEqual(catalog.byKey('criterion.unsatisfy').key, catalog.byKey('criterion.satisfy').key,
|
|
286
|
+
'satisfy and unsatisfy must stay separate atoms, whatever their floors');
|
|
287
|
+
assert.equal(MODULE_EXPECTED_RANKS.lifecycle['POST /done-when/:criterionId/unsatisfy'],
|
|
288
|
+
'perm:criterion.unsatisfy');
|
|
266
289
|
});
|
|
267
290
|
|
|
268
291
|
await test('the route-rank PIN records metic+archon (fitness reds the build on drift)', () => {
|