@bongos/core 1.19.1032 → 1.19.1034
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 +322 -117
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/grade-audit/SKILL.md +1 -1
- package/.claude/skills/grade-recover/SKILL.md +2 -1
- package/.claude/skills/grader-health/SKILL.md +5 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/docs/adr/0089-modular-multi-model-adversarial-grader.md +15 -0
- package/docs/adr/0175-the-board-room-full-idea-ratification.md +3 -2
- package/docs/adr/0178-the-copy-desk-flag-and-queue.md +3 -3
- package/docs/adr/0188-patrol-runs-on-ci-not-the-server.md +2 -2
- package/docs/adr/0189-baseline-floors-and-the-freeze-detector.md +1 -1
- package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +1 -1
- package/docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md +11 -3
- package/docs/adr/README.md +4 -4
- package/docs/api/openapi.json +1 -1
- package/docs/architecture.md +4 -3
- package/docs/canonical-permissions.md +2 -2
- package/docs/copy-inventory.md +34 -30
- package/docs/copy-registry.json +69 -33
- package/docs/file-map.md +4 -3
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/autobongos-windows-host.md +3 -1
- package/docs/recipes/local-dev.md +45 -3
- package/docs/recipes/packaging-the-core.md +3 -1
- package/docs/recipes/redteam-patrol.md +2 -2
- package/docs/recipes/upgrading-the-core.md +1 -1
- package/modules/autonomy/cadence.js +52 -0
- package/modules/autonomy/gauge.js +57 -9
- package/modules/government/protected-surfaces.json +1 -0
- package/modules/grading/grade-engine.js +267 -0
- package/modules/grading/grader-subagent.js +4 -3
- package/modules/grading/grader-workers/index.js +67 -11
- package/modules/grading/grader-workers/worker-verdict.js +32 -2
- package/modules/grading/grader.js +109 -88
- package/modules/hall-ui/public/deploy.js +35 -0
- package/modules/hall-ui/public/project-settings.css +7 -0
- package/modules/hall-ui/records/builders-roster.md +10 -0
- package/modules/hall-ui/records/catalog.md +10 -0
- package/modules/hall-ui/records/collaboration.md +14 -0
- package/modules/hall-ui/records/goal-dependency-forest.md +11 -0
- package/modules/hall-ui/records/oversight-family.md +19 -0
- package/modules/hall-ui/records/panel-family.md +19 -0
- package/modules/hall-ui/records/reading-rooms.md +12 -0
- package/modules/hall-ui/records/record-family.md +13 -0
- package/modules/hall-ui/records/reference-room.md +9 -0
- package/modules/hall-ui/records/roster-and-ledger.md +9 -0
- package/modules/hall-ui/records/sky.md +23 -0
- package/modules/lifecycle/dependency-advisory.js +2 -1
- package/modules/lifecycle/routes/tasks.js +19 -4
- package/modules/lifecycle/ship-preflight.js +91 -0
- package/modules/provisioning/control-plane-guard.js +14 -13
- package/modules/provisioning/core-upgrade.js +30 -17
- package/modules/provisioning/public-refusal.js +44 -0
- package/modules/provisioning/routes/disconnect.js +2 -1
- package/modules/provisioning/routes/env-manifest.js +5 -4
- package/modules/provisioning/routes/provisioning.js +16 -15
- package/modules/provisioning/routes/repo-private.js +2 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +7481 -0
- package/scripts/gds/address-park-sweep.sh +3 -1
- package/scripts/gds/autobongos-run.js +91 -3
- package/scripts/gds/backfill-cost.js +20 -9
- package/scripts/gds/baseline-staleness.js +1 -1
- package/scripts/gds/claim.js +2 -2
- package/scripts/gds/credential-presence.js +281 -0
- package/scripts/gds/db-backup-nightly.sh +9 -4
- package/scripts/gds/db-backup-offsite.sh +4 -2
- package/scripts/gds/deploy-main-audit-gate.sh +11 -4
- package/scripts/gds/dev-lib.js +212 -5
- package/scripts/gds/dev.js +102 -50
- package/scripts/gds/doctor.js +3 -2
- package/scripts/gds/grade-correlation-audit.js +280 -0
- package/scripts/gds/grade-replay.js +237 -0
- package/scripts/gds/hooks/pre-push.js +7 -3
- package/scripts/gds/main-audit.js +19 -10
- package/scripts/gds/memory-map.js +3 -1
- package/scripts/gds/onboarding-config.js +3 -2
- package/scripts/gds/package-core.js +72 -1
- package/scripts/gds/plain-cards.js +4 -0
- package/scripts/gds/preflight.js +12 -4
- package/scripts/gds/provision-core-upgrade.js +282 -80
- package/scripts/gds/provision-repo-auth.js +15 -0
- package/scripts/gds/provision.js +1 -1
- package/scripts/gds/publish-credential-check.js +3 -0
- package/scripts/gds/push-main.js +14 -3
- package/scripts/gds/push-path-brief.js +3 -2
- package/scripts/gds/redteam-patrol.js +4 -4
- package/scripts/gds/release-notes.js +271 -0
- package/scripts/gds/run-routine.js +18 -5
- package/scripts/gds/run-unit-tests.js +14 -3
- package/scripts/gds/seed-board-room-tasks.js +1 -1
- package/scripts/gds/session-cost.js +12 -3
- package/scripts/gds/ship-deploy-target.js +11 -6
- package/scripts/gds/ship-flow.js +5 -3
- package/scripts/gds/ship-grade-steps.js +33 -0
- package/scripts/gds/ship-land.js +241 -16
- package/scripts/gds/ship-merge.js +3 -2
- package/scripts/gds/ship-preflight-steps.js +86 -11
- package/scripts/gds/ship.js +1 -1
- package/scripts/gds/stray-unit-sweep.sh +3 -1
- package/scripts/gds/upgrade-migrate.js +67 -0
- package/scripts/gds/upgrade.js +9 -7
- package/scripts/hall-preview/README.md +20 -3
- package/scripts/hall-preview/live-identity.js +1 -1
- package/scripts/hall-preview/refresh-fixtures.js +71 -11
- package/scripts/hall-preview/scrub-identity.js +296 -0
- package/src/bongos/routes/security.js +3 -3
- package/src/build-info.js +4 -2
- package/src/module-api.js +1 -1
- package/tests/adr_0089_scope.mjs +70 -0
- package/tests/adr_0162_citation.mjs +184 -0
- package/tests/autobongos_cadence.mjs +74 -0
- package/tests/autobongos_loop.mjs +114 -0
- package/tests/autonomy_gauge.mjs +56 -0
- package/tests/consumer_layout_boot.mjs +363 -31
- package/tests/core_upgrade_door.mjs +40 -6
- package/tests/core_upgrade_runner.mjs +329 -5
- package/tests/credential_presence.mjs +153 -0
- package/tests/db_backup_nightly.mjs +21 -3
- package/tests/db_backup_offsite.mjs +81 -0
- package/tests/deploy_audit_gate.mjs +5 -2
- package/tests/deploy_page_projects.mjs +63 -2
- package/tests/dev_lib.mjs +2 -1
- package/tests/dev_windows.mjs +324 -0
- package/tests/env_knobs_neutral_prefix.mjs +203 -0
- package/tests/fixtures/grade-replay/recorded-panel-grades.json +27 -0
- package/tests/government_protected_surfaces.mjs +10 -0
- package/tests/grade_correlation_audit.mjs +159 -0
- package/tests/grade_engine.mjs +249 -0
- package/tests/grade_engine_replay.mjs +99 -0
- package/tests/grader_prompt_window_bound.mjs +132 -0
- package/tests/grader_unbacked_score.mjs +194 -0
- package/tests/hall_preview_scrub.mjs +214 -0
- package/tests/hall_records.mjs +85 -0
- package/tests/helpers.mjs +25 -0
- package/tests/instance_db_migrated_on_code_move.mjs +8 -1
- package/tests/integration_parked_ratchet.mjs +24 -1
- package/tests/plain_cards.mjs +1 -1
- package/tests/provisioning_public_refusal.mjs +195 -0
- package/tests/publish_status_branch.mjs +128 -0
- package/tests/redteam_patrol.mjs +1 -1
- package/tests/release_notes.mjs +327 -0
- package/tests/shell_stat_portability.mjs +49 -0
- package/tests/ship_land_conflict.mjs +181 -0
- package/tests/ship_land_red_pr.mjs +236 -0
- package/tests/ship_preflight.mjs +226 -3
- package/tests/start_env_guard.mjs +16 -0
- package/tests/sweep_app_account.mjs +15 -2
- package/tests/upgrade.mjs +20 -1
- package/tests/upgrade_migrate.mjs +195 -0
|
@@ -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;
|
package/tests/redteam_patrol.mjs
CHANGED
|
@@ -83,7 +83,7 @@ test('disposition: red is reserved for findings that could not be recorded', ()
|
|
|
83
83
|
// A dry run prints; it never claims to have filed, so it never fails.
|
|
84
84
|
assert.deepEqual(p.disposition({ findingClasses: 3, doFile: false, blocker: null }),
|
|
85
85
|
{ action: 'dry-run', exitCode: 0 });
|
|
86
|
-
// Findings filed for adjudication = the patrol working as designed (ADR
|
|
86
|
+
// Findings filed for adjudication = the patrol working as designed (ADR 0158 §2).
|
|
87
87
|
// If this ever became a failure, a weekly red would train everyone to ignore it.
|
|
88
88
|
assert.deepEqual(p.disposition({ findingClasses: 3, doFile: true, blocker: null }),
|
|
89
89
|
{ action: 'file', exitCode: 0 });
|