@bongos/core 1.19.1033 → 1.19.1035

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.
Files changed (114) hide show
  1. package/.bongos-core.json +204 -99
  2. package/.claude/skills/builder-start/SKILL.md +1 -1
  3. package/.claude/skills/grade-recover/SKILL.md +2 -1
  4. package/.claude/skills/grader-health/SKILL.md +4 -0
  5. package/.claude/skills/merge-mode/SKILL.md +1 -1
  6. package/clients/bongos-client/README.md +1 -1
  7. package/clients/bongos-client/bongos-client.global.js +6 -0
  8. package/clients/bongos-client/index.cjs +6 -0
  9. package/clients/bongos-client/index.d.ts +8 -0
  10. package/clients/bongos-client/index.mjs +6 -0
  11. package/docs/adr/0089-modular-multi-model-adversarial-grader.md +15 -0
  12. package/docs/adr/README.md +1 -1
  13. package/docs/api/openapi.json +120 -4
  14. package/docs/api-reference.md +9 -2
  15. package/docs/architecture.md +3 -2
  16. package/docs/canonical-permissions.md +2 -2
  17. package/docs/copy-inventory.md +34 -30
  18. package/docs/copy-registry.json +69 -33
  19. package/docs/module-api-changelog.md +5 -1
  20. package/docs/recipes/local-dev.md +45 -3
  21. package/docs/recipes/packaging-the-core.md +1 -1
  22. package/modules/autonomy/cadence.js +52 -0
  23. package/modules/autonomy/db.js +19 -0
  24. package/modules/autonomy/routes/autonomy.js +28 -0
  25. package/modules/government/catalog.js +2 -2
  26. package/modules/government/protected-surfaces.json +1 -0
  27. package/modules/grading/grade-engine.js +267 -0
  28. package/modules/grading/grader-subagent.js +4 -3
  29. package/modules/grading/grader-workers/index.js +67 -11
  30. package/modules/grading/grader-workers/worker-verdict.js +32 -2
  31. package/modules/grading/grader.js +109 -88
  32. package/modules/hall-ui/public/deploy.js +35 -0
  33. package/modules/hall-ui/public/project-settings.css +7 -0
  34. package/modules/lifecycle/routes/tasks.js +19 -4
  35. package/modules/lifecycle/ship-assurance.js +17 -1
  36. package/modules/provisioning/control-plane-guard.js +14 -13
  37. package/modules/provisioning/core-upgrade.js +30 -17
  38. package/modules/provisioning/public-refusal.js +44 -0
  39. package/modules/provisioning/routes/disconnect.js +2 -1
  40. package/modules/provisioning/routes/env-manifest.js +5 -4
  41. package/modules/provisioning/routes/provisioning.js +16 -15
  42. package/modules/provisioning/routes/repo-private.js +2 -1
  43. package/package-lock.json +2 -2
  44. package/package.json +1 -1
  45. package/release-notes.json +80 -0
  46. package/scripts/gds/autobongos-run.js +10 -2
  47. package/scripts/gds/backfill-cost.js +20 -9
  48. package/scripts/gds/claim.js +2 -2
  49. package/scripts/gds/credential-presence.js +281 -0
  50. package/scripts/gds/deploy-main-audit-gate.sh +11 -4
  51. package/scripts/gds/dev-lib.js +212 -5
  52. package/scripts/gds/dev.js +102 -50
  53. package/scripts/gds/doctor.js +3 -2
  54. package/scripts/gds/grade-correlation-audit.js +280 -0
  55. package/scripts/gds/grade-replay.js +237 -0
  56. package/scripts/gds/hooks/pre-push.js +7 -3
  57. package/scripts/gds/main-audit.js +19 -10
  58. package/scripts/gds/memory-map.js +3 -1
  59. package/scripts/gds/onboarding-config.js +3 -2
  60. package/scripts/gds/plain-cards.js +4 -0
  61. package/scripts/gds/preflight.js +12 -4
  62. package/scripts/gds/provision-core-upgrade.js +155 -4
  63. package/scripts/gds/provision-repo-auth.js +15 -0
  64. package/scripts/gds/provision.js +1 -1
  65. package/scripts/gds/publish-credential-check.js +3 -0
  66. package/scripts/gds/push-main.js +14 -3
  67. package/scripts/gds/push-path-brief.js +3 -2
  68. package/scripts/gds/run-routine.js +18 -5
  69. package/scripts/gds/run-unit-tests.js +14 -3
  70. package/scripts/gds/session-cost.js +12 -3
  71. package/scripts/gds/ship-deploy-target.js +11 -6
  72. package/scripts/gds/ship-grade-steps.js +33 -0
  73. package/scripts/gds/ship-land.js +157 -11
  74. package/scripts/gds/ship-merge.js +3 -2
  75. package/scripts/gds/ship-preflight-steps.js +30 -8
  76. package/scripts/gds/ship.js +1 -1
  77. package/scripts/gds/upgrade.js +3 -3
  78. package/scripts/hall-preview/README.md +20 -3
  79. package/scripts/hall-preview/live-identity.js +1 -1
  80. package/scripts/hall-preview/refresh-fixtures.js +71 -11
  81. package/scripts/hall-preview/scrub-identity.js +296 -0
  82. package/src/bongos/core-update.js +23 -1
  83. package/src/bongos/routes/core-update.js +27 -2
  84. package/src/bongos/routes/security.js +3 -3
  85. package/src/build-info.js +4 -2
  86. package/src/module-api.js +1 -1
  87. package/tests/adr_0089_scope.mjs +70 -0
  88. package/tests/autobongos_cadence.mjs +74 -0
  89. package/tests/consumer_layout_boot.mjs +363 -31
  90. package/tests/core_upgrade_door.mjs +40 -6
  91. package/tests/core_upgrade_runner.mjs +240 -4
  92. package/tests/credential_presence.mjs +153 -0
  93. package/tests/deploy_audit_gate.mjs +5 -2
  94. package/tests/deploy_page_projects.mjs +63 -2
  95. package/tests/dev_lib.mjs +2 -1
  96. package/tests/dev_windows.mjs +324 -0
  97. package/tests/env_knobs_neutral_prefix.mjs +203 -0
  98. package/tests/fixtures/grade-replay/recorded-panel-grades.json +27 -0
  99. package/tests/government_protected_surfaces.mjs +10 -0
  100. package/tests/grade_correlation_audit.mjs +159 -0
  101. package/tests/grade_engine.mjs +249 -0
  102. package/tests/grade_engine_replay.mjs +99 -0
  103. package/tests/grader_prompt_window_bound.mjs +132 -0
  104. package/tests/grader_unbacked_score.mjs +194 -0
  105. package/tests/hall_preview_scrub.mjs +214 -0
  106. package/tests/integration_parked_ratchet.mjs +24 -1
  107. package/tests/provisioning_public_refusal.mjs +195 -0
  108. package/tests/publish_status_branch.mjs +128 -0
  109. package/tests/routine_observability_api.mjs +195 -0
  110. package/tests/ship_assurance.mjs +86 -3
  111. package/tests/ship_land_red_pr.mjs +236 -0
  112. package/tests/ship_preflight.mjs +67 -1
  113. package/tests/start_env_guard.mjs +16 -0
  114. package/tests/upgrade.mjs +20 -1
@@ -41,7 +41,18 @@ const { INTEGRATION, INTEGRATION_PARKED, KNOWN_FAILING } = require('../scripts/g
41
41
  // Two later readings — 2/2 locally on a clean database, and the first CI run after
42
42
  // the parking landed — showed the pass was the anomaly. No new breakage was parked
43
43
  // here; one straggler joined the cohort it always belonged to.
44
- const MAX_PARKED = 24;
44
+ //
45
+ // LOWERED 24 → 23 by task 1002863: consumer_layout_boot was fixed and un-parked, and the
46
+ // ceiling follows it down so the freed slot cannot quietly be spent on a new failure.
47
+ const MAX_PARKED = 23;
48
+
49
+ // The one test that proves the PUBLISHED package works end to end: packaged, installed into
50
+ // a consumer, installed again from its own shipped lockfile, migrated onto EMPTY databases,
51
+ // and serving a database-backed route (tasks 2142 + 1002863 — "PROVE PUBLISH"). Every other
52
+ // test runs the source tree; none installs the package and runs it. So while this one is
53
+ // parked or unselected that guarantee silently does not exist — which is exactly how it sat,
54
+ // parked, from task 1004034 until task 1002863 found out why it failed.
55
+ const PUBLISH_PROOF = 'consumer_layout_boot';
45
56
 
46
57
  test('every parked entry names a test file that exists', () => {
47
58
  const ghosts = [...INTEGRATION_PARKED]
@@ -71,6 +82,18 @@ test('no test is parked in both INTEGRATION_PARKED and KNOWN_FAILING', () => {
71
82
  'parked in two places at once — pick the mechanism that owns each test.');
72
83
  });
73
84
 
85
+ test('the packaged-core proof is selected by the integration lane and parked nowhere', () => {
86
+ assert.ok(fs.existsSync(path.join(TESTS_DIR, `${PUBLISH_PROOF}.mjs`)),
87
+ `tests/${PUBLISH_PROOF}.mjs is gone — nothing proves the published package installs, migrates and serves any more.`);
88
+ assert.ok(INTEGRATION.has(PUBLISH_PROOF),
89
+ `${PUBLISH_PROOF} is not in the INTEGRATION set, so either the DB-free unit gate would try to run it `
90
+ + '(it needs npm, a network and a Postgres) or no lane selects it at all.');
91
+ assert.ok(!INTEGRATION_PARKED.has(PUBLISH_PROOF) && !KNOWN_FAILING.has(PUBLISH_PROOF),
92
+ `${PUBLISH_PROOF} is parked. It is the only proof that the PUBLISHED core installs, migrates an `
93
+ + 'empty database and serves from it; parking it re-opens that gap without a single red check. '
94
+ + 'Fix the failure instead — or, if parking is truly the call, make it deliberately by editing this guard.');
95
+ });
96
+
74
97
  test('the parked count is ratcheted and cannot grow silently', () => {
75
98
  const n = INTEGRATION_PARKED.size;
76
99
  assert.ok(n <= MAX_PARKED,
@@ -0,0 +1,195 @@
1
+ // tests/provisioning_public_refusal.mjs — a refusal reaches the person as its own sentence,
2
+ // and nothing else ever does (task 1004126).
3
+ //
4
+ // THE BUG. The control-plane guard refuses a destructive action on the platform's own row
5
+ // with a code, a 409 and a sentence written for the owner. Every catch in the provisioning
6
+ // module answered a blanket 500 "internal error" instead, so an owner who pressed Take
7
+ // offline or Disconnect on that row read "internal error" while the real sentence went only
8
+ // to the journal. Task 1004125 fixed ONE catch by duck-typing three fields; twenty-three more
9
+ // had the same shape.
10
+ //
11
+ // WHAT THESE PIN.
12
+ // • failFrom forwards a declared PublicRefusal, and ONLY that: a look-alike carrying the
13
+ // same three fields and a Postgres error naming a table both get the opaque 500.
14
+ // • the guard throws a PublicRefusal, so there is one definition of "public", not one per catch.
15
+ // • the three routes that can reach the guard today (owner teardown, force-teardown,
16
+ // disconnect) answer 409 with the guard's sentence, driven through the REAL router, the
17
+ // REAL enqueue helper and the REAL guard; an unexpected error on the same routes still
18
+ // answers 500 and leaks nothing.
19
+ // • the ratchet: no catch in the module answers a 5xx except through failFrom.
20
+ //
21
+ // DB-free: the shared pool's query is stubbed and auth is faked. No network.
22
+ //
23
+ // Run: node --test tests/provisioning_public_refusal.mjs
24
+
25
+ import assert from 'node:assert/strict';
26
+ import { test, after } from 'node:test';
27
+ import { createRequire } from 'node:module';
28
+ import fs from 'node:fs';
29
+ import http from 'node:http';
30
+ import path from 'node:path';
31
+ import { fileURLToPath } from 'node:url';
32
+
33
+ const require = createRequire(import.meta.url);
34
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
35
+ const { PublicRefusal, isPublicRefusal, failFrom } = require('../modules/provisioning/public-refusal.js');
36
+ const guard = require('../modules/provisioning/control-plane-guard.js');
37
+ const provisioning = require('../modules/provisioning/provisioning.js');
38
+
39
+ // A res that records what failFrom answered, through the same envelope a route uses.
40
+ function recorder() {
41
+ const out = {};
42
+ return { out, fail: (code, opts) => { Object.assign(out, { code, ...opts }); } };
43
+ }
44
+ const selfRowDb = { query: async () => ({ rows: [{ hosting_shape: 'control-plane' }] }) };
45
+ const guardRefusal = () => guard.assertIntentAllowed(selfRowDb, 1, 'teardown').then(() => null, (e) => e);
46
+
47
+ // ---- the helper -----------------------------------------------------------------
48
+
49
+ test('failFrom forwards a PublicRefusal as its own code, status and PUBLIC sentence', () => {
50
+ const r = recorder();
51
+ failFrom(r, new PublicRefusal({ code: 'nope', status: 409, publicMessage: 'You cannot do that here.', message: 'journal: row 7 is special' }), 'x_failed');
52
+ assert.deepEqual(r.out, { code: 'nope', status: 409, message: 'You cannot do that here.' }, 'the journal sentence stays in the journal');
53
+ });
54
+
55
+ test('a look-alike with the same three fields is NOT public — only a declared refusal is', () => {
56
+ // Task 1004125's catch duck-typed on code && status && publicMessage. Anything can carry
57
+ // those; only the class says it was written for a person.
58
+ const r = recorder();
59
+ failFrom(r, Object.assign(new Error('x'), { code: 'lookalike', status: 409, publicMessage: 'leaked' }), 'x_failed');
60
+ assert.deepEqual(r.out, { code: 'x_failed', status: 500, message: 'internal error' });
61
+ });
62
+
63
+ test('an unexpected error is opaque — a Postgres error naming a table never reaches the client', () => {
64
+ const r = recorder();
65
+ const pg = Object.assign(new Error('relation "provisioning_intents" does not exist'), { code: '42P01', status: 500 });
66
+ failFrom(r, pg, 'teardown_failed');
67
+ assert.deepEqual(r.out, { code: 'teardown_failed', status: 500, message: 'internal error' });
68
+ assert.equal(isPublicRefusal(pg), false);
69
+ });
70
+
71
+ test('a refusal is never an outage: PublicRefusal will not carry a non-4xx status', () => {
72
+ assert.throws(() => new PublicRefusal({ code: 'boom', status: 500, publicMessage: 'x' }), /4xx/);
73
+ assert.throws(() => new PublicRefusal({ code: 'boom', status: 409 }), /publicMessage/);
74
+ });
75
+
76
+ test('the control-plane guard throws a PublicRefusal: the owner sentence public, the journal one not', async () => {
77
+ const err = await guardRefusal();
78
+ assert.ok(isPublicRefusal(err), 'the guard declares its refusal public');
79
+ assert.equal(err.code, 'control_plane_protected');
80
+ assert.equal(err.status, 409);
81
+ assert.match(err.publicMessage, /cannot be aimed at the machine the platform itself runs on/);
82
+ assert.match(err.message, /'teardown' cannot be aimed at the control plane/, 'the journal keeps the action it refused');
83
+ assert.equal(await guard.assertIntentAllowed(selfRowDb, 1, 'restart'), undefined, 'an allowed action is still allowed');
84
+ });
85
+
86
+ // ---- the routes, through the real router, enqueue helper and guard --------------
87
+
88
+ const api = require('../src/module-api.js');
89
+ api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, github_login: 'owner', rank: 'archon' }; next(); };
90
+ api.requirePermission = () => (_req, _res, next) => next();
91
+ const selfRow = { id: 1, slug: 'platform', owner_builder_id: 7, status: 'active', hosting_shape: 'control-plane' };
92
+ provisioning.getInstanceById = async () => ({ ...selfRow });
93
+ provisioning.recordEvent = async () => {};
94
+
95
+ // The shared pool: the guard's shape lookup answers 'control-plane'; every other read and
96
+ // the teardown's supersede UPDATE answer nothing. `fault` makes every query throw instead.
97
+ let fault = null;
98
+ api.pool.query = async (sql) => {
99
+ if (fault) throw fault;
100
+ if (/SELECT hosting_shape FROM provisioning_instances/.test(sql)) return { rows: [{ hosting_shape: 'control-plane' }] };
101
+ return { rows: [] };
102
+ };
103
+
104
+ const express = require('express');
105
+ const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
106
+ const app = express();
107
+ app.use(express.json());
108
+ app.use(attachFail);
109
+ app.use('/api/bongos', require('../modules/provisioning/routes/provisioning.js')());
110
+ app.use('/api/bongos', require('../modules/provisioning/routes/disconnect.js')());
111
+ const server = app.listen(0);
112
+ await new Promise((r) => server.once('listening', r));
113
+ after(() => server.close());
114
+
115
+ function post(p, body = {}) {
116
+ return new Promise((resolve, reject) => {
117
+ const payload = JSON.stringify(body);
118
+ const req = http.request(`http://127.0.0.1:${server.address().port}/api/bongos${p}`, {
119
+ method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
120
+ }, (res) => {
121
+ let d = '';
122
+ res.on('data', (c) => { d += c; });
123
+ res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(d), raw: d }));
124
+ });
125
+ req.on('error', reject);
126
+ req.end(payload);
127
+ });
128
+ }
129
+
130
+ const REACHABLE = [
131
+ ['owner teardown', '/provisioning/instances/1/teardown', {}, 'teardown_failed'],
132
+ ['force-teardown', '/provisioning/instances/1/force-teardown', {}, 'teardown_failed'],
133
+ ['disconnect', '/provisioning/instances/1/disconnect', { confirm_slug: 'platform' }, 'disconnect_failed'],
134
+ ];
135
+
136
+ for (const [name, p, body, fallback] of REACHABLE) {
137
+ test(`${name} on the platform's own row answers 409 with the guard's sentence, not "internal error"`, async () => {
138
+ fault = null;
139
+ const r = await post(p, body);
140
+ assert.equal(r.status, 409, r.raw);
141
+ assert.equal(r.body.error.code, 'control_plane_protected');
142
+ assert.equal(r.body.error.message, (await guardRefusal()).publicMessage);
143
+ });
144
+
145
+ test(`${name}: an unexpected failure is still an opaque 500 (${fallback}) that leaks nothing`, async () => {
146
+ fault = new Error('relation "provisioning_intents" does not exist');
147
+ const r = await post(p, body);
148
+ fault = null;
149
+ assert.equal(r.status, 500, r.raw);
150
+ assert.deepEqual(r.body.error, { code: fallback, message: 'internal error' });
151
+ });
152
+ }
153
+
154
+ // ---- the ratchet ------------------------------------------------------------------
155
+
156
+ // Every `catch (e) { … }` / `catch { … }` body in a file, with the line it opens on. A
157
+ // promise's `.catch(() => …)` is not a catch block and is not matched.
158
+ function catchBodies(src) {
159
+ const out = [];
160
+ const re = /\bcatch\s*(?:\(\s*\w*\s*\))?\s*\{/g;
161
+ for (let m = re.exec(src); m; m = re.exec(src)) {
162
+ let depth = 1;
163
+ let i = re.lastIndex;
164
+ for (; i < src.length && depth; i++) depth += src[i] === '{' ? 1 : src[i] === '}' ? -1 : 0;
165
+ out.push({ line: src.slice(0, m.index).split('\n').length, body: src.slice(re.lastIndex, i - 1) });
166
+ }
167
+ return out;
168
+ }
169
+
170
+ test('no catch in the provisioning module answers a 5xx except through failFrom', () => {
171
+ const dir = path.join(ROOT, 'modules', 'provisioning');
172
+ const files = fs.readdirSync(dir, { recursive: true }).map(String)
173
+ .filter((f) => f.endsWith('.js') && !f.split(path.sep).includes('tests'));
174
+ const HAND_ROLLED_5XX = /res\.fail\([^;]*?(status:\s*5\d\d|,\s*5\d\d\s*[,)])/;
175
+ const offenders = [];
176
+ let viaHelper = 0;
177
+ for (const f of files) {
178
+ for (const c of catchBodies(fs.readFileSync(path.join(dir, f), 'utf8'))) {
179
+ if (HAND_ROLLED_5XX.test(c.body)) offenders.push(`${f}:${c.line}`);
180
+ if (/\bfailFrom\(res, /.test(c.body)) viaHelper++;
181
+ }
182
+ }
183
+ assert.deepEqual(offenders, [], 'answer with failFrom(res, err, <code>) so a declared refusal can reach the person');
184
+ assert.ok(viaHelper >= 20, `the scan must reach the converted catches (24 at task 1004126), saw ${viaHelper}`);
185
+ });
186
+
187
+ test('the ratchet reads a hand-rolled 500 in a catch, and not one outside a catch', () => {
188
+ const src = "try { a(); } catch (err) {\n log.error('x', err);\n res.fail('x_failed', { status: 500, message: 'internal error' });\n}\nres.fail('config_unset', { status: 500, message: 'no origin' });\n"
189
+ + "p.catch(() => {});\ntry { b(); } catch { res.fail('y_failed', 500); }";
190
+ const bodies = catchBodies(src);
191
+ assert.equal(bodies.length, 2, 'a catch with no binding counts; a promise .catch does not');
192
+ assert.match(bodies[1].body, /y_failed/);
193
+ assert.match(bodies[0].body, /x_failed/);
194
+ assert.doesNotMatch(bodies[0].body, /config_unset/, 'a config refusal outside a catch is a real sentence, not a swallowed error');
195
+ });
@@ -0,0 +1,128 @@
1
+ // tests/publish_status_branch.mjs — GET /tasks/:id/publish-status finds the task's
2
+ // branch itself when the caller names none (task 1003764, gap 1).
3
+ //
4
+ // WHY. This route is the diagnostic every agent is told to use INSTEAD of raw git
5
+ // probes (push-path-brief.js, the stuck-strike nudge), and it required ?branch=. Called
6
+ // the way those briefs spell it — no query — it answered 400 bad_branch for task
7
+ // 1003747 while origin carried its live, server-pushed branch and PR 673 sat open. The
8
+ // sanctioned diagnostic was the one thing that could not diagnose a live publish.
9
+ //
10
+ // Drives the real route handler with fake req/res and stubbed github-push reads: no
11
+ // DB, no server, no network. Run: node tests/publish_status_branch.mjs
12
+
13
+ import { strict as assert } from 'node:assert';
14
+ import { createRequire } from 'node:module';
15
+
16
+ process.env.NODE_ENV = 'test';
17
+ const require = createRequire(import.meta.url);
18
+
19
+ const gp = require('../modules/lifecycle/github-push.js');
20
+ const buildTasksRouter = require('../modules/lifecycle/routes/tasks.js');
21
+
22
+ let pass = 0;
23
+ let fail = 0;
24
+ const t = async (name, fn) => {
25
+ try { await fn(); pass++; console.log(`PASS ${name}`); }
26
+ catch (e) { fail++; console.log(`FAIL ${name}\n ${e.stack || e.message}`); }
27
+ };
28
+
29
+ const CRED_KEYS = ['GITHUB_PUSH_TOKEN', 'GITHUB_APP_ID', 'GITHUB_APP_INSTALLATION_ID', 'GITHUB_APP_PRIVATE_KEY'];
30
+
31
+ // Stub the github-push reads the route makes, run the handler once, restore everything.
32
+ // `calls` records what the route asked GitHub for, so a test can prove what it did NOT do.
33
+ async function callRoute(query, stubs = {}) {
34
+ const saved = {};
35
+ for (const k of ['findTaskPullRequest', 'getPublishStatus', 'mergeIfGreen']) saved[k] = gp[k];
36
+ const savedEnv = CRED_KEYS.map((k) => [k, process.env[k]]);
37
+ const calls = { findTaskPullRequest: [], getPublishStatus: [], mergeIfGreen: [] };
38
+ gp.findTaskPullRequest = async (a) => { calls.findTaskPullRequest.push(a); return stubs.taskPr === undefined ? null : stubs.taskPr; };
39
+ gp.getPublishStatus = async (a) => {
40
+ calls.getPublishStatus.push(a);
41
+ return stubs.status || { pr_state: 'open', merged: false, merge_sha: null, deploy_status: 'pending', pr_url: 'https://github.test/pr/673', pr_number: 673 };
42
+ };
43
+ gp.mergeIfGreen = async (a) => { calls.mergeIfGreen.push(a); return stubs.mergeDriver || { reason: 'failing_check:unit', failing_tests: '1 failed: fitness' }; };
44
+ for (const k of CRED_KEYS) delete process.env[k];
45
+ process.env.GITHUB_PUSH_TOKEN = 'ghp_fake_for_isConfigured';
46
+ try {
47
+ const router = buildTasksRouter();
48
+ const layer = router.stack.find((l) => l.route && l.route.path === '/tasks/:id/publish-status' && l.route.methods.get);
49
+ const handler = layer.route.stack[layer.route.stack.length - 1].handle;
50
+ const out = { failCode: null, failOpts: null, body: null };
51
+ const res = {
52
+ status() { return this; },
53
+ json(b) { out.body = b; return this; },
54
+ fail(code, opts) { out.failCode = code; out.failOpts = opts; return this; },
55
+ };
56
+ // rank archon → gateTaskOwnership passes without a DB lookup, keeping this hermetic.
57
+ await handler({ params: { id: '1003747' }, query, builder: { id: 25, rank: 'archon' } }, res);
58
+ return { ...out, calls };
59
+ } finally {
60
+ Object.assign(gp, saved);
61
+ for (const [k, v] of savedEnv) { if (v === undefined) delete process.env[k]; else process.env[k] = v; }
62
+ }
63
+ }
64
+
65
+ await t('no ?branch= → the route finds the task\'s own PR branch instead of answering 400 (the task 1003747 repro)', async () => {
66
+ const r = await callRoute({}, { taskPr: { branch: 'task-1003747', pr_number: 673, pr_state: 'open', merged: false } });
67
+ assert.equal(r.failCode, null, `must not refuse — got ${r.failCode}`);
68
+ assert.equal(r.body.ok, true);
69
+ assert.equal(r.body.branch, 'task-1003747', 'reports which branch it answered about');
70
+ assert.equal(r.body.branch_source, 'task_pr', 'and how it found it');
71
+ assert.deepEqual(r.calls.findTaskPullRequest, [{ taskId: 1003747 }], 'looked the PR up by THIS task id');
72
+ assert.deepEqual(r.calls.getPublishStatus, [{ branch: 'task-1003747' }]);
73
+ assert.equal(r.body.pr_number, 673);
74
+ // The whole point: the red check is now one call away, not twenty minutes of git.
75
+ assert.equal(r.body.merge_driver.failing_tests, '1 failed: fitness');
76
+ assert.deepEqual(r.calls.mergeIfGreen, [{ branch: 'task-1003747' }]);
77
+ });
78
+
79
+ await t('no ?branch= and no PR for the task → 200 "none" with a hint, never a 400, and nothing else is probed', async () => {
80
+ const r = await callRoute({}, { taskPr: null });
81
+ assert.equal(r.failCode, null);
82
+ assert.equal(r.body.ok, true);
83
+ assert.equal(r.body.pr_state, 'none');
84
+ assert.equal(r.body.merged, false);
85
+ assert.equal(r.body.branch, null);
86
+ assert.equal(r.body.branch_source, null);
87
+ assert.match(r.body.branch_hint, /\?branch=/, 'says how to ask about a specific branch');
88
+ assert.equal(r.body.reconciled, false);
89
+ assert.equal(r.body.merge_driver, null);
90
+ assert.deepEqual(r.calls.getPublishStatus, [], 'no branch → no branch-status read');
91
+ assert.deepEqual(r.calls.mergeIfGreen, [], 'no PR → the merge driver never runs');
92
+ });
93
+
94
+ await t('an empty ?branch= is treated as absent, not as a bad branch', async () => {
95
+ const r = await callRoute({ branch: '' }, { taskPr: { branch: 'task-1003747', pr_number: 673 } });
96
+ assert.equal(r.failCode, null);
97
+ assert.equal(r.body.branch_source, 'task_pr');
98
+ });
99
+
100
+ await t('an explicit ?branch= still wins, and the task-PR lookup is skipped', async () => {
101
+ const r = await callRoute({ branch: 'claude/some-worktree' });
102
+ assert.equal(r.failCode, null);
103
+ assert.equal(r.body.branch, 'claude/some-worktree');
104
+ assert.equal(r.body.branch_source, 'query');
105
+ assert.deepEqual(r.calls.findTaskPullRequest, [], 'the caller named the branch; nothing to look up');
106
+ assert.deepEqual(r.calls.getPublishStatus, [{ branch: 'claude/some-worktree' }]);
107
+ });
108
+
109
+ await t('a fully-qualified refs/heads/<branch> names the same branch and is accepted', async () => {
110
+ const r = await callRoute({ branch: 'refs/heads/task-1003747' });
111
+ assert.equal(r.failCode, null);
112
+ assert.equal(r.body.branch, 'task-1003747');
113
+ assert.deepEqual(r.calls.getPublishStatus, [{ branch: 'task-1003747' }]);
114
+ });
115
+
116
+ await t('a genuinely unsafe branch is still refused 400 — in a sentence, before any GitHub call', async () => {
117
+ for (const branch of ['-rf', 'a/../../etc', 'refs/tags/x', ['a', 'b']]) {
118
+ const r = await callRoute({ branch });
119
+ assert.equal(r.failCode, 'bad_branch', `${JSON.stringify(branch)} must be refused`);
120
+ assert.equal(r.failOpts.status, 400);
121
+ assert.match(r.failOpts.message, /left out/, 'the refusal says the branch can simply be omitted');
122
+ assert.deepEqual(r.calls.findTaskPullRequest, []);
123
+ assert.deepEqual(r.calls.getPublishStatus, []);
124
+ }
125
+ });
126
+
127
+ console.log(`\npublish-status branch resolution (task 1003764): ${pass} passed, ${fail} failed`);
128
+ process.exitCode = fail > 0 ? 1 : 0;
@@ -0,0 +1,195 @@
1
+ // tests/routine_observability_api.mjs — the unattended routines are diagnosable from off
2
+ // the box (task 1002669).
3
+ //
4
+ // Every autonomy-gate decision already lands in `autonomy_runs` and every core upgrade in
5
+ // `core_upgrades`, but no route read either, so "did the routine fire, what did it
6
+ // decide, and when did the pin last move" needed an SSH session (blocker 1000106). Two
7
+ // read-only routes answer it now:
8
+ // GET /api/bongos/autonomy/runs — behind `autonomy.precheck` (Metic+), the atom the
9
+ // precheck itself uses
10
+ // GET /api/bongos/core-upgrades — behind `core.pin.move` (Archon), the atom the
11
+ // upgrade banner and the deploy door use
12
+ // No new atom, so no migration. These tests drive REAL requests through the assembled
13
+ // routers with only the two auth gates stubbed (the core_update_banner precedent: an
14
+ // arity-mangled handler 500s, and only a real request sees that), read the atom each
15
+ // gate was built with off the router stack, and pin the SQL against a stubbed pool.
16
+ //
17
+ // DB-free, no network. Run: node --test tests/routine_observability_api.mjs
18
+
19
+ import { test } from 'node:test';
20
+ import assert from 'node:assert/strict';
21
+ import { createRequire } from 'node:module';
22
+ import path from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
26
+ const req = createRequire(import.meta.url);
27
+ const express = req('express');
28
+ const { attachFail } = req(path.join(ROOT, 'src/bongos/middleware/error-envelope.js'));
29
+
30
+ // A permission stub that remembers which atom it was built with, so the test can read
31
+ // the gate's authority off the router instead of grepping the source for it.
32
+ const permStub = (key) => {
33
+ const mw = (_req, _res, next) => next();
34
+ mw.permKey = key;
35
+ return mw;
36
+ };
37
+ const passBuilder = (reqq, _res, next) => { reqq.builder = { id: 3, rank: 'archon' }; next(); };
38
+
39
+ async function serve(router) {
40
+ const app = express();
41
+ app.use(attachFail);
42
+ app.use(router);
43
+ const server = app.listen(0);
44
+ await new Promise((r) => server.once('listening', r));
45
+ const base = `http://127.0.0.1:${server.address().port}`;
46
+ return { base, close: () => server.close() };
47
+ }
48
+
49
+ function layerFor(router, p) {
50
+ const layer = router.stack.find((l) => l.route && l.route.path === p);
51
+ assert.ok(layer, `${p} is registered`);
52
+ return layer;
53
+ }
54
+
55
+ // ── GET /autonomy/runs ──────────────────────────────────────────────────────────────
56
+
57
+ function autonomyRouter() {
58
+ const api = req(path.join(ROOT, 'src/module-api.js'));
59
+ const saved = { b: api.requireBuilder, p: api.requirePermission };
60
+ api.requireBuilder = passBuilder;
61
+ api.requirePermission = permStub;
62
+ const routePath = req.resolve(path.join(ROOT, 'modules/autonomy/routes/autonomy.js'));
63
+ delete req.cache[routePath];
64
+ try {
65
+ return req(routePath)();
66
+ } finally {
67
+ api.requireBuilder = saved.b;
68
+ api.requirePermission = saved.p;
69
+ }
70
+ }
71
+
72
+ test('GET /autonomy/runs is gated by requireBuilder + the precheck\'s own atom', () => {
73
+ const layer = layerFor(autonomyRouter(), '/autonomy/runs');
74
+ assert.equal(layer.route.methods.get, true);
75
+ assert.equal(layer.route.stack.length, 3, 'requireBuilder + requirePermission + handler');
76
+ assert.equal(layer.route.stack[1].handle.permKey, 'autonomy.precheck');
77
+ });
78
+
79
+ test('GET /autonomy/runs returns the decisions, narrowed by routine and a clamped limit', async () => {
80
+ const db = req(path.join(ROOT, 'modules/autonomy/db.js'));
81
+ const saved = db.readAutonomyRuns;
82
+ const seen = [];
83
+ const rows = [{ id: '7', routine: 'overnight-builder', decision: 'no-go', reason: 'no claimable work', head_sha: 'abc', claimable_count: 0, enabled: true, created_at: '2026-09-25T02:00:00.000Z' }];
84
+ db.readAutonomyRuns = async (args) => { seen.push(args); return rows; };
85
+ const s = await serve(autonomyRouter());
86
+ try {
87
+ let r = await fetch(`${s.base}/autonomy/runs?routine=overnight-builder&limit=500`);
88
+ assert.equal(r.status, 200);
89
+ assert.deepEqual(await r.json(), { routine: 'overnight-builder', limit: 100, runs: rows });
90
+ assert.deepEqual(seen.pop(), { routine: 'overnight-builder', limit: 100 }, 'a limit above 100 is clamped, not refused');
91
+
92
+ r = await fetch(`${s.base}/autonomy/runs`);
93
+ assert.equal(r.status, 200);
94
+ assert.deepEqual(seen.pop(), { routine: null, limit: 20 }, 'no routine = every routine; the default limit is 20');
95
+
96
+ await fetch(`${s.base}/autonomy/runs?limit=banana`);
97
+ assert.equal(seen.pop().limit, 20, 'a non-number falls back to the default');
98
+ await fetch(`${s.base}/autonomy/runs?limit=0`);
99
+ assert.equal(seen.pop().limit, 1);
100
+
101
+ db.readAutonomyRuns = async () => { throw new Error('relation "autonomy_runs" does not exist'); };
102
+ r = await fetch(`${s.base}/autonomy/runs`);
103
+ assert.equal(r.status, 503, 'an unreadable table is a 503 that says so, never an empty list');
104
+ const body = await r.json();
105
+ assert.equal(body.error.code, 'autonomy_runs_unreadable');
106
+ assert.ok(!JSON.stringify(body).includes('does not exist'), 'the database error text stays in the log');
107
+ } finally {
108
+ s.close();
109
+ db.readAutonomyRuns = saved;
110
+ }
111
+ });
112
+
113
+ test('readAutonomyRuns: newest first, one routine or all, ids as strings', async () => {
114
+ const api = req(path.join(ROOT, 'src/module-api.js'));
115
+ const db = req(path.join(ROOT, 'modules/autonomy/db.js'));
116
+ const saved = api.pool.query;
117
+ const calls = [];
118
+ api.pool.query = async (sql, params) => { calls.push({ sql, params }); return { rows: [{ id: 42, routine: 'bfg', decision: 'go' }] }; };
119
+ try {
120
+ const out = await db.readAutonomyRuns({ routine: 'bfg', limit: 5 });
121
+ assert.deepEqual(out, [{ id: '42', routine: 'bfg', decision: 'go' }]);
122
+ assert.match(calls[0].sql, /FROM autonomy_runs/);
123
+ assert.match(calls[0].sql, /\$1::text IS NULL OR routine = \$1/);
124
+ assert.match(calls[0].sql, /ORDER BY created_at DESC/);
125
+ assert.deepEqual(calls[0].params, ['bfg', 5]);
126
+ await db.readAutonomyRuns();
127
+ assert.deepEqual(calls[1].params, [null, 20]);
128
+ assert.doesNotMatch(calls[0].sql, /\b(INSERT|UPDATE|DELETE)\b/i, 'read-only');
129
+ } finally {
130
+ api.pool.query = saved;
131
+ }
132
+ });
133
+
134
+ // ── GET /core-upgrades ──────────────────────────────────────────────────────────────
135
+
136
+ function coreUpdateRouter(deps) {
137
+ const auth = req(path.join(ROOT, 'src/bongos/auth.js'));
138
+ const saved = { b: auth.requireBuilder, p: auth.requirePermission };
139
+ auth.requireBuilder = passBuilder;
140
+ auth.requirePermission = permStub;
141
+ const routePath = req.resolve(path.join(ROOT, 'src/bongos/routes/core-update.js'));
142
+ delete req.cache[routePath];
143
+ try {
144
+ return req(routePath)(deps);
145
+ } finally {
146
+ auth.requireBuilder = saved.b;
147
+ auth.requirePermission = saved.p;
148
+ delete req.cache[routePath];
149
+ }
150
+ }
151
+
152
+ test('GET /core-upgrades is gated by requireBuilder + core.pin.move, like the banner beside it', () => {
153
+ const router = coreUpdateRouter();
154
+ const layer = layerFor(router, '/core-upgrades');
155
+ assert.equal(layer.route.stack.length, 3, 'requireBuilder + requirePermission + handler');
156
+ assert.equal(layer.route.stack[1].handle.permKey, 'core.pin.move');
157
+ assert.equal(layerFor(router, '/core-update').route.stack[1].handle.permKey, 'core.pin.move', 'the existing route is untouched');
158
+ });
159
+
160
+ test('GET /core-upgrades answers "when did the pin last move", newest first', async () => {
161
+ const seen = [];
162
+ const ledger = [
163
+ { id: '12', from_version: '1.19.1022', to_version: '1.19.1023', source_commit: 'b50d32f1', note: null, applied_at: '2026-09-24T10:00:00.000Z', source: 'door', requested_by_builder_id: '3' },
164
+ { id: '11', from_version: '1.19.1010', to_version: '1.19.1022', source_commit: null, note: 'auto-subscription (patch)', applied_at: '2026-09-23T10:00:00.000Z', source: 'cli', requested_by_builder_id: null },
165
+ ];
166
+ const s = await serve(coreUpdateRouter({ upgrades: async (args) => { seen.push(args); return ledger; } }));
167
+ try {
168
+ const r = await fetch(`${s.base}/core-upgrades?limit=2`);
169
+ assert.equal(r.status, 200, 'the handler runs — a 500 here is the asyncHandler arity bug');
170
+ const body = await r.json();
171
+ assert.equal(body.ok, true);
172
+ assert.equal(body.limit, 2);
173
+ assert.deepEqual(body.upgrades, ledger);
174
+ assert.equal(body.upgrades[0].applied_at, '2026-09-24T10:00:00.000Z', 'the first row is the last pin move');
175
+ assert.ok('running' in body, 'the running core version rides along, so behind-or-not is one read');
176
+ assert.deepEqual(seen, [{ limit: 2 }]);
177
+ } finally {
178
+ s.close();
179
+ }
180
+ });
181
+
182
+ test('readCoreUpgrades: the ledger, newest first, read-only, ids as strings', async () => {
183
+ const { readCoreUpgrades } = req(path.join(ROOT, 'src/bongos/core-update.js'));
184
+ const calls = [];
185
+ const pool = { query: async (sql, params) => { calls.push({ sql, params }); return { rows: [{ id: 5, to_version: '1.19.1023', requested_by_builder_id: 3 }, { id: 4, to_version: '1.19.1022', requested_by_builder_id: null }] }; } };
186
+ const out = await readCoreUpgrades({ limit: 7, pool });
187
+ assert.deepEqual(out, [
188
+ { id: '5', to_version: '1.19.1023', requested_by_builder_id: '3' },
189
+ { id: '4', to_version: '1.19.1022', requested_by_builder_id: null },
190
+ ]);
191
+ assert.match(calls[0].sql, /FROM core_upgrades/);
192
+ assert.match(calls[0].sql, /ORDER BY applied_at DESC/);
193
+ assert.deepEqual(calls[0].params, [7]);
194
+ assert.doesNotMatch(calls[0].sql, /\b(INSERT|UPDATE|DELETE)\b/i, 'read-only');
195
+ });