@bongos/core 1.19.1080 → 1.20.1
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 +573 -188
- 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 +34 -0
- package/clients/bongos-client/index.cjs +34 -0
- package/clients/bongos-client/index.d.ts +53 -4
- package/clients/bongos-client/index.mjs +34 -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/0343-a-module-score-is-a-security-gate-then-an-average-of-visible-parts.md +2 -0
- package/docs/adr/0347-every-store-module-ships-a-how-to.md +135 -0
- 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 +4 -0
- package/docs/api/openapi.json +995 -40
- package/docs/api-reference.md +33 -8
- 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 +1674 -1566
- 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/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/migrations/provisioning_028_render_standup.sql +46 -0
- package/modules/provisioning/module.json +4 -2
- package/modules/provisioning/pollers/app-liveness.js +110 -0
- package/modules/provisioning/provisioning.js +5 -5
- package/modules/provisioning/render-lookup.js +68 -0
- package/modules/provisioning/render-standup.js +134 -0
- package/modules/provisioning/routes/render-standup.js +165 -0
- 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 +173 -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 +189 -0
- package/scripts/gds/provision-teardown.js +9 -2
- package/scripts/gds/provision-units.js +31 -0
- package/scripts/gds/provision.js +12 -12
- package/scripts/gds/publish-manifest.js +1 -0
- package/scripts/gds/render-api.js +197 -0
- package/scripts/gds/render-payload.js +84 -0
- 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 +362 -0
- package/tests/provision_settings_apply.mjs +62 -0
- package/tests/provisioning_app_status.mjs +204 -0
- package/tests/provisioning_render_route.mjs +336 -0
- package/tests/provisioning_settings_apply.mjs +1 -1
- package/tests/provisioning_teardown_intent.mjs +2 -2
- package/tests/render_api.mjs +153 -0
- package/tests/render_check.mjs +2 -1
- package/tests/render_deploy.mjs +495 -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
package/tests/autonomy_gauge.mjs
CHANGED
|
@@ -398,3 +398,65 @@ test('a derived seven-day window is NOT paced against a boundary we invented', (
|
|
|
398
398
|
assert.equal(d.derived, true);
|
|
399
399
|
assert.equal(d.go, true);
|
|
400
400
|
});
|
|
401
|
+
|
|
402
|
+
// --- a derived mark must be in the CURRENT window (task 1004376) ------------
|
|
403
|
+
//
|
|
404
|
+
// Regression from the 1004178 fix, found on the host the moment the runner was
|
|
405
|
+
// restarted onto it. derivedSince reported the RETIRED reading's resets_at,
|
|
406
|
+
// which on an unattended host is days old — so the caller's cap counted work
|
|
407
|
+
// done days earlier, arrived pre-exhausted, and the runner held forever having
|
|
408
|
+
// correctly derived headroom. Exactly the state 1004178 existed to end.
|
|
409
|
+
|
|
410
|
+
test('a week-old retired reading yields a mark inside the CURRENT window', () => {
|
|
411
|
+
const now = 10_000_000;
|
|
412
|
+
const weekAgoReset = now - 7 * 24 * HOUR; // the retired boundary
|
|
413
|
+
const d = decideCapacity({
|
|
414
|
+
gauge: {
|
|
415
|
+
written_at: weekAgoReset - HOUR,
|
|
416
|
+
five_hour: { used_percentage: 90, resets_at: weekAgoReset, observed_at: weekAgoReset - HOUR },
|
|
417
|
+
},
|
|
418
|
+
now,
|
|
419
|
+
});
|
|
420
|
+
assert.equal(d.derived, true);
|
|
421
|
+
assert.notEqual(d.derivedSince, weekAgoReset, 'the retired boundary is exactly what must NOT come back');
|
|
422
|
+
assert.ok(now - d.derivedSince < 5 * HOUR, 'the mark must sit inside the current five-hour window');
|
|
423
|
+
assert.ok(d.derivedSince <= now, 'and never in the future');
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test('the advanced mark lands on a whole number of periods from the observed one', () => {
|
|
427
|
+
const now = 10_000_000;
|
|
428
|
+
const reset = now - (3 * 5 * HOUR) - HOUR; // three full windows, plus an hour
|
|
429
|
+
const d = decideCapacity({
|
|
430
|
+
gauge: { written_at: reset - 60, five_hour: { used_percentage: 80, resets_at: reset, observed_at: reset - 60 } },
|
|
431
|
+
now,
|
|
432
|
+
});
|
|
433
|
+
assert.equal(d.derivedSince, reset + 3 * 5 * HOUR, 'stepped forward by whole periods, not guessed');
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test('derivedSince takes the MOST RECENT boundary when both windows derive', () => {
|
|
437
|
+
const now = 10_000_000;
|
|
438
|
+
const d = decideCapacity({
|
|
439
|
+
gauge: {
|
|
440
|
+
written_at: now - 8 * 24 * HOUR,
|
|
441
|
+
five_hour: { used_percentage: 50, resets_at: now - 6 * HOUR, observed_at: now - 8 * 24 * HOUR },
|
|
442
|
+
seven_day: { used_percentage: 50, resets_at: now - 8 * 24 * HOUR, observed_at: now - 8 * 24 * HOUR },
|
|
443
|
+
},
|
|
444
|
+
now,
|
|
445
|
+
});
|
|
446
|
+
assert.equal(d.derived, true);
|
|
447
|
+
assert.ok(now - d.derivedSince < 5 * HOUR,
|
|
448
|
+
'the seven-day boundary would make the cap a one-time budget that never refills');
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test('a reading whose window is still running is unaffected by the advance', () => {
|
|
452
|
+
const now = 10_000_000;
|
|
453
|
+
const d = decideCapacity({
|
|
454
|
+
gauge: {
|
|
455
|
+
written_at: now - 10 * HOUR,
|
|
456
|
+
five_hour: { used_percentage: 92, resets_at: now + HOUR, observed_at: now - 10 * HOUR },
|
|
457
|
+
},
|
|
458
|
+
now,
|
|
459
|
+
});
|
|
460
|
+
assert.equal(d.go, false, 'still unknown — usage only grows inside a live window');
|
|
461
|
+
assert.equal(d.unknown, true);
|
|
462
|
+
});
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// tests/blocker_hall_live_proof.mjs — task 1003457 (BONGOS-V2, goal 1000089).
|
|
2
|
+
//
|
|
3
|
+
// CLAUDE.md states that resolving a blocker auto-promotes any linked `blocked`
|
|
4
|
+
// task back to `ready`, via a DB TRIGGER (migration 009, tightened by core_223).
|
|
5
|
+
// Nothing had ever driven that trigger through the HALL'S OWN PATH: the real
|
|
6
|
+
// HTTP routes modules/hall-ui/public/blockers.js calls (POST /blockers,
|
|
7
|
+
// POST /blockers/:id/link, POST /blockers/:id/resolve). The existing coverage
|
|
8
|
+
// is real but narrower — blocker_resolve_guard.mjs and blocker_unlink.mjs pin
|
|
9
|
+
// the migrations' SQL TEXT statically (no live trigger ever fires), and
|
|
10
|
+
// blocker_link.mjs unit-tests the pure `blocksOnLink` predicate the route's
|
|
11
|
+
// SQL is built from (its own header: "The SQL itself is exercised by the
|
|
12
|
+
// post-deploy smoke run"). None of them prove the trigger actually fires when
|
|
13
|
+
// a write arrives through the real router, its own middleware stack and its
|
|
14
|
+
// own pooled connection — prove it end to end rather than reasoning about it.
|
|
15
|
+
//
|
|
16
|
+
// So this: real Postgres, the real buildGdsRouter() booted in-process (the
|
|
17
|
+
// government_live_proof.mjs pattern), mounted at the CANONICAL prefix
|
|
18
|
+
// (src/bongos/api-prefix.js API_PREFIX = '/api/bongos') so the path is
|
|
19
|
+
// byte-identical to what the hall UI calls — '/api/gds' is only a permanent
|
|
20
|
+
// alias for older clients, and using it here would leave open exactly the
|
|
21
|
+
// "same handlers, different path" gap this proof exists to close. Real HTTP
|
|
22
|
+
// calls with a real bearer session — file a blocker, link it to a real
|
|
23
|
+
// `ready` task (parking it `blocked`), resolve the blocker THROUGH THE ROUTE,
|
|
24
|
+
// then independently re-read the task's status straight from the database.
|
|
25
|
+
// If the trigger is ever dropped, disconnected from this write path, or fired
|
|
26
|
+
// on a different transaction than the one the route actually commits, the
|
|
27
|
+
// task stays `blocked` and the assertion below fails loudly — not a skip,
|
|
28
|
+
// not a stub.
|
|
29
|
+
//
|
|
30
|
+
// WHY THIS CANNOT BE THE "always rolled back" CLASS (idea_credit_streams_db.mjs,
|
|
31
|
+
// core_258_box_session_claims_db.mjs): those call a module function directly
|
|
32
|
+
// with an injected client, so the whole test IS one transaction. Here the
|
|
33
|
+
// three acts are three separate HTTP requests, and the route handler opens
|
|
34
|
+
// ITS OWN connection from the real pool — a connection that can only see
|
|
35
|
+
// COMMITTED rows on another connection. Proving the trigger fires on the
|
|
36
|
+
// hall's real, independent connection therefore requires the fixture rows to
|
|
37
|
+
// really be committed; there is no transaction this file could wrap around
|
|
38
|
+
// three separate request/response round trips that would still let the
|
|
39
|
+
// server see them. The mitigation is scope and speed, not a rollback:
|
|
40
|
+
// high, unmistakably-synthetic fixture ids; the single most sensitive row
|
|
41
|
+
// (the live bearer session) is deleted FIRST in cleanup, before anything
|
|
42
|
+
// else, so the shortest possible cleanup still revokes it; and cleanup runs
|
|
43
|
+
// via a tight id/source_ref match, never a free-text title scan.
|
|
44
|
+
//
|
|
45
|
+
// Real-DB integration test (the needs_rebase.mjs class): SELF-SKIPS (exit 0)
|
|
46
|
+
// when DATABASE_URL is unset/unreachable or the schema it needs is missing,
|
|
47
|
+
// and is listed in run-unit-tests.js's INTEGRATION set so the DB-free lane
|
|
48
|
+
// never runs it. DATABASE_URL is what the sibling integration tests in this
|
|
49
|
+
// set (claim_gate_open_blocker.mjs, government_live_proof.mjs) already treat
|
|
50
|
+
// as "this is a throwaway CI/dev Postgres" — production never sets it
|
|
51
|
+
// (src/bongos/pool.js resolves an unset DATABASE_URL to the peer-auth local
|
|
52
|
+
// socket instead), so the same assumption applies here.
|
|
53
|
+
//
|
|
54
|
+
// Run: DATABASE_URL=... node tests/blocker_hall_live_proof.mjs
|
|
55
|
+
|
|
56
|
+
import { strict as assert } from 'node:assert';
|
|
57
|
+
import crypto from 'node:crypto';
|
|
58
|
+
import { createRequire } from 'node:module';
|
|
59
|
+
|
|
60
|
+
process.env.NODE_ENV = process.env.NODE_ENV || 'test';
|
|
61
|
+
const require = createRequire(import.meta.url);
|
|
62
|
+
|
|
63
|
+
if (!process.env.DATABASE_URL) {
|
|
64
|
+
console.log('blocker_hall_live_proof.mjs SKIPPED — DATABASE_URL not set.');
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const { Pool } = require('pg');
|
|
69
|
+
const { sessionTokenHash } = require('../src/bongos/db-kernel.js');
|
|
70
|
+
const { API_PREFIX } = require('../src/bongos/api-prefix.js');
|
|
71
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
72
|
+
|
|
73
|
+
// ── readiness guard (government_live_proof.mjs's shape, task 1003091's lesson:
|
|
74
|
+
// a schema this file cannot recognise must FAIL naming the tables, never skip
|
|
75
|
+
// silently as though there were simply no Postgres here) ────────────────────
|
|
76
|
+
const REQUIRED_TABLES = [
|
|
77
|
+
'builders', 'builder_sessions', 'versions', 'tasks',
|
|
78
|
+
'blockers', 'task_blockers',
|
|
79
|
+
'government_builder_ranks', 'government_rank_permissions',
|
|
80
|
+
];
|
|
81
|
+
try {
|
|
82
|
+
await pool.query('SELECT 1');
|
|
83
|
+
} catch (e) {
|
|
84
|
+
console.log(`blocker_hall_live_proof.mjs SKIPPED — Postgres unreachable (${e.code || e.message}).`);
|
|
85
|
+
await pool.end().catch(() => {});
|
|
86
|
+
process.exit(0);
|
|
87
|
+
}
|
|
88
|
+
{
|
|
89
|
+
const { rows } = await pool.query(
|
|
90
|
+
`SELECT table_name FROM information_schema.tables
|
|
91
|
+
WHERE table_schema = 'public' AND table_name = ANY($1::text[])`,
|
|
92
|
+
[REQUIRED_TABLES],
|
|
93
|
+
);
|
|
94
|
+
const present = new Set(rows.map((r) => r.table_name));
|
|
95
|
+
const missing = REQUIRED_TABLES.filter((t) => !present.has(t));
|
|
96
|
+
if (missing.length === REQUIRED_TABLES.length) {
|
|
97
|
+
console.log('blocker_hall_live_proof.mjs SKIPPED — reachable Postgres with no instance schema (run scripts/migrate.sh).');
|
|
98
|
+
await pool.end().catch(() => {});
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
if (missing.length) {
|
|
102
|
+
await pool.end().catch(() => {});
|
|
103
|
+
throw new Error(
|
|
104
|
+
`blocker_hall_live_proof.mjs CANNOT RUN: this database is migrated but is missing ${missing.join(', ')}. `
|
|
105
|
+
+ 'Either the schema was renamed under this proof or the target is not a Cloud Bongos instance database.'
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── fixtures (high-ID range; cleaned before AND after so reruns are safe) ────
|
|
111
|
+
const VER = 'ZZZ-BLK';
|
|
112
|
+
const BUILDER = 90000801;
|
|
113
|
+
const TASK = 90000802;
|
|
114
|
+
const RANK_KEY = 'metic'; // the seeded floor for blocker.review (catalog.js)
|
|
115
|
+
// A stable source/source_ref pair, never a free-text title, is what cleanup
|
|
116
|
+
// keys the blocker fixture on — a title match could reach any real blocker
|
|
117
|
+
// anyone else ever happened to file with the same words.
|
|
118
|
+
const BLOCKER_SOURCE = 'blocker_hall_live_proof';
|
|
119
|
+
const BLOCKER_SOURCE_REF = 'fixture';
|
|
120
|
+
const TOKEN = 'bhlp' + crypto.randomBytes(24).toString('hex');
|
|
121
|
+
|
|
122
|
+
const q = (sql, params) => pool.query(sql, params).then((r) => r.rows);
|
|
123
|
+
|
|
124
|
+
// Ordered so the single most sensitive row — the LIVE bearer session — goes
|
|
125
|
+
// first: even a cleanup that dies partway through has already revoked it.
|
|
126
|
+
// Every step is ATTEMPTED regardless of an earlier failure (collect, don't
|
|
127
|
+
// abort), and then a combined error is thrown if anything failed — so a
|
|
128
|
+
// cleanup failure surfaces loudly (naming what actually blocked it, matching
|
|
129
|
+
// government_live_proof.mjs's shape) without one bad DELETE stranding every
|
|
130
|
+
// row after it.
|
|
131
|
+
async function cleanup() {
|
|
132
|
+
const steps = [
|
|
133
|
+
['builder_sessions', () => q(`DELETE FROM builder_sessions WHERE builder_id = $1`, [BUILDER])],
|
|
134
|
+
['task_blockers', () => q(`DELETE FROM task_blockers WHERE task_id = $1`, [TASK])],
|
|
135
|
+
['blockers', () => q(`DELETE FROM blockers WHERE source = $1 AND source_ref = $2`, [BLOCKER_SOURCE, BLOCKER_SOURCE_REF])],
|
|
136
|
+
['tasks', () => q(`DELETE FROM tasks WHERE id = $1`, [TASK])],
|
|
137
|
+
['government_builder_ranks', () => q(`DELETE FROM government_builder_ranks WHERE builder_id = $1`, [BUILDER])],
|
|
138
|
+
['audit_log', () => q(`DELETE FROM audit_log WHERE builder_id = $1`, [BUILDER])],
|
|
139
|
+
['builders', () => q(`DELETE FROM builders WHERE id = $1`, [BUILDER])],
|
|
140
|
+
['versions', () => q(`DELETE FROM versions WHERE id = $1`, [VER])],
|
|
141
|
+
];
|
|
142
|
+
const failures = [];
|
|
143
|
+
for (const [name, run] of steps) {
|
|
144
|
+
try { await run(); } catch (e) { failures.push(`${name}: ${e.message}`); }
|
|
145
|
+
}
|
|
146
|
+
if (failures.length) throw new Error(`cleanup: ${failures.length} step(s) failed — ${failures.join('; ')}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function seed() {
|
|
150
|
+
// `shipped`, deliberately not `planning` — the task's own row only needs a
|
|
151
|
+
// version_id FK target, and versions_one_planning_idx (core_233) is a
|
|
152
|
+
// partial UNIQUE index allowing exactly one `planning` row in the whole
|
|
153
|
+
// table. `ON CONFLICT (id)` guards only the primary key, not that second
|
|
154
|
+
// index, so seeding `planning` here would 23505 on any database that
|
|
155
|
+
// already has one in progress (nearly every real dev/CI database does).
|
|
156
|
+
// `shipped` carries no such cardinality constraint.
|
|
157
|
+
await q(
|
|
158
|
+
`INSERT INTO versions (id,name,status,track) VALUES ($1,'blk','shipped','internal')
|
|
159
|
+
ON CONFLICT (id) DO NOTHING`,
|
|
160
|
+
[VER],
|
|
161
|
+
);
|
|
162
|
+
await q(
|
|
163
|
+
`INSERT INTO builders (id,github_id,github_login,display_name,rank,status)
|
|
164
|
+
VALUES ($1,'bhlp-builder','bhlp-builder','BHLP Fixture Builder','metic','active')`,
|
|
165
|
+
[BUILDER],
|
|
166
|
+
);
|
|
167
|
+
// R95b: the resolver reads government_builder_ranks, not builders.rank — a
|
|
168
|
+
// direct seed has to mirror the rank-role sync the same way
|
|
169
|
+
// government_live_proof.mjs does, or the fixture builder resolves ZERO
|
|
170
|
+
// permissions and 403s its own /resolve and /link calls.
|
|
171
|
+
await q(
|
|
172
|
+
`INSERT INTO government_builder_ranks (builder_id, rank_key) VALUES ($1,$2)`,
|
|
173
|
+
[BUILDER, RANK_KEY],
|
|
174
|
+
);
|
|
175
|
+
await q(
|
|
176
|
+
`INSERT INTO builder_sessions (builder_id, token_hash, source) VALUES ($1,$2,'web')`,
|
|
177
|
+
[BUILDER, sessionTokenHash(TOKEN)],
|
|
178
|
+
);
|
|
179
|
+
// A `ready` task — the one status blockers.linkBlockerToTask actually parks
|
|
180
|
+
// as `blocked` (LINK_BLOCKABLE_STATUSES), so the resolve trigger has
|
|
181
|
+
// something real to promote back. The discipline is named because the column
|
|
182
|
+
// default is still 'unclassified', which tasks_discipline_check has refused
|
|
183
|
+
// since core_247 (task 1002988); every real create path resolves one.
|
|
184
|
+
await q(
|
|
185
|
+
`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline)
|
|
186
|
+
VALUES ($1,$2,'bhlp fixture task','ready',10,'feature','engineer')`,
|
|
187
|
+
[TASK, VER],
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── the server: the real router, in-process, at the CANONICAL prefix ────────
|
|
192
|
+
const express = require('express');
|
|
193
|
+
const { buildGdsRouter } = require('../src/bongos/routes.js');
|
|
194
|
+
const app = express();
|
|
195
|
+
app.use(API_PREFIX, buildGdsRouter());
|
|
196
|
+
const server = await new Promise((resolve) => {
|
|
197
|
+
const s = app.listen(0, '127.0.0.1', () => resolve(s));
|
|
198
|
+
});
|
|
199
|
+
const base = `http://127.0.0.1:${server.address().port}`;
|
|
200
|
+
|
|
201
|
+
const call = async (method, path, body) => {
|
|
202
|
+
const res = await fetch(`${base}${API_PREFIX}${path}`, {
|
|
203
|
+
method,
|
|
204
|
+
headers: {
|
|
205
|
+
authorization: `Bearer ${TOKEN}`,
|
|
206
|
+
...(body ? { 'content-type': 'application/json' } : {}),
|
|
207
|
+
},
|
|
208
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
209
|
+
});
|
|
210
|
+
const json = await res.json().catch(() => ({}));
|
|
211
|
+
// Status + error code only — never the body, so an auth-failure response
|
|
212
|
+
// can never print session material into this test's own log.
|
|
213
|
+
const code = json?.error?.code || json?.error || '';
|
|
214
|
+
console.log(` ${method.padEnd(6)} ${path} → ${res.status}${code ? ` ${code}` : ''}`);
|
|
215
|
+
return { status: res.status, body: json };
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const taskStatus = async () => {
|
|
219
|
+
const rows = await q(`SELECT status FROM tasks WHERE id = $1`, [TASK]);
|
|
220
|
+
return rows[0]?.status;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
let pass = 0;
|
|
224
|
+
const ok = (cond, msg) => { assert.ok(cond, msg); console.log(` ok ${msg}`); pass++; };
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
await cleanup();
|
|
228
|
+
await seed();
|
|
229
|
+
console.log(`blocker_hall_live_proof against ${base}${API_PREFIX} (task fixture #${TASK})`);
|
|
230
|
+
|
|
231
|
+
ok((await taskStatus()) === 'ready', 'BEFORE: the fixture task starts ready');
|
|
232
|
+
|
|
233
|
+
// Act 1 — file the blocker, through the route (any builder may file).
|
|
234
|
+
let r = await call('POST', '/blockers', {
|
|
235
|
+
title: 'blocker_hall_live_proof fixture blocker',
|
|
236
|
+
source: BLOCKER_SOURCE,
|
|
237
|
+
source_ref: BLOCKER_SOURCE_REF,
|
|
238
|
+
});
|
|
239
|
+
ok(r.status === 201 && r.body?.blocker?.id, 'POST /blockers files the blocker (201)');
|
|
240
|
+
const blockerId = r.body.blocker.id;
|
|
241
|
+
|
|
242
|
+
// Act 2 — link it to the task, through the route. This is what parks the
|
|
243
|
+
// task `blocked` — the state the resolve-side trigger has to release it from.
|
|
244
|
+
r = await call('POST', `/blockers/${blockerId}/link`, { task_id: TASK });
|
|
245
|
+
ok(r.status === 200 && r.body?.blocked === true, 'POST /blockers/:id/link blocks the task (200, blocked:true)');
|
|
246
|
+
ok((await taskStatus()) === 'blocked', 'the task is independently confirmed BLOCKED after the link');
|
|
247
|
+
|
|
248
|
+
// Act 3 — resolve it, through the route. THE PROOF: no code here ever issues
|
|
249
|
+
// `UPDATE tasks SET status = 'ready'` — only the DB trigger can move it.
|
|
250
|
+
r = await call('POST', `/blockers/${blockerId}/resolve`, { resolution_note: 'live proof' });
|
|
251
|
+
ok(r.status === 200, 'POST /blockers/:id/resolve succeeds (200)');
|
|
252
|
+
ok(Array.isArray(r.body?.promoted_tasks) && r.body.promoted_tasks.some((t) => Number(t.id) === TASK),
|
|
253
|
+
'the resolve response itself reports the task as promoted');
|
|
254
|
+
|
|
255
|
+
// Act 4 — THE independent proof: re-read straight from Postgres, off the
|
|
256
|
+
// route entirely. If the trigger never fired on this write path — wrong
|
|
257
|
+
// transaction, wrong connection, dropped migration — this is where it shows.
|
|
258
|
+
ok((await taskStatus()) === 'ready',
|
|
259
|
+
'AFTER: the linked task is independently confirmed READY — the DB trigger fired on the hall path');
|
|
260
|
+
|
|
261
|
+
console.log(`\nblocker_hall_live_proof (task 1003457): ${pass} assertions passed`);
|
|
262
|
+
} catch (e) {
|
|
263
|
+
console.error('blocker_hall_live_proof.mjs FAILED:', e.stack || e.message);
|
|
264
|
+
process.exitCode = 1;
|
|
265
|
+
} finally {
|
|
266
|
+
// A failed final cleanup can strand the fixture builder/session for the
|
|
267
|
+
// NEXT run — that is a real problem for this test, not a footnote, so it
|
|
268
|
+
// fails the run rather than only logging.
|
|
269
|
+
await cleanup().catch((e) => {
|
|
270
|
+
console.error('cleanup failed:', e.message);
|
|
271
|
+
process.exitCode = 1;
|
|
272
|
+
});
|
|
273
|
+
await new Promise((resolve) => server.close(resolve));
|
|
274
|
+
await pool.end().catch(() => {});
|
|
275
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// tests/claim_gate_ci_unblock.mjs — the owed-rebase gate's one exemption (task 1004370).
|
|
2
|
+
//
|
|
3
|
+
// When main's CI goes red for a reason outside every branch, no confirmed task can
|
|
4
|
+
// land, so ADR 0120 part 3's gate refuses the builder every new claim — including
|
|
5
|
+
// the fix that would let their confirmed task land. A P1 kind=infra task (the shape
|
|
6
|
+
// a CI-unblock is filed in) passes the gate; nothing else does.
|
|
7
|
+
//
|
|
8
|
+
// Two altitudes: the pure rule is RUN here, and the wiring into claimTask is driven
|
|
9
|
+
// through the real function with a fake pool whose dispatch is exact per statement,
|
|
10
|
+
// so a gate that stopped consulting the rule (or consulted it for the wrong task)
|
|
11
|
+
// reds here without a Postgres. The real-DB proof is in tests/claim_gate_rebase.mjs.
|
|
12
|
+
import { strict as assert } from 'node:assert';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
|
|
17
|
+
let passed = 0;
|
|
18
|
+
let failed = 0;
|
|
19
|
+
async function t(name, fn) {
|
|
20
|
+
try { await fn(); passed++; console.log(` PASS ${name}`); }
|
|
21
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.stack || e.message}`); }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { isRebaseGateExempt } = require('../modules/lifecycle/db-claims.js');
|
|
25
|
+
|
|
26
|
+
await t('a P1 infra task is exempt', () => {
|
|
27
|
+
assert.equal(isRebaseGateExempt({ kind: 'infra', priority: 1 }), true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await t('the DB hands priority back as a string sometimes, and that still counts', () => {
|
|
31
|
+
assert.equal(isRebaseGateExempt({ kind: 'infra', priority: '1' }), true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await t('both halves are required: infra at any other priority stays gated', () => {
|
|
35
|
+
for (const priority of [2, 3, 5, null, undefined]) {
|
|
36
|
+
assert.equal(isRebaseGateExempt({ kind: 'infra', priority }), false, `infra P${priority}`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await t('both halves are required: a P1 of any other kind stays gated', () => {
|
|
41
|
+
for (const kind of ['bug', 'feature', 'refactor', 'decision', 'spike', null]) {
|
|
42
|
+
assert.equal(isRebaseGateExempt({ kind, priority: 1 }), false, `P1 ${kind}`);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await t('a missing task row is never exempt', () => {
|
|
47
|
+
assert.equal(isRebaseGateExempt(undefined), false);
|
|
48
|
+
assert.equal(isRebaseGateExempt(null), false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ---- the wiring: claimTask actually consults the rule ------------------------
|
|
52
|
+
// The gate throws before claimTask reaches anything else, so the fake only has to
|
|
53
|
+
// answer the queries up to that point. An unexpected statement throws, which is
|
|
54
|
+
// what makes a green here mean the path really ran.
|
|
55
|
+
const dbPath = require.resolve('../src/module-api.js');
|
|
56
|
+
function fakeClientFor(task) {
|
|
57
|
+
const seen = [];
|
|
58
|
+
const client = {
|
|
59
|
+
seen,
|
|
60
|
+
async query(sql, params) {
|
|
61
|
+
const s = String(sql).replace(/\s+/g, ' ').trim();
|
|
62
|
+
seen.push(s);
|
|
63
|
+
if (s === 'BEGIN' || s === 'ROLLBACK' || s === 'COMMIT') return { rows: [] };
|
|
64
|
+
if (s.startsWith('SELECT status, rank FROM builders')) return { rows: [{ status: 'active', rank: 'archon' }] };
|
|
65
|
+
if (s.startsWith('SELECT id, title, needs_rebase_reason, needs_rebase_at FROM tasks WHERE needs_rebase = true')) {
|
|
66
|
+
return { rows: [{ id: '900', title: 'stalled', needs_rebase_reason: 'strand:pr_not_landed', needs_rebase_at: new Date() }] };
|
|
67
|
+
}
|
|
68
|
+
if (s === 'SELECT kind, priority FROM tasks WHERE id = $1') {
|
|
69
|
+
assert.equal(String(params[0]), '901', 'the rule is asked about the task being CLAIMED, not the stalled one');
|
|
70
|
+
return { rows: task ? [task] : [] };
|
|
71
|
+
}
|
|
72
|
+
// Past the gate: an exempt claim reaches the task read. Stop there, loudly
|
|
73
|
+
// and recognisably, so the test can tell "passed the gate" from any error.
|
|
74
|
+
if (s.startsWith('SELECT id, status, touches')) throw Object.assign(new Error('past the gate'), { code: 'PAST_GATE' });
|
|
75
|
+
throw new Error(`unexpected statement: ${s.slice(0, 120)}`);
|
|
76
|
+
},
|
|
77
|
+
release() {},
|
|
78
|
+
};
|
|
79
|
+
return client;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function claimWith(task) {
|
|
83
|
+
const client = fakeClientFor(task);
|
|
84
|
+
const poolMod = require(dbPath);
|
|
85
|
+
const saved = poolMod.pool.connect;
|
|
86
|
+
poolMod.pool.connect = async () => client;
|
|
87
|
+
try {
|
|
88
|
+
const { claimTask } = require('../modules/lifecycle/db-claims.js');
|
|
89
|
+
await claimTask({ taskId: '901', builderId: '77', worktreeName: 'wt' });
|
|
90
|
+
return { client, err: null };
|
|
91
|
+
} catch (err) {
|
|
92
|
+
return { client, err };
|
|
93
|
+
} finally {
|
|
94
|
+
poolMod.pool.connect = saved;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await t('a gated builder claiming an ordinary task is refused REBASE_REQUIRED', async () => {
|
|
99
|
+
const { err } = await claimWith({ kind: 'feature', priority: 1 });
|
|
100
|
+
assert.equal(err && err.code, 'REBASE_REQUIRED');
|
|
101
|
+
assert.ok(err.rebaseTasks.some((r) => String(r.id) === '900'), 'and the refusal still names what is owed');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await t('a gated builder claiming a P1 infra task goes PAST the gate', async () => {
|
|
105
|
+
const { err, client } = await claimWith({ kind: 'infra', priority: 1 });
|
|
106
|
+
assert.equal(err && err.code, 'PAST_GATE', `expected to reach the task read, got ${err && (err.code || err.message)}`);
|
|
107
|
+
assert.ok(client.seen.includes('SELECT kind, priority FROM tasks WHERE id = $1'), 'the rule was consulted');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await t('a gated builder claiming a task that does not exist still meets the gate first', async () => {
|
|
111
|
+
const { err } = await claimWith(null);
|
|
112
|
+
assert.equal(err && err.code, 'REBASE_REQUIRED');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
console.log(`\nclaim_gate_ci_unblock: ${passed} passed, ${failed} failed`);
|
|
116
|
+
process.exit(failed ? 1 : 0);
|
|
@@ -30,13 +30,15 @@ const db = require('../modules/lifecycle/db.js');
|
|
|
30
30
|
const VER = 'ZZZ-CG', WORKER = 90000201, STALLED = 90000201, FRESH = 90000202;
|
|
31
31
|
// task 1003294 — the unprovable-strand cases below use their own rows.
|
|
32
32
|
const AGED = 90000203, FRESH2 = 90000204, AGED_OTHER = 90000205;
|
|
33
|
+
// task 1004370 — the CI-unblock exemption.
|
|
34
|
+
const CIFIX = 90000206;
|
|
33
35
|
const q = (sql, params) => pool.query(sql, params).then((r) => r.rows);
|
|
34
36
|
|
|
35
37
|
async function cleanup() {
|
|
36
38
|
await q(`DELETE FROM builder_achievements WHERE builder_id=$1`, [WORKER]).catch(() => {});
|
|
37
|
-
await q(`DELETE FROM credit_log WHERE builder_id=$1 OR task_id = ANY($2::bigint[])`, [WORKER, [STALLED, FRESH, AGED, FRESH2, AGED_OTHER]]).catch(() => {});
|
|
38
|
-
await q(`DELETE FROM claims WHERE task_id = ANY($1::bigint[])`, [[STALLED, FRESH, AGED, FRESH2, AGED_OTHER]]).catch(() => {});
|
|
39
|
-
await q(`DELETE FROM tasks WHERE id = ANY($1::bigint[])`, [[STALLED, FRESH, AGED, FRESH2, AGED_OTHER]]).catch(() => {});
|
|
39
|
+
await q(`DELETE FROM credit_log WHERE builder_id=$1 OR task_id = ANY($2::bigint[])`, [WORKER, [STALLED, FRESH, AGED, FRESH2, AGED_OTHER, CIFIX]]).catch(() => {});
|
|
40
|
+
await q(`DELETE FROM claims WHERE task_id = ANY($1::bigint[])`, [[STALLED, FRESH, AGED, FRESH2, AGED_OTHER, CIFIX]]).catch(() => {});
|
|
41
|
+
await q(`DELETE FROM tasks WHERE id = ANY($1::bigint[])`, [[STALLED, FRESH, AGED, FRESH2, AGED_OTHER, CIFIX]]).catch(() => {});
|
|
40
42
|
await q(`DELETE FROM builders WHERE id=$1`, [WORKER]).catch(() => {});
|
|
41
43
|
await q(`DELETE FROM versions WHERE id=$1`, [VER]).catch(() => {});
|
|
42
44
|
}
|
|
@@ -139,6 +141,16 @@ try {
|
|
|
139
141
|
ok(threw2 && threw2.code === 'REBASE_REQUIRED',
|
|
140
142
|
'an aged pr_not_landed strand still gates — the builder CAN fix that one');
|
|
141
143
|
|
|
144
|
+
// 7) task 1004370 — the one task the gate lets through: a P1 kind=infra fix,
|
|
145
|
+
// the shape a CI-unblock is filed in. With AGED_OTHER still gating, claiming
|
|
146
|
+
// it succeeds; FRESH2 (a P1 feature) was refused in 6), so both halves count.
|
|
147
|
+
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,priority) VALUES ($1,$2,'ci fix','ready',10,'infra',1)`, [CIFIX, VER]);
|
|
148
|
+
const ciClaim = await db.claimTask({ taskId: String(CIFIX), builderId: String(WORKER), worktreeName: 'cg-cifix' });
|
|
149
|
+
ok(ciClaim && Number(ciClaim.task_id) === CIFIX,
|
|
150
|
+
'a P1 infra task is claimable while a rebase is owed — the CI-unblock fix is never gated out');
|
|
151
|
+
ok((await db.needsRebaseTasksForBuilder(String(WORKER))).some((t) => Number(t.id) === AGED_OTHER),
|
|
152
|
+
'and the owed rebase is still reported: the exemption lets one claim through, it clears nothing');
|
|
153
|
+
|
|
142
154
|
console.log(`\nclaim_gate_rebase (ADR 0120 part 3): ${pass} passed, 0 failed`);
|
|
143
155
|
} catch (e) {
|
|
144
156
|
console.error('claim_gate_rebase.mjs FAILED:', e.stack || e.message);
|
package/tests/collab_page.mjs
CHANGED
|
@@ -590,6 +590,16 @@ test('an ask can be answered — and the answer surface stops well short of a ch
|
|
|
590
590
|
assert.ok(!/parent_reply|data-reply-to|@mention|mentions/i.test(js), 'no nesting and no mentions');
|
|
591
591
|
});
|
|
592
592
|
|
|
593
|
+
// Task 1004234: the reply box once hinted with a leftover reply from one
|
|
594
|
+
// thread, which every reader took for a real message about something they had
|
|
595
|
+
// never heard of. The hint says what the box is for, and nothing more.
|
|
596
|
+
test('the reply box hint is neutral, not a stray reply from some thread', () => {
|
|
597
|
+
const js = read('modules', 'hall-ui', 'public', 'collab.js');
|
|
598
|
+
const m = js.match(/class="cb-reply-input"[^>]*placeholder="([^"]*)"/);
|
|
599
|
+
assert.ok(m, 'the reply box carries a placeholder');
|
|
600
|
+
assert.equal(m[1], 'Write a reply…');
|
|
601
|
+
});
|
|
602
|
+
|
|
593
603
|
// RUN, not grepped — this file's own convention for everything in collab-lib.js,
|
|
594
604
|
// and the grader was right that the first pass broke it. canClose in particular
|
|
595
605
|
// decides whether a close control is drawn at all, so each branch of it needs a
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// tests/conductor_main_e2e.mjs — the Conductor hook, run END TO END as
|
|
2
|
+
// settings.json runs it (task 1004398).
|
|
3
|
+
//
|
|
4
|
+
// Every other Conductor test drives evaluateTriggers() with hand-built inputs.
|
|
5
|
+
// That is how task 1003855 shipped a main() that passed an undeclared
|
|
6
|
+
// `specialities` into it: the hook threw ReferenceError on every prompt,
|
|
7
|
+
// `main().catch(() => {})` swallowed it, and for thirteen days no session got
|
|
8
|
+
// its interaction profile, its focus contract, the no-claim warning or the
|
|
9
|
+
// Hacker dispatch — with every Conductor test green. This file runs the real
|
|
10
|
+
// hook as a child process against a stub GET /me, so a crash anywhere between
|
|
11
|
+
// the fetch and the render shows up as missing output.
|
|
12
|
+
//
|
|
13
|
+
// Run: node tests/conductor_main_e2e.mjs
|
|
14
|
+
|
|
15
|
+
import { strict as assert } from 'node:assert';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import { spawn } from 'node:child_process';
|
|
18
|
+
import http from 'node:http';
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import os from 'node:os';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { makeRunner } from './helpers.mjs';
|
|
24
|
+
|
|
25
|
+
const require = createRequire(import.meta.url);
|
|
26
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
|
+
const HOOK = path.join(ROOT, '.claude', 'hooks', 'conductor.js');
|
|
28
|
+
const ic = require('../src/instance-config.js');
|
|
29
|
+
const { test, summary } = makeRunner();
|
|
30
|
+
|
|
31
|
+
const INTERACTION = 'PROFILE-CONTRACT: pitch replies for a non-technical reader.';
|
|
32
|
+
const SPECIALITY = 'SPECIALITY-CONTRACT: the engineer expert contract.';
|
|
33
|
+
const ARTIST_SPECIALITY = 'ARTIST-CONTRACT: must not appear in an engineering session.';
|
|
34
|
+
|
|
35
|
+
// The /me payload the hook consumes, in the route's own shape.
|
|
36
|
+
function mePayload({ discipline = 'engineer' } = {}) {
|
|
37
|
+
return {
|
|
38
|
+
builder: { id: 4242, rank: 'metic' },
|
|
39
|
+
active_claims: [{ task_id: 77, title: 'a claimed task', touches: [], worktree_name: 'task-77', discipline }],
|
|
40
|
+
wandering: { effective_level: 'normal', inject: false, contract: '' },
|
|
41
|
+
interaction: { inject: true, contract: INTERACTION },
|
|
42
|
+
specialities: [
|
|
43
|
+
{ discipline: 'engineer', inject: true, contract: SPECIALITY },
|
|
44
|
+
{ discipline: 'artist', inject: true, contract: ARTIST_SPECIALITY },
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Run the hook once in a throwaway HOME whose session points at `base`.
|
|
50
|
+
async function runHook(me) {
|
|
51
|
+
const server = http.createServer((req, res) => {
|
|
52
|
+
if (req.url.startsWith('/api/bongos/me')) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(me)); return; }
|
|
53
|
+
res.writeHead(404); res.end('{}');
|
|
54
|
+
});
|
|
55
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
56
|
+
const base = `http://127.0.0.1:${server.address().port}`;
|
|
57
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'conductor-e2e-'));
|
|
58
|
+
// The session lives where the CLI looks for it, re-rooted under the fake home.
|
|
59
|
+
const rel = path.relative(os.homedir(), ic.configPath('gds-session.json'));
|
|
60
|
+
const sessionPath = path.join(home, rel);
|
|
61
|
+
fs.mkdirSync(path.dirname(sessionPath), { recursive: true });
|
|
62
|
+
fs.writeFileSync(sessionPath, JSON.stringify({ token: 'test-token', api_base: base, builder: { id: 4242 } }));
|
|
63
|
+
const env = { ...process.env, HOME: home, USERPROFILE: home };
|
|
64
|
+
for (const k of Object.keys(env)) if (/_API_BASE$/.test(k) || k === 'OTB_CONDUCTOR_OFF' || k === 'OTB_SUBAGENT') delete env[k];
|
|
65
|
+
try {
|
|
66
|
+
const child = spawn(process.execPath, [HOOK], { env, cwd: home, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
67
|
+
let out = ''; let err = '';
|
|
68
|
+
child.stdout.on('data', (d) => { out += d; });
|
|
69
|
+
child.stderr.on('data', (d) => { err += d; });
|
|
70
|
+
child.stdin.end(JSON.stringify({ session_id: 'e2e-' + Date.now(), prompt: 'hello', cwd: home }));
|
|
71
|
+
const code = await new Promise((r) => child.on('close', r));
|
|
72
|
+
return { out, err, code };
|
|
73
|
+
} finally {
|
|
74
|
+
await new Promise((r) => server.close(r));
|
|
75
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await test('the hook, run as settings.json runs it, injects the interaction profile (task 1004398)', async () => {
|
|
80
|
+
const r = await runHook(mePayload());
|
|
81
|
+
assert.equal(r.code, 0, 'the hook never fails the prompt');
|
|
82
|
+
assert.ok(r.out.includes(INTERACTION), `the interaction contract reaches the session; stdout was: ${JSON.stringify(r.out.slice(0, 300))}`);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await test('the speciality contract for the CLAIMED craft reaches the session, and no other craft\'s', async () => {
|
|
86
|
+
const r = await runHook(mePayload({ discipline: 'engineer' }));
|
|
87
|
+
assert.ok(r.out.includes(SPECIALITY), 'the engineer speciality is injected for an engineering claim');
|
|
88
|
+
assert.ok(!r.out.includes(ARTIST_SPECIALITY), 'the artist contract is not');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await test('with an artist claim, the artist contract is the one injected', async () => {
|
|
92
|
+
const r = await runHook(mePayload({ discipline: 'artist' }));
|
|
93
|
+
assert.ok(r.out.includes(ARTIST_SPECIALITY));
|
|
94
|
+
assert.ok(!r.out.includes(SPECIALITY));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
summary();
|