@bongos/core 1.19.653 → 1.19.654
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 +15 -15
- package/docs/module-api-changelog.md +2 -0
- package/modules/ideas/routes/inbox.js +22 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/main-audit.js +22 -8
- package/scripts/gds/spark.js +26 -1
- package/src/module-api.js +1 -1
- package/tests/cli_exit_no_abort.mjs +109 -2
- package/tests/idea_develop_spark.mjs +43 -0
- package/tests/idea_resubmit.mjs +7 -3
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.654",
|
|
6
|
+
"core_contract": "1.19.654",
|
|
7
|
+
"source_commit": "29859bbb518d99362867ba988703fdbf1d4f9472",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-10T23:
|
|
9
|
+
"built_at": "2026-09-10T23:57:14.296Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 472,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 2606,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "c678060ed77c22de9946d24d4668466c564defa104b075a8076c1a5964fe937a",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/backlog-review/SKILL.md",
|
|
@@ -2782,7 +2782,7 @@
|
|
|
2782
2782
|
{
|
|
2783
2783
|
"path": "docs/module-api-changelog.md",
|
|
2784
2784
|
"mode": "0000644",
|
|
2785
|
-
"sha256": "
|
|
2785
|
+
"sha256": "d45c6981ad6bd64f4b0fc09e9d28fe87231b8d65da676620246f8eccc803f091"
|
|
2786
2786
|
},
|
|
2787
2787
|
{
|
|
2788
2788
|
"path": "docs/modules-contract.md",
|
|
@@ -5567,7 +5567,7 @@
|
|
|
5567
5567
|
{
|
|
5568
5568
|
"path": "modules/ideas/routes/inbox.js",
|
|
5569
5569
|
"mode": "0000644",
|
|
5570
|
-
"sha256": "
|
|
5570
|
+
"sha256": "0f5997e58ca3f8ee33e84948136697519f095670387f0ae26cadb325f2389268"
|
|
5571
5571
|
},
|
|
5572
5572
|
{
|
|
5573
5573
|
"path": "modules/ideas/routing.js",
|
|
@@ -7742,12 +7742,12 @@
|
|
|
7742
7742
|
{
|
|
7743
7743
|
"path": "package-lock.json",
|
|
7744
7744
|
"mode": "0000644",
|
|
7745
|
-
"sha256": "
|
|
7745
|
+
"sha256": "9370d237943d31161eb264dcd700e1fe6caea5f32a234e05cf17f94cdba7db5e"
|
|
7746
7746
|
},
|
|
7747
7747
|
{
|
|
7748
7748
|
"path": "package.json",
|
|
7749
7749
|
"mode": "0000644",
|
|
7750
|
-
"sha256": "
|
|
7750
|
+
"sha256": "bc4361fd3c305fcb47bc8769dbad015475a2f7ef4e1e6795c7027953e5b259ec"
|
|
7751
7751
|
},
|
|
7752
7752
|
{
|
|
7753
7753
|
"path": "public-docs/index.html",
|
|
@@ -8412,7 +8412,7 @@
|
|
|
8412
8412
|
{
|
|
8413
8413
|
"path": "scripts/gds/main-audit.js",
|
|
8414
8414
|
"mode": "0000644",
|
|
8415
|
-
"sha256": "
|
|
8415
|
+
"sha256": "4b27716f74bd13b0500c14b2607ea9f9e2584c4a27280bd075418772ac4dc644"
|
|
8416
8416
|
},
|
|
8417
8417
|
{
|
|
8418
8418
|
"path": "scripts/gds/me.js",
|
|
@@ -9012,7 +9012,7 @@
|
|
|
9012
9012
|
{
|
|
9013
9013
|
"path": "scripts/gds/spark.js",
|
|
9014
9014
|
"mode": "0000755",
|
|
9015
|
-
"sha256": "
|
|
9015
|
+
"sha256": "3cc086defcaf14df25db27646596f2332ac79dff97c95e72d4cff329d8db9189"
|
|
9016
9016
|
},
|
|
9017
9017
|
{
|
|
9018
9018
|
"path": "scripts/gds/spike-task-map.js",
|
|
@@ -9502,7 +9502,7 @@
|
|
|
9502
9502
|
{
|
|
9503
9503
|
"path": "src/module-api.js",
|
|
9504
9504
|
"mode": "0000644",
|
|
9505
|
-
"sha256": "
|
|
9505
|
+
"sha256": "175d5a93b971e2da73a24186a0e9ed7288da65aed61aa9f86d5a0c85f30bf53f"
|
|
9506
9506
|
},
|
|
9507
9507
|
{
|
|
9508
9508
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10107,7 +10107,7 @@
|
|
|
10107
10107
|
{
|
|
10108
10108
|
"path": "tests/cli_exit_no_abort.mjs",
|
|
10109
10109
|
"mode": "0000644",
|
|
10110
|
-
"sha256": "
|
|
10110
|
+
"sha256": "7b489890ab6e2baa6aefe784b81852eaaac39cdc50af5974de7eee815baee510"
|
|
10111
10111
|
},
|
|
10112
10112
|
{
|
|
10113
10113
|
"path": "tests/cli_lib_formatters.mjs",
|
|
@@ -11227,7 +11227,7 @@
|
|
|
11227
11227
|
{
|
|
11228
11228
|
"path": "tests/idea_develop_spark.mjs",
|
|
11229
11229
|
"mode": "0000644",
|
|
11230
|
-
"sha256": "
|
|
11230
|
+
"sha256": "fe55e01a0be2aa6ca0bc536469eefcee7d44f40dfa0767fedf1709aed3f7d81e"
|
|
11231
11231
|
},
|
|
11232
11232
|
{
|
|
11233
11233
|
"path": "tests/idea_full_capture.mjs",
|
|
@@ -11267,7 +11267,7 @@
|
|
|
11267
11267
|
{
|
|
11268
11268
|
"path": "tests/idea_resubmit.mjs",
|
|
11269
11269
|
"mode": "0000644",
|
|
11270
|
-
"sha256": "
|
|
11270
|
+
"sha256": "56ef1d4405ec63a9c095000411774f533b3e34cdb89184bd4351e58313dc2975"
|
|
11271
11271
|
},
|
|
11272
11272
|
{
|
|
11273
11273
|
"path": "tests/idea_route_attribution.mjs",
|
|
@@ -1755,5 +1755,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1755
1755
|
landed since 1.19.651 with no explicit bump. run 34529655648. (task 1002620)
|
|
1756
1756
|
1.19.653 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1757
1757
|
landed since 1.19.652 with no explicit bump. run 34543713846. (task 1002620)
|
|
1758
|
+
1.19.654 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1759
|
+
landed since 1.19.653 with no explicit bump. run 34544345437. (task 1002620)
|
|
1758
1760
|
---------------------------------------------------------------------------
|
|
1759
1761
|
```
|
|
@@ -63,6 +63,25 @@ function registerSeams() {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// The author-facing shape of a ratification window this request just opened —
|
|
67
|
+
// spelled once because all three open sites (capture, develop, resubmit) return
|
|
68
|
+
// the same thing to the same readers (task 1003654).
|
|
69
|
+
//
|
|
70
|
+
// `closes_at` is legitimately NULL: it is the no-clock constitution, not a
|
|
71
|
+
// missing value (modules/government/db.js — a NULL deadline never expires).
|
|
72
|
+
// That is exactly why `pass_rule` travels WITH it. A caller holding only the
|
|
73
|
+
// timestamp cannot tell a clockless sitting from a broken one, so it prints the
|
|
74
|
+
// raw null — which is what the R157 proof walk caught spark.js doing ("open
|
|
75
|
+
// until null"). With the rule in hand a renderer can say HOW the sitting closes
|
|
76
|
+
// instead, the way the Board Room's own clockLine() already does.
|
|
77
|
+
function boardWindowPayload(item) {
|
|
78
|
+
return {
|
|
79
|
+
item_id: item.id,
|
|
80
|
+
closes_at: item.closes_at ?? null,
|
|
81
|
+
pass_rule: (item.constitution && item.constitution.pass_rule) || null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
66
85
|
module.exports = function buildInboxRouter() {
|
|
67
86
|
registerSeams();
|
|
68
87
|
const router = express.Router();
|
|
@@ -227,7 +246,7 @@ module.exports = function buildInboxRouter() {
|
|
|
227
246
|
completenessScore: row.completeness_score,
|
|
228
247
|
});
|
|
229
248
|
if (w && w.opened && w.item) {
|
|
230
|
-
idea.board_window =
|
|
249
|
+
idea.board_window = boardWindowPayload(w.item);
|
|
231
250
|
} else if (w && w.reason === 'below_bar') {
|
|
232
251
|
// R09 (goal 1000069, criterion C2): a sub-bar idea returns to
|
|
233
252
|
// its author WITH A REASON THEY CAN ACT ON — the weakest section
|
|
@@ -673,7 +692,7 @@ module.exports = function buildInboxRouter() {
|
|
|
673
692
|
try {
|
|
674
693
|
if (government && government.openBoardWindowForIdea) {
|
|
675
694
|
const w = await government.openBoardWindowForIdea({ ideaId: id, completenessScore: row.completeness_score });
|
|
676
|
-
if (w && w.opened && w.item) board_window =
|
|
695
|
+
if (w && w.opened && w.item) board_window = boardWindowPayload(w.item);
|
|
677
696
|
else if (w && w.reason === 'below_bar') {
|
|
678
697
|
const flag = weakestSectionPayload(req.body.field_values, templateKey ? { templateKey } : undefined);
|
|
679
698
|
if (flag) out.rejection = flag; // still short — the NEW weakest section, fresh feedback
|
|
@@ -957,7 +976,7 @@ module.exports = function buildInboxRouter() {
|
|
|
957
976
|
const government = api.resolveOptional && api.resolveOptional('government');
|
|
958
977
|
if (government && government.openBoardWindowForIdea) {
|
|
959
978
|
const w = await government.openBoardWindowForIdea({ ideaId: id, completenessScore: row.completeness_score });
|
|
960
|
-
if (w && w.opened && w.item) board_window =
|
|
979
|
+
if (w && w.opened && w.item) board_window = boardWindowPayload(w.item);
|
|
961
980
|
else if (w && w.reason === 'below_bar') {
|
|
962
981
|
// The same honest feedback the resubmit path gives: which section is
|
|
963
982
|
// thinnest, computed from what was just written. Derived, never stored.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.654",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.654",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.654",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
|
@@ -35,12 +35,21 @@
|
|
|
35
35
|
// node scripts/gds/main-audit.js --strict # also block on unresolved authors
|
|
36
36
|
// node scripts/gds/main-audit.js --db # resolve ranks from Postgres instead of the API
|
|
37
37
|
// node scripts/gds/main-audit.js --require-roster # CI: an empty roster is a loud DID-NOT-RUN, not a vacuous green
|
|
38
|
+
//
|
|
39
|
+
// EXIT CONTRACT — never `process.exit()` here, use `cliExit(code)` + `return`
|
|
40
|
+
// (task 1003772). Every exit in this file lands after the roster request, and a
|
|
41
|
+
// hard exit mid-teardown aborts node natively on Windows (UV_HANDLE_CLOSING),
|
|
42
|
+
// printing the verdict and THEN dying at 127. That is worse here than in any
|
|
43
|
+
// other CLI: --verify-head IS the deploy gate, so the destroyed code makes a
|
|
44
|
+
// clean audit look like a refusal and a real blocker indistinguishable from a
|
|
45
|
+
// crash. Guarded by the static check in tests/cli_exit_no_abort.mjs.
|
|
46
|
+
// Codes: 0 = clean · 1 = blocker found · 2 = DID NOT RUN (never a pass).
|
|
38
47
|
|
|
39
48
|
'use strict';
|
|
40
49
|
|
|
41
50
|
const { execSync } = require('node:child_process');
|
|
42
51
|
const ppc = require(require('node:path').join(__dirname, '..', '..', 'src', 'bongos', 'permission-path-check.js'));
|
|
43
|
-
const { cliClient, API_BASE } = require('./cli-lib');
|
|
52
|
+
const { cliClient, cliExit, API_BASE } = require('./cli-lib');
|
|
44
53
|
|
|
45
54
|
// ---------- pure analyzer (tested in tests/main_audit.mjs) ----------
|
|
46
55
|
|
|
@@ -319,14 +328,16 @@ async function main() {
|
|
|
319
328
|
+ `${Number.isFinite(depth) ? `${depth} commit(s)` : 'unreadable'} deep. `
|
|
320
329
|
+ 'Pass --range <A..B> or set OTB_AUDIT_RANGE. This is a check that did not execute, not a clean audit.'
|
|
321
330
|
);
|
|
322
|
-
|
|
331
|
+
cliExit(2);
|
|
332
|
+
return;
|
|
323
333
|
}
|
|
324
334
|
}
|
|
325
335
|
|
|
326
336
|
const parsed = parseCommits(range);
|
|
327
337
|
if (parsed.error) {
|
|
328
338
|
console.error(`main-audit: ${parsed.error}`);
|
|
329
|
-
|
|
339
|
+
cliExit(2);
|
|
340
|
+
return;
|
|
330
341
|
}
|
|
331
342
|
const rankMap = await fetchRankMap({ useDb });
|
|
332
343
|
if (Object.keys(rankMap).length === 0) {
|
|
@@ -336,7 +347,8 @@ async function main() {
|
|
|
336
347
|
// clean audit (the same false-green shape parseCommits guards against).
|
|
337
348
|
if (process.argv.includes('--require-roster')) {
|
|
338
349
|
console.error('main-audit: DID NOT RUN — the roster is unavailable (offline / no session / public leaderboard unreachable). An empty rank map can never produce a blocker, so exiting green would be a vacuous pass. Fix connectivity or drop --require-roster for a best-effort local read.');
|
|
339
|
-
|
|
350
|
+
cliExit(2);
|
|
351
|
+
return;
|
|
340
352
|
}
|
|
341
353
|
console.error('main-audit: WARNING — could not load the GDS roster (offline / insufficient rank / no DB). Authors will be unresolved; run as a Metic+ builder or with --db (on the droplet) for a definitive audit.');
|
|
342
354
|
}
|
|
@@ -346,7 +358,8 @@ async function main() {
|
|
|
346
358
|
console.log(`\nMain audit (SEC #863) — range ${range}, ${parsed.commits.length} commit(s)`);
|
|
347
359
|
if (result.findings.length === 0) {
|
|
348
360
|
console.log(' ✓ clean — no sub-Metic edits to permission-sensitive paths.\n');
|
|
349
|
-
|
|
361
|
+
cliExit(0);
|
|
362
|
+
return;
|
|
350
363
|
}
|
|
351
364
|
for (const f of result.findings) {
|
|
352
365
|
const tag = f.severity === 'blocker' ? '\x1b[31m[BLOCKER]\x1b[0m' : '\x1b[33m[review]\x1b[0m';
|
|
@@ -364,16 +377,17 @@ async function main() {
|
|
|
364
377
|
// verify-head is the deploy gate: any blocker (and, unless allowed, any
|
|
365
378
|
// unresolved author) must stop the deploy.
|
|
366
379
|
if (verifyHead) {
|
|
367
|
-
|
|
380
|
+
cliExit(result.hardFail ? 1 : 0);
|
|
381
|
+
return;
|
|
368
382
|
}
|
|
369
383
|
// report mode: non-zero on a real blocker so CI can fail.
|
|
370
|
-
|
|
384
|
+
cliExit(result.hardFail ? 1 : 0);
|
|
371
385
|
}
|
|
372
386
|
|
|
373
387
|
if (require.main === module) {
|
|
374
388
|
main().catch((e) => {
|
|
375
389
|
console.error(`main-audit: ${e.message}`);
|
|
376
|
-
|
|
390
|
+
cliExit(2);
|
|
377
391
|
});
|
|
378
392
|
}
|
|
379
393
|
|
package/scripts/gds/spark.js
CHANGED
|
@@ -157,6 +157,31 @@ function templateSkeleton() {
|
|
|
157
157
|
return lines.join('\n');
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
// How the freshly-opened ratification window is described to its author.
|
|
161
|
+
//
|
|
162
|
+
// A NULL `closes_at` is the no-clock constitution, not a missing deadline —
|
|
163
|
+
// several pass rules run a sitting with no clock at all, and the window simply
|
|
164
|
+
// never expires. Printing the field raw turned that into "open until null" for
|
|
165
|
+
// every author on a clockless instance (task 1003654, caught five times by the
|
|
166
|
+
// R157 proof walk). So the rule decides the sentence, and the wording tracks
|
|
167
|
+
// the Board Room's own clockLine() (modules/hall-ui/public/board-room.js) —
|
|
168
|
+
// one vocabulary for one fact, whichever surface the author reads it on.
|
|
169
|
+
//
|
|
170
|
+
// ALL FOUR of config.js's PASS_RULES get a branch, `consent` included: it is
|
|
171
|
+
// not clock-safe (CLOCK_SAFE_PASS_RULES is majority + unanimous only), so a
|
|
172
|
+
// consent constitution is one of the likeliest to arrive here with no deadline
|
|
173
|
+
// at all. The default arm is for an unreadable rule, not for a known one.
|
|
174
|
+
function boardWindowLine(w) {
|
|
175
|
+
if (w.closes_at) return ` A board ratification window is open until ${w.closes_at}.`;
|
|
176
|
+
switch (w.pass_rule) {
|
|
177
|
+
case 'first_ratifier': return ' A board ratification window is open — no clock; it closes on the first vote.';
|
|
178
|
+
case 'consent': return ' A board ratification window is open — no clock; it stands unless someone objects.';
|
|
179
|
+
case 'majority': return ' A board ratification window is open — no clock; it waits for a majority.';
|
|
180
|
+
case 'unanimous': return ' A board ratification window is open — no clock; it waits for a unanimous yes.';
|
|
181
|
+
default: return ' A board ratification window is open — no clock; it stays open until the board decides.';
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
160
185
|
// What the developer is told once it lands. The completeness score is the number
|
|
161
186
|
// the R147 credit split and the board bar both read, so it is the headline.
|
|
162
187
|
function renderDeveloped(data) {
|
|
@@ -168,7 +193,7 @@ function renderDeveloped(data) {
|
|
|
168
193
|
? ' You had the thought and you developed it.'
|
|
169
194
|
: ` The thought stays credited to builder ${author_id}; you are recorded as its developer (${developer_id}).`);
|
|
170
195
|
if (board_window) {
|
|
171
|
-
lines.push(
|
|
196
|
+
lines.push(boardWindowLine(board_window));
|
|
172
197
|
} else if (idea.rejection) {
|
|
173
198
|
lines.push(` Not yet at the bar — thinnest section: ${idea.rejection.section_key}.`);
|
|
174
199
|
if (idea.rejection.reason_md) lines.push(` ${idea.rejection.reason_md}`);
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.654'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
// tests/cli_exit_no_abort.mjs — the GDS CLIs must not abort natively on exit
|
|
2
2
|
// (task 1002734; the pre-push twin of this is tests/pre_push_hook.mjs, task 1002725).
|
|
3
3
|
//
|
|
4
|
+
// Guards api.js AND main-audit.js (task 1003772). main-audit --verify-head IS the
|
|
5
|
+
// deploy gate (infra/deploy.sh calls it so a poisoned main never reaches
|
|
6
|
+
// production), so there the destroyed exit code is not cosmetic: the abort fires
|
|
7
|
+
// AFTER the verdict prints, and a caller reading the code cannot tell a clean
|
|
8
|
+
// audit from a real blocker. It reproduced 1/1 on Windows + Node 24 before the fix
|
|
9
|
+
// and went 5/5 clean after it.
|
|
10
|
+
//
|
|
4
11
|
// THE BUG. On Windows + Node 24, calling process.exit() after the CLI has made a
|
|
5
12
|
// request aborts the process before it can exit cleanly:
|
|
6
13
|
//
|
|
@@ -26,13 +33,24 @@ import { test } from 'node:test';
|
|
|
26
33
|
import { spawnSync, spawn } from 'node:child_process';
|
|
27
34
|
import fs from 'node:fs';
|
|
28
35
|
import path from 'node:path';
|
|
36
|
+
import { createRequire } from 'node:module';
|
|
29
37
|
import { fileURLToPath } from 'node:url';
|
|
30
38
|
|
|
31
39
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
32
40
|
const API = path.join(ROOT, 'scripts', 'gds', 'api.js');
|
|
41
|
+
const MAIN_AUDIT = path.join(ROOT, 'scripts', 'gds', 'main-audit.js');
|
|
33
42
|
const CLI_LIB = path.join(ROOT, 'scripts', 'gds', 'cli-lib.js');
|
|
34
43
|
const DEAD_BASE = 'http://127.0.0.1:9'; // discard port — connection refused, instantly
|
|
35
44
|
|
|
45
|
+
// Point a CLI at a base URL. DERIVED, not hardcoded: instance-config resolves
|
|
46
|
+
// <PREFIX>_API_BASE first and only then the legacy GDS_/OTB_/PMS_ spellings, so a
|
|
47
|
+
// literal here would (a) add to the legacy-GDS ratchet the rename is retiring and
|
|
48
|
+
// (b) silently stop steering the CLI on any instance whose brand pack sets a
|
|
49
|
+
// different prefix — the test would then hit the REAL API instead of the stub.
|
|
50
|
+
const { envPrefix } = createRequire(import.meta.url)(path.join(ROOT, 'src', 'instance-config.js'));
|
|
51
|
+
const API_BASE_ENV = `${envPrefix()}_API_BASE`;
|
|
52
|
+
const atBase = (url) => ({ [API_BASE_ENV]: url });
|
|
53
|
+
|
|
36
54
|
// The native abort prints this and nothing else can be relied on; it is NOT a
|
|
37
55
|
// JS-level error, so it never appears as a caught exception.
|
|
38
56
|
const ABORT_RE = /Assertion failed|UV_HANDLE_CLOSING/i;
|
|
@@ -41,7 +59,7 @@ function runApi(args, env = {}) {
|
|
|
41
59
|
return spawnSync(process.execPath, [API, ...args], {
|
|
42
60
|
cwd: ROOT,
|
|
43
61
|
encoding: 'utf8',
|
|
44
|
-
env: { ...process.env,
|
|
62
|
+
env: { ...process.env, ...atBase(DEAD_BASE), ...env },
|
|
45
63
|
});
|
|
46
64
|
}
|
|
47
65
|
|
|
@@ -104,7 +122,7 @@ test('api.js exits 0 after a SUCCESSFUL request — the path that aborted in the
|
|
|
104
122
|
const { proc, port } = await startStub();
|
|
105
123
|
try {
|
|
106
124
|
for (let i = 0; i < 3; i++) {
|
|
107
|
-
const r = runApi(['GET', '/api/gds/me', '--quiet'],
|
|
125
|
+
const r = runApi(['GET', '/api/gds/me', '--quiet'], atBase(`http://127.0.0.1:${port}`));
|
|
108
126
|
assert.doesNotMatch(r.stderr || '', ABORT_RE, `run ${i + 1} aborted natively after a 200:\n${r.stderr}`);
|
|
109
127
|
assert.equal(r.status, 0, `a successful request must exit 0, got ${r.status} (127 = the abort); stderr: ${r.stderr}`);
|
|
110
128
|
assert.match(r.stdout, /"ok"/, 'the JSON body must still reach stdout');
|
|
@@ -133,3 +151,92 @@ test('api.js routes its post-request exits through cliExit, not process.exit', (
|
|
|
133
151
|
);
|
|
134
152
|
assert.match(afterRequest, /cliExit\(/, 'the post-request paths must set the exit code via cliExit');
|
|
135
153
|
});
|
|
154
|
+
|
|
155
|
+
// ---- main-audit.js — the deploy gate (task 1003772) ------------------------
|
|
156
|
+
// Ranges are always HEAD..HEAD: it resolves on every checkout including a shallow
|
|
157
|
+
// CI clone, audits zero commits, and still runs the WHOLE pipeline — fetchRankMap
|
|
158
|
+
// is called before the verdict, so the exit is a post-request exit either way.
|
|
159
|
+
//
|
|
160
|
+
// THE STATIC CHECK IS THE REAL NET, and this was measured, not assumed: with the
|
|
161
|
+
// hard exit deliberately put back, the static test failed while all four
|
|
162
|
+
// behavioural tests below still PASSED. The same run against a real range
|
|
163
|
+
// (`--range origin/main~15..origin/main`, 31 commits, a live roster call) aborted
|
|
164
|
+
// at exit 127. So the abort needs the full stack to line up — enough commits and a
|
|
165
|
+
// real request — which a network-free test cannot stage, exactly as the api.js
|
|
166
|
+
// note above says. The behavioural tests are still worth their runtime: they pin
|
|
167
|
+
// the exit CODES (0 / 2) that callers branch on, and their timeouts catch the
|
|
168
|
+
// documented conversion trap where a leaked timer turns the crash into a hang.
|
|
169
|
+
|
|
170
|
+
function runAudit(args, env = {}) {
|
|
171
|
+
return spawnSync(process.execPath, [MAIN_AUDIT, ...args], {
|
|
172
|
+
cwd: ROOT,
|
|
173
|
+
encoding: 'utf8',
|
|
174
|
+
timeout: 60_000, // dropping a hard exit trades a crash for a HANG if a timer leaks
|
|
175
|
+
env: { ...process.env, ...atBase(DEAD_BASE), ...env },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
test('main-audit.js routes every exit through cliExit, not process.exit', () => {
|
|
180
|
+
const src = fs.readFileSync(MAIN_AUDIT, 'utf8');
|
|
181
|
+
const stripped = src.replace(/\/\/[^\n]*/g, '');
|
|
182
|
+
// Unlike api.js, main-audit has no pre-request exits worth keeping separate —
|
|
183
|
+
// all seven sites were converted, so the guard is simply "none left".
|
|
184
|
+
assert.ok(
|
|
185
|
+
!/process\.exit\s*\(/.test(stripped),
|
|
186
|
+
'no process.exit() may remain in main-audit.js — that is what aborts node on Windows, '
|
|
187
|
+
+ 'and this script IS the deploy gate whose exit code the abort destroys'
|
|
188
|
+
);
|
|
189
|
+
assert.match(src, /cliExit\(/, 'the exit paths must set the exit code via cliExit');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('main-audit.js exits cleanly after a FAILED roster request', () => {
|
|
193
|
+
const r = runAudit(['--range', 'HEAD..HEAD']);
|
|
194
|
+
assert.notEqual(r.signal, 'SIGTERM', 'timed out — a leaked timer turned the crash into a hang');
|
|
195
|
+
assert.doesNotMatch(r.stderr || '', ABORT_RE, `native abort on the audit path:\n${r.stderr}`);
|
|
196
|
+
assert.equal(r.status, 0, `a clean audit must exit 0, not ${r.status} (127 = the abort)`);
|
|
197
|
+
assert.match(r.stdout, /✓ clean/, 'the verdict must still reach stdout');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('main-audit.js --verify-head exits 0 on a clean audit — the deploy gate reads this code', () => {
|
|
201
|
+
// The gate path specifically: infra/deploy.sh treats non-zero as "do not deploy",
|
|
202
|
+
// so an abort here blocks a deploy that nothing is actually wrong with.
|
|
203
|
+
for (let i = 0; i < 3; i++) {
|
|
204
|
+
const r = runAudit(['--verify-head', '--range', 'HEAD..HEAD']);
|
|
205
|
+
assert.notEqual(r.signal, 'SIGTERM', `run ${i + 1} timed out — leaked handle`);
|
|
206
|
+
assert.doesNotMatch(r.stderr || '', ABORT_RE, `run ${i + 1} aborted natively:\n${r.stderr}`);
|
|
207
|
+
assert.equal(r.status, 0, `run ${i + 1} exited ${r.status}; stderr: ${r.stderr}`);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('main-audit.js --require-roster still exits 2 when the roster is unreachable', () => {
|
|
212
|
+
// The post-request DID-NOT-RUN path. Converting it to cliExit must not turn a
|
|
213
|
+
// refusal into a vacuous green — exit 2 is what stops CI calling this a pass.
|
|
214
|
+
const r = runAudit(['--require-roster', '--range', 'HEAD..HEAD']);
|
|
215
|
+
assert.notEqual(r.signal, 'SIGTERM', 'timed out — leaked handle');
|
|
216
|
+
assert.doesNotMatch(r.stderr || '', ABORT_RE);
|
|
217
|
+
assert.equal(r.status, 2, `an unavailable roster must exit 2, got ${r.status}`);
|
|
218
|
+
assert.match(r.stderr, /DID NOT RUN/);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('main-audit.js reports an unresolvable range as exit 2 (pre-request path)', () => {
|
|
222
|
+
const r = runAudit(['--range', 'bogusref..HEAD']);
|
|
223
|
+
assert.doesNotMatch(r.stderr || '', ABORT_RE);
|
|
224
|
+
assert.equal(r.status, 2, 'an unresolvable range is exit 2, and must not be mistaken for clean');
|
|
225
|
+
assert.match(r.stderr, /DID NOT RUN/);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('main-audit.js exits 0 after a SUCCESSFUL roster request — the teardown that aborted in the wild', async () => {
|
|
229
|
+
// A refused connection tears down far less machinery than a 200. The wild repro
|
|
230
|
+
// was a successful roster call, so this is the case that actually matters.
|
|
231
|
+
const { proc, port } = await startStub();
|
|
232
|
+
try {
|
|
233
|
+
for (let i = 0; i < 3; i++) {
|
|
234
|
+
const r = runAudit(['--range', 'HEAD..HEAD'], atBase(`http://127.0.0.1:${port}`));
|
|
235
|
+
assert.notEqual(r.signal, 'SIGTERM', `run ${i + 1} timed out — leaked handle`);
|
|
236
|
+
assert.doesNotMatch(r.stderr || '', ABORT_RE, `run ${i + 1} aborted natively after a 200:\n${r.stderr}`);
|
|
237
|
+
assert.equal(r.status, 0, `run ${i + 1} exited ${r.status} (127 = the abort); stderr: ${r.stderr}`);
|
|
238
|
+
}
|
|
239
|
+
} finally {
|
|
240
|
+
proc.kill();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
@@ -382,6 +382,49 @@ await test('the CLI names both parties, and never prints "undefined"', () => {
|
|
|
382
382
|
assert.doesNotMatch(out, /undefined|null/);
|
|
383
383
|
});
|
|
384
384
|
|
|
385
|
+
// task 1003654 — the R157 proof walk read "A board ratification window is open
|
|
386
|
+
// until null" five times on cloudbongos.com. A NULL closes_at is the no-clock
|
|
387
|
+
// constitution, NOT a broken window, so the copy has to name how the sitting
|
|
388
|
+
// closes. One case per pass rule, plus the unknown-rule fallback, because the
|
|
389
|
+
// whole failure was one branch that assumed a deadline always exists.
|
|
390
|
+
for (const [rule, expected] of [
|
|
391
|
+
['first_ratifier', /no clock; it closes on the first vote/],
|
|
392
|
+
['consent', /no clock; it stands unless someone objects/],
|
|
393
|
+
['majority', /no clock; it waits for a majority/],
|
|
394
|
+
['unanimous', /no clock; it waits for a unanimous yes/],
|
|
395
|
+
[null, /no clock; it stays open until the board decides/],
|
|
396
|
+
]) {
|
|
397
|
+
await test(`a clockless ${rule || 'unknown-rule'} window says how it closes, never "until null"`, () => {
|
|
398
|
+
const out = cli.renderDeveloped({
|
|
399
|
+
idea: { id: '77', completeness_score: 88 },
|
|
400
|
+
author_id: '9', developer_id: '42', self_developed: false,
|
|
401
|
+
board_window: { item_id: '3', closes_at: null, pass_rule: rule },
|
|
402
|
+
});
|
|
403
|
+
assert.match(out, /ratification window is open/);
|
|
404
|
+
assert.match(out, expected);
|
|
405
|
+
assert.doesNotMatch(out, /until null|undefined/);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Held equal to government's own rule list, the way board.js's FULL_IDEA_PASS_BAR
|
|
410
|
+
// is held equal to economy's — the CLI cannot require() a sibling module, so the
|
|
411
|
+
// test is where the two lists meet. Without this, adding a fifth pass rule
|
|
412
|
+
// silently drops its authors onto the unreadable-rule fallback.
|
|
413
|
+
await test('EVERY pass rule government declares gets its own sentence, not the fallback', () => {
|
|
414
|
+
const { PASS_RULES } = require('../modules/government/config.js');
|
|
415
|
+
const fallback = cli.renderDeveloped({
|
|
416
|
+
idea: { id: '77', completeness_score: 88 }, author_id: '9', developer_id: '42', self_developed: false,
|
|
417
|
+
board_window: { item_id: '3', closes_at: null, pass_rule: '__no_such_rule__' },
|
|
418
|
+
});
|
|
419
|
+
for (const rule of PASS_RULES) {
|
|
420
|
+
const out = cli.renderDeveloped({
|
|
421
|
+
idea: { id: '77', completeness_score: 88 }, author_id: '9', developer_id: '42', self_developed: false,
|
|
422
|
+
board_window: { item_id: '3', closes_at: null, pass_rule: rule },
|
|
423
|
+
});
|
|
424
|
+
assert.notEqual(out, fallback, `pass rule '${rule}' falls through to the unreadable-rule fallback`);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
385
428
|
await test('self-development reads as one person, not a handoff to yourself', () => {
|
|
386
429
|
const out = cli.renderDeveloped({ idea: { id: '1', completeness_score: 70 }, author_id: '9', developer_id: '9', self_developed: true });
|
|
387
430
|
assert.match(out, /You had the thought and you developed it/);
|
package/tests/idea_resubmit.mjs
CHANGED
|
@@ -25,7 +25,7 @@ const api = require('../src/module-api.js');
|
|
|
25
25
|
|
|
26
26
|
// ---- the government port stub (registered BEFORE the routes load) ----------
|
|
27
27
|
let rejectionAnswer = null; // what latestRejectionForIdea returns
|
|
28
|
-
let windowAnswer = { opened: true, item: { id: '31', closes_at: null } };
|
|
28
|
+
let windowAnswer = { opened: true, item: { id: '31', closes_at: null, constitution: { pass_rule: 'first_ratifier' } } };
|
|
29
29
|
const portCalls = [];
|
|
30
30
|
if (!api.hasProvider('government')) {
|
|
31
31
|
api.registerProvider('government', {
|
|
@@ -84,7 +84,7 @@ const fullIdea = (over = {}) => ({
|
|
|
84
84
|
let passed = 0, failed = 0;
|
|
85
85
|
async function test(name, fn) {
|
|
86
86
|
queries = []; portCalls.length = 0;
|
|
87
|
-
rejectionAnswer = null; windowAnswer = { opened: true, item: { id: '31', closes_at: null } };
|
|
87
|
+
rejectionAnswer = null; windowAnswer = { opened: true, item: { id: '31', closes_at: null, constitution: { pass_rule: 'first_ratifier' } } };
|
|
88
88
|
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
89
89
|
catch (err) { failed++; console.error(` FAIL ${name}\n ${err.message}`); }
|
|
90
90
|
}
|
|
@@ -113,7 +113,11 @@ await test('a board objection outranks the mechanical flag, and an ANSWERED sect
|
|
|
113
113
|
assert.equal(r.payload.flagged_section, 'purpose', 'the human objection wins over the weakest-section flag');
|
|
114
114
|
assert.equal(r.payload.flagged_section_changed, true);
|
|
115
115
|
assert.equal(r.payload.rejection_source, 'board');
|
|
116
|
-
|
|
116
|
+
// The pass rule rides along with the deadline (task 1003654): a clockless
|
|
117
|
+
// sitting reports closes_at null, and without the rule a caller cannot tell
|
|
118
|
+
// that apart from a broken window — which is how "open until null" reached
|
|
119
|
+
// authors in the first place.
|
|
120
|
+
assert.deepEqual(r.payload.board_window, { item_id: '31', closes_at: null, pass_rule: 'first_ratifier' }, 'a new window opened, and it says how it closes');
|
|
117
121
|
});
|
|
118
122
|
|
|
119
123
|
await test('reworking everything EXCEPT the flagged section reads changed:false', async () => {
|