forgedock 1.17.0 → 1.19.0
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/bin/engine/outcome-codes.mjs +37 -2
- package/bin/engine/phases.mjs +207 -106
- package/bin/engine/runlog.mjs +17 -2
- package/bin/engine/telemetry-seam.mjs +3 -0
- package/bin/engine/telemetry.test.mjs +3 -2
- package/bin/engine-cli.mjs +37 -2
- package/bin/engine.mjs +221 -15
- package/bin/forgedock.mjs +1 -1
- package/bin/hooks/pre-tool-use.mjs +65 -32
- package/bin/runner.mjs +125 -6
- package/bin/stats/aggregate.mjs +7 -3
- package/bin/stats/cache.mjs +7 -3
- package/bin/stats/github-source.mjs +203 -24
- package/bin/stats/index.mjs +46 -9
- package/bin/stats/render.mjs +1 -1
- package/bin/stats/stats.test.mjs +264 -15
- package/commands/cleanup.md +10 -3
- package/commands/deploy-pr.md +64 -0
- package/commands/fix-ci.md +20 -6
- package/commands/milestone.md +34 -17
- package/commands/orchestrate/phase-4-execution.md +11 -4
- package/commands/review-pr-staging.md +145 -522
- package/commands/review-pr.md +496 -3124
- package/commands/test-gate.md +25 -10
- package/commands/work-on/build/architect.md +66 -1
- package/commands/work-on/build/implement.md +21 -1
- package/commands/work-on/build/validate.md +322 -28
- package/commands/work-on/build.md +173 -95
- package/commands/work-on/close.md +50 -15
- package/commands/work-on/decompose.md +72 -34
- package/commands/work-on/investigate.md +16 -12
- package/commands/work-on/remediate.md +58 -25
- package/commands/work-on/review.md +36 -21
- package/commands/work-on.md +1 -2
- package/docs/CONFIG.md +132 -26
- package/docs/OUTCOME-CODES.md +12 -3
- package/docs/RELEASE-NOTES.md +12 -0
- package/docs/STATS.md +13 -5
- package/docs/TELEMETRY.md +7 -4
- package/docs/WORK-ON-RUNTIME.md +24 -0
- package/docs/spec/forge-protocol-v1.md +2 -2
- package/package.json +2 -2
- package/packages/protocol/src/phases.js +5 -5
- package/packages/protocol/src/types.js +1 -1
- package/scripts/acceptance-red-green.sh +90 -0
- package/scripts/acceptance-red-green.test.sh +74 -0
- package/scripts/cap-findings.sh +1 -1
- package/scripts/cap-findings.test.sh +3 -1
- package/scripts/check-contract-scope.test.sh +2 -0
- package/scripts/check-edge-cases.sh +143 -0
- package/scripts/check-edge-cases.test.sh +175 -0
- package/scripts/check-spec-bash.sh +5 -1
- package/scripts/check-spec-bash.test.sh +20 -0
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/check-test-tmp-cleanup.sh +56 -0
- package/scripts/check-test-tmp-cleanup.test.sh +69 -0
- package/scripts/ci-local/adapters/azure-pipelines.mjs +113 -0
- package/scripts/ci-local/adapters/bitbucket.mjs +64 -0
- package/scripts/ci-local/adapters/buildkite.mjs +58 -0
- package/scripts/ci-local/adapters/circleci.mjs +101 -0
- package/scripts/ci-local/adapters/common.mjs +115 -0
- package/scripts/ci-local/adapters/explicit.mjs +21 -0
- package/scripts/ci-local/adapters/github-actions.mjs +349 -0
- package/scripts/ci-local/adapters/gitlab-ci.mjs +184 -0
- package/scripts/ci-local/adapters/portability.mjs +104 -0
- package/scripts/ci-local/adapters/task-runners.mjs +67 -0
- package/scripts/ci-local/cli.mjs +50 -0
- package/scripts/ci-local/lib.mjs +371 -0
- package/scripts/ci-local/plan.mjs +186 -0
- package/scripts/ci-local/run.mjs +345 -0
- package/scripts/ci-local/scoping.mjs +508 -0
- package/scripts/ci-local.sh +97 -0
- package/scripts/ci-local.test.sh +919 -0
- package/scripts/classify-finding.sh +32 -6
- package/scripts/classify-finding.test.sh +23 -0
- package/scripts/classify-test-run.sh +69 -0
- package/scripts/classify-test-run.test.sh +90 -0
- package/scripts/close-parent-c3.test.sh +71 -0
- package/scripts/consumer-sweep.sh +569 -0
- package/scripts/consumer-sweep.test.sh +357 -0
- package/scripts/detect-append-registries.sh +106 -0
- package/scripts/detect-append-registries.test.sh +64 -0
- package/scripts/diff-size.sh +93 -4
- package/scripts/diff-size.test.sh +59 -2
- package/scripts/fixtures/ci-local/azure/azure-pipelines.yml +11 -0
- package/scripts/fixtures/ci-local/bitbucket/bitbucket-pipelines.yml +12 -0
- package/scripts/fixtures/ci-local/buildkite/.buildkite/pipeline.yml +10 -0
- package/scripts/fixtures/ci-local/circleci/.circleci/config.yml +29 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/ci.yml +59 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/docs.yml +10 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/nightly.yml +10 -0
- package/scripts/fixtures/ci-local/gha-basic/docs/index.md +1 -0
- package/scripts/fixtures/ci-local/gha-basic/src/greet.sh +2 -0
- package/scripts/fixtures/ci-local/gha-basic/tests/greet.test.sh +3 -0
- package/scripts/fixtures/ci-local/gha-dorny/.github/workflows/ci.yml +51 -0
- package/scripts/fixtures/ci-local/gha-dorny/backend/app.py +1 -0
- package/scripts/fixtures/ci-local/gha-dorny/docs/a.md +1 -0
- package/scripts/fixtures/ci-local/gha-dorny/frontend/app.js +1 -0
- package/scripts/fixtures/ci-local/gha-run/.github/workflows/ci.yml +39 -0
- package/scripts/fixtures/ci-local/gha-run/src/x.txt +1 -0
- package/scripts/fixtures/ci-local/gitlab/.gitlab-ci.yml +43 -0
- package/scripts/fixtures/ci-local/gitlab/docs/d.md +1 -0
- package/scripts/fixtures/ci-local/gitlab/src/a.txt +1 -0
- package/scripts/fixtures/ci-local/monorepo/.github/workflows/ci.yml +13 -0
- package/scripts/fixtures/ci-local/monorepo/package-lock.json +1 -0
- package/scripts/fixtures/ci-local/monorepo/packages/a/src/a.sh +2 -0
- package/scripts/fixtures/ci-local/monorepo/packages/a/tests/a.test.sh +3 -0
- package/scripts/fixtures/ci-local/monorepo/packages/b/src/b.sh +2 -0
- package/scripts/fixtures/ci-local/monorepo/packages/b/tests/b.test.sh +3 -0
- package/scripts/fixtures/ci-local/no-ci/Makefile +6 -0
- package/scripts/fixtures/ci-local/no-ci/package.json +1 -0
- package/scripts/fixtures/ci-local/no-ci/src/a.js +1 -0
- package/scripts/fixtures/ci-local/no-ci/src/b.js +1 -0
- package/scripts/fixtures/ci-local/no-ci/tests/a.test.js +2 -0
- package/scripts/fixtures/ci-local/no-ci/tests/b.test.js +2 -0
- package/scripts/forge-root.test.sh +14 -12
- package/scripts/lib/telemetry.sh +1 -0
- package/scripts/publish-staging-sync.test.sh +48 -0
- package/scripts/remediate-approved-head.test.sh +2 -0
- package/scripts/remediate-basesync.test.sh +1 -1
- package/scripts/resolve-deploy-gate.sh +32 -6
- package/scripts/resolve-deploy-gate.test.sh +42 -1
- package/scripts/review-acceptance-gate.sh +2 -2
- package/scripts/review-acceptance-gate.test.sh +20 -0
- package/scripts/review-delta.sh +8 -1
- package/scripts/review-delta.test.sh +19 -0
- package/scripts/review-finalize.sh +1579 -0
- package/scripts/review-finalize.test.sh +1040 -0
- package/scripts/review-pr-delta-wiring.test.sh +32 -72
- package/scripts/review-pr-domain-map.test.sh +18 -13
- package/scripts/review-pr-gate2.test.sh +9 -65
- package/scripts/review-pr-staging-scope.test.sh +14 -98
- package/scripts/review-prepare.sh +53 -15
- package/scripts/review-prepare.test.sh +37 -3
- package/scripts/review-roster.sh +16 -2
- package/scripts/review-roster.test.sh +22 -0
- package/scripts/run-shell-tests.sh +5 -0
- package/scripts/telemetry.test.sh +2 -0
- package/scripts/trusted-comments.test.sh +11 -3
- package/scripts/verify-phase-trail.test.sh +8 -0
- package/scripts/wait-ci-green.sh +61 -9
- package/scripts/wait-ci-green.test.sh +44 -6
- package/scripts/with-run-tmpdir.sh +20 -0
|
@@ -10,12 +10,18 @@
|
|
|
10
10
|
# Usage:
|
|
11
11
|
# classify-finding.sh --severity <S> [--confidence <C>] [--agent <A>]
|
|
12
12
|
# [--lineage none|review-finding] [--text <T> | --text-file <F>]
|
|
13
|
-
# [--inpr-diff <F> --file <P>] [--contract-scope <F> --contract-open <F> [--pr-files <F>] [--merge-issue <N>]]
|
|
13
|
+
# [--inpr-diff <F> --file <P> [--scope introduced|pre-existing|unknown]] [--contract-scope <F> --contract-open <F> [--pr-files <F>] [--merge-issue <N>]]
|
|
14
14
|
# [--fix-efficacy fails|regresses] [--fixes-issue <N>]
|
|
15
15
|
# [--prior-findings <F> [--title <T>] [--line-start <N> [--line-end <M>]] [--diff-map <F>]]
|
|
16
|
+
# [--unstructured]
|
|
16
17
|
#
|
|
17
18
|
# --severity CRITICAL | HIGH | MEDIUM | LOW (case-insensitive). Missing or
|
|
18
19
|
# unparseable → ISSUE (fail toward filing, never drop).
|
|
20
|
+
# --unstructured The finding has no `FINDING:` marker (it was not read from a structured
|
|
21
|
+
# findings block). With an empty or unparseable severity it is `NOTE unstructured`,
|
|
22
|
+
# never ISSUE: a marker-less finding cannot block a PR. HIGH/CRITICAL/MEDIUM/LOW
|
|
23
|
+
# still classify normally. A real marker with a bad severity (no flag) stays
|
|
24
|
+
# `ISSUE unparseable-severity`.
|
|
19
25
|
# --confidence CONFIRMED | LIKELY | POSSIBLE (case-insensitive). Missing is
|
|
20
26
|
# treated as not-POSSIBLE.
|
|
21
27
|
# --agent The `**Agent**:` value of the finding (e.g. "Security", "Auth").
|
|
@@ -37,6 +43,11 @@
|
|
|
37
43
|
# are given, a MEDIUM CONFIRMED finding whose path is in the PR diff is
|
|
38
44
|
# classified INPR_FIX — fix it on the PR before merge instead of filing it.
|
|
39
45
|
# Callers pass these only for the first fix round of an auto-merge review.
|
|
46
|
+
# --scope <S> introduced | pre-existing | unknown: whether the finding's line sits in code the PR
|
|
47
|
+
# added or changed (computed from the diff by review-finalize.sh). With `introduced`,
|
|
48
|
+
# a MEDIUM LIKELY finding in the PR diff is also INPR_FIX (a defect in new code is fixed
|
|
49
|
+
# in the PR, not filed). Absent, `pre-existing` or `unknown` keeps the CONFIRMED-only
|
|
50
|
+
# behaviour. Any other value exits 2. Always recorded as `scope=` on the telemetry event.
|
|
40
51
|
#
|
|
41
52
|
# --contract-scope <F> --contract-open <F> --file <P> Contract-declared scope gate: F is the TSV
|
|
42
53
|
# from `check-contract-scope.sh list` (disposition<TAB>path<TAB>issue). A finding
|
|
@@ -115,9 +126,11 @@ TITLE=""
|
|
|
115
126
|
LINE_START=""
|
|
116
127
|
LINE_END=""
|
|
117
128
|
DIFF_MAP=""
|
|
129
|
+
UNSTRUCTURED=0
|
|
130
|
+
SCOPE=""
|
|
118
131
|
|
|
119
132
|
usage() {
|
|
120
|
-
echo "ERROR: Usage: classify-finding.sh --severity <S> [--confidence <C>] [--agent <A>] [--lineage none|review-finding] [--text <T> | --text-file <F>] [--inpr-diff <F> --file <P>] [--contract-scope <F> --contract-open <F> [--pr-files <F>] [--merge-issue <N>]] [--fix-efficacy fails|regresses] [--fixes-issue <N>] [--prior-findings <F> [--title <T>] [--line-start <N> [--line-end <M>]] [--diff-map <F>]]" >&2
|
|
133
|
+
echo "ERROR: Usage: classify-finding.sh --severity <S> [--confidence <C>] [--agent <A>] [--lineage none|review-finding] [--text <T> | --text-file <F>] [--inpr-diff <F> --file <P> [--scope introduced|pre-existing|unknown]] [--contract-scope <F> --contract-open <F> [--pr-files <F>] [--merge-issue <N>]] [--fix-efficacy fails|regresses] [--fixes-issue <N>] [--prior-findings <F> [--title <T>] [--line-start <N> [--line-end <M>]] [--diff-map <F>]] [--unstructured]" >&2
|
|
121
134
|
exit 2
|
|
122
135
|
}
|
|
123
136
|
|
|
@@ -130,6 +143,7 @@ while [ "$#" -gt 0 ]; do
|
|
|
130
143
|
--text) [ "$#" -ge 2 ] || usage; TEXT="$2"; shift 2 ;;
|
|
131
144
|
--inpr-diff) [ "$#" -ge 2 ] || usage; INPR_DIFF="$2"; shift 2 ;;
|
|
132
145
|
--file) [ "$#" -ge 2 ] || usage; FINDING_FILE="$2"; shift 2 ;;
|
|
146
|
+
--scope) [ "$#" -ge 2 ] || usage; SCOPE=$(printf '%s' "$2" | tr '[:upper:]' '[:lower:]'); shift 2 ;;
|
|
133
147
|
--contract-scope) [ "$#" -ge 2 ] || usage; CONTRACT_SCOPE="$2"; shift 2 ;;
|
|
134
148
|
--contract-open) [ "$#" -ge 2 ] || usage; CONTRACT_OPEN="$2"; shift 2 ;;
|
|
135
149
|
--pr-files) [ "$#" -ge 2 ] || usage; PR_FILES="$2"; shift 2 ;;
|
|
@@ -141,6 +155,7 @@ while [ "$#" -gt 0 ]; do
|
|
|
141
155
|
--line-start) [ "$#" -ge 2 ] || usage; LINE_START="$2"; shift 2 ;;
|
|
142
156
|
--line-end) [ "$#" -ge 2 ] || usage; LINE_END="$2"; shift 2 ;;
|
|
143
157
|
--diff-map) [ "$#" -ge 2 ] || usage; DIFF_MAP="$2"; shift 2 ;;
|
|
158
|
+
--unstructured) UNSTRUCTURED=1; shift ;;
|
|
144
159
|
--text-file)
|
|
145
160
|
[ "$#" -ge 2 ] || usage
|
|
146
161
|
[ -r "$2" ] || { echo "ERROR: --text-file not readable: $2" >&2; exit 2; }
|
|
@@ -159,6 +174,11 @@ case "$FIX_EFF" in
|
|
|
159
174
|
*) echo "ERROR: --fix-efficacy must be 'fails' or 'regresses' (got '$FIX_EFF')" >&2; exit 2 ;;
|
|
160
175
|
esac
|
|
161
176
|
|
|
177
|
+
case "$SCOPE" in
|
|
178
|
+
''|introduced|pre-existing|unknown) ;;
|
|
179
|
+
*) echo "ERROR: --scope must be 'introduced', 'pre-existing' or 'unknown'" >&2; exit 2 ;;
|
|
180
|
+
esac
|
|
181
|
+
|
|
162
182
|
if [ -n "$INPR_DIFF" ] && [ ! -r "$INPR_DIFF" ]; then
|
|
163
183
|
echo "ERROR: --inpr-diff not readable: $INPR_DIFF" >&2; exit 2
|
|
164
184
|
fi
|
|
@@ -186,14 +206,18 @@ CONF=$(upper "$CONFIDENCE")
|
|
|
186
206
|
_TEL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/telemetry.sh"; [ -f "$_TEL" ] && . "$_TEL" 2>/dev/null
|
|
187
207
|
type forge_emit >/dev/null 2>&1 || forge_emit() { :; }
|
|
188
208
|
disp() { # "<DISPOSITION> <reason>"
|
|
189
|
-
forge_emit finding_disposition severity="$SEV" confidence="${CONF:-UNKNOWN}" agent="${AGENT:-none}" disposition="${1%% *}" reason="${1#* }"
|
|
209
|
+
forge_emit finding_disposition severity="$SEV" confidence="${CONF:-UNKNOWN}" agent="${AGENT:-none}" scope="${SCOPE:-unknown}" disposition="${1%% *}" reason="${1#* }"
|
|
190
210
|
echo "$1"
|
|
191
211
|
}
|
|
192
212
|
|
|
193
213
|
# In-PR fix gate predicate (#3387, widened to HIGH/CRITICAL in #3592): CONFIRMED, linked to a
|
|
194
214
|
# --inpr-diff file list, and the finding's file is one of those files. Fails toward ISSUE.
|
|
215
|
+
# MEDIUM LIKELY also matches, only when --scope introduced (a defect in code this PR added).
|
|
195
216
|
inpr_match() {
|
|
196
|
-
[ -n "$INPR_DIFF" ] && [ -n "$FINDING_FILE" ]
|
|
217
|
+
[ -n "$INPR_DIFF" ] && [ -n "$FINDING_FILE" ] || return 1
|
|
218
|
+
if [ "$CONF" != "CONFIRMED" ]; then
|
|
219
|
+
[ "$SEV" = "MEDIUM" ] && [ "$CONF" = "LIKELY" ] && [ "$SCOPE" = "introduced" ] || return 1
|
|
220
|
+
fi
|
|
197
221
|
_path="${FINDING_FILE#./}"; _path="${_path%%:*}"
|
|
198
222
|
grep -Fxq -- "$_path" "$INPR_DIFF"
|
|
199
223
|
}
|
|
@@ -207,7 +231,9 @@ case "$SEV" in
|
|
|
207
231
|
fi
|
|
208
232
|
disp "ISSUE severity-$SEV"; exit 0 ;;
|
|
209
233
|
MEDIUM|LOW) ;;
|
|
210
|
-
*)
|
|
234
|
+
*)
|
|
235
|
+
if [ "$UNSTRUCTURED" = "1" ]; then disp "NOTE unstructured"; exit 0; fi
|
|
236
|
+
disp "ISSUE unparseable-severity"; exit 0 ;;
|
|
211
237
|
esac
|
|
212
238
|
|
|
213
239
|
# Cross-head / cross-PR re-flag gate (#3641). Runs after the severity parse so HIGH/CRITICAL and
|
|
@@ -282,7 +308,7 @@ fi
|
|
|
282
308
|
# (lineage included), so checking here changes only where it is handled, never whether.
|
|
283
309
|
# HIGH/CRITICAL were handled in the severity case above.
|
|
284
310
|
if [ "$SEV" = "MEDIUM" ] && inpr_match; then
|
|
285
|
-
disp "INPR_FIX medium-confirmed-in-diff"
|
|
311
|
+
if [ "$CONF" = "CONFIRMED" ]; then disp "INPR_FIX medium-confirmed-in-diff"; else disp "INPR_FIX medium-likely-introduced-in-diff"; fi
|
|
286
312
|
exit 0
|
|
287
313
|
fi
|
|
288
314
|
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
# Run: bash scripts/classify-finding.test.sh (bash 3.2 compatible)
|
|
7
7
|
|
|
8
8
|
set -u
|
|
9
|
+
# Every mktemp below lands in one private dir that the EXIT trap removes (forge#4016).
|
|
10
|
+
_SUITE_TMP=$(mktemp -d); export TMPDIR="$_SUITE_TMP"; trap 'rm -rf "$_SUITE_TMP"' EXIT
|
|
9
11
|
DIR=$(cd "$(dirname "$0")" && pwd)
|
|
10
12
|
S="$DIR/classify-finding.sh"
|
|
11
13
|
pass=0; fail=0
|
|
@@ -29,6 +31,16 @@ expect ISSUE "missing severity files" -- --severity "" --confidence LIKE
|
|
|
29
31
|
expect ISSUE "unknown severity files" -- --severity INFO
|
|
30
32
|
expect ISSUE "lowercase high files" -- --severity high
|
|
31
33
|
|
|
34
|
+
# --- --unstructured: a marker-less finding can never be ISSUE on a bad severity ---
|
|
35
|
+
expect NOTE "unstructured empty severity is NOTE" -- --unstructured --severity "" --confidence LIKELY
|
|
36
|
+
expect NOTE "unstructured missing severity is NOTE" -- --unstructured
|
|
37
|
+
expect NOTE "unstructured garbage severity is NOTE" -- --unstructured --severity INFO
|
|
38
|
+
expect ISSUE "unstructured HIGH still files" -- --unstructured --severity HIGH --confidence POSSIBLE
|
|
39
|
+
expect ISSUE "unstructured CRITICAL still files" -- --unstructured --severity CRITICAL
|
|
40
|
+
expect ISSUE "unstructured MEDIUM CONFIRMED still files" -- --unstructured --severity MEDIUM --confidence CONFIRMED --text "loop exits early"
|
|
41
|
+
expect ISSUE "marker finding, empty severity stays ISSUE" -- --severity "" --confidence LIKELY
|
|
42
|
+
expect ISSUE "marker finding, bad severity stays ISSUE" -- --severity INFO
|
|
43
|
+
|
|
32
44
|
# --- Rule 4: non-lineage ---
|
|
33
45
|
expect ISSUE "MEDIUM CONFIRMED files" -- --severity MEDIUM --confidence CONFIRMED --text "loop exits early"
|
|
34
46
|
expect ISSUE "MEDIUM LIKELY files" -- --severity MEDIUM --confidence LIKELY --text "loop exits early"
|
|
@@ -112,6 +124,17 @@ expect INPR_FIX "./ prefix matches" -- --severity MEDIUM --con
|
|
|
112
124
|
expect INPR_FIX "lineage PR still fixes in-PR" -- --severity MEDIUM --confidence CONFIRMED --lineage review-finding --inpr-diff "$DIFF" --file src/api/jobs.py
|
|
113
125
|
expect ISSUE "MEDIUM CONFIRMED outside diff files" -- --severity MEDIUM --confidence CONFIRMED --inpr-diff "$DIFF" --file src/api/other.py
|
|
114
126
|
expect ISSUE "MEDIUM LIKELY in diff is not gated" -- --severity MEDIUM --confidence LIKELY --inpr-diff "$DIFF" --file src/api/jobs.py
|
|
127
|
+
# --- scope-aware gate (#3975): MEDIUM LIKELY in diff is INPR_FIX only when introduced ---
|
|
128
|
+
expect INPR_FIX "MEDIUM LIKELY introduced in diff" -- --severity MEDIUM --confidence LIKELY --scope introduced --inpr-diff "$DIFF" --file src/api/jobs.py
|
|
129
|
+
expect INPR_FIX "MEDIUM CONFIRMED introduced in diff" -- --severity MEDIUM --confidence CONFIRMED --scope introduced --inpr-diff "$DIFF" --file src/api/jobs.py
|
|
130
|
+
expect ISSUE "MEDIUM LIKELY pre-existing stays ISSUE" -- --severity MEDIUM --confidence LIKELY --scope pre-existing --inpr-diff "$DIFF" --file src/api/jobs.py
|
|
131
|
+
expect ISSUE "MEDIUM LIKELY unknown stays ISSUE" -- --severity MEDIUM --confidence LIKELY --scope unknown --inpr-diff "$DIFF" --file src/api/jobs.py
|
|
132
|
+
expect ISSUE "MEDIUM LIKELY introduced outside diff" -- --severity MEDIUM --confidence LIKELY --scope introduced --inpr-diff "$DIFF" --file src/api/other.py
|
|
133
|
+
expect NOTE "MEDIUM POSSIBLE introduced stays NOTE" -- --severity MEDIUM --confidence POSSIBLE --scope introduced --inpr-diff "$DIFF" --file src/api/jobs.py
|
|
134
|
+
expect NOTE "LOW LIKELY introduced is not INPR_FIX" -- --severity LOW --confidence LIKELY --scope introduced --inpr-diff "$DIFF" --file src/api/jobs.py
|
|
135
|
+
expect ISSUE "HIGH LIKELY introduced stays ISSUE" -- --severity HIGH --confidence LIKELY --scope introduced --inpr-diff "$DIFF" --file src/api/jobs.py
|
|
136
|
+
expect INPR_FIX "HIGH CONFIRMED pre-existing scope unchanged" -- --severity HIGH --confidence CONFIRMED --scope pre-existing --inpr-diff "$DIFF" --file src/api/jobs.py
|
|
137
|
+
expect_exit 2 "bad --scope value" -- --severity MEDIUM --scope bogus
|
|
115
138
|
expect INPR_FIX "HIGH CONFIRMED in diff (#3592)" -- --severity HIGH --confidence CONFIRMED --inpr-diff "$DIFF" --file src/api/jobs.py
|
|
116
139
|
expect INPR_FIX "CRITICAL CONFIRMED in diff (#3592)" -- --severity CRITICAL --confidence CONFIRMED --inpr-diff "$DIFF" --file "src/api/jobs.py:9"
|
|
117
140
|
expect INPR_FIX "HIGH in diff with safety keyword" -- --severity HIGH --confidence CONFIRMED --title "billing refund leg unreachable" --inpr-diff "$DIFF" --file src/api/jobs.py
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# classify-test-run.sh — classify a finished test-command run as RAN, RAN_NOTHING or FAILED.
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
#
|
|
6
|
+
# Usage: classify-test-run.sh <exit_code> <output_file>
|
|
7
|
+
# Prints exactly one word on stdout:
|
|
8
|
+
# FAILED non-zero exit that is not a "ran nothing" signal (a real failure)
|
|
9
|
+
# RAN_NOTHING the runner exited without exercising anything: pytest exit 5, "no tests ran/collected" output,
|
|
10
|
+
# every collected test skipped, or a connection/service-unavailable error that caused the skips
|
|
11
|
+
# RAN anything else (including exit 0 with no recognizable runner output; the caller only classifies test commands)
|
|
12
|
+
# Conservative by design: RAN_NOTHING needs positive evidence, and any "N passed" count means something ran.
|
|
13
|
+
# The script only reads the exit code and the output file; it never executes their contents.
|
|
14
|
+
# Exit: 0 classified | 2 usage error (bad exit code or unreadable file; stdout empty so callers fail closed)
|
|
15
|
+
# Bash 3.2 portable (no mapfile, associative arrays, grep -P).
|
|
16
|
+
set -uo pipefail
|
|
17
|
+
|
|
18
|
+
CODE="${1-}"; FILE="${2-}"
|
|
19
|
+
case "$CODE" in ''|*[!0-9]*) echo "usage: classify-test-run.sh <exit_code> <output_file>" >&2; exit 2 ;; esac
|
|
20
|
+
[ -n "$FILE" ] && [ -r "$FILE" ] && [ -f "$FILE" ] || { echo "classify-test-run.sh: output file unreadable: ${FILE}" >&2; exit 2; }
|
|
21
|
+
|
|
22
|
+
has() { grep -qiE -e "$1" "$FILE"; }
|
|
23
|
+
|
|
24
|
+
# pytest exit 5 is "no tests were collected"; it is never a verified run.
|
|
25
|
+
if [ "$CODE" -eq 5 ]; then echo RAN_NOTHING; exit 0; fi
|
|
26
|
+
|
|
27
|
+
# Any other non-zero exit is a real failure; the existing blocking rules apply.
|
|
28
|
+
if [ "$CODE" -ne 0 ]; then echo FAILED; exit 0; fi
|
|
29
|
+
|
|
30
|
+
# Positive evidence that something executed and passed: a "N passed" / "# pass N" / "ok N" count with N >= 1.
|
|
31
|
+
PASSED=false
|
|
32
|
+
if grep -qE '(^|[^0-9A-Za-z])[1-9][0-9]* passed' "$FILE" || grep -qE '^# pass [1-9][0-9]*' "$FILE" \
|
|
33
|
+
|| grep -qE '^Ran [1-9][0-9]* tests?' "$FILE" \
|
|
34
|
+
|| grep -qE '^ok [0-9]+' "$FILE"; then
|
|
35
|
+
PASSED=true
|
|
36
|
+
fi
|
|
37
|
+
# forge#4037: pass summaries of other common runners count as positive evidence BEFORE any error-keyword heuristic, so a
|
|
38
|
+
# passing run whose output merely mentions ECONNREFUSED / "skip" is not RAN_NOTHING. node --test spec ("ℹ pass N", "✔ name (Nms)"),
|
|
39
|
+
# jest/vitest ("Tests: N passed" is covered above), go test ("ok pkg 0.3s", "--- PASS"), cargo ("test result: ok. N passed").
|
|
40
|
+
if [ "$PASSED" = "false" ] && { grep -qE '^[[:space:]]*ℹ pass [1-9][0-9]*' "$FILE" \
|
|
41
|
+
|| grep -qE '^[[:space:]]*✔ .*\([0-9.]+ ?m?s\)[[:space:]]*$' "$FILE" \
|
|
42
|
+
|| grep -qE '^ok[[:space:]]+[^[:space:]]+[[:space:]]+[0-9.]+s' "$FILE" \
|
|
43
|
+
|| grep -qE '^[[:space:]]*--- PASS: ' "$FILE" \
|
|
44
|
+
|| grep -qE '^test result: ok\. [1-9][0-9]* passed' "$FILE"; }; then
|
|
45
|
+
PASSED=true
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Explicit "nothing ran" output.
|
|
49
|
+
if has 'no tests ran' || has 'no tests collected' || has 'collected 0 items' || has '^Ran 0 tests' \
|
|
50
|
+
|| has '^# tests 0$' || has 'no test files found' || has 'No tests found'; then
|
|
51
|
+
[ "$PASSED" = "true" ] || { echo RAN_NOTHING; exit 0; }
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Every collected test skipped: a pytest-style summary with a skipped count and no passed count.
|
|
55
|
+
if [ "$PASSED" = "false" ] && grep -qE '(^|[^0-9A-Za-z])[1-9][0-9]* skipped' "$FILE" \
|
|
56
|
+
&& ! grep -qE '(^|[^0-9A-Za-z])[1-9][0-9]* (failed|error)' "$FILE"; then
|
|
57
|
+
echo RAN_NOTHING; exit 0
|
|
58
|
+
fi
|
|
59
|
+
# node:test / TAP all-skipped: "# skipped N" with "# pass 0".
|
|
60
|
+
if has '^# skipped [1-9]' && has '^# pass 0$'; then echo RAN_NOTHING; exit 0; fi
|
|
61
|
+
|
|
62
|
+
# Unreachable DB or service that turned tests into skips (or an exit-0 runner that swallowed the error).
|
|
63
|
+
if [ "$PASSED" = "false" ] \
|
|
64
|
+
&& has 'connection refused|ECONNREFUSED|could not connect to server|service unavailable|database (is )?(unavailable|not available)|connection to .* failed' \
|
|
65
|
+
&& has 'skip'; then
|
|
66
|
+
echo RAN_NOTHING; exit 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
echo RAN
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# classify-test-run.test.sh — tests for scripts/classify-test-run.sh
|
|
3
|
+
# One case per class plus a positive control (normal pass) and a real failure. No network.
|
|
4
|
+
# Usage: bash scripts/classify-test-run.test.sh
|
|
5
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
CLS="$SCRIPT_DIR/classify-test-run.sh"
|
|
10
|
+
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
|
|
11
|
+
PASS=0; FAILN=0
|
|
12
|
+
check() { # name expected exit_code output
|
|
13
|
+
local name="$1" want="$2" code="$3" out="$4" got
|
|
14
|
+
printf '%s\n' "$out" > "$TMP/out.txt"
|
|
15
|
+
got=$(bash "$CLS" "$code" "$TMP/out.txt" 2>/dev/null)
|
|
16
|
+
if [ "$got" = "$want" ]; then PASS=$((PASS+1)); echo "ok - $name"; else FAILN=$((FAILN+1)); echo "FAIL - $name: want $want got '$got'"; fi
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
check "positive control: normal pass is RAN" RAN 0 "collected 12 items
|
|
20
|
+
============ 12 passed in 0.31s ============"
|
|
21
|
+
check "pytest passed with some skipped is RAN" RAN 0 "=== 10 passed, 2 skipped in 1.2s ==="
|
|
22
|
+
check "pytest exit 5 is RAN_NOTHING" RAN_NOTHING 5 "collected 0 items
|
|
23
|
+
no tests ran in 0.01s"
|
|
24
|
+
check "exit 0 with 'no tests ran' is RAN_NOTHING" RAN_NOTHING 0 "============ no tests ran in 0.02s ============"
|
|
25
|
+
check "collected 0 items is RAN_NOTHING" RAN_NOTHING 0 "collected 0 items"
|
|
26
|
+
check "all tests skipped is RAN_NOTHING" RAN_NOTHING 0 "collected 8 items
|
|
27
|
+
=========== 8 skipped in 0.12s ==========="
|
|
28
|
+
check "connection refused causing skips is RAN_NOTHING" RAN_NOTHING 0 "SKIPPED [3] tests/test_db.py: could not connect to server: Connection refused
|
|
29
|
+
=========== 3 skipped in 0.5s ==========="
|
|
30
|
+
check "connection refused without a skip summary but with skip text is RAN_NOTHING" RAN_NOTHING 0 "ECONNREFUSED 127.0.0.1:5432 - skipping DB tests"
|
|
31
|
+
check "real failure is FAILED" FAILED 1 "=========== 1 failed, 4 passed in 0.9s ==========="
|
|
32
|
+
check "failure that mentions connection refused is still FAILED" FAILED 1 "ConnectionRefusedError: connection refused
|
|
33
|
+
1 failed in 0.2s"
|
|
34
|
+
check "exit 0 with no runner output is RAN" RAN 0 "done"
|
|
35
|
+
check "node:test TAP all skipped is RAN_NOTHING" RAN_NOTHING 0 "# tests 3
|
|
36
|
+
# pass 0
|
|
37
|
+
# skipped 3"
|
|
38
|
+
check "node:test TAP pass is RAN" RAN 0 "# tests 3
|
|
39
|
+
# pass 3
|
|
40
|
+
# skipped 0"
|
|
41
|
+
|
|
42
|
+
# forge#4037: pass summaries of common runners are positive evidence ahead of any error-keyword heuristic.
|
|
43
|
+
check "node --test spec reporter pass mentioning ECONNREFUSED and skip is RAN" RAN 0 "▶ db client
|
|
44
|
+
✔ retries after ECONNREFUSED and skip logging (2.1ms)
|
|
45
|
+
✔ connects (1.0ms)
|
|
46
|
+
✔ db client (4.2ms)
|
|
47
|
+
ℹ tests 2
|
|
48
|
+
ℹ suites 1
|
|
49
|
+
ℹ pass 2
|
|
50
|
+
ℹ fail 0
|
|
51
|
+
ℹ skipped 0"
|
|
52
|
+
check "node --test spec reporter lines alone are RAN" RAN 0 "✔ works with ECONNREFUSED text, skip (0.4ms)"
|
|
53
|
+
check "node --test spec all skipped is still RAN_NOTHING" RAN_NOTHING 0 "ℹ tests 3
|
|
54
|
+
ℹ pass 0
|
|
55
|
+
ℹ skipped 3
|
|
56
|
+
# skipped 3
|
|
57
|
+
# pass 0"
|
|
58
|
+
check "go test pass mentioning connection refused is RAN" RAN 0 "--- PASS: TestRetry (0.00s)
|
|
59
|
+
connection refused handled, skip
|
|
60
|
+
ok example.com/pkg 0.012s"
|
|
61
|
+
check "cargo test pass mentioning ECONNREFUSED is RAN" RAN 0 "test net::retries ... ok
|
|
62
|
+
ECONNREFUSED skip
|
|
63
|
+
test result: ok. 4 passed; 0 failed; 0 ignored"
|
|
64
|
+
check "jest summary mentioning ECONNREFUSED is RAN" RAN 0 "ECONNREFUSED skip
|
|
65
|
+
Tests: 3 passed, 3 total"
|
|
66
|
+
check "vitest summary mentioning ECONNREFUSED is RAN" RAN 0 "ECONNREFUSED skip
|
|
67
|
+
Tests 5 passed (5)"
|
|
68
|
+
check "pytest passed mentioning connection refused is RAN" RAN 0 "connection refused skip
|
|
69
|
+
==== 6 passed in 0.4s ===="
|
|
70
|
+
|
|
71
|
+
# forge#4037: validate.md runs test commands under a timeout; rc 124 is DEGRADED (recorded), never FAILED. Uses the real functions.
|
|
72
|
+
VAL="$SCRIPT_DIR/../commands/work-on/build/validate.md"
|
|
73
|
+
for fn in degrade_check run_verification_command run_classified_test; do sed -n "/^${fn}() {/,/^}/p" "$VAL"; done > "$TMP/fns.sh"
|
|
74
|
+
if [ -s "$TMP/fns.sh" ] && command -v timeout >/dev/null 2>&1; then
|
|
75
|
+
vres=$(cd "$TMP" && DEGRADED_CHECKS="" TEST_FAILED=0 CLASSIFY_SCRIPT="$CLS" FORGEDOCK_VERIFICATION_TIMEOUT_SECONDS=1 bash -c ". '$TMP/fns.sh'; run_classified_test slow.test 'sleep 5' >/dev/null; echo \"FAILED=\$TEST_FAILED DEG=\$DEGRADED_CHECKS\"")
|
|
76
|
+
case "$vres" in
|
|
77
|
+
"FAILED=0 DEG=slow.test"*"timed out"*) PASS=$((PASS+1)); echo "ok - test timeout (rc 124) is DEGRADED, not FAILED" ;;
|
|
78
|
+
*) FAILN=$((FAILN+1)); echo "FAIL - test timeout classification: $vres" ;;
|
|
79
|
+
esac
|
|
80
|
+
vres=$(cd "$TMP" && DEGRADED_CHECKS="" TEST_FAILED=0 CLASSIFY_SCRIPT="$CLS" bash -c ". '$TMP/fns.sh'; run_classified_test bad.test 'exit 3' >/dev/null; echo \"FAILED=\$TEST_FAILED\"")
|
|
81
|
+
[ "$vres" = "FAILED=1" ] && { PASS=$((PASS+1)); echo "ok - a real non-zero exit is still FAILED"; } || { FAILN=$((FAILN+1)); echo "FAIL - real failure: $vres"; }
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
: > "$TMP/empty.txt"
|
|
85
|
+
[ "$(bash "$CLS" 0 "$TMP/empty.txt" 2>/dev/null)" = "RAN" ] && { PASS=$((PASS+1)); echo "ok - empty output exit 0 is RAN"; } || { FAILN=$((FAILN+1)); echo "FAIL - empty output exit 0"; }
|
|
86
|
+
bash "$CLS" abc "$TMP/empty.txt" >/dev/null 2>&1; [ $? -eq 2 ] && { PASS=$((PASS+1)); echo "ok - bad exit code is a usage error"; } || { FAILN=$((FAILN+1)); echo "FAIL - bad exit code"; }
|
|
87
|
+
bash "$CLS" 0 "$TMP/nope.txt" >/dev/null 2>&1; [ $? -eq 2 ] && { PASS=$((PASS+1)); echo "ok - missing file is a usage error"; } || { FAILN=$((FAILN+1)); echo "FAIL - missing file"; }
|
|
88
|
+
|
|
89
|
+
echo "$PASS passed, $FAILN failed"
|
|
90
|
+
[ "$FAILN" -eq 0 ]
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
# Usage: bash scripts/close-parent-c3.test.sh
|
|
7
7
|
|
|
8
8
|
set -euo pipefail
|
|
9
|
+
# Every mktemp below lands in one private dir that the EXIT trap removes (forge#4016).
|
|
10
|
+
_SUITE_TMP=$(mktemp -d); export TMPDIR="$_SUITE_TMP"; trap 'rm -rf "$_SUITE_TMP"' EXIT
|
|
9
11
|
|
|
10
12
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
11
13
|
CLOSE="$ROOT/commands/work-on/close.md"
|
|
@@ -51,4 +53,73 @@ printf '%s\n' "$OUT" | grep -q 'gh issue close 5 .*All sub-issues complete' || f
|
|
|
51
53
|
printf '%s\n' "$OUT" | grep -q 'set_workflow_state 5 merged' || fail "open parent: merged not set"
|
|
52
54
|
printf '%s\n' "$OUT" | grep -q 'gh issue comment' && fail "open parent: no separate comment expected"
|
|
53
55
|
|
|
56
|
+
# ---- Verified-retry checkbox write + state-derived OPEN_SUBS (concurrent sibling close race) ----
|
|
57
|
+
WRITE_BLOCK=$(awk '/^PARENT_UPDATED=false$/{p=1} p{print} p&&/^\[ "\$PARENT_UPDATED" = true \]/{exit}' "$CLOSE")
|
|
58
|
+
DERIVE_BLOCK=$(awk '/^PARENT_FRESH=/{p=1} p{print} p&&/^OPEN_SUBS=\$\{OPEN_SUBS:-0\}$/{exit}' "$CLOSE")
|
|
59
|
+
[ -n "$WRITE_BLOCK" ] || fail "close.md C3 verified-retry write block not found"
|
|
60
|
+
[ -n "$DERIVE_BLOCK" ] || fail "close.md C3 state-derivation block not found"
|
|
61
|
+
|
|
62
|
+
# run_derive <parent body> <states "N=STATE ..."> [lost_edits]
|
|
63
|
+
# gh stub: parent body lives in a file; the first <lost_edits> edits are clobbered by a stale sibling write.
|
|
64
|
+
run_derive() {
|
|
65
|
+
local body="$1" states="$2" lost="${3:-0}" dir rendered
|
|
66
|
+
dir=$(mktemp -d)
|
|
67
|
+
printf '%s\n' "$body" >"$dir/body"; printf '%s\n' "$body" >"$dir/stale"
|
|
68
|
+
echo 0 >"$dir/edits"
|
|
69
|
+
gh() {
|
|
70
|
+
case "$1 $2" in
|
|
71
|
+
"issue view")
|
|
72
|
+
if [ "$6" = "--json" ] && [ "$7" = "body" ]; then cat "$dir/body"; return 0; fi
|
|
73
|
+
if [ "$6" = "--json" ] && [ "$7" = "state" ]; then
|
|
74
|
+
local kv; for kv in $states; do [ "${kv%%=*}" = "$3" ] && { echo "${kv#*=}"; return 0; }; done
|
|
75
|
+
return 1
|
|
76
|
+
fi ;;
|
|
77
|
+
"issue edit")
|
|
78
|
+
local n; n=$(( $(cat "$dir/edits") + 1 )); echo "$n" >"$dir/edits"
|
|
79
|
+
if [ "$n" -le "$lost" ]; then cp "$dir/stale" "$dir/body"; else printf '%s\n' "$7" >"$dir/body"; fi ;;
|
|
80
|
+
esac
|
|
81
|
+
}
|
|
82
|
+
sleep() { :; }
|
|
83
|
+
rendered=$(printf '%s\n%s\n' "$WRITE_BLOCK" "$DERIVE_BLOCK" | sed -e 's/{GH_FLAG}/-R o\/r/g' -e 's/{GH_REPO}/o\/r/g' -e 's/{NUMBER}/7/g')
|
|
84
|
+
PARENT_REF=5
|
|
85
|
+
eval "$rendered" >/dev/null 2>&1 || true
|
|
86
|
+
printf 'OPEN_SUBS=%s UPDATED=%s EDITS=%s CHECKED7=%s\n' "$OPEN_SUBS" "$PARENT_UPDATED" "$(cat "$dir/edits")" \
|
|
87
|
+
"$(grep -cE '^- \[x\] #7( |$)' "$dir/body" || true)"
|
|
88
|
+
rm -rf "$dir"; unset -f gh sleep
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
TWO=$'- [ ] #7 first\n- [ ] #8 second'
|
|
92
|
+
|
|
93
|
+
# Lost checkbox is re-applied by the retry loop (first write clobbered by a sibling).
|
|
94
|
+
OUT=$(run_derive "$TWO" "7=CLOSED 8=CLOSED" 1)
|
|
95
|
+
printf '%s\n' "$OUT" | grep -q 'CHECKED7=1' || fail "retry: checkbox not restored after a lost write ($OUT)"
|
|
96
|
+
printf '%s\n' "$OUT" | grep -q 'UPDATED=true' || fail "retry: PARENT_UPDATED not set ($OUT)"
|
|
97
|
+
printf '%s\n' "$OUT" | grep -q 'EDITS=2' || fail "retry: expected exactly 2 writes ($OUT)"
|
|
98
|
+
|
|
99
|
+
# Race: every write is lost, boxes stay unchecked, but all subs are CLOSED => 0 open (parent completes).
|
|
100
|
+
OUT=$(run_derive "$TWO" "7=CLOSED 8=CLOSED" 9)
|
|
101
|
+
printf '%s\n' "$OUT" | grep -q 'OPEN_SUBS=0' || fail "race: stale checklist with all subs CLOSED must derive 0 open ($OUT)"
|
|
102
|
+
printf '%s\n' "$OUT" | grep -q 'EDITS=3' || fail "race: retries must be bounded at 3 writes ($OUT)"
|
|
103
|
+
|
|
104
|
+
# A still-OPEN sibling keeps the parent open.
|
|
105
|
+
OUT=$(run_derive "$TWO" "7=CLOSED 8=OPEN")
|
|
106
|
+
printf '%s\n' "$OUT" | grep -q 'OPEN_SUBS=1' || fail "one open sibling must derive 1 open ($OUT)"
|
|
107
|
+
|
|
108
|
+
# State lookup failure falls back to the checklist count (fail closed, never 0 from a failed lookup).
|
|
109
|
+
OUT=$(run_derive "$TWO" "7=CLOSED")
|
|
110
|
+
printf '%s\n' "$OUT" | grep -q 'OPEN_SUBS=1' || fail "lookup failure must fall back to checklist count ($OUT)"
|
|
111
|
+
|
|
112
|
+
# Already-checked box: no write needed.
|
|
113
|
+
OUT=$(run_derive $'- [x] #7 first\n- [ ] #8 second' "7=CLOSED 8=CLOSED")
|
|
114
|
+
printf '%s\n' "$OUT" | grep -q 'EDITS=0' || fail "already checked: must not rewrite the parent ($OUT)"
|
|
115
|
+
|
|
116
|
+
# #7 must not match #70.
|
|
117
|
+
OUT=$(run_derive $'- [ ] #70 other\n- [ ] #7 first' "7=CLOSED 70=OPEN")
|
|
118
|
+
printf '%s\n' "$OUT" | grep -q 'OPEN_SUBS=1' || fail "#70 must stay open and not be confused with #7 ($OUT)"
|
|
119
|
+
printf '%s\n' "$OUT" | grep -q 'CHECKED7=1' || fail "#7 checkbox must be checked without touching #70 ($OUT)"
|
|
120
|
+
|
|
121
|
+
# A non-leading #N mention on an unchecked line is an open item, not a sub-issue ref (no fail-open close).
|
|
122
|
+
OUT=$(run_derive $'- [ ] #7 first\n- [ ] #8 second\n- [ ] Write docs (see #9)' "7=CLOSED 8=CLOSED")
|
|
123
|
+
printf '%s\n' "$OUT" | grep -q 'OPEN_SUBS=1' || fail "non-leading ref line must count as open ($OUT)"
|
|
124
|
+
|
|
54
125
|
printf 'PASS: close-parent-c3\n'
|