@bongos/core 1.18.19 → 1.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +514 -194
- package/.claude/skills/builder-box/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +1 -1
- package/.claude/skills/idea-triage/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +5 -5
- package/.claude/skills/scan-before-install/SKILL.md +165 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +63 -41
- package/clients/bongos-client/index.cjs +63 -41
- package/clients/bongos-client/index.d.ts +78 -44
- package/clients/bongos-client/index.mjs +63 -41
- package/docs/adr/0020-security-model-public-stub.md +1 -1
- package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +2 -0
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +2 -1
- package/docs/adr/0155-adopt-private-repo-widen-oauth-scope.md +51 -0
- package/docs/adr/0156-user-set-display-name-wins.md +62 -0
- package/docs/adr/0157-archon-is-rank-and-identity-only.md +93 -0
- package/docs/adr/0158-choke-point-verdict-taxonomy.md +174 -0
- package/docs/adr/0159-merge-gate-reconciliation.md +88 -0
- package/docs/adr/0160-main-audit-repo-verifiable-caller.md +77 -0
- package/docs/adr/README.md +8 -0
- package/docs/api/openapi.json +1209 -233
- package/docs/api-reference.md +61 -46
- package/docs/architecture.md +1 -1
- package/docs/canonical-permissions.md +61 -16
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +4 -1
- package/docs/onboarding/diagrams/assertions.json +3 -3
- package/docs/recipes/core-release-pipeline.md +179 -0
- package/docs/recipes/packaging-the-core.md +2 -0
- package/docs/recipes/parallel-goal-waves.md +1 -3
- package/docs/recipes/private-npm-distribution.md +2 -0
- package/docs/recipes/upgrading-the-core.md +2 -0
- package/migrations/core_197_display_name_source.sql +56 -0
- package/migrations/core_204_rekey_active_claim_per_session_worktree.sql +77 -0
- package/modules/autonomy/routes/autonomy.js +1 -1
- package/modules/builder-settings/db.js +7 -1
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +63 -41
- package/modules/dev-box/routes/box.js +11 -9
- package/modules/discord/auth.js +3 -1
- package/modules/discord/routes/discord.js +8 -2
- package/modules/discord/ship-broadcast.js +48 -1
- package/modules/governance/catalog.js +70 -20
- package/modules/governance/db.js +102 -0
- package/modules/governance/migrations/governance_006_version_create.sql +38 -0
- package/modules/governance/migrations/governance_007_delegate_archon_ops_to_metic.sql +72 -0
- package/modules/governance/routes/governance.js +235 -6
- package/modules/grading/grader-workers/index.js +18 -9
- package/modules/hall-ui/public/gate.js +6 -2
- package/modules/hall-ui/public/goals-page.js +6 -2
- package/modules/hall-ui/public/governance.css +100 -0
- package/modules/hall-ui/public/governance.html +77 -0
- package/modules/hall-ui/public/governance.js +353 -0
- package/modules/hall-ui/public/harbor.js +14 -8
- package/modules/hall-ui/public/instances-panel.js +3 -1
- package/modules/hall-ui/public/new-project-wizard.js +6 -0
- package/modules/hall-ui/public/people.html +51 -0
- package/modules/hall-ui/public/people.js +100 -0
- package/modules/hall-ui/public/profile.html +2 -1
- package/modules/hall-ui/public/profile.js +9 -3
- package/modules/hall-ui/public/sessions.js +4 -2
- package/modules/hall-ui/public/shell.js +28 -7
- package/modules/hall-ui/public/task-detail.js +6 -3
- package/modules/hall-ui/public/watch.js +17 -9
- package/modules/ideas/blockers.js +44 -7
- package/modules/ideas/routes/blockers.js +12 -7
- package/modules/ideas/routes/inbox.js +2 -2
- package/modules/lifecycle/db.js +84 -17
- package/modules/lifecycle/github-push.js +37 -10
- package/modules/lifecycle/publish-reconciler.js +128 -31
- package/modules/lifecycle/routes/claims.js +10 -4
- package/modules/lifecycle/routes/dependencies.js +2 -2
- package/modules/lifecycle/routes/done-when.js +4 -4
- package/modules/lifecycle/routes/gate-approvals.js +9 -3
- package/modules/lifecycle/routes/goals.js +5 -5
- package/modules/lifecycle/routes/tasks.js +16 -16
- package/modules/lifecycle/routes/versions.js +13 -2
- package/modules/lifecycle/ship-card.js +38 -7
- package/modules/memory/routes/memory.js +2 -2
- package/modules/onboarding/routes/access-requests.js +3 -3
- package/modules/platform-identity/routes/projects.js +10 -6
- package/modules/platform-identity/routes/scouting.js +2 -2
- package/modules/provisioning/migrations/provisioning_004_onboard_mode.sql +30 -0
- package/modules/provisioning/provisioning.js +24 -6
- package/modules/provisioning/routes/provisioning.js +24 -6
- package/modules/security/artifact-scan/aggregate.js +133 -0
- package/modules/security/artifact-scan/detectors/capabilities.js +53 -0
- package/modules/security/artifact-scan/detectors/config-obfuscation.js +41 -0
- package/modules/security/artifact-scan/detectors/credentials.js +112 -0
- package/modules/security/artifact-scan/detectors/dependencies.js +115 -0
- package/modules/security/artifact-scan/detectors/hooks.js +116 -0
- package/modules/security/artifact-scan/detectors/injection.js +118 -0
- package/modules/security/artifact-scan/detectors/mcp.js +102 -0
- package/modules/security/artifact-scan/detectors/network.js +67 -0
- package/modules/security/artifact-scan/detectors/npm-lifecycle.js +67 -0
- package/modules/security/artifact-scan/detectors/permissions.js +91 -0
- package/modules/security/artifact-scan/detectors/shared.js +219 -0
- package/modules/security/artifact-scan/fence-local.js +99 -0
- package/modules/security/artifact-scan/fetch.js +330 -0
- package/modules/security/artifact-scan/floor.js +319 -0
- package/modules/security/artifact-scan/limits.js +66 -0
- package/modules/security/artifact-scan/manifest.js +203 -0
- package/modules/security/artifact-scan/mirror.js +158 -0
- package/modules/security/artifact-scan/quarantine.js +149 -0
- package/modules/security/artifact-scan/rebuild-spec.js +120 -0
- package/modules/security/artifact-scan/report.js +155 -0
- package/modules/security/artifact-scan/sanitize.js +317 -0
- package/modules/security/migrations/security_001_tool_scans.sql +145 -0
- package/modules/security/module.json +2 -2
- package/modules/security/routes/reports.js +11 -7
- package/modules/security/routes/scans.js +115 -0
- package/modules/security/scan-store.js +214 -0
- package/modules/security/tests/aggregate.mjs +283 -0
- package/modules/security/tests/artifact_scan.mjs +591 -0
- package/modules/security/tests/floor.mjs +549 -0
- package/modules/security/tests/mirror.mjs +350 -0
- package/modules/security/tests/sanitize_rebuild.mjs +499 -0
- package/modules/security/tests/scan_store.mjs +250 -0
- package/modules/security/tests/tool_scans_migration.mjs +116 -0
- package/modules/sessions/routes/sessions.js +6 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +13 -3
- package/scripts/gds/abandon.js +1 -1
- package/scripts/gds/audit-authorship.sh +6 -0
- package/scripts/gds/box.js +1 -1
- package/scripts/gds/claim.js +15 -0
- package/scripts/gds/cli-lib.js +16 -4
- package/scripts/gds/gating.js +28 -13
- package/scripts/gds/gen-api-docs.js +1 -1
- package/scripts/gds/goal.js +234 -0
- package/scripts/gds/init.js +70 -15
- package/scripts/gds/main-audit.js +80 -11
- package/scripts/gds/mirror-redact.js +6 -0
- package/scripts/gds/newcomer-floor.js +2 -2
- package/scripts/gds/provision.js +230 -94
- package/scripts/gds/run-unit-tests.js +9 -1
- package/scripts/gds/sandbox-stage.js +66 -9
- package/scripts/gds/search-ingest-md.js +1 -0
- package/scripts/gds/sequence.js +67 -8
- package/scripts/gds/ship.js +183 -50
- package/scripts/hall-preview/server.js +1 -1
- package/scripts/migrate.sh +5 -5
- package/src/bongos/auth.js +129 -27
- package/src/bongos/db-kernel.js +66 -5
- package/src/bongos/db.js +6 -7
- package/src/bongos/github-repos.js +19 -7
- package/src/bongos/goal-scope-check.js +1 -1
- package/src/bongos/module-scope-map.js +3 -1
- package/src/bongos/permission-path-check.js +1 -1
- package/src/bongos/route-rank-check.js +73 -30
- package/src/bongos/routes/audit-log.js +4 -2
- package/src/bongos/routes/auth.js +8 -4
- package/src/bongos/routes/backup.js +7 -4
- package/src/bongos/routes/builders.js +23 -11
- package/src/bongos/routes/github.js +15 -4
- package/src/bongos/routes/llm-cache.js +5 -2
- package/src/bongos/routes/modules.js +9 -5
- package/src/bongos/routes/security.js +21 -12
- package/src/bongos/serve-internal.js +22 -7
- package/src/module-api.js +86 -13
- package/src/platform-server.js +14 -0
- package/tests/api_client.mjs +18 -2
- package/tests/api_path_404.mjs +3 -0
- package/tests/art_stage.mjs +2 -1
- package/tests/atlas.mjs +32 -4
- package/tests/auth_admission_gate.mjs +17 -5
- package/tests/auth_error_brand.mjs +41 -21
- package/tests/auth_resolve_failure.mjs +155 -0
- package/tests/blocker_link.mjs +88 -0
- package/tests/bongos_downloads.mjs +2 -1
- package/tests/box_dns_repoint.mjs +2 -0
- package/tests/box_ship_permissions.mjs +2 -0
- package/tests/box_terminal_ssrf.mjs +23 -17
- package/tests/claim_error_surface.mjs +23 -0
- package/tests/claude_materialize.mjs +4 -0
- package/tests/cli_box_scope.mjs +40 -0
- package/tests/cloud_bongos_pack.mjs +34 -4
- package/tests/conductor.mjs +2 -1
- package/tests/criterion_authoring_gate.mjs +28 -5
- package/tests/currency_label.mjs +10 -1
- package/tests/deploy_instance.mjs +2 -0
- package/tests/devbox_downloads.mjs +2 -1
- package/tests/display_name.mjs +95 -6
- package/tests/eviction_policy.mjs +2 -1
- package/tests/file_map.mjs +10 -0
- package/tests/first_admin_bootstrap.mjs +10 -2
- package/tests/first_run_engine_check.mjs +2 -1
- package/tests/fitness.mjs +26 -6
- package/tests/gate_approvals.mjs +15 -2
- package/tests/github_push_land_proof.mjs +50 -4
- package/tests/github_repos_helpers.mjs +30 -5
- package/tests/github_token_store.mjs +23 -0
- package/tests/goal_abuse_matrix.mjs +13 -3
- package/tests/goal_cli.mjs +89 -0
- package/tests/governance_abuse_matrix.mjs +386 -0
- package/tests/governance_fold.mjs +156 -0
- package/tests/governance_fold_bench.mjs +50 -0
- package/tests/governance_live_proof.mjs +201 -0
- package/tests/governance_page_gates.mjs +21 -9
- package/tests/governance_parity.mjs +11 -2
- package/tests/governance_require_permission.mjs +125 -1
- package/tests/governance_routes.mjs +334 -0
- package/tests/grader.mjs +7 -3
- package/tests/hall_nav.mjs +14 -4
- package/tests/helpers.mjs +73 -0
- package/tests/hook_core_resolve.mjs +2 -1
- package/tests/host_topology_skips.mjs +97 -0
- package/tests/infra_exec_paths.mjs +14 -4
- package/tests/init.mjs +128 -0
- package/tests/known_failing_ratchet.mjs +89 -0
- package/tests/lifecycle-card.mjs +2 -1
- package/tests/main_audit.mjs +72 -1
- package/tests/memory_isolation.mjs +56 -9
- package/tests/migrate_root_resolution.mjs +1 -1
- package/tests/mirror_redact.mjs +19 -0
- package/tests/module_api.mjs +3 -1
- package/tests/module_loader.mjs +4 -0
- package/tests/modules.mjs +35 -7
- package/tests/modules_route.mjs +29 -5
- package/tests/onboarding_observability.mjs +49 -11
- package/tests/onboarding_route_signals.mjs +1 -0
- package/tests/one_active_claim_per_session.mjs +80 -0
- package/tests/pre_compact.mjs +2 -1
- package/tests/preview_server.mjs +2 -1
- package/tests/profile_route.mjs +1 -0
- package/tests/provision.mjs +170 -0
- package/tests/provisioning_onboard_mode.mjs +180 -0
- package/tests/publish_manifest.mjs +16 -3
- package/tests/publish_reconciler.mjs +181 -10
- package/tests/rank_tier_single_source.mjs +205 -0
- package/tests/recall_hints.mjs +2 -1
- package/tests/repo_map.mjs +4 -0
- package/tests/route_permission_check.mjs +14 -5
- package/tests/rubric_fixtures.mjs +3 -0
- package/tests/run_routine_two_root.mjs +2 -0
- package/tests/sandbox_stage.mjs +69 -7
- package/tests/search_ingest.mjs +7 -0
- package/tests/security_docs.mjs +28 -6
- package/tests/sequence.mjs +64 -0
- package/tests/session-cards.mjs +2 -1
- package/tests/session_start_freshness.mjs +2 -1
- package/tests/ship_broadcast_server.mjs +63 -1
- package/tests/ship_card.mjs +66 -0
- package/tests/ship_ci_deploy.mjs +66 -1
- package/tests/ship_smoke_gate.mjs +76 -0
- package/tests/versions_route.mjs +15 -0
- package/tests/workflow_exec_paths.mjs +16 -4
- package/tests/worktree_guard.mjs +2 -1
|
@@ -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`);
|
|
@@ -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)', () => {
|
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) {
|