@bongos/core 1.19.1081 → 1.20.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 +528 -193
- package/.claude/skills/planning-session/SKILL.md +6 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +32 -0
- package/clients/bongos-client/index.cjs +32 -0
- package/clients/bongos-client/index.d.ts +51 -6
- package/clients/bongos-client/index.mjs +32 -0
- package/docs/adr/0099-delayed-redacted-mirror-export.md +5 -1
- package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +17 -0
- package/docs/adr/0176-private-repo-deploy-keys.md +2 -0
- package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
- package/docs/adr/0348-the-web-tier-may-look-read-only-at-what-an-owners-render-key-can-see.md +53 -0
- package/docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md +106 -0
- package/docs/adr/0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md +103 -0
- package/docs/adr/README.md +3 -0
- package/docs/api/openapi.json +851 -19
- package/docs/api-reference.md +33 -9
- package/docs/architecture.md +6 -2
- package/docs/copy-inventory.md +620 -553
- package/docs/copy-registry.json +1467 -820
- package/docs/file-map.md +2 -0
- package/docs/module-api-changelog.md +8 -0
- package/docs/modules-contract.md +1 -0
- package/docs/page-inventory.json +38 -4
- package/docs/page-readings.json +1666 -1558
- package/migrations/core_260_goals_working_area.sql +63 -0
- package/migrations/core_261_grade_attempts_verdicts.sql +24 -0
- package/migrations/core_262_drop_builder_box_blocked.sql +33 -0
- package/modules/agents/lib/validate.js +43 -0
- package/modules/autonomy/gauge.js +38 -2
- package/modules/grading/grader-subagent.js +37 -2
- package/modules/grading/grader-workers/reader.js +65 -5
- package/modules/hall-ui/public/approval-queue.css +33 -7
- package/modules/hall-ui/public/approval-queue.js +9 -2
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/brand-holes.js +121 -0
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/collab.js +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +6 -1
- package/modules/hall-ui/public/deploy.js +22 -8
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/dom-utils.js +20 -0
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-inbox.js +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/idea.js +28 -0
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/ideas.js +19 -3
- package/modules/hall-ui/public/index.html +5 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile-nudge.js +21 -4
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/profile.js +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +19 -30
- package/modules/hall-ui/public/settings.js +24 -177
- package/modules/hall-ui/public/settings.states.json +1 -1
- package/modules/hall-ui/public/shell.js +4 -2
- package/modules/hall-ui/public/studio.css +14 -8
- package/modules/hall-ui/public/studio.html +5 -4
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.css +6 -3
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor-lib.js +82 -1
- package/modules/hall-ui/public/tweak-editor.css +142 -11
- package/modules/hall-ui/public/tweak-editor.html +25 -9
- package/modules/hall-ui/public/tweak-editor.js +243 -10
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/approval-queue.md +6 -0
- package/modules/hall-ui/records/tweak-editor.md +6 -0
- package/modules/ideas/ratify.js +36 -8
- package/modules/ideas/routes/ratify-goal.js +6 -0
- package/modules/lifecycle/db-analytics.js +79 -10
- package/modules/lifecycle/db-claims.js +48 -1
- package/modules/lifecycle/db-deps-criteria.js +94 -40
- package/modules/lifecycle/db-goals.js +4 -1
- package/modules/lifecycle/db-grade.js +9 -1
- package/modules/lifecycle/db-relevance-flags.js +183 -0
- package/modules/lifecycle/db-versions.js +4 -3
- package/modules/lifecycle/db.js +19 -0
- package/modules/lifecycle/done-when.js +5 -3
- package/modules/lifecycle/est-advisory.js +31 -4
- package/modules/lifecycle/goal-task-relevance-advisory.js +153 -0
- package/modules/lifecycle/goal-task-relevance-judge.js +226 -0
- package/modules/lifecycle/migrations/lifecycle_014_goal_task_relevance_flags.sql +82 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/routes/claims.js +8 -1
- package/modules/lifecycle/routes/goal-task-relevance.js +72 -0
- package/modules/lifecycle/routes/goals.js +1 -1
- package/modules/lifecycle/routes/tasks.js +27 -24
- package/modules/lifecycle/routes/visuals.js +9 -0
- package/modules/lifecycle/ship-preflight.js +7 -11
- package/modules/lifecycle/task-visuals.js +109 -5
- package/modules/npm-release/module.json +2 -1
- package/modules/npm-release/preview/commands.js +81 -0
- package/modules/npm-release/preview/divert.js +95 -0
- package/modules/npm-release/preview/env.js +78 -0
- package/modules/npm-release/preview/proxy.js +107 -0
- package/modules/npm-release/preview/runtime.js +83 -0
- package/modules/npm-release/preview/supervisor.js +175 -0
- package/modules/npm-release/public/work.js +65 -1
- package/modules/npm-release/routes/preview.js +133 -0
- package/modules/provisioning/app-status.js +83 -0
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/pollers/app-liveness.js +110 -0
- package/modules/provisioning/render-lookup.js +68 -0
- package/modules/provisioning/render-standup.js +44 -1
- package/modules/provisioning/routes/render-standup.js +81 -6
- package/modules/provisioning/starter-bundles.js +4 -1
- package/modules/public-landing/public/account-private.states.json +14 -0
- package/modules/public-landing/public/account.html +513 -0
- package/modules/public-landing/public/account.probes.json +37 -0
- package/modules/public-landing/public/account.states.json +16 -0
- package/modules/public-landing/public/index.html +5 -2
- package/modules/public-landing/public/projects.html +661 -18
- package/modules/public-landing/public/projects.states.json +8 -1
- package/modules/render-deploy/deploys.js +203 -0
- package/modules/render-deploy/migrations/render_deploy_001_app.sql +22 -0
- package/modules/render-deploy/module.json +25 -0
- package/modules/render-deploy/public/deploy.css +10 -0
- package/modules/render-deploy/public/deploy.js +249 -0
- package/modules/render-deploy/render.js +56 -0
- package/modules/render-deploy/routes/act.js +88 -0
- package/modules/render-deploy/routes/app.js +65 -0
- package/modules/render-deploy/routes/history.js +38 -0
- package/modules/specialities/db.js +27 -8
- package/modules/specialities/migrations/specialities_003_skills.sql +46 -0
- package/modules/specialities/routes/specialities.js +51 -3
- package/modules/specialities/skills.js +94 -0
- package/modules/specialities/specialities.js +56 -9
- package/modules/ui-design/kit/fixtures/me-cross-project-private.json +12 -0
- package/modules/ui-design/kit/fixtures/me__cross-project.json +15 -0
- package/modules/ui-design/kit/fixtures/provisioning-instance-render.json +39 -0
- package/modules/ui-design/kit/lib.js +3 -1
- package/modules/ui-design/kit/serve.js +96 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +169 -0
- package/scripts/gds/agents-sync.js +13 -3
- package/scripts/gds/autobongos-run.js +80 -2
- package/scripts/gds/autobongos-service.cmd +12 -0
- package/scripts/gds/copy-apply.js +14 -0
- package/scripts/gds/dev-box-guard.js +3 -2
- package/scripts/gds/fitness.js +9 -0
- package/scripts/gds/grade-correlation-audit.js +42 -12
- package/scripts/gds/grade-replay.js +1 -1
- package/scripts/gds/provision-core-upgrade.js +38 -7
- package/scripts/gds/provision-pin-key.js +176 -0
- package/scripts/gds/provision-render.js +62 -31
- package/scripts/gds/provision-teardown.js +7 -2
- package/scripts/gds/provision-units.js +31 -0
- package/scripts/gds/provision.js +10 -10
- package/scripts/gds/publish-manifest.js +1 -0
- package/scripts/gds/render-api.js +19 -0
- package/scripts/gds/render-payload.js +39 -65
- package/scripts/gds/run-unit-tests.js +10 -0
- package/scripts/gds/ship-finish.js +19 -20
- package/scripts/gds/smoke-dependencies.sh +37 -8
- package/scripts/gds/status.js +12 -4
- package/scripts/gds/upgrade.js +2 -2
- package/scripts/public-mirror-export.js +7 -1
- package/src/bongos/module-scope-map.js +13 -0
- package/src/bongos/serve-internal.js +3 -1
- package/src/module-api.js +10 -1
- package/src/platform-server.js +39 -0
- package/tests/account_privacy_flags.mjs +10 -6
- package/tests/agents_authoring.mjs +15 -1
- package/tests/agents_sync.mjs +106 -5
- package/tests/agents_validate.mjs +45 -0
- package/tests/api_path_404.mjs +6 -0
- package/tests/autobongos_cadence.mjs +7 -0
- package/tests/autobongos_loop.mjs +136 -1
- package/tests/autonomy_gauge.mjs +62 -0
- package/tests/blocker_hall_live_proof.mjs +275 -0
- package/tests/claim_gate_ci_unblock.mjs +116 -0
- package/tests/claim_gate_rebase.mjs +15 -3
- package/tests/collab_page.mjs +10 -0
- package/tests/conductor_main_e2e.mjs +97 -0
- package/tests/core_262_drop_box_blocked_db.mjs +143 -0
- package/tests/core_upgrade_runner.mjs +1 -0
- package/tests/dependency_writes_atomic.mjs +191 -0
- package/tests/deploy_page_projects.mjs +2 -1
- package/tests/effective_visibility_predicate.mjs +15 -0
- package/tests/est_advisory.mjs +27 -0
- package/tests/goal_map_page.mjs +26 -0
- package/tests/goal_task_relevance.mjs +518 -0
- package/tests/goal_working_area.mjs +146 -0
- package/tests/grade_attempts.mjs +61 -0
- package/tests/grade_attribution.mjs +23 -0
- package/tests/grade_correlation_audit.mjs +54 -1
- package/tests/grader_reader_lens.mjs +55 -2
- package/tests/grader_root_outage.mjs +6 -1
- package/tests/grader_subagent_tools_arg.mjs +102 -0
- package/tests/hall_approval_queue.mjs +54 -2
- package/tests/hall_settings_world.mjs +3 -1
- package/tests/hall_tweak_editor.mjs +316 -11
- package/tests/hub_account_page.mjs +394 -0
- package/tests/idea_detail_page.mjs +64 -0
- package/tests/idea_goal_ratification.mjs +64 -0
- package/tests/idea_spark_hall.mjs +60 -9
- package/tests/ideator_full_idea_shapes_space_proof.mjs +1 -1
- package/tests/landing_page.mjs +6 -3
- package/tests/module-scope-map.mjs +31 -8
- package/tests/module_api.mjs +1 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/no_phantom_mirror_workflow.mjs +24 -0
- package/tests/npm_release_preview_commands.mjs +89 -0
- package/tests/npm_release_preview_divert.mjs +164 -0
- package/tests/npm_release_preview_env.mjs +100 -0
- package/tests/npm_release_preview_proxy.mjs +162 -0
- package/tests/npm_release_preview_routes.mjs +208 -0
- package/tests/npm_release_preview_supervisor.mjs +203 -0
- package/tests/pin_write_key.mjs +383 -0
- package/tests/planning_session_skill.mjs +27 -0
- package/tests/platform_boot.mjs +71 -0
- package/tests/profile_nudge_links.mjs +86 -0
- package/tests/profile_ui_cross_project.mjs +1 -1
- package/tests/projects_hub.mjs +2 -2
- package/tests/projects_hub_app_status.mjs +249 -0
- package/tests/projects_hub_app_step.mjs +282 -52
- package/tests/projects_hub_render_connect.mjs +295 -0
- package/tests/provision_render.mjs +121 -28
- package/tests/provision_settings_apply.mjs +62 -0
- package/tests/provisioning_app_status.mjs +204 -0
- package/tests/provisioning_render_route.mjs +126 -6
- package/tests/render_check.mjs +2 -1
- package/tests/render_deploy.mjs +495 -0
- package/tests/rot_inbox.mjs +8 -0
- package/tests/ship_preflight.mjs +61 -8
- package/tests/smoke_dependencies_witness.mjs +46 -0
- package/tests/speciality_skills.mjs +214 -0
- package/tests/studio_room_height.mjs +70 -0
- package/tests/task_detail_includes.mjs +10 -0
- package/tests/task_visuals_instance_root.mjs +150 -0
- package/tests/tweak_batch_apply.mjs +21 -0
- package/tests/ui_design_kit.mjs +36 -4
- package/tests/upgrade.mjs +1 -1
- package/tests/upgrade_persist_pin.mjs +24 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// tests/core_262_drop_box_blocked_db.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1004360 — core_262 drops the four builders.box_blocked* columns migration 104
|
|
4
|
+
// added, and re-runs cleanly.
|
|
5
|
+
//
|
|
6
|
+
// HOW THIS PROVES IT. The lane database is already migrated, so core_262 has run: the
|
|
7
|
+
// first case asserts `builders` carries no box_blocked column, which is the done-when's
|
|
8
|
+
// `\d builders` check. The second case, inside ONE transaction that is ALWAYS rolled
|
|
9
|
+
// back, puts the columns back the way migration 104 had them (with a blocked builder
|
|
10
|
+
// whose box_blocked_by points at another builder, so the foreign key is live), runs
|
|
11
|
+
// core_262's own statements (its BEGIN and COMMIT stripped so they join this
|
|
12
|
+
// transaction), and then runs them a second time. Nothing is committed.
|
|
13
|
+
//
|
|
14
|
+
// Real-DB (self-skips without Postgres; belongs in the INTEGRATION set).
|
|
15
|
+
//
|
|
16
|
+
// Run: DATABASE_URL=postgres://... node tests/core_262_drop_box_blocked_db.mjs
|
|
17
|
+
|
|
18
|
+
import { strict as assert } from 'node:assert';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
import { readFileSync } from 'node:fs';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { dirname, join } from 'node:path';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const { Pool } = require('pg');
|
|
26
|
+
|
|
27
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
|
+
const migrationText = readFileSync(join(root, 'migrations/core_262_drop_builder_box_blocked.sql'), 'utf8');
|
|
29
|
+
const addedBy104 = readFileSync(join(root, 'migrations/104_builder_box_blocked.sql'), 'utf8')
|
|
30
|
+
.match(/ALTER TABLE builders[\s\S]*?;/)[0];
|
|
31
|
+
|
|
32
|
+
const wrapper = /^\s*(BEGIN|COMMIT);\s*$/gm;
|
|
33
|
+
assert.equal((migrationText.match(wrapper) || []).length, 2, 'core_262 is expected to carry exactly one BEGIN and one COMMIT');
|
|
34
|
+
const migrationBody = migrationText.replace(wrapper, '');
|
|
35
|
+
|
|
36
|
+
const COLUMNS = ['box_blocked', 'box_blocked_reason', 'box_blocked_at', 'box_blocked_by'];
|
|
37
|
+
|
|
38
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
|
|
39
|
+
|
|
40
|
+
let passed = 0;
|
|
41
|
+
let failed = 0;
|
|
42
|
+
let client;
|
|
43
|
+
async function test(name, fn) {
|
|
44
|
+
await client.query('SAVEPOINT t');
|
|
45
|
+
try {
|
|
46
|
+
await fn();
|
|
47
|
+
passed++; console.log(` ok ${name}`);
|
|
48
|
+
await client.query('RELEASE SAVEPOINT t');
|
|
49
|
+
} catch (err) {
|
|
50
|
+
failed++; console.error(` FAIL ${name}\n ${err.message}`);
|
|
51
|
+
await client.query('ROLLBACK TO SAVEPOINT t');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Schema-driven insert (the helper of tests/session_earnings_mirror_db.mjs): fills any
|
|
56
|
+
// NOT NULL column with no default. Identifiers come from this file or information_schema.
|
|
57
|
+
const schemaCache = new Map();
|
|
58
|
+
async function insertRow(table, explicit = {}) {
|
|
59
|
+
if (!schemaCache.has(table)) {
|
|
60
|
+
const { rows } = await client.query(
|
|
61
|
+
`SELECT column_name, data_type, is_nullable, column_default, is_identity
|
|
62
|
+
FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1`, [table]);
|
|
63
|
+
schemaCache.set(table, rows);
|
|
64
|
+
}
|
|
65
|
+
const cols = schemaCache.get(table);
|
|
66
|
+
if (cols.length === 0) throw new Error(`table ${table} not found — is this database migrated?`);
|
|
67
|
+
const values = { ...explicit };
|
|
68
|
+
for (const c of cols) {
|
|
69
|
+
if (c.column_name in values || c.is_nullable === 'YES' || c.column_default !== null || c.is_identity === 'YES') continue;
|
|
70
|
+
switch (c.data_type) {
|
|
71
|
+
case 'integer': case 'bigint': case 'smallint': case 'numeric': case 'real': case 'double precision':
|
|
72
|
+
values[c.column_name] = 0; break;
|
|
73
|
+
case 'boolean': values[c.column_name] = false; break;
|
|
74
|
+
case 'timestamp with time zone': case 'timestamp without time zone': case 'date':
|
|
75
|
+
values[c.column_name] = new Date(); break;
|
|
76
|
+
case 'json': case 'jsonb': values[c.column_name] = {}; break;
|
|
77
|
+
case 'ARRAY': values[c.column_name] = []; break;
|
|
78
|
+
default: values[c.column_name] = `c261-${c.column_name}`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const names = Object.keys(values);
|
|
82
|
+
const { rows } = await client.query(
|
|
83
|
+
`INSERT INTO ${table} (${names.map((n) => `"${n}"`).join(', ')})
|
|
84
|
+
VALUES (${names.map((_, i) => `$${i + 1}`).join(', ')}) RETURNING *`,
|
|
85
|
+
names.map((n) => values[n]));
|
|
86
|
+
return rows[0];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function boxColumns() {
|
|
90
|
+
const { rows } = await client.query(
|
|
91
|
+
`SELECT column_name FROM information_schema.columns
|
|
92
|
+
WHERE table_schema = 'public' AND table_name = 'builders' AND column_name LIKE 'box\\_blocked%'
|
|
93
|
+
ORDER BY column_name`);
|
|
94
|
+
return rows.map((r) => r.column_name);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function main() {
|
|
98
|
+
try {
|
|
99
|
+
client = await pool.connect();
|
|
100
|
+
} catch {
|
|
101
|
+
console.log('core_262_drop_box_blocked_db: no Postgres (DATABASE_URL unset/unreachable) — skipping');
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await client.query('BEGIN');
|
|
106
|
+
try {
|
|
107
|
+
await test('the migrated database has no box_blocked column on builders', async () => {
|
|
108
|
+
assert.deepEqual(await boxColumns(), []);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await test('core_262 drops the columns migration 104 added, FK and all, and re-runs as a no-op', async () => {
|
|
112
|
+
await client.query(addedBy104);
|
|
113
|
+
assert.deepEqual(await boxColumns(), [...COLUMNS].sort(), 'migration 104 should have put all four back');
|
|
114
|
+
|
|
115
|
+
const tag = `c261-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
116
|
+
const builder = (n) => insertRow('builders', { github_id: `${tag}-${n}`, github_login: `${tag}-${n}`, display_name: tag, total_credits: 0 });
|
|
117
|
+
const archon = await builder('a');
|
|
118
|
+
const blocked = await builder('b');
|
|
119
|
+
await client.query(
|
|
120
|
+
`UPDATE builders SET box_blocked = true, box_blocked_reason = 'test', box_blocked_at = now(), box_blocked_by = $1
|
|
121
|
+
WHERE id = $2`, [archon.id, blocked.id]);
|
|
122
|
+
|
|
123
|
+
await client.query(migrationBody);
|
|
124
|
+
assert.deepEqual(await boxColumns(), [], 'every box_blocked column must be gone');
|
|
125
|
+
const { rows } = await client.query('SELECT id FROM builders WHERE id = ANY($1::bigint[])', [[archon.id, blocked.id]]);
|
|
126
|
+
assert.equal(rows.length, 2, 'dropping the columns must not touch the builder rows');
|
|
127
|
+
|
|
128
|
+
await client.query(migrationBody);
|
|
129
|
+
assert.deepEqual(await boxColumns(), [], 'a re-run must be a clean no-op');
|
|
130
|
+
});
|
|
131
|
+
} finally {
|
|
132
|
+
await client.query('ROLLBACK');
|
|
133
|
+
client.release();
|
|
134
|
+
await pool.end();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log(`\ncore_262_drop_box_blocked_db: ${passed} passed, ${failed} failed`);
|
|
138
|
+
// Let the socket close before exiting (a libuv abort otherwise on some boxes).
|
|
139
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
140
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
main().catch((err) => { console.error(err); process.exit(1); });
|
|
@@ -1184,6 +1184,7 @@ function pinDeps({ served = '1.19.762', token = OWNER_TOKEN, branch = 'main', pu
|
|
|
1184
1184
|
d.ran.push(cmd);
|
|
1185
1185
|
if (/upgrade\.js/.test(cmd) && upgradeThrows) throw new Error('health check failed after restart — rolled back');
|
|
1186
1186
|
if (/\/version/.test(cmd)) return { ok: true, stdout: JSON.stringify({ coreVersion: served }) };
|
|
1187
|
+
if (/^id -u /.test(cmd)) return { ok: true, stdout: '1001\n' }; // the project has its own account (ADR 0350)
|
|
1187
1188
|
if (/symbolic-ref/.test(cmd)) {
|
|
1188
1189
|
return branch ? { ok: true, stdout: `${branch}\n` } : { ok: true, softFailed: true, stdout: '', stderr: 'fatal: ref HEAD is not a symbolic ref' };
|
|
1189
1190
|
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// tests/dependency_writes_atomic.mjs
|
|
2
|
+
//
|
|
3
|
+
// DB-free guard for the dependency WRITE paths (task 1004191, the audit of the
|
|
4
|
+
// nine never-verified findings split out of task 1000379):
|
|
5
|
+
//
|
|
6
|
+
// findings 63 / 81 POST /tasks/:id/dependencies wrote edge-by-edge on the pool,
|
|
7
|
+
// so an error on the Nth id left ids 1..N-1 committed and the
|
|
8
|
+
// caller got an error that did not mention them.
|
|
9
|
+
// finding 75 the cycle check and the INSERT ran on different pool
|
|
10
|
+
// connections, so two concurrent writers (A->B and B->A) could
|
|
11
|
+
// each pass the check and together commit a cycle.
|
|
12
|
+
// finding 64 PUT /tasks/:id/dependencies never checked the task or the
|
|
13
|
+
// dependency ids existed (there is no FK — the target table
|
|
14
|
+
// varies by kind), so a typo wrote an orphan edge that blocks
|
|
15
|
+
// the task forever, and a non-numeric id surfaced as a 500.
|
|
16
|
+
//
|
|
17
|
+
// Every writer now runs in ONE transaction that first takes one advisory lock over
|
|
18
|
+
// the whole graph, then checks existence and cycles on the SAME client it inserts
|
|
19
|
+
// with. These tests pin that shape with a recording fake pool — the same posture as
|
|
20
|
+
// dependencies_polymorphic.mjs (no local DB; the SQL runtime is proven at deploy).
|
|
21
|
+
//
|
|
22
|
+
// Run: node tests/dependency_writes_atomic.mjs
|
|
23
|
+
|
|
24
|
+
import { strict as assert } from 'node:assert';
|
|
25
|
+
import { createRequire } from 'node:module';
|
|
26
|
+
import { readFileSync } from 'node:fs';
|
|
27
|
+
import { makeRunner } from './helpers.mjs';
|
|
28
|
+
|
|
29
|
+
process.env.NODE_ENV = 'test';
|
|
30
|
+
const require = createRequire(import.meta.url);
|
|
31
|
+
const db = require('../modules/lifecycle/db.js');
|
|
32
|
+
const { addTaskDependencies, addTaskDependency, setTaskDependencies, addDependency } = db;
|
|
33
|
+
|
|
34
|
+
const { test, summary } = makeRunner();
|
|
35
|
+
|
|
36
|
+
// A fake pool whose connect() hands out ONE recording client. `existing` is the set
|
|
37
|
+
// of task ids the fake tasks table holds; `cycleFor` returns a path (or null) for a
|
|
38
|
+
// (fromId, toId) cycle probe; `nodes` (when set) is the id set for generic nodes.
|
|
39
|
+
function fakePool({ existing = [], cycleFor = () => null, nodes = null } = {}) {
|
|
40
|
+
const log = [];
|
|
41
|
+
let connects = 0;
|
|
42
|
+
const norm = (s) => String(s).replace(/\s+/g, ' ').trim();
|
|
43
|
+
const client = {
|
|
44
|
+
query: async (sql, params = []) => {
|
|
45
|
+
const s = norm(sql);
|
|
46
|
+
log.push({ sql: s, params });
|
|
47
|
+
if (/^SELECT id FROM tasks WHERE id = ANY/.test(s)) {
|
|
48
|
+
const want = params[0].map(Number);
|
|
49
|
+
return { rows: want.filter((id) => existing.includes(id)).map((id) => ({ id: String(id) })) };
|
|
50
|
+
}
|
|
51
|
+
if (/^SELECT 1 FROM (tasks|goals|done_when_criteria) WHERE id = \$1/.test(s)) {
|
|
52
|
+
const ok = nodes ? nodes.includes(Number(params[0])) : true;
|
|
53
|
+
return { rows: ok ? [{ '?column?': 1 }] : [] };
|
|
54
|
+
}
|
|
55
|
+
if (/WITH RECURSIVE walk/.test(s)) {
|
|
56
|
+
// params: [toKind, toId, fromKind, fromId]
|
|
57
|
+
const path = cycleFor(Number(params[3]), Number(params[1]));
|
|
58
|
+
return { rows: path ? [{ path }] : [] };
|
|
59
|
+
}
|
|
60
|
+
return { rows: [], rowCount: 0 };
|
|
61
|
+
},
|
|
62
|
+
release: () => {},
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
log,
|
|
66
|
+
get connects() { return connects; },
|
|
67
|
+
connect: async () => { connects += 1; return client; },
|
|
68
|
+
// Anything that reaches for the pool directly (not the tx client) is the bug.
|
|
69
|
+
query: async (sql) => { throw new Error(`write path used the bare pool, not the tx client: ${norm(sql)}`); },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const idx = (log, re) => log.findIndex((q) => re.test(q.sql));
|
|
73
|
+
const has = (log, re) => log.some((q) => re.test(q.sql));
|
|
74
|
+
|
|
75
|
+
// ============================ POST (findings 63/81 + 75) ============================
|
|
76
|
+
|
|
77
|
+
await test('addTaskDependencies: one transaction, lock BEFORE the reads, inserts on the same client', async () => {
|
|
78
|
+
const p = fakePool({ existing: [1, 2, 3] });
|
|
79
|
+
const out = await addTaskDependencies(1, [2, 3], { pool: p });
|
|
80
|
+
assert.deepEqual(out, [2, 3]);
|
|
81
|
+
assert.equal(p.connects, 1, 'exactly one connection for the whole batch');
|
|
82
|
+
const L = p.log;
|
|
83
|
+
assert.equal(L[0].sql, 'BEGIN');
|
|
84
|
+
const lock = idx(L, /pg_advisory_xact_lock/);
|
|
85
|
+
const exist = idx(L, /SELECT id FROM tasks WHERE id = ANY/);
|
|
86
|
+
const cycle = idx(L, /WITH RECURSIVE walk/);
|
|
87
|
+
const insert = idx(L, /INSERT INTO dependencies/);
|
|
88
|
+
assert.ok(lock > 0 && lock < exist && exist < cycle && cycle < insert,
|
|
89
|
+
`order must be BEGIN, lock, existence, cycle, insert — got ${L.map((q) => q.sql.slice(0, 20)).join(' | ')}`);
|
|
90
|
+
assert.equal(L.filter((q) => /INSERT INTO dependencies/.test(q.sql)).length, 2);
|
|
91
|
+
assert.equal(L[L.length - 1].sql, 'COMMIT');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await test('addTaskDependencies: a cycle on the 2nd id ROLLS BACK the 1st (no partial write)', async () => {
|
|
95
|
+
const p = fakePool({ existing: [1, 2, 3], cycleFor: (from, to) => (to === 3 ? ['task:3', 'task:1'] : null) });
|
|
96
|
+
await assert.rejects(addTaskDependencies(1, [2, 3], { pool: p }), (e) => e.code === 'CYCLE');
|
|
97
|
+
assert.ok(has(p.log, /INSERT INTO dependencies/), 'the first edge was written inside the tx…');
|
|
98
|
+
assert.ok(has(p.log, /^ROLLBACK$/), '…and the tx rolled it back');
|
|
99
|
+
assert.ok(!has(p.log, /^COMMIT$/), 'nothing committed');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await test('addTaskDependencies: a missing dependency id is TASK_NOT_FOUND naming it, before any INSERT', async () => {
|
|
103
|
+
const p = fakePool({ existing: [1, 2] });
|
|
104
|
+
await assert.rejects(addTaskDependencies(1, [2, 99], { pool: p }),
|
|
105
|
+
(e) => e.code === 'TASK_NOT_FOUND' && JSON.stringify(e.missing) === '[99]');
|
|
106
|
+
assert.ok(!has(p.log, /INSERT INTO dependencies/));
|
|
107
|
+
assert.ok(has(p.log, /^ROLLBACK$/));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await test('addTaskDependencies: self-dep and bad ids refuse without opening a connection', async () => {
|
|
111
|
+
const p = fakePool({ existing: [1, 2] });
|
|
112
|
+
await assert.rejects(addTaskDependencies(1, [2, 1], { pool: p }), (e) => e.code === 'SELF_DEP');
|
|
113
|
+
await assert.rejects(addTaskDependencies(1, ['abc'], { pool: p }), (e) => e.code === 'BAD_ID');
|
|
114
|
+
await assert.rejects(addTaskDependencies(1, [0], { pool: p }), (e) => e.code === 'BAD_ID');
|
|
115
|
+
assert.equal(p.connects, 0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await test('addTaskDependency (singular, goals.js caller) keeps its return shape and goes through the tx', async () => {
|
|
119
|
+
const p = fakePool({ existing: [5, 6] });
|
|
120
|
+
const out = await addTaskDependency(5, 6, { pool: p });
|
|
121
|
+
assert.deepEqual(out, { task_id: 5, depends_on_task_id: 6 });
|
|
122
|
+
assert.ok(has(p.log, /pg_advisory_xact_lock/));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ============================ PUT (finding 64 + 75) ============================
|
|
126
|
+
|
|
127
|
+
await test('setTaskDependencies: a missing TASK is TASK_NOT_FOUND before the DELETE (was: 200 + orphan edges)', async () => {
|
|
128
|
+
const p = fakePool({ existing: [2] });
|
|
129
|
+
await assert.rejects(setTaskDependencies(424242, [2], { pool: p }),
|
|
130
|
+
(e) => e.code === 'TASK_NOT_FOUND' && e.missing.includes(424242));
|
|
131
|
+
assert.ok(!has(p.log, /^DELETE FROM dependencies/), 'never deletes for a task that does not exist');
|
|
132
|
+
assert.ok(!has(p.log, /INSERT INTO dependencies/));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await test('setTaskDependencies: a missing DEP id is TASK_NOT_FOUND, not an orphan edge', async () => {
|
|
136
|
+
const p = fakePool({ existing: [1] });
|
|
137
|
+
await assert.rejects(setTaskDependencies(1, [77], { pool: p }), (e) => e.code === 'TASK_NOT_FOUND');
|
|
138
|
+
assert.ok(!has(p.log, /INSERT INTO dependencies/));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await test('setTaskDependencies: a non-numeric id is BAD_ID (was: a pg cast error surfacing as 500)', async () => {
|
|
142
|
+
const p = fakePool({ existing: [1] });
|
|
143
|
+
await assert.rejects(setTaskDependencies(1, ['nope'], { pool: p }), (e) => e.code === 'BAD_ID');
|
|
144
|
+
assert.equal(p.connects, 0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await test('setTaskDependencies: lock precedes existence + DELETE; empty list still clears', async () => {
|
|
148
|
+
const p = fakePool({ existing: [1] });
|
|
149
|
+
const out = await setTaskDependencies(1, [], { pool: p });
|
|
150
|
+
assert.deepEqual(out, []);
|
|
151
|
+
const lock = idx(p.log, /pg_advisory_xact_lock/);
|
|
152
|
+
assert.ok(lock > 0 && lock < idx(p.log, /SELECT id FROM tasks/) && lock < idx(p.log, /^DELETE FROM dependencies/));
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ============================ generic edge (finding 75) ============================
|
|
156
|
+
|
|
157
|
+
await test('addDependency: existence, cycle check and insert share one locked transaction', async () => {
|
|
158
|
+
const p = fakePool();
|
|
159
|
+
await addDependency('task', 1, 'goal', 2, { pool: p });
|
|
160
|
+
assert.equal(p.connects, 1);
|
|
161
|
+
const lock = idx(p.log, /pg_advisory_xact_lock/);
|
|
162
|
+
assert.ok(lock > 0 && lock < idx(p.log, /WITH RECURSIVE walk/) && lock < idx(p.log, /INSERT INTO dependencies/));
|
|
163
|
+
assert.equal(p.log[p.log.length - 1].sql, 'COMMIT');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await test('addDependency: a missing node rolls back with NODE_NOT_FOUND', async () => {
|
|
167
|
+
const p = fakePool({ nodes: [1] });
|
|
168
|
+
await assert.rejects(addDependency('task', 1, 'goal', 2, { pool: p }), (e) => e.code === 'NODE_NOT_FOUND');
|
|
169
|
+
assert.ok(!has(p.log, /INSERT INTO dependencies/));
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ============================ routes map the new codes ============================
|
|
173
|
+
|
|
174
|
+
await test('routes/tasks.js: POST uses the atomic batch writer and both writers map TASK_NOT_FOUND→404, BAD_ID→400', () => {
|
|
175
|
+
const src = readFileSync(new URL('../modules/lifecycle/routes/tasks.js', import.meta.url), 'utf8');
|
|
176
|
+
const post = src.slice(src.indexOf("router.post('/tasks/:id/dependencies'"), src.indexOf("router.delete('/tasks/:id/dependencies/:depId'"));
|
|
177
|
+
const put = src.slice(src.indexOf("router.put('/tasks/:id/dependencies'"), src.indexOf('task↔criterion links'));
|
|
178
|
+
assert.match(post, /db\.addTaskDependencies\(/, 'POST writes the whole list in one call');
|
|
179
|
+
assert.doesNotMatch(post, /for \(const depId of depIds\)/, 'no per-id write loop left in the route');
|
|
180
|
+
for (const [name, body] of [['POST', post], ['PUT', put]]) {
|
|
181
|
+
assert.match(body, /mapTaskDepError\(/, `${name} maps writer errors through the shared mapper`);
|
|
182
|
+
}
|
|
183
|
+
const at = src.indexOf('function mapTaskDepError');
|
|
184
|
+
assert.ok(at >= 0, 'mapTaskDepError is defined');
|
|
185
|
+
const mapper = src.slice(at, at + 900);
|
|
186
|
+
assert.match(mapper, /TASK_NOT_FOUND[\s\S]*404/);
|
|
187
|
+
assert.match(mapper, /BAD_ID[\s\S]*400/);
|
|
188
|
+
assert.match(mapper, /CYCLE[\s\S]*409/);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
summary();
|
|
@@ -616,7 +616,8 @@ test('when the fix is reconnecting GitHub, the page links straight to it and com
|
|
|
616
616
|
const html = b.panel();
|
|
617
617
|
assert.match(html, /href="\/api\/bongos\/auth\/web\/start\?repos=1&return=%2Fdeploy%3Fproject%3Dmercury"/,
|
|
618
618
|
`${code}: the repo-access sign-in, returning to this project's deploy page — got ${html}`);
|
|
619
|
-
assert.match(html, /
|
|
619
|
+
assert.match(html, /lasting/i, `${code}: and it says reconnecting sets up LASTING access (task 1004291)`);
|
|
620
|
+
assert.doesNotMatch(html, /within the hour|about an hour/i, `${code}: no more one-hour promise`);
|
|
620
621
|
}
|
|
621
622
|
});
|
|
622
623
|
|
|
@@ -130,6 +130,21 @@ const SPELLING_EXEMPTIONS = [
|
|
|
130
130
|
obligation: (code) => /require\(\s*'\.\/account-visibility'\s*\)/.test(code),
|
|
131
131
|
demands: 'still imports the shared predicate rather than re-deriving one',
|
|
132
132
|
},
|
|
133
|
+
{
|
|
134
|
+
// task 1004259: the kit's DB-less stub answers the hub account page's own-scoped
|
|
135
|
+
// GET/PATCH /me/cross-project from ONE fixture account, so a probe that saves
|
|
136
|
+
// "private" re-reads what the real route would answer. It mirrors the route's
|
|
137
|
+
// DERIVED answer (recruiter_discoverable_effective) and setAccountPrivacy's
|
|
138
|
+
// consent reset for that fixture — it gates no read of any real account and
|
|
139
|
+
// serves no public surface, so it is a test double, not a variant.
|
|
140
|
+
file: 'modules/ui-design/kit/serve.js',
|
|
141
|
+
allow: [
|
|
142
|
+
{ text: "account_visibility !== '", count: 2, why: "effectiveReach (the fixture's recruiter_discoverable_effective, the stand-in for isRecruiterDiscoverable) and the 'was it public before this PATCH' half of the consent reset — both over the one fixture account a render or probe edits, never a real row." },
|
|
143
|
+
{ text: "account_visibility === '", count: 1, why: "the other half of that consent reset: going private without naming reach clears it, as setAccountPrivacy does — a write-side mirror over the fixture, gating no read." },
|
|
144
|
+
],
|
|
145
|
+
obligation: (code) => !/\.query\s*\(/.test(code) && !/require\(\s*'pg'\s*\)/.test(code),
|
|
146
|
+
demands: 'issues no database query and loads no database driver — it answers from fixtures only',
|
|
147
|
+
},
|
|
133
148
|
{
|
|
134
149
|
file: 'scripts/gds/seed-bv1-privacy-invites-tasks.js',
|
|
135
150
|
allow: [
|
package/tests/est_advisory.mjs
CHANGED
|
@@ -167,6 +167,33 @@ await t('buildEstAdvisory returns the advisory on the happy path', async () => {
|
|
|
167
167
|
assert.match(a.next_step, /PATCH \/tasks\/77/);
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
+
// ---- 6. the population is cached, not re-aggregated on every create (task 1003656) ----
|
|
171
|
+
await t('calibrationSummary caches the aggregate for a TTL, coalesces, and never caches a failure', async () => {
|
|
172
|
+
const { calibrationSummary, resetCalibrationCache, CALIBRATION_TTL_MS } = require('../modules/lifecycle/est-advisory.js');
|
|
173
|
+
assert.ok(CALIBRATION_TTL_MS > 0);
|
|
174
|
+
resetCalibrationCache();
|
|
175
|
+
let queries = 0;
|
|
176
|
+
let fail = false;
|
|
177
|
+
const pool = { query: async () => { queries++; if (fail) throw new Error('pg down'); return { rows: SUMMARY }; } };
|
|
178
|
+
let clock = 1000;
|
|
179
|
+
const now = () => clock;
|
|
180
|
+
// Concurrent creates share ONE query.
|
|
181
|
+
await Promise.all([calibrationSummary({ pool, now }), calibrationSummary({ pool, now }), calibrationSummary({ pool, now })]);
|
|
182
|
+
assert.equal(queries, 1, 'concurrent callers coalesce onto one aggregate');
|
|
183
|
+
assert.deepEqual(await calibrationSummary({ pool, now }), SUMMARY);
|
|
184
|
+
assert.equal(queries, 1, 'within the TTL the cached rows are served');
|
|
185
|
+
clock += CALIBRATION_TTL_MS + 1;
|
|
186
|
+
await calibrationSummary({ pool, now });
|
|
187
|
+
assert.equal(queries, 2, 'after the TTL it recomputes');
|
|
188
|
+
// A failure propagates and is not cached: the next call retries.
|
|
189
|
+
clock += CALIBRATION_TTL_MS + 1; fail = true;
|
|
190
|
+
await assert.rejects(calibrationSummary({ pool, now }), /pg down/);
|
|
191
|
+
fail = false;
|
|
192
|
+
assert.deepEqual(await calibrationSummary({ pool, now }), SUMMARY);
|
|
193
|
+
assert.equal(queries, 4, 'a failed read left nothing cached');
|
|
194
|
+
resetCalibrationCache();
|
|
195
|
+
});
|
|
196
|
+
|
|
170
197
|
await t('the route attaches est_advisory to the 201 body, non-blocking', () => {
|
|
171
198
|
// Mirrors the goal_advisory contract test. The call is folded into the response
|
|
172
199
|
// literal because modules/lifecycle/routes/tasks.js is over the size ratchet's
|
package/tests/goal_map_page.mjs
CHANGED
|
@@ -446,5 +446,31 @@ await test('the ledger drawer accessible name matches its visible heading (docs/
|
|
|
446
446
|
assert.equal(label, 'The ledger');
|
|
447
447
|
});
|
|
448
448
|
|
|
449
|
+
// task 1004342: "the sky got skewed left". The stage clipped its overflow with
|
|
450
|
+
// `overflow: hidden`, and a hidden-overflow box is still a SCROLL container: the
|
|
451
|
+
// footer's one-line hint (#alt, white-space: nowrap) ran past the stage's right
|
|
452
|
+
// edge, and when focus moved (skipping the tour did it) the browser scrolled the
|
|
453
|
+
// stage sideways to show it, sliding the whole sky left, 233px at 1024 wide. A
|
|
454
|
+
// zoomed-in window is narrower, so more of the hint overflowed and it got worse.
|
|
455
|
+
// `overflow: clip` clips the same pixels and is NOT scrollable, so nothing can
|
|
456
|
+
// slide the sky again whatever overflows; and the hint now shrinks instead.
|
|
457
|
+
await test('the sky cannot be scrolled sideways: its box clips, it does not hide (task 1004342)', () => {
|
|
458
|
+
const css = read('thinking.css');
|
|
459
|
+
const rule = /body\[data-page="goal-map"\], body\[data-page="thinking"\] \.sky-stage \{ margin: 0;[^}]*\}/.exec(css);
|
|
460
|
+
assert.ok(rule, 'the shared stage rule is still there');
|
|
461
|
+
// hidden first as the fallback for an engine without clip, then clip, which wins where supported
|
|
462
|
+
assert.match(rule[0], /overflow: hidden; overflow: clip;/, 'the stage clips (not scrollable), with hidden only as a fallback');
|
|
463
|
+
assert.match(css, /body\[data-page="thinking"\] \{ overflow: hidden; overflow: clip; \}/, 'and so does the page body');
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
await test('the footer hint shrinks with an ellipsis instead of running past the stage (task 1004342)', () => {
|
|
467
|
+
const css = read('thinking.css');
|
|
468
|
+
const alt = /\n\.alt \{[^}]*\}/.exec(css);
|
|
469
|
+
assert.ok(alt, 'the .alt rule');
|
|
470
|
+
assert.match(alt[0], /min-width: 0/, 'a flex child only shrinks below its content with min-width: 0');
|
|
471
|
+
assert.match(alt[0], /overflow: hidden/);
|
|
472
|
+
assert.match(alt[0], /text-overflow: ellipsis/);
|
|
473
|
+
});
|
|
474
|
+
|
|
449
475
|
console.log('');
|
|
450
476
|
summary();
|