@bongos/core 1.19.1074 → 1.19.1076
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 +196 -211
- package/.claude/skills/blocker-solve/SKILL.md +1 -1
- package/README.md +1 -1
- package/bin/bongos.js +1 -3
- package/docs/adr/0031-cloud-dev-environments-for-builders.md +1 -1
- package/docs/adr/0035-builder-onboarding-three-paths.md +1 -1
- package/docs/adr/0044-per-box-live-game-preview.md +1 -1
- package/docs/adr/0045-devbox-desktop-app.md +1 -1
- package/docs/adr/0046-sandbox-first-review-gate.md +1 -1
- package/docs/adr/0052-sandbox-for-everyone-game-only-preview.md +1 -1
- package/docs/adr/0053-scoped-dev-box-session.md +1 -1
- package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
- package/docs/adr/0057-container-cost-ledger.md +1 -1
- package/docs/adr/0059-single-approval-remove-devbox-approval-gate.md +1 -1
- package/docs/adr/0071-box-confirm-before-destroyed-and-drift-reconcile.md +1 -1
- package/docs/adr/0072-bongos-app-mac-signed-first-windows-deferred.md +1 -1
- package/docs/adr/0072-dev-box-code-staleness-visibility.md +1 -1
- package/docs/adr/0104-trust-gds-api-channel-in-auto-mode.md +1 -1
- package/docs/adr/0123-box-idle-sweep-autosave-before-destroy.md +1 -1
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
- package/docs/adr/0145-devbox-app-branding-driven-module.md +1 -1
- package/docs/adr/0148-task-scoped-box-source-access.md +2 -2
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +1 -1
- package/docs/adr/0193-pause-task-scoped-box-slices.md +1 -1
- package/docs/adr/0277-a-box-is-in-use-only-while-a-human-is-attached.md +1 -1
- package/docs/adr/0346-dev-boxes-are-retired.md +69 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +4 -4
- package/docs/architecture.md +5 -6
- package/docs/branding-contract.md +2 -2
- package/docs/canonical-permissions.md +10 -18
- package/docs/copy-inventory.md +3 -3
- package/docs/copy-registry.json +3 -3
- package/docs/design/gate-navigation-direction.md +1 -1
- package/docs/design/hall-direction-v2.md +3 -3
- package/docs/design/modular-architecture/00-research-report.md +1 -1
- package/docs/design/modular-architecture/01-architecture.md +3 -3
- package/docs/design/modular-architecture/02-module-map.md +1 -2
- package/docs/design/modular-architecture/03-builder-flows.md +22 -20
- package/docs/design/modular-architecture/04-module-lifecycle.md +1 -1
- package/docs/design/modular-architecture/README.md +4 -2
- package/docs/design/reviews/hall-v2/README.md +2 -2
- package/docs/design/vanilla-hall-ui-redesign-scope.md +4 -4
- package/docs/file-map.md +5 -5
- package/docs/handoff-template.md +1 -1
- package/docs/module-api-changelog.md +8 -0
- package/docs/modules-contract.md +11 -14
- package/docs/page-readings.json +63 -63
- package/docs/recipes/bongos-cli-release.md +1 -2
- package/docs/recipes/multi-builder-merge.md +1 -1
- package/docs/recipes/ops-gotchas.md +0 -9
- package/docs/recipes/self-host.md +1 -1
- package/docs/recipes/windows-builders.md +1 -1
- package/migrations/core_258_drop_dev_box_tables.sql +3 -0
- package/modules/copy-desk/routes/copy-desk.js +1 -1
- package/modules/discord/ship-broadcast.js +3 -3
- package/modules/economy/credits.js +2 -2
- package/modules/hall-ui/public/palette.js +143 -3
- package/modules/ideas/projection.js +1 -1
- package/modules/lifecycle/db-ship.js +4 -4
- package/modules/lifecycle/github-push.js +12 -14
- package/modules/lifecycle/publish-reconciler.js +10 -10
- package/modules/lifecycle/routes/tasks.js +8 -8
- package/modules/lifecycle/ship-card.js +1 -1
- package/modules/lifecycle/task-visuals.js +2 -2
- package/modules/memory/routes/memory.js +2 -2
- package/modules/platform-identity/routes/sso.js +3 -2
- package/modules/provisioning/capacity.js +1 -1
- package/modules/provisioning/provisioning.js +4 -3
- package/modules/provisioning/routes/provisioning.js +4 -4
- package/modules/sessions/db.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +28 -0
- package/scripts/gds/artifact-staleness.js +2 -2
- package/scripts/gds/backfill-task-rewards.js +5 -5
- package/scripts/gds/claude-materialize.js +2 -2
- package/scripts/gds/cli-lib.js +7 -7
- package/scripts/gds/codemod-rename-src-gds.js +1 -1
- package/scripts/gds/context-pack.js +5 -7
- package/scripts/gds/dev-box-guard.js +165 -0
- package/scripts/gds/doctor.js +3 -3
- package/scripts/gds/fitness-checks-packaging.js +4 -4
- package/scripts/gds/fitness-lib.js +10 -0
- package/scripts/gds/fitness-ratchets.js +1 -1
- package/scripts/gds/fitness.js +2 -10
- package/scripts/gds/gen-api-client.js +4 -4
- package/scripts/gds/gen-api-docs.js +1 -1
- package/scripts/gds/gen-repo-map.js +13 -14
- package/scripts/gds/gen-session-index.js +8 -8
- package/scripts/gds/leak-scan-allowlist.js +1 -1
- package/scripts/gds/plain-cards.js +1 -1
- package/scripts/gds/push-path-brief.js +10 -10
- package/scripts/gds/rename-history-check.js +1 -1
- package/scripts/gds/run-unit-tests.js +4 -0
- package/scripts/gds/session-digest-build.js +2 -2
- package/scripts/gds/ship-deploy-target.js +13 -13
- package/scripts/gds/ship-flow.js +1 -1
- package/scripts/gds/ship-land.js +6 -6
- package/scripts/gds/ship-merge.js +4 -4
- package/scripts/gds/ship-preflight-steps.js +4 -4
- package/scripts/gds/ship-regen.js +9 -12
- package/scripts/gds/ship.js +9 -9
- package/scripts/gds/skill-preflight.js +9 -8
- package/src/bongos/api-prefix.js +4 -4
- package/src/bongos/module-scope-map.js +1 -1
- package/src/bongos/platform-visibility-gate.js +1 -1
- package/src/bongos/routes/me.js +1 -1
- package/src/bongos/routes/security.js +3 -3
- package/src/bongos/routes.js +2 -2
- package/src/bongos/serve-internal.js +5 -6
- package/src/bongos-downloads.js +1 -6
- package/src/branding.js +2 -2
- package/src/build-info.js +4 -6
- package/src/instance-config.js +2 -2
- package/src/module-api.js +3 -3
- package/src/module-loader/manifest-schema.js +1 -1
- package/tests/api_alias_caller_ratchet.mjs +1 -1
- package/tests/api_alias_exceptions.mjs +1 -1
- package/tests/api_client.mjs +1 -1
- package/tests/api_path_404.mjs +1 -1
- package/tests/backfill_task_rewards.mjs +1 -1
- package/tests/{box_ship_permissions.mjs → cli_allowlist_permissions.mjs} +5 -5
- package/tests/cli_sessions.mjs +1 -1
- package/tests/context_pack.mjs +13 -13
- package/tests/copy_inventory.mjs +7 -7
- package/tests/core_258_box_session_claims_db.mjs +180 -0
- package/tests/credit_grant.mjs +2 -2
- package/tests/criterion_suggest.mjs +1 -1
- package/tests/design_tokens_sync.mjs +3 -2
- package/tests/fitness.mjs +89 -0
- package/tests/fitness_ratchets.mjs +2 -2
- package/tests/github_push_land_proof.mjs +1 -1
- package/tests/goal_advisory.mjs +3 -3
- package/tests/goal_suggest.mjs +3 -3
- package/tests/government_abuse_matrix.mjs +4 -3
- package/tests/hall_audit.mjs +1 -1
- package/tests/hall_palette.mjs +224 -11
- package/tests/helpers.mjs +1 -1
- package/tests/host_topology_skips.mjs +2 -2
- package/tests/migration_namespace.mjs +6 -6
- package/tests/module_api.mjs +2 -2
- package/tests/module_cli.mjs +5 -5
- package/tests/module_manifest.mjs +13 -13
- package/tests/module_route_rank.mjs +2 -2
- package/tests/modules_route_wiring.mjs +1 -1
- package/tests/onboarding_route_signals.mjs +1 -1
- package/tests/paste_token_session_store.mjs +1 -1
- package/tests/platform_visibility_gate.mjs +1 -1
- package/tests/project_modules_ui.mjs +17 -17
- package/tests/projects_hub_module_picker.mjs +75 -74
- package/tests/provisioning_capacity.mjs +1 -1
- package/tests/provisioning_recommendations.mjs +3 -2
- package/tests/publish_branch_route.mjs +1 -1
- package/tests/publish_manifest.mjs +1 -1
- package/tests/publish_reconciler.mjs +8 -8
- package/tests/push_path_brief.mjs +10 -9
- package/tests/regrade_eligibility.mjs +1 -1
- package/tests/rename_history_restraint.mjs +1 -1
- package/tests/repo_map.mjs +9 -9
- package/tests/seam_wiring_guard.mjs +11 -11
- package/tests/search_isolation.mjs +3 -3
- package/tests/session_records.mjs +1 -1
- package/tests/session_rename_fallback.mjs +1 -1
- package/tests/session_start_freshness.mjs +3 -3
- package/tests/session_token_hash_db.mjs +1 -1
- package/tests/ship_card.mjs +1 -1
- package/tests/ship_ci_deploy.mjs +17 -17
- package/tests/ship_premerge.mjs +1 -1
- package/tests/ship_resume.mjs +1 -1
- package/tests/skill_preflight.mjs +2 -2
- package/tests/skip_is_not_pass.mjs +1 -1
- package/tests/terms_acceptance.mjs +1 -1
- package/docs/design/reviews/hall-v2/harbor--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/harbor--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/harbor--xenos.webp +0 -0
- package/docs/design/reviews/hall-v2/pair--archon.webp +0 -0
- package/docs/design/reviews/hall-v2/pair--metic.webp +0 -0
- package/docs/design/reviews/hall-v2/pair--xenos.webp +0 -0
package/src/branding.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// src/branding.js — the branding contract (ADR 0062 §3, task 1192 / V4.R53).
|
|
2
2
|
//
|
|
3
3
|
// The single source for every instance-identity string the platform renders:
|
|
4
|
-
// product/world name, company, domains/origins, cookie + OAuth
|
|
4
|
+
// product/world name, company, domains/origins, cookie + OAuth
|
|
5
5
|
// hosts, currency label, the track set, theme, and the project name injected
|
|
6
|
-
// into LLM prompts.
|
|
6
|
+
// into LLM prompts.
|
|
7
7
|
//
|
|
8
8
|
// Resolution precedence (highest wins):
|
|
9
9
|
// env override -> instance config (config/branding.json) -> neutral
|
package/src/build-info.js
CHANGED
|
@@ -41,12 +41,10 @@ function readGitSha() {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// The deployed commit's date (ISO 8601),
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
// committer date is the right reference. Null when git is unavailable (then the
|
|
49
|
-
// "behind" check is skipped and only never-reported boxes flag).
|
|
44
|
+
// The deployed commit's date (ISO 8601), served on /version beside the commit.
|
|
45
|
+
// Best-effort: after a `git reset --hard origin/main` deploy, HEAD is the
|
|
46
|
+
// deployed commit, so its committer date is the right reference. Null when git
|
|
47
|
+
// is unavailable.
|
|
50
48
|
function readGitCommitDate() {
|
|
51
49
|
try {
|
|
52
50
|
return execSync('git show -s --format=%cI HEAD', {
|
package/src/instance-config.js
CHANGED
|
@@ -40,7 +40,7 @@ const LEGACY_DIRS = ['otb'];
|
|
|
40
40
|
//
|
|
41
41
|
// WHY THESE MUST OUTLIVE THIS TASK: these names are set OUTSIDE the repo —
|
|
42
42
|
// /etc/cloudbongos/web.env on the droplet, systemd units, CI secrets, shipped
|
|
43
|
-
//
|
|
43
|
+
// CLI binaries, builders' own machines. Deleting a spelling here is an outage the
|
|
44
44
|
// next time one of those restarts. Task 1003706 (C3d) retires them deliberately,
|
|
45
45
|
// after evidence — and asserts their ABSENCE, which is why the fallback needs a
|
|
46
46
|
// test that it FIRES rather than one that merely tolerates it.
|
|
@@ -140,7 +140,7 @@ function configPath(name, b = safeBrand()) {
|
|
|
140
140
|
// Lives here, not in cli-lib.js which owns the session STORE, because this module
|
|
141
141
|
// is the lowest one that has to read the file (resolveApiBase below) and cli-lib
|
|
142
142
|
// imports this — so this is the only place both can share. A rename APPENDS a
|
|
143
|
-
// name; dropping one signs out every builder machine
|
|
143
|
+
// name; dropping one signs out every builder machine at
|
|
144
144
|
// once. Part 4 (task 1003706) retires the tail. (task 1003704)
|
|
145
145
|
const SESSION_FILENAMES = Object.freeze([
|
|
146
146
|
'bongos-session.json',
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.19.
|
|
78
|
+
const CORE_VERSION = '1.19.1076'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -137,8 +137,8 @@ module.exports = {
|
|
|
137
137
|
get requireBfgPrincipal() { return require('./bongos/auth').requireBfgPrincipal; },
|
|
138
138
|
// allowBoxScope — DEPRECATED no-op, kept so the doorway loses no export inside
|
|
139
139
|
// 1.x (removing one is a MAJOR, and every module declares ^1). It used to opt
|
|
140
|
-
// a route in to the narrowing
|
|
141
|
-
// were retired (task 1003894), so an undeclared session source is now denied
|
|
140
|
+
// a route in to the narrowing machine-baked session scope; that scope and its
|
|
141
|
+
// tokens were retired (goal 1000120, task 1003894), so an undeclared session source is now denied
|
|
142
142
|
// on every route whatever a route mounts. Passing it changes nothing. Drop it
|
|
143
143
|
// at the next MAJOR.
|
|
144
144
|
get allowBoxScope() { return deprecatedNoopMiddleware; },
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// THE SCHEMA (module.json):
|
|
11
11
|
// {
|
|
12
12
|
// "key": "art-pipeline", // REQUIRED. ^[a-z][a-z0-9-]*$ ; must match the module's dir name.
|
|
13
|
-
// "title": "
|
|
13
|
+
// "title": "Art pipeline", // REQUIRED. Human label.
|
|
14
14
|
// "description": "…", // REQUIRED. One sentence.
|
|
15
15
|
// "version": "1.4.2", // REQUIRED. The module's OWN exact version "X.Y.Z" (task 1003781) —
|
|
16
16
|
// // what update, rollout channels and re-assessment name.
|
|
@@ -66,7 +66,7 @@ test('the exclusions are real and each one is reachable', () => {
|
|
|
66
66
|
'scripts/gds/fitness-ratchets.js',
|
|
67
67
|
'scripts/gds/api-alias-caller-scan.js',
|
|
68
68
|
'tests/api_alias_exceptions.mjs', // 5 tests
|
|
69
|
-
'modules/
|
|
69
|
+
'modules/agents/app/test/thing.mjs',
|
|
70
70
|
'docs/adr/0016-permissions.md', // 6 the FROZEN RECORD under docs/ (narrowed, task 1004113)
|
|
71
71
|
'docs/session-logs/2026-09-16-x.md',
|
|
72
72
|
'docs/audits/2026-08-01-x.md',
|
|
@@ -27,7 +27,7 @@ const ALIAS = '/api/gds';
|
|
|
27
27
|
test('the alias is still mounted — everything below depends on it', () => {
|
|
28
28
|
const { API_PREFIX, LEGACY_API_PREFIXES, ALL_API_PREFIXES } = require(path.join(ROOT, 'src', 'bongos', 'api-prefix.js'));
|
|
29
29
|
assert.equal(API_PREFIX, '/api/bongos', 'the canonical prefix is what in-repo callers now ask for');
|
|
30
|
-
assert.ok(LEGACY_API_PREFIXES.includes(ALIAS), 'shipped
|
|
30
|
+
assert.ok(LEGACY_API_PREFIXES.includes(ALIAS), 'shipped binaries and live instances call the alias');
|
|
31
31
|
assert.ok(ALL_API_PREFIXES.includes(ALIAS), 'and the router still mounts it');
|
|
32
32
|
});
|
|
33
33
|
|
package/tests/api_client.mjs
CHANGED
|
@@ -208,7 +208,7 @@ test('generated .d.ts types success responses (task 2061)', () => {
|
|
|
208
208
|
});
|
|
209
209
|
|
|
210
210
|
// The CJS + browser-global builds are pure transforms of the ESM source (task
|
|
211
|
-
// 1996 / R09).
|
|
211
|
+
// 1996 / R09). CommonJS callers (require) and classic <script> web pages consume them, so
|
|
212
212
|
// guard that they load and shape requests IDENTICALLY to the ESM build.
|
|
213
213
|
test('CJS build behaves identically to the ESM build', async () => {
|
|
214
214
|
const cjs = require('../clients/bongos-client/index.cjs');
|
package/tests/api_path_404.mjs
CHANGED
|
@@ -38,7 +38,7 @@ t('API-shaped: /api/*, /gds/*, and bare GDS roots (/tasks, /me, …) match', ()
|
|
|
38
38
|
|
|
39
39
|
t('NOT API-shaped: game/hall/static paths fall through to HTML/static', () => {
|
|
40
40
|
for (const p of ['/', '/index.html', '/builders', '/builders/work', '/atlas-ab12.png',
|
|
41
|
-
'/vendor/phaser.min.js', '/downloads/
|
|
41
|
+
'/vendor/phaser.min.js', '/downloads/bongos/latest.json', '/game', '/healthz', '/version']) {
|
|
42
42
|
assert.equal(isApiShapedPath(p), false, `${p} should NOT be API-shaped`);
|
|
43
43
|
}
|
|
44
44
|
});
|
|
@@ -77,7 +77,7 @@ await test('computeTaskReward: 0 raw → 0 effective (a 0-mapping pays nothing)'
|
|
|
77
77
|
|
|
78
78
|
await test('netForBuilder: would-be minus already-paid', () => {
|
|
79
79
|
assert.equal(netForBuilder(100, 30), 70);
|
|
80
|
-
assert.equal(netForBuilder(100, 0), 100); // cost-plus paid nothing (
|
|
80
|
+
assert.equal(netForBuilder(100, 0), 100); // cost-plus paid nothing (the CI case)
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
await test('netForBuilder: cost-plus over-paid → floored at 0 (never claws back)', () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// tests/
|
|
1
|
+
// tests/cli_allowlist_permissions.mjs
|
|
2
2
|
//
|
|
3
3
|
// task 1051 (D): the blessed methodology CLI scripts must stay pre-allowed in the
|
|
4
|
-
// project .claude/settings.json so a
|
|
4
|
+
// project .claude/settings.json so a Claude session in auto permission mode
|
|
5
5
|
// does not hard-DENY the very command it exists to run. The #1027 nightmare
|
|
6
6
|
// dead-ended on `node scripts/gds/ship.js` at the auto classifier; this guards
|
|
7
7
|
// against that allow-rule being silently dropped.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// on every routine read. The list of tools is `.claude/hooks/shell-tools.js`, the same
|
|
13
13
|
// one the guards read: a rule may only exist for a tool git-guard and secret-gate see.
|
|
14
14
|
//
|
|
15
|
-
// Run: node tests/
|
|
15
|
+
// Run: node tests/cli_allowlist_permissions.mjs
|
|
16
16
|
|
|
17
17
|
import { strict as assert } from 'node:assert';
|
|
18
18
|
import fs from 'node:fs';
|
|
@@ -62,7 +62,7 @@ perTool('permissions.allow pre-allows the generic GDS API channel api.js (auto-m
|
|
|
62
62
|
// idea-triage promote, create-task, blocker-resolve, criterion ratify, goal
|
|
63
63
|
// create, cost-via-API, security reports — rides the generic caller
|
|
64
64
|
// `node scripts/gds/api.js <METHOD> /api/gds/...`. If it is not pre-allowed, a
|
|
65
|
-
//
|
|
65
|
+
// session in AUTO permission mode has the classifier
|
|
66
66
|
// hard-DENY those writes as "[External System Writes] ... beyond the user's
|
|
67
67
|
// request" — mistaking a sanctioned idea-triage session for scope creep. The
|
|
68
68
|
// GDS server enforces builders.rank on every request (ADR 0016), so this rule
|
|
@@ -309,5 +309,5 @@ test('the instance starter mirrors this list in verb form (claude-materialize.js
|
|
|
309
309
|
});
|
|
310
310
|
|
|
311
311
|
console.log('');
|
|
312
|
-
console.log(`
|
|
312
|
+
console.log(`cli_allowlist_permissions: ${passed} passed, ${failed} failed`);
|
|
313
313
|
process.exit(failed === 0 ? 0 : 1);
|
package/tests/cli_sessions.mjs
CHANGED
|
@@ -201,7 +201,7 @@ test('signing into a second instance PRESERVES the first — including one writt
|
|
|
201
201
|
});
|
|
202
202
|
|
|
203
203
|
test('the ACTIVE pointer still lands exactly where every existing reader looks', () => {
|
|
204
|
-
// In-repo skills
|
|
204
|
+
// In-repo skills and hooks all read the active session file in the configured dir. The
|
|
205
205
|
// store is additive; if this moved, every one of them would silently stop finding a session.
|
|
206
206
|
const { out } = inSandbox(`
|
|
207
207
|
await lib.saveSession({ token:'T', api_base:'https://a.example.com', builder:{github_login:'x'} });
|
package/tests/context_pack.mjs
CHANGED
|
@@ -339,14 +339,14 @@ t('formatContextPackCompact: two lines, worktree + prefix, no undefined', () =>
|
|
|
339
339
|
assert.ok(!/undefined/.test(out));
|
|
340
340
|
});
|
|
341
341
|
|
|
342
|
-
// ----
|
|
342
|
+
// ---- a checkout with no modules/lifecycle on disk (task 1003085) ------------
|
|
343
343
|
//
|
|
344
|
-
//
|
|
345
|
-
// claim put in scope
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
344
|
+
// When modules/lifecycle is not on disk (it was found on a checkout that held only
|
|
345
|
+
// the modules an active claim put in scope, so with no claim it was absent), this
|
|
346
|
+
// file's hard `require` of task-classifier threw MODULE_NOT_FOUND at IMPORT time,
|
|
347
|
+
// which killed /builder-start and /builder-claim: the two commands needed to escape
|
|
348
|
+
// the state, because a claim is what fetches the module. The live escape was
|
|
349
|
+
// POST /claims by curl.
|
|
350
350
|
//
|
|
351
351
|
// This reproduces the condition for real rather than asserting on source text:
|
|
352
352
|
// a temp tree holding context-pack.js and the one other thing it imports, with
|
|
@@ -357,7 +357,7 @@ const os = require('node:os');
|
|
|
357
357
|
const path = require('node:path');
|
|
358
358
|
const url = require('node:url');
|
|
359
359
|
|
|
360
|
-
function
|
|
360
|
+
function checkoutWithoutLifecycle() {
|
|
361
361
|
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
362
362
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ctxpack-nolifecycle-'));
|
|
363
363
|
fs.mkdirSync(path.join(root, 'scripts', 'gds'), { recursive: true });
|
|
@@ -369,13 +369,13 @@ function boxWithoutLifecycle() {
|
|
|
369
369
|
// coupling has to be looked at — which is the point.
|
|
370
370
|
fs.copyFileSync(path.join(here, '..', 'src', 'bongos', 'api-prefix.js'),
|
|
371
371
|
path.join(root, 'src', 'bongos', 'api-prefix.js'));
|
|
372
|
-
// No modules/ — that IS the
|
|
372
|
+
// No modules/ — that IS the condition.
|
|
373
373
|
assert.ok(!fs.existsSync(path.join(root, 'modules')), 'the fixture must not carry modules/');
|
|
374
374
|
return { root, mod: require(path.join(root, 'scripts', 'gds', 'context-pack.js')) };
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
-
t('a
|
|
378
|
-
const { mod } =
|
|
377
|
+
t('a checkout with no modules/lifecycle can still LOAD the pack (the deadlock)', () => {
|
|
378
|
+
const { mod } = checkoutWithoutLifecycle();
|
|
379
379
|
const api = mod.apiContract();
|
|
380
380
|
assert.equal(api.enums_available, false, 'it must say so, not pretend');
|
|
381
381
|
assert.equal(api.api_prefix, '/api/bongos', 'everything not behind the module still works');
|
|
@@ -386,8 +386,8 @@ t('a box with no modules/lifecycle can still LOAD the pack (the deadlock)', () =
|
|
|
386
386
|
assert.equal(api.manual_degree, null);
|
|
387
387
|
});
|
|
388
388
|
|
|
389
|
-
t('both formatters render on that
|
|
390
|
-
const { mod } =
|
|
389
|
+
t('both formatters render on that checkout instead of throwing', () => {
|
|
390
|
+
const { mod } = checkoutWithoutLifecycle();
|
|
391
391
|
const pack = mod.buildContextPack({
|
|
392
392
|
activeClaims: [], cwd: '/fallback', platform: 'linux',
|
|
393
393
|
runGit: () => '/work/tree\n', runWhich: () => '',
|
package/tests/copy_inventory.mjs
CHANGED
|
@@ -29,11 +29,11 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
29
29
|
const inv = require(path.join(ROOT, 'scripts', 'gds', 'copy-inventory.js'));
|
|
30
30
|
|
|
31
31
|
test('stable ids survive a move and change with the text', () => {
|
|
32
|
-
const a = inv.stableId('builders-hall', 'Close your
|
|
33
|
-
assert.equal(a, inv.stableId('builders-hall', 'Close your
|
|
34
|
-
assert.equal(a, inv.stableId('builders-hall', ' Close your
|
|
35
|
-
assert.notEqual(a, inv.stableId('builders-hall', 'Close your
|
|
36
|
-
assert.notEqual(a, inv.stableId('status-dashboard', 'Close your
|
|
32
|
+
const a = inv.stableId('builders-hall', 'Close your tab');
|
|
33
|
+
assert.equal(a, inv.stableId('builders-hall', 'Close your tab'), 'same surface + text → same id, whatever the file/line');
|
|
34
|
+
assert.equal(a, inv.stableId('builders-hall', ' Close your tab '), 'whitespace is normalized before hashing');
|
|
35
|
+
assert.notEqual(a, inv.stableId('builders-hall', 'Close your browser tab'), 'a text change must break the id — that is the signal a flag is stale');
|
|
36
|
+
assert.notEqual(a, inv.stableId('status-dashboard', 'Close your tab'), 'the same words on another surface are a different piece of copy');
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
test('the JS extractor refuses code — the 2144-row failure mode cannot come back', () => {
|
|
@@ -55,14 +55,14 @@ test('the JS extractor keeps copy, and names how a person sees it', () => {
|
|
|
55
55
|
const src = [
|
|
56
56
|
"toast('Priority order saved.');",
|
|
57
57
|
"el.textContent = 'How the whole build is doing.';",
|
|
58
|
-
"
|
|
58
|
+
"panel.innerHTML = `<p class=\"setup__msg\">Closing your session… (a few seconds)</p>`;",
|
|
59
59
|
"row.innerHTML = `<span class=\"pill\">Release task ${id}</span>`;",
|
|
60
60
|
].join('\n');
|
|
61
61
|
const rows = inv.fromJs({ source: src, rel: 'x.js' });
|
|
62
62
|
const byOrigin = Object.fromEntries(rows.map((r) => [r.origin, r.text]));
|
|
63
63
|
assert.equal(byOrigin['js-notice'], 'Priority order saved.');
|
|
64
64
|
assert.equal(byOrigin['js-text-assign'], 'How the whole build is doing.');
|
|
65
|
-
assert.ok(rows.some((r) => r.origin === 'js-markup' && r.text === 'Closing your
|
|
65
|
+
assert.ok(rows.some((r) => r.origin === 'js-markup' && r.text === 'Closing your session… (a few seconds)'),
|
|
66
66
|
'text inside script-built markup is copy; the tags around it are not');
|
|
67
67
|
assert.ok(rows.some((r) => r.text === 'Release task {…}'),
|
|
68
68
|
'a template interpolation becomes a placeholder so the sentence stays readable and the hole stays visible');
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// tests/core_258_box_session_claims_db.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1004362 — core_258 must apply on a database holding a claim created from a
|
|
4
|
+
// dev-box session.
|
|
5
|
+
//
|
|
6
|
+
// WHAT BROKE. core_258 (task 1003895) deletes every builder_sessions row with source
|
|
7
|
+
// 'box'. claims.creator_session_id (migration 013) references builder_sessions with no
|
|
8
|
+
// ON DELETE, so on the live database, which had one such claim, the DELETE failed the
|
|
9
|
+
// foreign key, the whole migration rolled back, and so did every core upgrade carrying
|
|
10
|
+
// it. The fix nulls the link first; NULL is what a web claim already carries.
|
|
11
|
+
//
|
|
12
|
+
// HOW THIS PROVES IT. The lane database is already migrated, so core_258 has run and the
|
|
13
|
+
// source CHECK no longer admits 'box'. Inside ONE transaction that is ALWAYS rolled back,
|
|
14
|
+
// the test widens the CHECK back the way migration 090 had it, seeds a box session and a
|
|
15
|
+
// claim created from it, then runs core_258's own statements (its BEGIN and COMMIT
|
|
16
|
+
// stripped so they join this transaction). Every table core_258 drops is already gone,
|
|
17
|
+
// and each DROP is IF EXISTS, so they are no-ops here. Nothing is committed or truncated.
|
|
18
|
+
//
|
|
19
|
+
// Real-DB (self-skips without Postgres; belongs in the INTEGRATION set).
|
|
20
|
+
//
|
|
21
|
+
// Run: DATABASE_URL=postgres://... node tests/core_258_box_session_claims_db.mjs
|
|
22
|
+
|
|
23
|
+
import { strict as assert } from 'node:assert';
|
|
24
|
+
import { createRequire } from 'node:module';
|
|
25
|
+
import { readFileSync } from 'node:fs';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { dirname, join } from 'node:path';
|
|
28
|
+
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const { Pool } = require('pg');
|
|
31
|
+
|
|
32
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
33
|
+
const migrationText = readFileSync(join(root, 'migrations/core_258_drop_dev_box_tables.sql'), 'utf8');
|
|
34
|
+
|
|
35
|
+
// The migration's own statements, minus its transaction wrapper. The count check keeps
|
|
36
|
+
// the strip honest: if the file stopped using BEGIN/COMMIT this would silently be a
|
|
37
|
+
// different migration, so say so instead.
|
|
38
|
+
const wrapper = /^\s*(BEGIN|COMMIT);\s*$/gm;
|
|
39
|
+
assert.equal((migrationText.match(wrapper) || []).length, 2, 'core_258 is expected to carry exactly one BEGIN and one COMMIT');
|
|
40
|
+
const migrationBody = migrationText.replace(wrapper, '');
|
|
41
|
+
// The pre-fix body: the same statements without the unlinking UPDATE, so the bug stays
|
|
42
|
+
// executable rather than described.
|
|
43
|
+
const preFixBody = migrationBody.replace(/UPDATE claims SET creator_session_id = NULL[\s\S]*?;/, '');
|
|
44
|
+
assert.notEqual(preFixBody, migrationBody, 'core_258 no longer unlinks claims before deleting box sessions');
|
|
45
|
+
|
|
46
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
|
|
47
|
+
|
|
48
|
+
let passed = 0;
|
|
49
|
+
let failed = 0;
|
|
50
|
+
let client;
|
|
51
|
+
async function test(name, fn) {
|
|
52
|
+
await client.query('SAVEPOINT t');
|
|
53
|
+
try {
|
|
54
|
+
await fn();
|
|
55
|
+
passed++; console.log(` ok ${name}`);
|
|
56
|
+
await client.query('RELEASE SAVEPOINT t');
|
|
57
|
+
} catch (err) {
|
|
58
|
+
failed++; console.error(` FAIL ${name}\n ${err.message}`);
|
|
59
|
+
await client.query('ROLLBACK TO SAVEPOINT t');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Schema-driven insert (the helper of tests/session_earnings_mirror_db.mjs): fills any
|
|
64
|
+
// NOT NULL column with no default. Identifiers come from this file or information_schema.
|
|
65
|
+
const schemaCache = new Map();
|
|
66
|
+
async function insertRow(table, explicit = {}) {
|
|
67
|
+
if (!schemaCache.has(table)) {
|
|
68
|
+
const { rows } = await client.query(
|
|
69
|
+
`SELECT column_name, data_type, is_nullable, column_default, is_identity
|
|
70
|
+
FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1`, [table]);
|
|
71
|
+
schemaCache.set(table, rows);
|
|
72
|
+
}
|
|
73
|
+
const cols = schemaCache.get(table);
|
|
74
|
+
if (cols.length === 0) throw new Error(`table ${table} not found — is this database migrated?`);
|
|
75
|
+
const values = { ...explicit };
|
|
76
|
+
for (const c of cols) {
|
|
77
|
+
if (c.column_name in values || c.is_nullable === 'YES' || c.column_default !== null || c.is_identity === 'YES') continue;
|
|
78
|
+
switch (c.data_type) {
|
|
79
|
+
case 'integer': case 'bigint': case 'smallint': case 'numeric': case 'real': case 'double precision':
|
|
80
|
+
values[c.column_name] = 0; break;
|
|
81
|
+
case 'boolean': values[c.column_name] = false; break;
|
|
82
|
+
case 'timestamp with time zone': case 'timestamp without time zone': case 'date':
|
|
83
|
+
values[c.column_name] = new Date(); break;
|
|
84
|
+
case 'json': case 'jsonb': values[c.column_name] = {}; break;
|
|
85
|
+
case 'ARRAY': values[c.column_name] = []; break;
|
|
86
|
+
default: values[c.column_name] = `c258-${c.column_name}`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const names = Object.keys(values);
|
|
90
|
+
const { rows } = await client.query(
|
|
91
|
+
`INSERT INTO ${table} (${names.map((n) => `"${n}"`).join(', ')})
|
|
92
|
+
VALUES (${names.map((_, i) => `$${i + 1}`).join(', ')}) RETURNING *`,
|
|
93
|
+
names.map((n) => values[n]));
|
|
94
|
+
return rows[0];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// A builder with one box session and one cli session, and a released claim from each.
|
|
98
|
+
async function fixture() {
|
|
99
|
+
const tag = `c258-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
100
|
+
const builder = await insertRow('builders', { github_id: `${tag}-gh`, github_login: tag, display_name: tag, total_credits: 0 });
|
|
101
|
+
const version = await insertRow('versions', { id: `${tag}-v`, status: 'building' });
|
|
102
|
+
const claimFrom = async (source) => {
|
|
103
|
+
const session = await insertRow('builder_sessions', { builder_id: builder.id, token_hash: `${tag}-${source}`, source });
|
|
104
|
+
// discipline + kind: core_247's CHECK refuses the column default (task 1004363).
|
|
105
|
+
const task = await insertRow('tasks', { title: `${tag} ${source} task`, status: 'shipped', version_id: version.id, discipline: 'engineer', kind: 'feature' });
|
|
106
|
+
const claim = await insertRow('claims', {
|
|
107
|
+
task_id: task.id, builder_id: builder.id, creator_session_id: session.id,
|
|
108
|
+
worktree_name: `${tag}-${source}`, released_at: new Date(), outcome: 'shipped',
|
|
109
|
+
});
|
|
110
|
+
return { session, claim };
|
|
111
|
+
};
|
|
112
|
+
return { box: await claimFrom('box'), cli: await claimFrom('cli') };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function main() {
|
|
116
|
+
try {
|
|
117
|
+
client = await pool.connect();
|
|
118
|
+
} catch {
|
|
119
|
+
console.log('core_258_box_session_claims_db: no Postgres (DATABASE_URL unset/unreachable) — skipping');
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await client.query('BEGIN');
|
|
124
|
+
try {
|
|
125
|
+
// The source CHECK as migration 090 left it, so a box session can be seeded.
|
|
126
|
+
await client.query('ALTER TABLE builder_sessions DROP CONSTRAINT IF EXISTS builder_sessions_source_check');
|
|
127
|
+
await client.query(`ALTER TABLE builder_sessions ADD CONSTRAINT builder_sessions_source_check CHECK (source IN ('cli', 'web', 'box'))`);
|
|
128
|
+
|
|
129
|
+
await test('without the unlink, the box-session DELETE fails the claims foreign key (the bug)', async () => {
|
|
130
|
+
await fixture();
|
|
131
|
+
await client.query('SAVEPOINT pre');
|
|
132
|
+
let err = null;
|
|
133
|
+
try { await client.query(preFixBody); } catch (e) { err = e; }
|
|
134
|
+
await client.query('ROLLBACK TO SAVEPOINT pre');
|
|
135
|
+
assert.ok(err, 'the pre-fix body should have failed');
|
|
136
|
+
assert.equal(err.code, '23503', `expected a foreign-key violation, got ${err.code}: ${err.message}`);
|
|
137
|
+
assert.match(err.message, /claims_creator_session_id_fkey/);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await test('core_258 applies, unlinks the box claim, keeps it, and leaves the cli claim alone', async () => {
|
|
141
|
+
const { box, cli } = await fixture();
|
|
142
|
+
// Case 1 passed and released its savepoint, so its box session and claim are still
|
|
143
|
+
// here too; this run unlinks and deletes those as well. That is harmless and even
|
|
144
|
+
// realistic (more than one box claim), and every assertion below is scoped by id.
|
|
145
|
+
await client.query(migrationBody);
|
|
146
|
+
|
|
147
|
+
const { rows: claims } = await client.query(
|
|
148
|
+
'SELECT id, creator_session_id, worktree_name, outcome FROM claims WHERE id = ANY($1::bigint[]) ORDER BY id',
|
|
149
|
+
[[box.claim.id, cli.claim.id]]);
|
|
150
|
+
assert.equal(claims.length, 2, 'both claims must survive the migration');
|
|
151
|
+
const boxClaim = claims.find((c) => String(c.id) === String(box.claim.id));
|
|
152
|
+
const cliClaim = claims.find((c) => String(c.id) === String(cli.claim.id));
|
|
153
|
+
assert.equal(boxClaim.creator_session_id, null, 'the box claim must be unlinked');
|
|
154
|
+
assert.equal(boxClaim.outcome, 'shipped', 'the box claim keeps its history');
|
|
155
|
+
assert.equal(String(cliClaim.creator_session_id), String(cli.session.id), 'a cli claim must keep its session');
|
|
156
|
+
|
|
157
|
+
const { rows: boxSessions } = await client.query(`SELECT count(*)::int AS n FROM builder_sessions WHERE source = 'box'`);
|
|
158
|
+
assert.equal(boxSessions[0].n, 0, 'every box session must be deleted');
|
|
159
|
+
const { rows: cliSession } = await client.query('SELECT id FROM builder_sessions WHERE id = $1', [cli.session.id]);
|
|
160
|
+
assert.equal(cliSession.length, 1, 'a cli session must survive');
|
|
161
|
+
|
|
162
|
+
await client.query('SAVEPOINT chk');
|
|
163
|
+
let err = null;
|
|
164
|
+
try { await insertRow('builder_sessions', { builder_id: cli.session.builder_id, token_hash: `${cli.session.token_hash}-again`, source: 'box' }); } catch (e) { err = e; }
|
|
165
|
+
await client.query('ROLLBACK TO SAVEPOINT chk');
|
|
166
|
+
assert.equal(err && err.code, '23514', 'the narrowed CHECK must refuse a new box session');
|
|
167
|
+
});
|
|
168
|
+
} finally {
|
|
169
|
+
await client.query('ROLLBACK');
|
|
170
|
+
client.release();
|
|
171
|
+
await pool.end();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log(`\ncore_258_box_session_claims_db: ${passed} passed, ${failed} failed`);
|
|
175
|
+
// Let the socket close before exiting (a libuv abort otherwise on some boxes).
|
|
176
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
177
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
main().catch((err) => { console.error(err); process.exit(1); });
|
package/tests/credit_grant.mjs
CHANGED
|
@@ -435,7 +435,7 @@ await test('1002474 the INSERT still carries a DB-level double-pay backstop', as
|
|
|
435
435
|
assert.match(insert.sql, /reward_basis/, 'and it must be keyed on (session_id, reward_basis)');
|
|
436
436
|
});
|
|
437
437
|
|
|
438
|
-
// task 1676 — the cost-plus zero-out fix for the
|
|
438
|
+
// task 1676 — the cost-plus zero-out fix for the CI agent ship path.
|
|
439
439
|
// Symptom (ADR 0097): those sessions uploaded empty/unpriceable model_usage, so
|
|
440
440
|
// priceModelUsage returned $0 and the reward rounded to 0 — even though real
|
|
441
441
|
// token spend happened. The fix prices the session's real total_tokens at the
|
|
@@ -444,7 +444,7 @@ await test('awardSessionTokenReward (task 1676): empty model_usage + real total_
|
|
|
444
444
|
const client = fakeClient();
|
|
445
445
|
client.enqueue([]); // idempotency SELECT → no prior row
|
|
446
446
|
client.enqueue([{ id: 9 }]); // INSERT ... RETURNING id
|
|
447
|
-
// The exact
|
|
447
|
+
// The exact CI shape: no priceable per-model usage, but the digest
|
|
448
448
|
// carried a real total token count (here 73M, an opus-marathon session).
|
|
449
449
|
const res = await awardSessionTokenReward(client, 7, 'sess-ci', {}, 73_000_000);
|
|
450
450
|
assert.equal(res.awarded, true, 'reward awarded despite empty model_usage');
|
|
@@ -30,7 +30,7 @@ const CRITERIA = [
|
|
|
30
30
|
crit(138, 'bx-hall', 'Core task actions work in the builders hall without invoking the LLM.'),
|
|
31
31
|
crit(139, 'bx-identity', 'A builder can view and edit their own profile and read a leaderboard.'),
|
|
32
32
|
crit(140, 'bx-onramp', 'Newcomers never dead-end: auth and re-auth are browser-clickable.'),
|
|
33
|
-
crit(141, 'bx-
|
|
33
|
+
crit(141, 'bx-recovery', 'A builder can resume after a crash: uncommitted work is not lost.'),
|
|
34
34
|
];
|
|
35
35
|
|
|
36
36
|
test('ranks the most lexically-relevant criterion first', () => {
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
// Claude Design round-trip). Nothing tied them together, so the design file kept
|
|
8
8
|
// the pre-1002749 palette — including the exact accents removed for failing WCAG
|
|
9
9
|
// AA — and every prototype started from colours the product no longer paints.
|
|
10
|
-
// Model:
|
|
10
|
+
// Model: fallback-equals-pack assertions — a copy of the pack's values is asserted
|
|
11
|
+
// equal to the pack, never trusted.
|
|
11
12
|
//
|
|
12
13
|
// Run: node tests/design_tokens_sync.mjs
|
|
13
14
|
|
|
@@ -25,7 +26,7 @@ const tokens = readJson('config', 'design-tokens.neutral.json');
|
|
|
25
26
|
|
|
26
27
|
// Strip comments so rationale prose — which must be free to name a removed
|
|
27
28
|
// colour — never trips a scan of the CODE (the stripCommentsForIdentity
|
|
28
|
-
// contract
|
|
29
|
+
// contract).
|
|
29
30
|
function codeOnly(text) {
|
|
30
31
|
return text
|
|
31
32
|
.replace(/\/\*[\s\S]*?\*\//g, '')
|
package/tests/fitness.mjs
CHANGED
|
@@ -973,3 +973,92 @@ test('KERNEL_FILES is exported exactly ONCE and still resolves (task 1002795)',
|
|
|
973
973
|
assert.ok(Array.isArray(fitness.KERNEL_FILES), 'and it still resolves to an array');
|
|
974
974
|
assert.ok(fitness.KERNEL_FILES.length > 0, 'with the real kernel roster in it');
|
|
975
975
|
});
|
|
976
|
+
|
|
977
|
+
// ---- the dev box stays retired (task 1003899, ADR 0346) ---------------------
|
|
978
|
+
//
|
|
979
|
+
// scripts/gds/dev-box-guard.js fails CI when a retired dev-box name reappears.
|
|
980
|
+
// Each rule is proven by PLANTING a violation through the injectable files/read
|
|
981
|
+
// seam, never by assuming the regex is right. The retired names are assembled at
|
|
982
|
+
// runtime (KEY = 'dev' + '-box', …) so this file never contains them literally and
|
|
983
|
+
// needs no exemption of its own.
|
|
984
|
+
const devBoxGuard = require(path.join(ROOT, 'scripts', 'gds', 'dev-box-guard.js'));
|
|
985
|
+
const KEY = 'dev' + '-box';
|
|
986
|
+
const TABLE = 'builder' + '_boxes';
|
|
987
|
+
const COLUMN = 'box' + '_blocked_reason';
|
|
988
|
+
const PERM = 'box' + '.fleet.manage';
|
|
989
|
+
const BOX = 'b' + 'ox';
|
|
990
|
+
|
|
991
|
+
function plant(rel, text) {
|
|
992
|
+
return devBoxGuard.checkNoDevBox({ files: [rel], read: () => text });
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
test('dev-box guard: the clean tree passes, and it scans the real tracked files', () => {
|
|
996
|
+
const r = devBoxGuard.checkNoDevBox();
|
|
997
|
+
assert.equal(r.hardFail, false, r.violations.join('\n'));
|
|
998
|
+
assert.match(r.note, /(\d{4,}) tracked file/, 'a real enumeration, not an empty one');
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
test('dev-box guard: it is wired into the fitness CHECKS that CI runs', () => {
|
|
1002
|
+
const src = fs.readFileSync(path.join(ROOT, 'scripts', 'gds', 'fitness.js'), 'utf8');
|
|
1003
|
+
assert.match(src, /require\('\.\/dev-box-guard\.js'\)\.checkNoDevBox/);
|
|
1004
|
+
const names = fitness.runAll().map((r) => r.name);
|
|
1005
|
+
assert.ok(names.includes('the dev box stays retired (ADR 0346)'), 'runAll() includes the guard');
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
test('dev-box guard: each rule FAILS on a planted violation', () => {
|
|
1009
|
+
const cases = [
|
|
1010
|
+
['module-key (quoted)', 'src/x.js', `enable('${KEY}');`],
|
|
1011
|
+
['module-key (path)', 'src/y.js', `require('../modules/${KEY}/boxes.js');`],
|
|
1012
|
+
['module-key (json key)', 'config/modules.neutral.json', `{ "modules": { "${KEY}": true } }`],
|
|
1013
|
+
['identifier (table)', 'src/z.js', `await q('SELECT * FROM ${TABLE}');`],
|
|
1014
|
+
['identifier (column)', 'src/w.js', `SELECT ${COLUMN} FROM builders`],
|
|
1015
|
+
['permission', 'modules/government/catalog.js', `{ key: '${PERM}', floor: 'metic' },`],
|
|
1016
|
+
['session-source', 'src/bongos/auth.js', `if (session.source === '${BOX}') return;`],
|
|
1017
|
+
];
|
|
1018
|
+
for (const [label, rel, text] of cases) {
|
|
1019
|
+
const r = plant(rel, text);
|
|
1020
|
+
assert.equal(r.hardFail, true, `${label}: a planted violation must fail the check`);
|
|
1021
|
+
assert.match(r.violations[0], /ADR 0346/, `${label}: the failure names the ADR`);
|
|
1022
|
+
assert.match(r.note, /DEV_BOX_EXEMPT in scripts\/gds\/dev-box-guard\.js/, `${label}: and the one exemption list`);
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
test('dev-box guard: a retired PATH fails even with harmless contents', () => {
|
|
1027
|
+
for (const rel of [`scripts/gds/${BOX}.js`, `infra/${BOX}-provision.sh`, `modules/${KEY}/module.json`, '.devcontainer/devcontainer.json']) {
|
|
1028
|
+
const r = plant(rel, '{}');
|
|
1029
|
+
assert.equal(r.hardFail, true, `${rel} must fail`);
|
|
1030
|
+
assert.match(r.violations.join('\n'), /path: a retired dev-box location/);
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
test('dev-box guard: near misses do NOT fire (plurals, filenames, prose, a server box)', () => {
|
|
1035
|
+
const clean = [
|
|
1036
|
+
`resolveModules({ modules: { '${KEY}es': true } })`, // the typo guard's test input
|
|
1037
|
+
`see docs/adr/0072-${KEY}-code-staleness-visibility.md`, // an ADR filename
|
|
1038
|
+
`the ${KEY} removal is recorded in ADR 0346`, // prose, not a key
|
|
1039
|
+
`ENV_FILE=/etc/cloudbongos/${BOX}.env`, // box.env is the control-plane runner's env file
|
|
1040
|
+
`ssh into the co-hosting ${BOX}`, // a server
|
|
1041
|
+
`const secret = require('./secret-${BOX}.js');`, // crypto, unrelated
|
|
1042
|
+
`if (session.source === 'cli') return;`,
|
|
1043
|
+
];
|
|
1044
|
+
for (const text of clean) {
|
|
1045
|
+
const r = plant('src/near-miss.js', text);
|
|
1046
|
+
assert.equal(r.hardFail, false, `should not fire: ${text}\n${r.violations.join('\n')}`);
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
test('dev-box guard: the exemption list is honoured, and it is the only way out', () => {
|
|
1051
|
+
const text = `const RETIRED = { '${KEY}': 'gone' };`;
|
|
1052
|
+
assert.equal(plant('src/modules.js', text).hardFail, false, 'the retirement guard file is exempt');
|
|
1053
|
+
assert.equal(plant('docs/adr/0999-anything.md', text).hardFail, false, 'the historical record is exempt by prefix');
|
|
1054
|
+
assert.equal(plant('src/other.js', text).hardFail, true, 'the same text anywhere else fails');
|
|
1055
|
+
for (const [entry, why] of devBoxGuard.DEV_BOX_EXEMPT) {
|
|
1056
|
+
assert.ok(typeof why === 'string' && why.length >= 12, `${entry} carries a reason`);
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
test('dev-box guard: an empty enumeration is a scan defect, not a pass', () => {
|
|
1061
|
+
const r = devBoxGuard.checkNoDevBox({ files: [], read: () => '' });
|
|
1062
|
+
assert.equal(r.hardFail, true);
|
|
1063
|
+
assert.match(r.violations[0], /scan defect/);
|
|
1064
|
+
});
|
|
@@ -161,8 +161,8 @@ test('the committed baseline file parses and covers every hermetic metric', () =
|
|
|
161
161
|
// `floor === undefined`, which compareToBaselines classifies as a WARNING — so a
|
|
162
162
|
// checkout with no config/ reported the ratchet PASSED. Measured before the fix: a
|
|
163
163
|
// planted 5,000-line + 94-cycle regression produced ZERO violations. That was not a
|
|
164
|
-
// hypothetical checkout —
|
|
165
|
-
//
|
|
164
|
+
// hypothetical checkout — a sparse checkout without config/ once left the builders
|
|
165
|
+
// who most needed the gate silently exempt from it.
|
|
166
166
|
//
|
|
167
167
|
// checkQualityRatchets() reads the real repo, so these tests drive compareToBaselines
|
|
168
168
|
// (pure) for the metric-level rules and exercise the file-level rule by pointing at a
|
|
@@ -154,7 +154,7 @@ test('landProvenForTask: a non-sha published tip ("HEAD") falls back to the PR c
|
|
|
154
154
|
|
|
155
155
|
// --- task 1002645: findTaskPullRequest — the PR-title fallback locator --------
|
|
156
156
|
//
|
|
157
|
-
// The reconciler derives claude/<worktree_name>
|
|
157
|
+
// The reconciler derives claude/<worktree_name> first; a laptop/wave
|
|
158
158
|
// ship pushes worktree-<worktree_name>, so the derived branch finds no PR and the
|
|
159
159
|
// sweep never engaged (wave PRs 34/35/36 stranded at 'confirmed'). This locates the
|
|
160
160
|
// task's PR by the "task <id>: …" TITLE-PREFIX convention — deliberately TIGHTER
|