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.
Files changed (143) hide show
  1. package/bin/engine/outcome-codes.mjs +37 -2
  2. package/bin/engine/phases.mjs +207 -106
  3. package/bin/engine/runlog.mjs +17 -2
  4. package/bin/engine/telemetry-seam.mjs +3 -0
  5. package/bin/engine/telemetry.test.mjs +3 -2
  6. package/bin/engine-cli.mjs +37 -2
  7. package/bin/engine.mjs +221 -15
  8. package/bin/forgedock.mjs +1 -1
  9. package/bin/hooks/pre-tool-use.mjs +65 -32
  10. package/bin/runner.mjs +125 -6
  11. package/bin/stats/aggregate.mjs +7 -3
  12. package/bin/stats/cache.mjs +7 -3
  13. package/bin/stats/github-source.mjs +203 -24
  14. package/bin/stats/index.mjs +46 -9
  15. package/bin/stats/render.mjs +1 -1
  16. package/bin/stats/stats.test.mjs +264 -15
  17. package/commands/cleanup.md +10 -3
  18. package/commands/deploy-pr.md +64 -0
  19. package/commands/fix-ci.md +20 -6
  20. package/commands/milestone.md +34 -17
  21. package/commands/orchestrate/phase-4-execution.md +11 -4
  22. package/commands/review-pr-staging.md +145 -522
  23. package/commands/review-pr.md +496 -3124
  24. package/commands/test-gate.md +25 -10
  25. package/commands/work-on/build/architect.md +66 -1
  26. package/commands/work-on/build/implement.md +21 -1
  27. package/commands/work-on/build/validate.md +322 -28
  28. package/commands/work-on/build.md +173 -95
  29. package/commands/work-on/close.md +50 -15
  30. package/commands/work-on/decompose.md +72 -34
  31. package/commands/work-on/investigate.md +16 -12
  32. package/commands/work-on/remediate.md +58 -25
  33. package/commands/work-on/review.md +36 -21
  34. package/commands/work-on.md +1 -2
  35. package/docs/CONFIG.md +132 -26
  36. package/docs/OUTCOME-CODES.md +12 -3
  37. package/docs/RELEASE-NOTES.md +12 -0
  38. package/docs/STATS.md +13 -5
  39. package/docs/TELEMETRY.md +7 -4
  40. package/docs/WORK-ON-RUNTIME.md +24 -0
  41. package/docs/spec/forge-protocol-v1.md +2 -2
  42. package/package.json +2 -2
  43. package/packages/protocol/src/phases.js +5 -5
  44. package/packages/protocol/src/types.js +1 -1
  45. package/scripts/acceptance-red-green.sh +90 -0
  46. package/scripts/acceptance-red-green.test.sh +74 -0
  47. package/scripts/cap-findings.sh +1 -1
  48. package/scripts/cap-findings.test.sh +3 -1
  49. package/scripts/check-contract-scope.test.sh +2 -0
  50. package/scripts/check-edge-cases.sh +143 -0
  51. package/scripts/check-edge-cases.test.sh +175 -0
  52. package/scripts/check-spec-bash.sh +5 -1
  53. package/scripts/check-spec-bash.test.sh +20 -0
  54. package/scripts/check-spec-markers.sh +2 -0
  55. package/scripts/check-test-tmp-cleanup.sh +56 -0
  56. package/scripts/check-test-tmp-cleanup.test.sh +69 -0
  57. package/scripts/ci-local/adapters/azure-pipelines.mjs +113 -0
  58. package/scripts/ci-local/adapters/bitbucket.mjs +64 -0
  59. package/scripts/ci-local/adapters/buildkite.mjs +58 -0
  60. package/scripts/ci-local/adapters/circleci.mjs +101 -0
  61. package/scripts/ci-local/adapters/common.mjs +115 -0
  62. package/scripts/ci-local/adapters/explicit.mjs +21 -0
  63. package/scripts/ci-local/adapters/github-actions.mjs +349 -0
  64. package/scripts/ci-local/adapters/gitlab-ci.mjs +184 -0
  65. package/scripts/ci-local/adapters/portability.mjs +104 -0
  66. package/scripts/ci-local/adapters/task-runners.mjs +67 -0
  67. package/scripts/ci-local/cli.mjs +50 -0
  68. package/scripts/ci-local/lib.mjs +371 -0
  69. package/scripts/ci-local/plan.mjs +186 -0
  70. package/scripts/ci-local/run.mjs +345 -0
  71. package/scripts/ci-local/scoping.mjs +508 -0
  72. package/scripts/ci-local.sh +97 -0
  73. package/scripts/ci-local.test.sh +919 -0
  74. package/scripts/classify-finding.sh +32 -6
  75. package/scripts/classify-finding.test.sh +23 -0
  76. package/scripts/classify-test-run.sh +69 -0
  77. package/scripts/classify-test-run.test.sh +90 -0
  78. package/scripts/close-parent-c3.test.sh +71 -0
  79. package/scripts/consumer-sweep.sh +569 -0
  80. package/scripts/consumer-sweep.test.sh +357 -0
  81. package/scripts/detect-append-registries.sh +106 -0
  82. package/scripts/detect-append-registries.test.sh +64 -0
  83. package/scripts/diff-size.sh +93 -4
  84. package/scripts/diff-size.test.sh +59 -2
  85. package/scripts/fixtures/ci-local/azure/azure-pipelines.yml +11 -0
  86. package/scripts/fixtures/ci-local/bitbucket/bitbucket-pipelines.yml +12 -0
  87. package/scripts/fixtures/ci-local/buildkite/.buildkite/pipeline.yml +10 -0
  88. package/scripts/fixtures/ci-local/circleci/.circleci/config.yml +29 -0
  89. package/scripts/fixtures/ci-local/gha-basic/.github/workflows/ci.yml +59 -0
  90. package/scripts/fixtures/ci-local/gha-basic/.github/workflows/docs.yml +10 -0
  91. package/scripts/fixtures/ci-local/gha-basic/.github/workflows/nightly.yml +10 -0
  92. package/scripts/fixtures/ci-local/gha-basic/docs/index.md +1 -0
  93. package/scripts/fixtures/ci-local/gha-basic/src/greet.sh +2 -0
  94. package/scripts/fixtures/ci-local/gha-basic/tests/greet.test.sh +3 -0
  95. package/scripts/fixtures/ci-local/gha-dorny/.github/workflows/ci.yml +51 -0
  96. package/scripts/fixtures/ci-local/gha-dorny/backend/app.py +1 -0
  97. package/scripts/fixtures/ci-local/gha-dorny/docs/a.md +1 -0
  98. package/scripts/fixtures/ci-local/gha-dorny/frontend/app.js +1 -0
  99. package/scripts/fixtures/ci-local/gha-run/.github/workflows/ci.yml +39 -0
  100. package/scripts/fixtures/ci-local/gha-run/src/x.txt +1 -0
  101. package/scripts/fixtures/ci-local/gitlab/.gitlab-ci.yml +43 -0
  102. package/scripts/fixtures/ci-local/gitlab/docs/d.md +1 -0
  103. package/scripts/fixtures/ci-local/gitlab/src/a.txt +1 -0
  104. package/scripts/fixtures/ci-local/monorepo/.github/workflows/ci.yml +13 -0
  105. package/scripts/fixtures/ci-local/monorepo/package-lock.json +1 -0
  106. package/scripts/fixtures/ci-local/monorepo/packages/a/src/a.sh +2 -0
  107. package/scripts/fixtures/ci-local/monorepo/packages/a/tests/a.test.sh +3 -0
  108. package/scripts/fixtures/ci-local/monorepo/packages/b/src/b.sh +2 -0
  109. package/scripts/fixtures/ci-local/monorepo/packages/b/tests/b.test.sh +3 -0
  110. package/scripts/fixtures/ci-local/no-ci/Makefile +6 -0
  111. package/scripts/fixtures/ci-local/no-ci/package.json +1 -0
  112. package/scripts/fixtures/ci-local/no-ci/src/a.js +1 -0
  113. package/scripts/fixtures/ci-local/no-ci/src/b.js +1 -0
  114. package/scripts/fixtures/ci-local/no-ci/tests/a.test.js +2 -0
  115. package/scripts/fixtures/ci-local/no-ci/tests/b.test.js +2 -0
  116. package/scripts/forge-root.test.sh +14 -12
  117. package/scripts/lib/telemetry.sh +1 -0
  118. package/scripts/publish-staging-sync.test.sh +48 -0
  119. package/scripts/remediate-approved-head.test.sh +2 -0
  120. package/scripts/remediate-basesync.test.sh +1 -1
  121. package/scripts/resolve-deploy-gate.sh +32 -6
  122. package/scripts/resolve-deploy-gate.test.sh +42 -1
  123. package/scripts/review-acceptance-gate.sh +2 -2
  124. package/scripts/review-acceptance-gate.test.sh +20 -0
  125. package/scripts/review-delta.sh +8 -1
  126. package/scripts/review-delta.test.sh +19 -0
  127. package/scripts/review-finalize.sh +1579 -0
  128. package/scripts/review-finalize.test.sh +1040 -0
  129. package/scripts/review-pr-delta-wiring.test.sh +32 -72
  130. package/scripts/review-pr-domain-map.test.sh +18 -13
  131. package/scripts/review-pr-gate2.test.sh +9 -65
  132. package/scripts/review-pr-staging-scope.test.sh +14 -98
  133. package/scripts/review-prepare.sh +53 -15
  134. package/scripts/review-prepare.test.sh +37 -3
  135. package/scripts/review-roster.sh +16 -2
  136. package/scripts/review-roster.test.sh +22 -0
  137. package/scripts/run-shell-tests.sh +5 -0
  138. package/scripts/telemetry.test.sh +2 -0
  139. package/scripts/trusted-comments.test.sh +11 -3
  140. package/scripts/verify-phase-trail.test.sh +8 -0
  141. package/scripts/wait-ci-green.sh +61 -9
  142. package/scripts/wait-ci-green.test.sh +44 -6
  143. 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" ] && [ "$CONF" = "CONFIRMED" ] || return 1
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
- *) disp "ISSUE unparseable-severity"; exit 0 ;;
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'