@bongos/core 1.19.705 → 1.19.706
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 +17 -12
- package/docs/module-api-changelog.md +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/smoke-write.sh +88 -0
- package/src/module-api.js +1 -1
- package/tests/smoke_write_contract.mjs +110 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
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.706",
|
|
6
|
+
"core_contract": "1.19.706",
|
|
7
|
+
"source_commit": "64c9df530070e324d8e24759cabe9f99b5f72e84",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-13T05:
|
|
9
|
+
"built_at": "2026-09-13T05:25:47.946Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 477,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2135,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2636,
|
|
20
|
+
"tree_sha256": "153b7b8ec9f73145d3a44c31dd75daa089b2c94be32c2b91ad61e59270e89c69",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2802,7 +2802,7 @@
|
|
|
2802
2802
|
{
|
|
2803
2803
|
"path": "docs/module-api-changelog.md",
|
|
2804
2804
|
"mode": "0000644",
|
|
2805
|
-
"sha256": "
|
|
2805
|
+
"sha256": "68adee401731b2252b8f8e5c32d1164da7abb13261d0bc9aa1e8b83ead4a2a99"
|
|
2806
2806
|
},
|
|
2807
2807
|
{
|
|
2808
2808
|
"path": "docs/modules-contract.md",
|
|
@@ -7782,12 +7782,12 @@
|
|
|
7782
7782
|
{
|
|
7783
7783
|
"path": "package-lock.json",
|
|
7784
7784
|
"mode": "0000644",
|
|
7785
|
-
"sha256": "
|
|
7785
|
+
"sha256": "c22b832366994675cd2cb205017f9d775c35f89cafd4ecbf2ff4a13f5e8e218c"
|
|
7786
7786
|
},
|
|
7787
7787
|
{
|
|
7788
7788
|
"path": "package.json",
|
|
7789
7789
|
"mode": "0000644",
|
|
7790
|
-
"sha256": "
|
|
7790
|
+
"sha256": "e85ec6c7db1b0f2ba643ff4178049b28c57c43ba417feff7c54a4184a6da111f"
|
|
7791
7791
|
},
|
|
7792
7792
|
{
|
|
7793
7793
|
"path": "public-docs/index.html",
|
|
@@ -9057,7 +9057,7 @@
|
|
|
9057
9057
|
{
|
|
9058
9058
|
"path": "scripts/gds/smoke-write.sh",
|
|
9059
9059
|
"mode": "0000644",
|
|
9060
|
-
"sha256": "
|
|
9060
|
+
"sha256": "beb3dd4f828a8fe6e3a800ce6481b373af00b10dad5b11f7cc9dbaaf6aa2dc1a"
|
|
9061
9061
|
},
|
|
9062
9062
|
{
|
|
9063
9063
|
"path": "scripts/gds/spark.js",
|
|
@@ -9562,7 +9562,7 @@
|
|
|
9562
9562
|
{
|
|
9563
9563
|
"path": "src/module-api.js",
|
|
9564
9564
|
"mode": "0000644",
|
|
9565
|
-
"sha256": "
|
|
9565
|
+
"sha256": "ea041db8186de51e61bdb52281d9d8a57627b10514f94a39e4120d9888c05aa5"
|
|
9566
9566
|
},
|
|
9567
9567
|
{
|
|
9568
9568
|
"path": "src/module-loader/catalog.js",
|
|
@@ -12789,6 +12789,11 @@
|
|
|
12789
12789
|
"mode": "0000644",
|
|
12790
12790
|
"sha256": "f9b1a7021c96c02a5eb667b2d10003b4c7e256fcaafbbc228b2585c710f1d162"
|
|
12791
12791
|
},
|
|
12792
|
+
{
|
|
12793
|
+
"path": "tests/smoke_write_contract.mjs",
|
|
12794
|
+
"mode": "0000644",
|
|
12795
|
+
"sha256": "d4938674f77288a6efd6cb0d03dc0d4faf76716be4c6e71fc16712022ca08dec"
|
|
12796
|
+
},
|
|
12792
12797
|
{
|
|
12793
12798
|
"path": "tests/social_mode.mjs",
|
|
12794
12799
|
"mode": "0000644",
|
|
@@ -1869,5 +1869,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1869
1869
|
landed since 1.19.703 with no explicit bump. run 34738864605. (task 1002620)
|
|
1870
1870
|
1.19.705 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1871
1871
|
landed since 1.19.704 with no explicit bump. run 34739785273. (task 1002620)
|
|
1872
|
+
1.19.706 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1873
|
+
landed since 1.19.705 with no explicit bump. run 34740247460. (task 1002620)
|
|
1872
1874
|
---------------------------------------------------------------------------
|
|
1873
1875
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.706",
|
|
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.706",
|
|
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.706",
|
|
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",
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
# session_logs row exists for this claim
|
|
12
12
|
# public/recent-shipped does NOT contain the smoke title
|
|
13
13
|
# public/progress weighting unchanged (smoke tasks filtered)
|
|
14
|
+
# 6. On PASS, abandon the throwaway task so the ledger does not refill with
|
|
15
|
+
# test noise (task 1002018). A FAILED run deliberately KEEPS its task and
|
|
16
|
+
# claim so the failure can be read from the ledger afterwards.
|
|
14
17
|
#
|
|
15
18
|
# The synthetic task has credits_reward=0 so no credit_log row is written
|
|
16
19
|
# and the leaderboard is unaffected. The credit-award path is implicitly
|
|
@@ -68,6 +71,51 @@ VERSION_ID=$(node "$(dirname "$0")/version-select.js") || {
|
|
|
68
71
|
}
|
|
69
72
|
echo "Version: ${VERSION_ID}"
|
|
70
73
|
|
|
74
|
+
# Caused by task 1003598 (BV1.R11, ADR 0250 D4), found and fixed under task
|
|
75
|
+
# 1002018: goal_id became REQUIRED on POST /tasks and this script still sent none, so step 1 died on a 400 goal_id_required
|
|
76
|
+
# and the whole smoke could not run. Resolved at RUNTIME, not hardcoded, for the
|
|
77
|
+
# same reason version-select.js exists (task 1003166): an id literal goes stale
|
|
78
|
+
# the moment the version rolls, and the failure then looks like a broken API
|
|
79
|
+
# rather than a stale constant. Every version auto-creates exactly ONE
|
|
80
|
+
# is_maintenance goal (BV1.R18), and a synthetic row is maintenance-goal work.
|
|
81
|
+
#
|
|
82
|
+
# node, not python3, on purpose: this script already shells out to node for
|
|
83
|
+
# version-select.js, and node is the interpreter a Windows builder box actually
|
|
84
|
+
# has — the python3 the rest of this file depends on is frequently absent there
|
|
85
|
+
# (it is the Microsoft Store stub), so a python3 step cannot be verified locally.
|
|
86
|
+
GOAL_ID=$(call GET "/api/gds/goals?version=${VERSION_ID}" | node -e '
|
|
87
|
+
let s = "";
|
|
88
|
+
process.stdin.on("data", (d) => { s += d; });
|
|
89
|
+
process.stdin.on("end", () => {
|
|
90
|
+
const goals = JSON.parse(s).goals || [];
|
|
91
|
+
const m = goals.filter((g) => g.is_maintenance && g.status === "open");
|
|
92
|
+
if (m.length !== 1) {
|
|
93
|
+
process.stderr.write("expected exactly 1 open maintenance goal, found " + m.length + "\n");
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
// The caller splices this straight into a python3 -c SOURCE string, unquoted,
|
|
97
|
+
// so a non-numeric id would be executed as Python rather than read as data.
|
|
98
|
+
// Validating the COUNT is not validating the ID: check its shape too.
|
|
99
|
+
const id = String(m[0].id);
|
|
100
|
+
if (!/^[0-9]+$/.test(id)) {
|
|
101
|
+
process.stderr.write("goal id is not numeric: " + JSON.stringify(id) + "\n");
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
process.stdout.write(id);
|
|
105
|
+
});
|
|
106
|
+
') || {
|
|
107
|
+
echo "FAIL: could not resolve the maintenance goal for ${VERSION_ID}" >&2
|
|
108
|
+
exit 1
|
|
109
|
+
}
|
|
110
|
+
# Belt-and-braces at the point of use: the resolver above already refuses a
|
|
111
|
+
# non-numeric id, but this value is interpolated into python3 source below as
|
|
112
|
+
# CODE, not as a quoted literal, so it is re-checked where the risk actually is.
|
|
113
|
+
[[ "${GOAL_ID}" =~ ^[0-9]+$ ]] || {
|
|
114
|
+
echo "FAIL: resolved goal id is not numeric: ${GOAL_ID}" >&2
|
|
115
|
+
exit 1
|
|
116
|
+
}
|
|
117
|
+
echo "Goal: ${GOAL_ID}"
|
|
118
|
+
|
|
71
119
|
echo "--- 0. snapshot progress before ---"
|
|
72
120
|
PROGRESS_BEFORE=$(snapshot_progress)
|
|
73
121
|
echo "${PROGRESS_BEFORE}"
|
|
@@ -78,6 +126,7 @@ CREATE_BODY=$(python3 -c "
|
|
|
78
126
|
import json
|
|
79
127
|
print(json.dumps({
|
|
80
128
|
'version_id': '${VERSION_ID}',
|
|
129
|
+
'goal_id': ${GOAL_ID},
|
|
81
130
|
'title': '${SMOKE_TITLE}',
|
|
82
131
|
'description': 'Synthetic smoke task. Auto-shipped by smoke-write.sh.',
|
|
83
132
|
'status': 'ready',
|
|
@@ -177,6 +226,45 @@ else
|
|
|
177
226
|
echo "SKIP session_logs DB check (set GDS_SMOKE_DB_SSH + GDS_SMOKE_DB_NAME to enable)"
|
|
178
227
|
fi
|
|
179
228
|
|
|
229
|
+
echo
|
|
230
|
+
echo "--- 5. clean up the throwaway task ---"
|
|
231
|
+
# task 1002018 — this smoke CREATES a real task to exercise the create API, and
|
|
232
|
+
# used to leave it in the ledger forever: 94 rows accumulated under PMS-V1 before
|
|
233
|
+
# the task-2016 sweep cleared them by hand. Abandon is the right terminal state —
|
|
234
|
+
# a smoke row is not shipped work, and the route refuses to abandon a shipped one
|
|
235
|
+
# anyway (cannot_abandon_shipped). The row sits at 'completed' after step 3, and
|
|
236
|
+
# only 'shipped' and an unreleased claim are refused, so this is always legal here.
|
|
237
|
+
#
|
|
238
|
+
# ONLY on success, deliberately: a FAILED run leaves its task and claim in place
|
|
239
|
+
# so the failure can be inspected in the ledger, which is why the FAIL branch of
|
|
240
|
+
# the summary below still prints both ids.
|
|
241
|
+
#
|
|
242
|
+
# Strictly best-effort. A cleanup that cannot run must never turn a passing smoke
|
|
243
|
+
# into a failing one, so this never touches ${fail} and never trips set -e:
|
|
244
|
+
# task.abandon is floor-metic (ADR 0152), so a Xenos/Thetes token gets a 403 here
|
|
245
|
+
# while every assertion above genuinely passed.
|
|
246
|
+
cleanup_throwaway_task() {
|
|
247
|
+
local out code
|
|
248
|
+
local reason='{"reason":"smoke cleanup (task 1002018) - write-path smoke passed; the throwaway row has served its purpose"}'
|
|
249
|
+
out=$(curl -sS -X POST \
|
|
250
|
+
-H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" \
|
|
251
|
+
-d "${reason}" -w "\n%{http_code}" \
|
|
252
|
+
"${API}/api/gds/tasks/${TASK_ID}/abandon") || true
|
|
253
|
+
code="${out##*$'\n'}"
|
|
254
|
+
case "${code}" in
|
|
255
|
+
2*) echo "PASS cleaned up throwaway task #${TASK_ID} (abandoned)" ;;
|
|
256
|
+
403) echo "SKIP cleanup — task.abandon needs Metic+ (ADR 0152); task #${TASK_ID} left in the ledger" ;;
|
|
257
|
+
000) echo "SKIP cleanup — POST /tasks/${TASK_ID}/abandon got no response; task #${TASK_ID} left in the ledger" ;;
|
|
258
|
+
*) echo "SKIP cleanup — POST /tasks/${TASK_ID}/abandon returned HTTP ${code:-none}; task #${TASK_ID} left in the ledger" ;;
|
|
259
|
+
esac
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if [[ "${fail}" -eq 0 ]]; then
|
|
263
|
+
cleanup_throwaway_task
|
|
264
|
+
else
|
|
265
|
+
echo "SKIP cleanup — run FAILED; task #${TASK_ID} and claim #${CLAIM_ID} kept for post-mortem"
|
|
266
|
+
fi
|
|
267
|
+
|
|
180
268
|
echo
|
|
181
269
|
echo "--- summary ---"
|
|
182
270
|
if [[ "${fail}" -eq 0 ]]; then
|
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.706'; // 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');
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// tests/smoke_write_contract.mjs — the write-path smoke's own contract
|
|
2
|
+
// (task 1002018).
|
|
3
|
+
//
|
|
4
|
+
// TWO DEFECTS, ONE FILE. Both were found by trying to RUN scripts/gds/smoke-write.sh
|
|
5
|
+
// rather than by reading it, and neither was visible to any existing test:
|
|
6
|
+
//
|
|
7
|
+
// 1. IT COULD NOT RUN AT ALL. goal_id became required on POST /tasks (task 1003598,
|
|
8
|
+
// ADR 0250 D4) and this script still sent none, so step 1 died on a 400
|
|
9
|
+
// goal_id_required. The production write-path smoke had been dead since that
|
|
10
|
+
// landed, and nothing said so — the same silent-rot shape as the PMS-V1 version
|
|
11
|
+
// literal that tests/version_literals.mjs exists to prevent, one field over.
|
|
12
|
+
//
|
|
13
|
+
// 2. IT LEAKED ITS THROWAWAY TASK. The smoke creates a REAL task to exercise the
|
|
14
|
+
// create API and never disposed of it: 94 rows accumulated under PMS-V1 before
|
|
15
|
+
// the task-2016 sweep cleared them by hand.
|
|
16
|
+
//
|
|
17
|
+
// WHY ASSERT ON SOURCE TEXT. This script talks to a live API, needs a session
|
|
18
|
+
// token and (for the rest of the file) python3, so it cannot be executed in the
|
|
19
|
+
// DB-free unit gate. Source assertions are what CAN run here. They are written
|
|
20
|
+
// narrowly — each one names a specific defect that actually happened, and the
|
|
21
|
+
// resolution assertions deliberately REJECT a hardcoded id rather than merely
|
|
22
|
+
// requiring the field to be present, because "send goal_id" is satisfied just as
|
|
23
|
+
// well by a literal that goes stale the moment the version rolls. That is the
|
|
24
|
+
// over-broad-source-assertion trap this repo keeps relearning (learning 1000196):
|
|
25
|
+
// the cure is specificity, not a bigger regex.
|
|
26
|
+
import { strict as assert } from 'node:assert';
|
|
27
|
+
import { test } from 'node:test';
|
|
28
|
+
import fs from 'node:fs';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
|
|
32
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
33
|
+
const SH = fs.readFileSync(path.join(ROOT, 'scripts', 'gds', 'smoke-write.sh'), 'utf8');
|
|
34
|
+
|
|
35
|
+
// ---- defect 1: the create must carry a goal_id, resolved not hardcoded -----
|
|
36
|
+
|
|
37
|
+
test('the create body sends goal_id at all (the 400 that killed the smoke)', () => {
|
|
38
|
+
assert.match(SH, /'goal_id':/, 'POST /tasks has required goal_id since task 1003598');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('goal_id is the RESOLVED id, never a literal', () => {
|
|
42
|
+
assert.match(SH, /'goal_id': \$\{GOAL_ID\}/, 'the create interpolates the resolved id');
|
|
43
|
+
assert.ok(
|
|
44
|
+
!/'goal_id': *[0-9]/.test(SH),
|
|
45
|
+
'a numeric goal id in the body would go stale the moment the version rolls'
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('GOAL_ID is resolved at runtime, from the is_maintenance flag', () => {
|
|
50
|
+
assert.match(SH, /GOAL_ID=\$\(call GET "\/api\/gds\/goals\?version=\$\{VERSION_ID\}"/,
|
|
51
|
+
'it asks the API for this version-s goals');
|
|
52
|
+
assert.match(SH, /is_maintenance/, 'and picks the maintenance goal by its flag');
|
|
53
|
+
// Exactly one open maintenance goal exists per version (BV1.R18). Ambiguity here
|
|
54
|
+
// means the instance is misconfigured, and guessing would file the smoke row into
|
|
55
|
+
// a real working area — so the resolution must refuse rather than pick.
|
|
56
|
+
assert.match(SH, /m\.length !== 1/, 'it refuses on anything but exactly one match');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ---- defect 2: the throwaway task is disposed of ---------------------------
|
|
60
|
+
|
|
61
|
+
test('a passing run abandons its throwaway task', () => {
|
|
62
|
+
assert.match(SH, /cleanup_throwaway_task\(\) \{/, 'the cleanup exists');
|
|
63
|
+
assert.match(SH, /tasks\/\$\{TASK_ID\}\/abandon/, 'and it abandons the row it created');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('cleanup runs ONLY on success — a failed run keeps its evidence', () => {
|
|
67
|
+
const guard = SH.slice(SH.indexOf('--- 5. clean up'));
|
|
68
|
+
assert.match(guard, /if \[\[ "\$\{fail\}" -eq 0 \]\]; then\s*\n\s*cleanup_throwaway_task/,
|
|
69
|
+
'the call is gated on fail==0');
|
|
70
|
+
assert.match(guard, /kept for post-mortem/,
|
|
71
|
+
'and a failing run says why the row is still there');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('cleanup is best-effort: it can never fail a passing smoke', () => {
|
|
75
|
+
const fn = SH.slice(SH.indexOf('cleanup_throwaway_task() {'), SH.indexOf('--- summary ---'));
|
|
76
|
+
// The three ways this could turn a PASS into a FAIL, each closed:
|
|
77
|
+
assert.match(fn, /\|\| true/, 'the curl cannot trip set -e');
|
|
78
|
+
assert.ok(!/fail=1/.test(fn), 'and it never touches the fail counter');
|
|
79
|
+
// task.abandon is floor-metic (ADR 0152), so a Xenos/Thetes token gets a 403
|
|
80
|
+
// here while every assertion above genuinely passed. That must read as SKIP.
|
|
81
|
+
assert.match(fn, /403\)/, 'a rank refusal is reported as SKIP, not as a failure');
|
|
82
|
+
assert.match(fn, /000\)/, 'so is no response at all');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ---- the id is spliced as CODE, so its SHAPE is a security property ---------
|
|
86
|
+
//
|
|
87
|
+
// Raised by the grader's hacker worker on the first round of this task, and it
|
|
88
|
+
// was right: `'goal_id': ${GOAL_ID},` lands inside a `python3 -c "..."` SOURCE
|
|
89
|
+
// string, unquoted. The resolver validated the goal COUNT and never the id's own
|
|
90
|
+
// shape, so a non-numeric id from /goals — schema drift, a misbehaving API, any
|
|
91
|
+
// future path where an id-like field is influenced — would have been executed as
|
|
92
|
+
// Python rather than read as data. Validating the count is not validating the id.
|
|
93
|
+
|
|
94
|
+
test('the resolver refuses a non-numeric goal id', () => {
|
|
95
|
+
const ext = SH.slice(SH.indexOf('GOAL_ID=$(call GET'), SH.indexOf("') || {"));
|
|
96
|
+
assert.match(ext, /\/\^\[0-9\]\+\$\//, 'the extracted id is shape-checked');
|
|
97
|
+
assert.match(ext, /is not numeric/, 'and says so when it refuses');
|
|
98
|
+
// The check must come BEFORE the value is emitted, or it guards nothing.
|
|
99
|
+
assert.ok(
|
|
100
|
+
ext.indexOf('is not numeric') < ext.lastIndexOf('process.stdout.write'),
|
|
101
|
+
'the refusal precedes the write'
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('and the interpolation site re-checks it', () => {
|
|
106
|
+
// Belt-and-braces where the risk actually is: between resolution and the
|
|
107
|
+
// python3 heredoc the value passes through shell, so it is re-validated there.
|
|
108
|
+
assert.match(SH, /\[\[ "\$\{GOAL_ID\}" =~ \^\[0-9\]\+\$ \]\]/,
|
|
109
|
+
'bash re-checks the id before the create body interpolates it');
|
|
110
|
+
});
|