forgedock 1.16.2 → 1.17.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 +92 -0
- package/bin/engine/phases.mjs +86 -26
- package/bin/engine/projector.mjs +48 -1
- package/bin/engine/runlog.mjs +13 -5
- package/bin/engine/telemetry-seam.mjs +62 -0
- package/bin/engine/telemetry-seam.test.mjs +60 -0
- package/bin/engine/telemetry.mjs +148 -0
- package/bin/engine/telemetry.test.mjs +120 -0
- package/bin/engine-cli.mjs +12 -1
- package/bin/engine.mjs +145 -40
- package/bin/forgedock.mjs +7 -1
- package/bin/hooks/interactive-engine.mjs +10 -0
- package/bin/hooks/pre-tool-use.mjs +187 -0
- package/bin/runner.mjs +7 -0
- package/bin/stats/aggregate.mjs +231 -0
- package/bin/stats/cache.mjs +40 -0
- package/bin/stats/github-source.mjs +140 -0
- package/bin/stats/index.mjs +154 -0
- package/bin/stats/local-source.mjs +79 -0
- package/bin/stats/package.json +6 -0
- package/bin/stats/render.mjs +82 -0
- package/bin/stats/stats.test.mjs +224 -0
- package/commands/cleanup.md +83 -2
- package/commands/deploy-info.md +8 -4
- package/commands/orchestrate/phase-1-resolve.md +4 -1
- package/commands/orchestrate/phase-4-execution.md +17 -2
- package/commands/orchestrate/phase-5-cleanup.md +18 -2
- package/commands/pipeline-health/phase-1-context.md +2 -0
- package/commands/pipeline-health/phase-2-metrics.md +12 -5
- package/commands/pipeline-status.md +21 -1
- package/commands/review-pr-agents/protocols.md +1 -0
- package/commands/review-pr-agents.md +10 -2
- package/commands/review-pr-staging.md +102 -25
- package/commands/review-pr.md +119 -103
- package/commands/work-on/build/implement.md +26 -5
- package/commands/work-on/build/validate.md +3 -2
- package/commands/work-on/build.md +95 -64
- package/commands/work-on/close.md +17 -6
- package/commands/work-on/decompose.md +45 -4
- package/commands/work-on/investigate.md +6 -2
- package/commands/work-on/remediate.md +22 -6
- package/commands/work-on/review.md +2 -2
- package/docs/CONFIG.md +33 -0
- package/docs/OUTCOME-CODES.md +48 -0
- package/docs/RELEASE-NOTES.md +12 -0
- package/docs/STATS.md +62 -0
- package/docs/TELEMETRY.md +65 -0
- package/docs/WORK-ON-RUNTIME.md +13 -0
- package/docs/site/command-reference.md +1 -1
- package/docs/spec/label-state-machine.md +1 -1
- package/package.json +3 -2
- package/scripts/builder-resume-match.test.sh +58 -0
- package/scripts/cap-findings.sh +110 -0
- package/scripts/cap-findings.test.sh +130 -0
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/classify-finding.sh +65 -16
- package/scripts/classify-finding.test.sh +43 -1
- package/scripts/classify-grep-exit.sh +20 -0
- package/scripts/classify-grep-exit.test.sh +46 -0
- package/scripts/close-parent-c3.test.sh +54 -0
- package/scripts/cost-report.sh +146 -0
- package/scripts/cost-report.test.sh +80 -0
- package/scripts/diff-size.sh +5 -0
- package/scripts/doctor-pipeline-state.sh +43 -0
- package/scripts/file-deferred-followups.sh +159 -0
- package/scripts/file-deferred-followups.test.sh +106 -0
- package/scripts/fixtures/cost-report/a-merged1.jsonl +6 -0
- package/scripts/fixtures/cost-report/a-merged2-remediated.jsonl +7 -0
- package/scripts/fixtures/cost-report/a-needs-human.jsonl +4 -0
- package/scripts/fixtures/cost-report/b-merged.jsonl +3 -0
- package/scripts/fixtures/cost-report/c-merged-attempt-usage.jsonl +8 -0
- package/scripts/fixtures/cost-report/hydrated-no-usage.jsonl +3 -0
- package/scripts/fixtures/cost-report/old-log-no-version.jsonl +3 -0
- package/scripts/fixtures/cost-report/unfinished-no-terminal.jsonl +2 -0
- package/scripts/fixtures/cost-report/unfinished-reopened.jsonl +5 -0
- package/scripts/forge-root.test.sh +8 -3
- package/scripts/lib/fence-bootstrap.sh +4 -3
- package/scripts/lib/pr-issue-number.sh +18 -0
- package/scripts/lib/review-template-resolve.sh +41 -0
- package/scripts/lib/telemetry.sh +168 -0
- package/scripts/remediate-liveness.sh +42 -6
- package/scripts/review-acceptance-gate.sh +16 -7
- package/scripts/review-acceptance-gate.test.sh +13 -0
- package/scripts/review-pr-domain-map.test.sh +8 -3
- package/scripts/review-pr-gate2.test.sh +15 -0
- package/scripts/review-pr-staging-finding-filter.test.sh +44 -0
- package/scripts/review-pr-staging-scope.test.sh +65 -5
- package/scripts/review-prepare.sh +517 -0
- package/scripts/review-prepare.test.sh +270 -0
- package/scripts/review-roster.sh +172 -0
- package/scripts/review-roster.test.sh +163 -0
- package/scripts/run-shell-tests.sh +3 -0
- package/scripts/telemetry.test.sh +106 -0
- package/scripts/wait-ci-green.sh +8 -1
- package/templates/forge.yaml.minimal +9 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# Fixtures for scripts/cap-findings.sh (review-pr §6B.5 / review-pr-staging §7B.5 per-review cap).
|
|
6
|
+
# Run: bash scripts/cap-findings.test.sh (bash 3.2 compatible)
|
|
7
|
+
|
|
8
|
+
set -u
|
|
9
|
+
DIR=$(cd "$(dirname "$0")" && pwd)
|
|
10
|
+
S="$DIR/cap-findings.sh"
|
|
11
|
+
pass=0; fail=0
|
|
12
|
+
T=$(printf '\t')
|
|
13
|
+
|
|
14
|
+
# rows <id:sev:conf:disp[:agent]>... -> TSV on stdout (disp may use ~ for space)
|
|
15
|
+
rows() {
|
|
16
|
+
local spec id sev conf disp agent
|
|
17
|
+
for spec in "$@"; do
|
|
18
|
+
IFS=: read -r id sev conf disp agent <<EOF2
|
|
19
|
+
$spec
|
|
20
|
+
EOF2
|
|
21
|
+
disp=$(printf '%s' "$disp" | tr '~' ' ')
|
|
22
|
+
printf '%s\t%s\t%s\t%s\t%s\n' "$id" "$sev" "$conf" "$disp" "$agent"
|
|
23
|
+
done
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# expect <description> <expected "id=CLASS ..."> <script args> -- <row specs>
|
|
27
|
+
# Compares the first word of each output row's disposition, in input order, plus the CAPPED trailer.
|
|
28
|
+
expect() {
|
|
29
|
+
local desc="$1" want="$2"; shift 2
|
|
30
|
+
local args=()
|
|
31
|
+
while [ "$1" != "--" ]; do args+=("$1"); shift; done
|
|
32
|
+
shift
|
|
33
|
+
local got
|
|
34
|
+
got=$(rows "$@" | bash "$S" ${args[@]+"${args[@]}"} 2>/dev/null | awk -F'\t' 'NF >= 2 { split($2, w, " "); printf "%s=%s%s ", $1, w[1], (w[2] == "capped" ? "(capped)" : "") } /^CAPPED=/ { printf "%s", $0 }')
|
|
35
|
+
if [ "$got" = "$want" ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: $desc"; echo " want: $want"; echo " got: $got"; fi
|
|
36
|
+
}
|
|
37
|
+
expect_exit() { # expect_exit <code> <description> -- args...
|
|
38
|
+
local want="$1" desc="$2"; shift 3
|
|
39
|
+
bash "$S" "$@" </dev/null >/dev/null 2>&1; local got=$?
|
|
40
|
+
if [ "$got" = "$want" ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: $desc (want exit $want, got $got)"; fi
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# --- Default cap is 2; ranking by severity, then confidence ---
|
|
44
|
+
expect "under the cap: nothing capped" "a=ISSUE b=ISSUE CAPPED=0" -- \
|
|
45
|
+
a:MEDIUM:LIKELY:ISSUE~x b:MEDIUM:CONFIRMED:ISSUE~x
|
|
46
|
+
expect "overflow becomes NOTE capped, lowest rank loses" "a=NOTE(capped) b=ISSUE c=ISSUE CAPPED=1" -- \
|
|
47
|
+
a:MEDIUM:POSSIBLE:ISSUE~x b:MEDIUM:CONFIRMED:ISSUE~x c:MEDIUM:LIKELY:ISSUE~x
|
|
48
|
+
expect "ranks by confidence CONFIRMED > LIKELY > POSSIBLE" "a=NOTE(capped) b=ISSUE c=ISSUE d=NOTE(capped) CAPPED=2" -- \
|
|
49
|
+
a:MEDIUM:POSSIBLE:ISSUE~x b:MEDIUM:CONFIRMED:ISSUE~x c:MEDIUM:LIKELY:ISSUE~x d:MEDIUM:POSSIBLE:ISSUE~x
|
|
50
|
+
expect "MEDIUM outranks LOW even at lower confidence" "a=NOTE(capped) b=ISSUE c=ISSUE CAPPED=1" -- \
|
|
51
|
+
a:LOW:CONFIRMED:ISSUE~x b:MEDIUM:POSSIBLE:ISSUE~x c:MEDIUM:LIKELY:ISSUE~x
|
|
52
|
+
expect "ties broken by safety-domain agent" "a=NOTE(capped) b=NOTE(capped) c=ISSUE d=ISSUE CAPPED=2" -- \
|
|
53
|
+
a:MEDIUM:LIKELY:ISSUE~x:General b:MEDIUM:LIKELY:ISSUE~x:General c:MEDIUM:LIKELY:ISSUE~x:Auth d:MEDIUM:LIKELY:ISSUE~x:Concurrency_reviewer
|
|
54
|
+
expect "remaining ties broken by input order (stable)" "a=ISSUE b=ISSUE c=NOTE(capped) CAPPED=1" -- \
|
|
55
|
+
a:MEDIUM:LIKELY:ISSUE~x b:MEDIUM:LIKELY:ISSUE~x c:MEDIUM:LIKELY:ISSUE~x
|
|
56
|
+
|
|
57
|
+
# --- HIGH/CRITICAL are never capped and do not consume the default budget ---
|
|
58
|
+
expect "HIGH and CRITICAL never capped past the cap" "a=ISSUE b=ISSUE c=ISSUE d=ISSUE e=ISSUE CAPPED=0" -- \
|
|
59
|
+
a:HIGH:POSSIBLE:ISSUE~x b:CRITICAL:LIKELY:ISSUE~x c:HIGH:CONFIRMED:ISSUE~x d:MEDIUM:CONFIRMED:ISSUE~x e:MEDIUM:LIKELY:ISSUE~x
|
|
60
|
+
expect "HIGH does not consume the default budget" "a=ISSUE b=ISSUE c=ISSUE d=NOTE(capped) CAPPED=1" -- \
|
|
61
|
+
a:HIGH:LIKELY:ISSUE~x b:MEDIUM:CONFIRMED:ISSUE~x c:MEDIUM:LIKELY:ISSUE~x d:MEDIUM:POSSIBLE:ISSUE~x
|
|
62
|
+
expect "lowercase high is never capped" "a=ISSUE b=NOTE(capped) CAPPED=1" --cap 0 -- \
|
|
63
|
+
a:high:POSSIBLE:ISSUE~x b:medium:LIKELY:ISSUE~x
|
|
64
|
+
|
|
65
|
+
# --- Unparseable severity/confidence ranks as MEDIUM/LIKELY and IS capped ---
|
|
66
|
+
expect "unparseable severity is capped like MEDIUM" "a=NOTE(capped) b=ISSUE c=ISSUE CAPPED=1" -- \
|
|
67
|
+
a:INFO:POSSIBLE:ISSUE~x b:MEDIUM:CONFIRMED:ISSUE~x c:MEDIUM:LIKELY:ISSUE~x
|
|
68
|
+
expect "missing severity and confidence rank MEDIUM/LIKELY (above POSSIBLE, below CONFIRMED)" "a=ISSUE b=NOTE(capped) c=ISSUE CAPPED=1" -- \
|
|
69
|
+
a:MEDIUM:CONFIRMED:ISSUE~x b:MEDIUM:POSSIBLE:ISSUE~x c::ISSUE~x
|
|
70
|
+
expect "unparseable severity cannot displace a ranked MEDIUM CONFIRMED when cap is 1" "a=NOTE(capped) b=ISSUE CAPPED=1" --cap 1 -- \
|
|
71
|
+
a::ISSUE~x b:MEDIUM:CONFIRMED:ISSUE~x
|
|
72
|
+
|
|
73
|
+
# --- Lineage review-finding: default 1, filed HIGH+ consumes the budget ---
|
|
74
|
+
expect "lineage default 1 keeps only the best" "a=NOTE(capped) b=ISSUE c=NOTE(capped) CAPPED=2" --lineage review-finding -- \
|
|
75
|
+
a:MEDIUM:LIKELY:ISSUE~x b:MEDIUM:CONFIRMED:ISSUE~x c:MEDIUM:LIKELY:ISSUE~x
|
|
76
|
+
expect "lineage: a filed HIGH consumes the budget, all MEDIUM capped" "a=ISSUE b=NOTE(capped) c=NOTE(capped) CAPPED=2" --lineage review-finding -- \
|
|
77
|
+
a:HIGH:LIKELY:ISSUE~x b:MEDIUM:CONFIRMED:ISSUE~x c:MEDIUM:LIKELY:ISSUE~x
|
|
78
|
+
expect "lineage: CRITICAL still never capped" "a=ISSUE b=ISSUE CAPPED=0" --lineage review-finding -- \
|
|
79
|
+
a:CRITICAL:CONFIRMED:ISSUE~x b:HIGH:CONFIRMED:ISSUE~x
|
|
80
|
+
expect "lineage: explicit cap 3 with one HIGH keeps two MEDIUM" "a=ISSUE b=ISSUE c=ISSUE d=NOTE(capped) CAPPED=1" --lineage review-finding --cap 3 -- \
|
|
81
|
+
a:HIGH:LIKELY:ISSUE~x b:MEDIUM:CONFIRMED:ISSUE~x c:MEDIUM:LIKELY:ISSUE~x d:MEDIUM:POSSIBLE:ISSUE~x
|
|
82
|
+
|
|
83
|
+
# --- Explicit cap, 0, invalid cap ---
|
|
84
|
+
expect "explicit cap 3 overrides default 2" "a=ISSUE b=ISSUE c=ISSUE CAPPED=0" --cap 3 -- \
|
|
85
|
+
a:MEDIUM:LIKELY:ISSUE~x b:MEDIUM:LIKELY:ISSUE~x c:MEDIUM:LIKELY:ISSUE~x
|
|
86
|
+
expect "cap 0 caps every cappable row, never HIGH" "a=NOTE(capped) b=ISSUE c=NOTE(capped) CAPPED=2" --cap 0 -- \
|
|
87
|
+
a:MEDIUM:CONFIRMED:ISSUE~x b:HIGH:LIKELY:ISSUE~x c:LOW:CONFIRMED:ISSUE~x
|
|
88
|
+
expect "invalid cap falls back to default 2" "a=ISSUE b=ISSUE c=NOTE(capped) CAPPED=1" --cap abc -- \
|
|
89
|
+
a:MEDIUM:CONFIRMED:ISSUE~x b:MEDIUM:LIKELY:ISSUE~x c:MEDIUM:POSSIBLE:ISSUE~x
|
|
90
|
+
expect "negative cap falls back to default 2" "a=ISSUE b=ISSUE c=NOTE(capped) CAPPED=1" --cap -1 -- \
|
|
91
|
+
a:MEDIUM:CONFIRMED:ISSUE~x b:MEDIUM:LIKELY:ISSUE~x c:MEDIUM:POSSIBLE:ISSUE~x
|
|
92
|
+
expect "invalid cap under lineage falls back to 1" "a=ISSUE b=NOTE(capped) CAPPED=1" --lineage review-finding --cap x -- \
|
|
93
|
+
a:MEDIUM:CONFIRMED:ISSUE~x b:MEDIUM:LIKELY:ISSUE~x
|
|
94
|
+
|
|
95
|
+
# --- Pass-through rows never consume the cap ---
|
|
96
|
+
expect "NOTE and INPR_FIX rows pass through" "a=NOTE b=INPR_FIX c=ISSUE d=ISSUE e=NOTE(capped) CAPPED=1" -- \
|
|
97
|
+
a:MEDIUM:LIKELY:NOTE~reflag-note b:MEDIUM:CONFIRMED:INPR_FIX~in-pr c:MEDIUM:CONFIRMED:ISSUE~x d:MEDIUM:LIKELY:ISSUE~x e:MEDIUM:POSSIBLE:ISSUE~x
|
|
98
|
+
expect "empty input prints only the trailer" "CAPPED=0" --
|
|
99
|
+
|
|
100
|
+
# --- Reason text and original rows survive ---
|
|
101
|
+
out=$(rows "a:MEDIUM:CONFIRMED:ISSUE~medium~confirmed" | bash "$S" 2>/dev/null | head -n 1)
|
|
102
|
+
if [ "$out" = "a${T}ISSUE medium confirmed" ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: kept row keeps its classifier reason (got '$out')"; fi
|
|
103
|
+
out=$(rows "a:MEDIUM:CONFIRMED:NOTE~stale" | bash "$S" 2>/dev/null | head -n 1)
|
|
104
|
+
if [ "$out" = "a${T}NOTE stale" ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: NOTE row passes through verbatim (got '$out')"; fi
|
|
105
|
+
|
|
106
|
+
# --- Severity text noise never demotes HIGH/CRITICAL (classifier strips whitespace and uppercases) ---
|
|
107
|
+
expect "whitespace/markdown HIGH still uncapped at cap 0" "a=ISSUE b=ISSUE c=ISSUE d=NOTE(capped) CAPPED=1" --cap 0 -- \
|
|
108
|
+
"a:HIGH :CONFIRMED:ISSUE~severity-HIGH" 'b:**HIGH**:CONFIRMED:ISSUE~severity-HIGH' c:High:LIKELY:ISSUE~severity-HIGH d:MEDIUM:LIKELY:ISSUE~x
|
|
109
|
+
expect "fix-efficacy ISSUE rows are never capped (#3857 exemption survives the cap)" "a=ISSUE b=ISSUE c=NOTE(capped) CAPPED=1" --cap 1 -- \
|
|
110
|
+
a:MEDIUM:LIKELY:ISSUE~fix-efficacy-fails b:MEDIUM:CONFIRMED:ISSUE~x c:MEDIUM:LIKELY:ISSUE~x
|
|
111
|
+
expect "lineage: fix-efficacy beats an unrelated CONFIRMED MEDIUM and consumes the budget" "a=NOTE(capped) b=ISSUE CAPPED=1" --lineage review-finding -- \
|
|
112
|
+
a:MEDIUM:CONFIRMED:ISSUE~lineage-medium-CONFIRMED b:MEDIUM:LIKELY:ISSUE~fix-efficacy-regresses
|
|
113
|
+
expect "classifier severity-HIGH/CRITICAL reason is never capped" "a=ISSUE b=ISSUE CAPPED=0" --cap 0 -- \
|
|
114
|
+
"a:weird:CONFIRMED:ISSUE~severity-HIGH" "b:weird:CONFIRMED:ISSUE~severity-CRITICAL"
|
|
115
|
+
expect "lineage budget counts noisy HIGH" "a=ISSUE b=NOTE(capped) CAPPED=1" --cap 1 --lineage review-finding -- \
|
|
116
|
+
"a:HIGH :CONFIRMED:ISSUE~severity-HIGH" b:MEDIUM:CONFIRMED:ISSUE~x
|
|
117
|
+
|
|
118
|
+
# --- --input file and usage errors ---
|
|
119
|
+
tmp=$(mktemp)
|
|
120
|
+
rows a:MEDIUM:LIKELY:ISSUE~x b:MEDIUM:LIKELY:ISSUE~x c:MEDIUM:LIKELY:ISSUE~x > "$tmp"
|
|
121
|
+
got=$(bash "$S" --input "$tmp" 2>/dev/null | tail -n 1)
|
|
122
|
+
rm -f "$tmp"
|
|
123
|
+
if [ "$got" = "CAPPED=1" ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: --input file (got '$got')"; fi
|
|
124
|
+
expect_exit 2 "unknown arg is a usage error" -- --bogus
|
|
125
|
+
expect_exit 2 "bad lineage is a usage error" -- --lineage sideways
|
|
126
|
+
expect_exit 2 "missing --input file is a usage error" -- --input /nonexistent/cap-findings-rows
|
|
127
|
+
expect_exit 0 "empty stdin exits 0" --
|
|
128
|
+
|
|
129
|
+
echo "cap-findings.test.sh: $pass passed, $fail failed"
|
|
130
|
+
[ "$fail" -eq 0 ]
|
|
@@ -165,12 +165,14 @@ MARKER_REGISTRY="
|
|
|
165
165
|
REMEDIATION
|
|
166
166
|
REMEDIATION_HELD
|
|
167
167
|
REMEDIATION_DEFERRED
|
|
168
|
+
REMEDIATION_DISPATCH
|
|
168
169
|
REREVIEW_DISPATCHED
|
|
169
170
|
REREVIEW_RELEASED
|
|
170
171
|
REREVIEW_SKIPPED
|
|
171
172
|
REVIEW
|
|
172
173
|
REVIEWER
|
|
173
174
|
REVIEW_ROUTE
|
|
175
|
+
REVIEW_TIER
|
|
174
176
|
REVIEW_STARTED
|
|
175
177
|
SECURITY_AUDIT
|
|
176
178
|
SIGNAL_RESOLVED
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
# classify-finding.sh --severity <S> [--confidence <C>] [--agent <A>]
|
|
12
12
|
# [--lineage none|review-finding] [--text <T> | --text-file <F>]
|
|
13
13
|
# [--inpr-diff <F> --file <P>] [--contract-scope <F> --contract-open <F> [--pr-files <F>] [--merge-issue <N>]]
|
|
14
|
+
# [--fix-efficacy fails|regresses] [--fixes-issue <N>]
|
|
14
15
|
# [--prior-findings <F> [--title <T>] [--line-start <N> [--line-end <M>]] [--diff-map <F>]]
|
|
15
16
|
#
|
|
16
17
|
# --severity CRITICAL | HIGH | MEDIUM | LOW (case-insensitive). Missing or
|
|
@@ -21,6 +22,14 @@
|
|
|
21
22
|
# --lineage `review-finding` when the PR's linked issue (MERGE_ISSUE) carries
|
|
22
23
|
# the `review-finding` label — i.e. this PR is itself a fix for a
|
|
23
24
|
# review finding. Default `none`.
|
|
25
|
+
# --fix-efficacy fails|regresses The finding says the PR's linked issue is not fixed (`fails`) or
|
|
26
|
+
# that the PR re-introduces its defect (`regresses`). A MEDIUM finding so marked is
|
|
27
|
+
# `ISSUE fix-efficacy-<value>` under any lineage and confidence (the cascade damper must
|
|
28
|
+
# not merge a known-broken fix). Without the flag the tag is read from `--text`
|
|
29
|
+
# (a line consisting only of `FIX_EFFICACY: fails|regresses` or `**Fix efficacy**: fails|regresses`;
|
|
30
|
+
# a mid-sentence mention never counts). Opt-in only: never keyword-based. Runs after the INPR_FIX and re-flag gates and
|
|
31
|
+
# before the contract-scope and lineage gates; LOW stays NOTE; HIGH/CRITICAL unchanged.
|
|
32
|
+
# --fixes-issue <N> The PR's linked issue; alias of `--merge-issue` (same variable).
|
|
24
33
|
# --text/--text-file Finding title + body + affected file paths, used for the
|
|
25
34
|
# content-based safety exemption.
|
|
26
35
|
# --inpr-diff <F> --file <P> In-PR fix gate (#3387, narrowed): F is a file listing the
|
|
@@ -100,6 +109,7 @@ CONTRACT_SCOPE=""
|
|
|
100
109
|
CONTRACT_OPEN=""
|
|
101
110
|
PR_FILES=""
|
|
102
111
|
MERGE_ISSUE=""
|
|
112
|
+
FIX_EFF=""
|
|
103
113
|
PRIOR_FINDINGS=""
|
|
104
114
|
TITLE=""
|
|
105
115
|
LINE_START=""
|
|
@@ -107,7 +117,7 @@ LINE_END=""
|
|
|
107
117
|
DIFF_MAP=""
|
|
108
118
|
|
|
109
119
|
usage() {
|
|
110
|
-
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>]] [--prior-findings <F> [--title <T>] [--line-start <N> [--line-end <M>]] [--diff-map <F>]]" >&2
|
|
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
|
|
111
121
|
exit 2
|
|
112
122
|
}
|
|
113
123
|
|
|
@@ -124,6 +134,8 @@ while [ "$#" -gt 0 ]; do
|
|
|
124
134
|
--contract-open) [ "$#" -ge 2 ] || usage; CONTRACT_OPEN="$2"; shift 2 ;;
|
|
125
135
|
--pr-files) [ "$#" -ge 2 ] || usage; PR_FILES="$2"; shift 2 ;;
|
|
126
136
|
--merge-issue) [ "$#" -ge 2 ] || usage; MERGE_ISSUE="${2#\#}"; shift 2 ;;
|
|
137
|
+
--fixes-issue) [ "$#" -ge 2 ] || usage; MERGE_ISSUE="${2#\#}"; shift 2 ;;
|
|
138
|
+
--fix-efficacy) [ "$#" -ge 2 ] || usage; FIX_EFF=$(printf '%s' "$2" | tr '[:upper:]' '[:lower:]'); shift 2 ;;
|
|
127
139
|
--prior-findings) [ "$#" -ge 2 ] || usage; PRIOR_FINDINGS="$2"; shift 2 ;;
|
|
128
140
|
--title) [ "$#" -ge 2 ] || usage; TITLE="$2"; shift 2 ;;
|
|
129
141
|
--line-start) [ "$#" -ge 2 ] || usage; LINE_START="$2"; shift 2 ;;
|
|
@@ -142,6 +154,11 @@ case "$LINEAGE" in
|
|
|
142
154
|
*) echo "ERROR: --lineage must be 'none' or 'review-finding' (got '$LINEAGE')" >&2; exit 2 ;;
|
|
143
155
|
esac
|
|
144
156
|
|
|
157
|
+
case "$FIX_EFF" in
|
|
158
|
+
''|fails|regresses) ;;
|
|
159
|
+
*) echo "ERROR: --fix-efficacy must be 'fails' or 'regresses' (got '$FIX_EFF')" >&2; exit 2 ;;
|
|
160
|
+
esac
|
|
161
|
+
|
|
145
162
|
if [ -n "$INPR_DIFF" ] && [ ! -r "$INPR_DIFF" ]; then
|
|
146
163
|
echo "ERROR: --inpr-diff not readable: $INPR_DIFF" >&2; exit 2
|
|
147
164
|
fi
|
|
@@ -165,10 +182,32 @@ upper() { printf '%s' "$1" | tr '[:lower:]' '[:upper:]' | tr -d '[:space:]'; }
|
|
|
165
182
|
SEV=$(upper "$SEVERITY")
|
|
166
183
|
CONF=$(upper "$CONFIDENCE")
|
|
167
184
|
|
|
185
|
+
# Telemetry (forge#3834): one best-effort record per finding, then the unchanged stdout line.
|
|
186
|
+
_TEL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/telemetry.sh"; [ -f "$_TEL" ] && . "$_TEL" 2>/dev/null
|
|
187
|
+
type forge_emit >/dev/null 2>&1 || forge_emit() { :; }
|
|
188
|
+
disp() { # "<DISPOSITION> <reason>"
|
|
189
|
+
forge_emit finding_disposition severity="$SEV" confidence="${CONF:-UNKNOWN}" agent="${AGENT:-none}" disposition="${1%% *}" reason="${1#* }"
|
|
190
|
+
echo "$1"
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# In-PR fix gate predicate (#3387, widened to HIGH/CRITICAL in #3592): CONFIRMED, linked to a
|
|
194
|
+
# --inpr-diff file list, and the finding's file is one of those files. Fails toward ISSUE.
|
|
195
|
+
inpr_match() {
|
|
196
|
+
[ -n "$INPR_DIFF" ] && [ -n "$FINDING_FILE" ] && [ "$CONF" = "CONFIRMED" ] || return 1
|
|
197
|
+
_path="${FINDING_FILE#./}"; _path="${_path%%:*}"
|
|
198
|
+
grep -Fxq -- "$_path" "$INPR_DIFF"
|
|
199
|
+
}
|
|
200
|
+
|
|
168
201
|
case "$SEV" in
|
|
169
|
-
CRITICAL|HIGH)
|
|
202
|
+
CRITICAL|HIGH)
|
|
203
|
+
# HIGH/CRITICAL skip the re-flag and safety gates below, so the in-PR check runs here, before
|
|
204
|
+
# the exit. Everything else stays ISSUE: a HIGH is never demoted to NOTE.
|
|
205
|
+
if inpr_match; then
|
|
206
|
+
disp "INPR_FIX $(printf '%s' "$SEV" | tr '[:upper:]' '[:lower:]')-confirmed-in-diff"; exit 0
|
|
207
|
+
fi
|
|
208
|
+
disp "ISSUE severity-$SEV"; exit 0 ;;
|
|
170
209
|
MEDIUM|LOW) ;;
|
|
171
|
-
*)
|
|
210
|
+
*) disp "ISSUE unparseable-severity"; exit 0 ;;
|
|
172
211
|
esac
|
|
173
212
|
|
|
174
213
|
# Cross-head / cross-PR re-flag gate (#3641). Runs after the severity parse so HIGH/CRITICAL and
|
|
@@ -236,17 +275,27 @@ if [ -n "$PRIOR_FINDINGS" ] && [ -n "$FINDING_FILE" ]; then
|
|
|
236
275
|
}
|
|
237
276
|
END { if (!found && note) print note }
|
|
238
277
|
' "$PRIOR_FINDINGS" 2>/dev/null) || _reflag=""
|
|
239
|
-
if [ -n "$_reflag" ]; then
|
|
278
|
+
if [ -n "$_reflag" ]; then disp "$_reflag"; exit 0; fi
|
|
240
279
|
fi
|
|
241
280
|
|
|
242
281
|
# In-PR fix gate (#3387, narrowed). MEDIUM CONFIRMED is an ISSUE under every rule below
|
|
243
282
|
# (lineage included), so checking here changes only where it is handled, never whether.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
283
|
+
# HIGH/CRITICAL were handled in the severity case above.
|
|
284
|
+
if [ "$SEV" = "MEDIUM" ] && inpr_match; then
|
|
285
|
+
disp "INPR_FIX medium-confirmed-in-diff"
|
|
286
|
+
exit 0
|
|
287
|
+
fi
|
|
288
|
+
|
|
289
|
+
# Fix-efficacy exemption: a MEDIUM finding saying the PR's linked issue is not fixed, or that the PR
|
|
290
|
+
# re-introduces its defect, is never damped (any lineage, any confidence, no contract demotion).
|
|
291
|
+
# Opt-in on an explicit flag or a line-anchored tag only; never keyword-based (a bare "regression test for #N" must not match).
|
|
292
|
+
if [ "$SEV" = "MEDIUM" ]; then
|
|
293
|
+
if [ -z "$FIX_EFF" ]; then
|
|
294
|
+
FIX_EFF=$(printf '%s' "$TEXT" | tr '[:upper:]' '[:lower:]' \
|
|
295
|
+
| grep -Eo '^[[:space:]]*(fix_efficacy:|\*\*fix efficacy\*\*:)[[:space:]]*(fails|regresses)[[:space:]]*$' | head -n 1 \
|
|
296
|
+
| grep -Eo '(fails|regresses)$') || FIX_EFF=""
|
|
249
297
|
fi
|
|
298
|
+
if [ -n "$FIX_EFF" ]; then disp "ISSUE fix-efficacy-$FIX_EFF"; exit 0; fi
|
|
250
299
|
fi
|
|
251
300
|
|
|
252
301
|
# Safety exemption. Keywords match whole words, where `_`, `-`, `/` and other
|
|
@@ -282,10 +331,10 @@ if [ -n "$CONTRACT_SCOPE" ] && [ -n "$FINDING_FILE" ]; then
|
|
|
282
331
|
deferred)
|
|
283
332
|
if [ -z "$SAFETY" ] && [ -n "$_inum" ] && [ "$_inum" != "$MERGE_ISSUE" ] \
|
|
284
333
|
&& [ -n "$CONTRACT_OPEN" ] && grep -Fxq -- "$_inum" "$CONTRACT_OPEN"; then
|
|
285
|
-
|
|
334
|
+
disp "NOTE contract-deferred #$_inum"; exit 0
|
|
286
335
|
fi ;;
|
|
287
336
|
accepted-risk)
|
|
288
|
-
if [ -z "$SAFETY" ]; then
|
|
337
|
+
if [ -z "$SAFETY" ]; then disp "NOTE contract-accepted-risk"; exit 0; fi ;;
|
|
289
338
|
esac
|
|
290
339
|
done < "$CONTRACT_SCOPE"
|
|
291
340
|
fi
|
|
@@ -293,21 +342,21 @@ fi
|
|
|
293
342
|
|
|
294
343
|
if [ "$LINEAGE" = "review-finding" ]; then
|
|
295
344
|
if [ "$SEV" = "MEDIUM" ] && { [ "$CONF" = "CONFIRMED" ] || { [ -n "$SAFETY" ] && [ "$CONF" = "LIKELY" ]; }; }; then
|
|
296
|
-
|
|
345
|
+
disp "ISSUE lineage-medium-${CONF:-UNKNOWN}"
|
|
297
346
|
else
|
|
298
|
-
|
|
347
|
+
disp "NOTE lineage-${SEV}-${CONF:-UNKNOWN}"
|
|
299
348
|
fi
|
|
300
349
|
exit 0
|
|
301
350
|
fi
|
|
302
351
|
|
|
303
352
|
if [ "$SEV" = "LOW" ] || [ "$CONF" = "POSSIBLE" ]; then
|
|
304
353
|
if [ -n "$SAFETY" ]; then
|
|
305
|
-
|
|
354
|
+
disp "ISSUE safety-exemption-$SAFETY"
|
|
306
355
|
else
|
|
307
|
-
|
|
356
|
+
disp "NOTE ${SEV}-${CONF:-UNKNOWN}"
|
|
308
357
|
fi
|
|
309
358
|
exit 0
|
|
310
359
|
fi
|
|
311
360
|
|
|
312
|
-
|
|
361
|
+
disp "ISSUE ${SEV}-${CONF:-UNKNOWN}"
|
|
313
362
|
exit 0
|
|
@@ -68,6 +68,35 @@ expect NOTE "lineage MEDIUM LIKELY no-safety note" -- --severity MEDIUM --confi
|
|
|
68
68
|
expect ISSUE "lineage MEDIUM LIKELY with keyword files" -- --severity MEDIUM --confidence LIKELY --lineage review-finding --text "token leaks"
|
|
69
69
|
expect NOTE "lineage MEDIUM POSSIBLE with keyword note" -- --severity MEDIUM --confidence POSSIBLE --lineage review-finding --text "token leaks"
|
|
70
70
|
|
|
71
|
+
# --- Fix-efficacy exemption (#3693): #3683 SEC-1 shape ---
|
|
72
|
+
SEC1="enqueue_breaker_decision helper never declared in the fences that call it; fresh shell never queues the breaker decision"
|
|
73
|
+
LN=(--severity MEDIUM --confidence LIKELY --lineage review-finding --agent Security --text "$SEC1")
|
|
74
|
+
expect NOTE "SEC-1 untagged stays damped" -- "${LN[@]}"
|
|
75
|
+
expect ISSUE "SEC-1 --fix-efficacy fails" -- "${LN[@]}" --fix-efficacy fails --fixes-issue 3621
|
|
76
|
+
expect ISSUE "SEC-1 --fix-efficacy regresses" -- "${LN[@]}" --fix-efficacy regresses
|
|
77
|
+
expect ISSUE "SEC-1 POSSIBLE with fix-efficacy" -- --severity MEDIUM --confidence POSSIBLE --lineage review-finding --text "x" --fix-efficacy fails
|
|
78
|
+
expect ISSUE "text tag FIX_EFFICACY fallback" -- --severity MEDIUM --confidence LIKELY --lineage review-finding --text "$(printf 'helper missing.\nFIX_EFFICACY: fails')"
|
|
79
|
+
expect ISSUE "bold Fix efficacy line fallback" -- --severity MEDIUM --confidence LIKELY --lineage review-finding --text "**Fix efficacy**: regresses"
|
|
80
|
+
expect NOTE "mid-line FIX_EFFICACY token stays NOTE" -- --severity MEDIUM --confidence LIKELY --lineage review-finding --text "helper missing. FIX_EFFICACY: fails"
|
|
81
|
+
expect NOTE "#N + still-fails phrase no longer opts in" -- --severity MEDIUM --confidence LIKELY --lineage review-finding --text "this still fails to fix #3621 in a fresh shell" --fixes-issue 3621
|
|
82
|
+
expect NOTE "regression test for #N stays NOTE" -- --severity MEDIUM --confidence LIKELY --lineage review-finding --text "Missing regression test for #3621" --fixes-issue 3621
|
|
83
|
+
expect NOTE "no regression #N stays NOTE" -- --severity MEDIUM --confidence LIKELY --lineage review-finding --text "no regression #3621, but wording is off" --fixes-issue 3621
|
|
84
|
+
expect NOTE "reintroduce-style phrase stays NOTE" -- --severity MEDIUM --confidence LIKELY --lineage review-finding --text "could reintroduce #3621 naming" --fixes-issue 3621
|
|
85
|
+
expect NOTE "LOW + fix-efficacy stays NOTE" -- --severity LOW --confidence CONFIRMED --lineage review-finding --text "x" --fix-efficacy fails
|
|
86
|
+
expect ISSUE "non-lineage MEDIUM POSSIBLE + efficacy" -- --severity MEDIUM --confidence POSSIBLE --text "x" --fix-efficacy fails
|
|
87
|
+
expect ISSUE "HIGH unchanged" -- --severity HIGH --confidence LIKELY --lineage review-finding --fix-efficacy fails
|
|
88
|
+
expect_exit 2 "bad --fix-efficacy value" -- --severity MEDIUM --fix-efficacy broken
|
|
89
|
+
FX_DIFF=$(mktemp "${TMPDIR:-/tmp}/classify-finding-fx.XXXXXX"); printf 'src/a.sh\n' > "$FX_DIFF"
|
|
90
|
+
expect INPR_FIX "CONFIRMED in-PR efficacy stays INPR_FIX" -- --severity MEDIUM --confidence CONFIRMED --lineage review-finding --fix-efficacy fails --inpr-diff "$FX_DIFF" --file src/a.sh
|
|
91
|
+
rm -f "$FX_DIFF"
|
|
92
|
+
FX_SCOPE=$(mktemp "${TMPDIR:-/tmp}/classify-finding-fxs.XXXXXX"); FX_OPEN=$(mktemp "${TMPDIR:-/tmp}/classify-finding-fxo.XXXXXX")
|
|
93
|
+
printf 'deferred\tsrc/b.sh\t3446\n' > "$FX_SCOPE"; printf '3446\n' > "$FX_OPEN"
|
|
94
|
+
expect NOTE "contract-deferred untagged demotes" -- --severity MEDIUM --confidence LIKELY --text "x" --file src/b.sh --contract-scope "$FX_SCOPE" --contract-open "$FX_OPEN"
|
|
95
|
+
expect ISSUE "contract-deferred + efficacy files" -- --severity MEDIUM --confidence LIKELY --text "x" --file src/b.sh --contract-scope "$FX_SCOPE" --contract-open "$FX_OPEN" --fix-efficacy fails
|
|
96
|
+
rm -f "$FX_SCOPE" "$FX_OPEN"
|
|
97
|
+
_a=$(bash "$S" --severity MEDIUM --lineage review-finding --text "x" --fix-efficacy fails --fixes-issue 5); _b=$(bash "$S" --severity MEDIUM --lineage review-finding --text "x" --fix-efficacy fails --merge-issue 5)
|
|
98
|
+
if [ "$_a" = "$_b" ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: --fixes-issue/--merge-issue equivalence"; fi
|
|
99
|
+
|
|
71
100
|
# --- text-file input ---
|
|
72
101
|
TMPF=$(mktemp "${TMPDIR:-/tmp}/classify-finding-test.XXXXXX")
|
|
73
102
|
printf 'Title: unchecked xss sink\n' > "$TMPF"
|
|
@@ -83,7 +112,20 @@ expect INPR_FIX "./ prefix matches" -- --severity MEDIUM --con
|
|
|
83
112
|
expect INPR_FIX "lineage PR still fixes in-PR" -- --severity MEDIUM --confidence CONFIRMED --lineage review-finding --inpr-diff "$DIFF" --file src/api/jobs.py
|
|
84
113
|
expect ISSUE "MEDIUM CONFIRMED outside diff files" -- --severity MEDIUM --confidence CONFIRMED --inpr-diff "$DIFF" --file src/api/other.py
|
|
85
114
|
expect ISSUE "MEDIUM LIKELY in diff is not gated" -- --severity MEDIUM --confidence LIKELY --inpr-diff "$DIFF" --file src/api/jobs.py
|
|
86
|
-
expect
|
|
115
|
+
expect INPR_FIX "HIGH CONFIRMED in diff (#3592)" -- --severity HIGH --confidence CONFIRMED --inpr-diff "$DIFF" --file src/api/jobs.py
|
|
116
|
+
expect INPR_FIX "CRITICAL CONFIRMED in diff (#3592)" -- --severity CRITICAL --confidence CONFIRMED --inpr-diff "$DIFF" --file "src/api/jobs.py:9"
|
|
117
|
+
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
|
|
118
|
+
expect ISSUE "HIGH LIKELY in diff stays ISSUE" -- --severity HIGH --confidence LIKELY --inpr-diff "$DIFF" --file src/api/jobs.py
|
|
119
|
+
expect ISSUE "CRITICAL LIKELY in diff stays ISSUE" -- --severity CRITICAL --confidence LIKELY --inpr-diff "$DIFF" --file src/api/jobs.py
|
|
120
|
+
expect ISSUE "HIGH CONFIRMED outside diff stays ISSUE" -- --severity HIGH --confidence CONFIRMED --inpr-diff "$DIFF" --file src/api/other.py
|
|
121
|
+
expect ISSUE "CRITICAL CONFIRMED outside diff" -- --severity CRITICAL --confidence CONFIRMED --inpr-diff "$DIFF" --file src/api/other.py
|
|
122
|
+
expect ISSUE "HIGH CONFIRMED without --inpr-diff" -- --severity HIGH --confidence CONFIRMED --file src/api/jobs.py
|
|
123
|
+
expect ISSUE "HIGH CONFIRMED without --file" -- --severity HIGH --confidence CONFIRMED --inpr-diff "$DIFF"
|
|
124
|
+
for _s in HIGH CRITICAL MEDIUM; do
|
|
125
|
+
_want="INPR_FIX $(printf '%s' "$_s" | tr '[:upper:]' '[:lower:]')-confirmed-in-diff"
|
|
126
|
+
_got=$(bash "$S" --severity "$_s" --confidence CONFIRMED --inpr-diff "$DIFF" --file src/api/jobs.py 2>/dev/null)
|
|
127
|
+
if [ "$_got" = "$_want" ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: reason carries severity $_s (want '$_want', got '$_got')"; fi
|
|
128
|
+
done
|
|
87
129
|
expect NOTE "LOW in diff stays NOTE" -- --severity LOW --confidence CONFIRMED --inpr-diff "$DIFF" --file src/api/jobs.py
|
|
88
130
|
expect ISSUE "no --inpr-diff: unchanged behaviour" -- --severity MEDIUM --confidence CONFIRMED --file src/api/jobs.py
|
|
89
131
|
expect ISSUE "no --file: unchanged behaviour" -- --severity MEDIUM --confidence CONFIRMED --inpr-diff "$DIFF"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# classify-grep-exit.sh <rc> <stderr> <target> — is a grep exit >= 2 a malformed check, or a real failure?
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
#
|
|
6
|
+
# Exit 0 + reason on stdout = malformed (a usage/regex error, a check defect, never a verdict on the code).
|
|
7
|
+
# Exit 1 = real verdict (exit 1 and exit 2 both fail the check). Never executes the target; it only reads its text.
|
|
8
|
+
# Wording covers GNU grep and BSD/macOS regcomp (`brackets ([ ]) not balanced`, `repetition-operator operand invalid`, ...).
|
|
9
|
+
# Fail closed <!-- Added: forge#3802 -->: malformed only when stderr POSITIVELY names a regex/option usage error.
|
|
10
|
+
# A target that suppresses stderr (`2>/dev/null`, `-s`) leaves it empty, so a missing/unreadable/looping operand
|
|
11
|
+
# file can never hide as malformed. Unknown or empty stderr is a real failure.
|
|
12
|
+
RC="${1:-0}"; ERR="${2:-}"; TARGET="${3:-}"
|
|
13
|
+
case "$RC" in ''|*[!0-9]*) exit 1 ;; esac
|
|
14
|
+
[ "$RC" -ge 2 ] || exit 1
|
|
15
|
+
case "$TARGET" in grep\ *|egrep\ *|fgrep\ *) ;; *) exit 1 ;; esac
|
|
16
|
+
if printf '%s' "$ERR" | LC_ALL=C grep -qiE 'Unmatched|Invalid (regular expression|preceding|back reference|range|character class|content of)|unrecognized option|invalid option|option requires an argument|Usage:|trailing backslash|character class syntax|conflicting matchers|error at position|mismatched|invalid escape|not balanced|operand invalid|empty \(sub\)expression|invalid repetition count|empty \(sub\)|illegal repetition|repetition-operator'; then
|
|
17
|
+
echo "grep usage error (exit $RC)"
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
exit 1
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# classify-grep-exit.test.sh — verdicts of scripts/classify-grep-exit.sh (forge#3802). Runs real grep, no network.
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
CLS="$SCRIPT_DIR/classify-grep-exit.sh"
|
|
8
|
+
PASS=0; FAILN=0
|
|
9
|
+
ok() { PASS=$((PASS+1)); echo "ok - $1"; }
|
|
10
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL - $1"; }
|
|
11
|
+
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
|
|
12
|
+
cd "$TMP" || exit 1
|
|
13
|
+
echo hello > present.txt
|
|
14
|
+
ln -s loop1 loop2; ln -s loop2 loop1
|
|
15
|
+
|
|
16
|
+
# run_target <target>: execute like B6.5 does, then classify. Prints "malformed" or "real".
|
|
17
|
+
verdict() {
|
|
18
|
+
local t="$1" err rc
|
|
19
|
+
err=$(eval "$t" 2>&1 >/dev/null); rc=$?
|
|
20
|
+
if bash "$CLS" "$rc" "$err" "$t" >/dev/null; then echo malformed; else echo real; fi
|
|
21
|
+
}
|
|
22
|
+
expect() { local got; got=$(verdict "$2"); if [ "$got" = "$3" ]; then ok "$1"; else bad "$1 (got $got, want $3)"; fi; }
|
|
23
|
+
|
|
24
|
+
expect "missing file, plain" "grep -qF -e x nofile.md" real
|
|
25
|
+
expect "missing file, 2>/dev/null" "grep -qF -e x nofile.md 2>/dev/null" real
|
|
26
|
+
expect "missing file, -s" "grep -sqF -e x nofile.md" real
|
|
27
|
+
expect "missing file, --no-messages" "grep --no-messages -qF -e x nofile.md" real
|
|
28
|
+
expect "symlink loop, stderr suppressed" "grep -qF -e x loop1 2>/dev/null" real
|
|
29
|
+
expect "directory operand, stderr suppressed" "grep -qF -e x . 2>/dev/null" real
|
|
30
|
+
expect "unmatched bracket regex" "grep -qE '[abc' present.txt" malformed
|
|
31
|
+
expect "unmatched paren regex" "grep -qE '(abc' present.txt" malformed
|
|
32
|
+
expect "pattern read as option" "grep -qE '--no-such-flag' present.txt" malformed
|
|
33
|
+
expect "option-looking pattern" "grep -qE '-z foo' present.txt" malformed
|
|
34
|
+
expect "non-grep target" "bash -c 'exit 2'" real
|
|
35
|
+
for _w in "grep: brackets ([ ]) not balanced" "grep: parentheses not balanced" "grep: braces not balanced" "grep: repetition-operator operand invalid" "grep: empty (sub)expression" "grep: invalid repetition count(s)"; do
|
|
36
|
+
bash "$CLS" 2 "$_w" "grep -E x f" >/dev/null && ok "BSD wording malformed: $_w" || bad "BSD wording malformed: $_w"
|
|
37
|
+
done
|
|
38
|
+
[ "$(bash "$CLS" 2 "grep: Unmatched [" "grep -q x f")" = "grep usage error (exit 2)" ] && ok "reason text" || bad "reason text"
|
|
39
|
+
bash "$CLS" 2 "grep: Unmatched [" "ls -l" >/dev/null && bad "non-grep with usage stderr is not malformed" || ok "non-grep with usage stderr is not malformed"
|
|
40
|
+
bash "$CLS" 1 "grep: Unmatched [" "grep -q x f" >/dev/null && bad "rc 1 is not malformed" || ok "rc 1 is not malformed"
|
|
41
|
+
bash "$CLS" 0 "" "grep -q x f" >/dev/null && bad "rc 0 is not malformed" || ok "rc 0 is not malformed"
|
|
42
|
+
bash "$CLS" 2 "" "grep -q x f" >/dev/null && bad "rc 2 empty stderr is a real failure" || ok "rc 2 empty stderr is a real failure"
|
|
43
|
+
bash "$CLS" 2 "" >/dev/null && bad "missing args fail closed" || ok "missing args fail closed"
|
|
44
|
+
|
|
45
|
+
echo "passed=$PASS failed=$FAILN"
|
|
46
|
+
[ "$FAILN" -eq 0 ]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# close-parent-c3.test.sh — Phase C3 (close.md) behaviour for decomposition.close_parent:
|
|
3
|
+
# the last child of a CLOSED parent posts "All sub-issues complete" and sets merged
|
|
4
|
+
# without closing or reopening; an OPEN parent is auto-closed as before.
|
|
5
|
+
#
|
|
6
|
+
# Usage: bash scripts/close-parent-c3.test.sh
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
11
|
+
CLOSE="$ROOT/commands/work-on/close.md"
|
|
12
|
+
|
|
13
|
+
# The all-children-checked-off branch of C3.
|
|
14
|
+
BLOCK=$(awk '/^if \[ "\$OPEN_SUBS" -eq 0 \]; then/{p=1} p{print} p&&/^fi$/{exit}' "$CLOSE")
|
|
15
|
+
[ -n "$BLOCK" ] || { printf 'FAIL: close.md C3 completion block not found\n' >&2; exit 1; }
|
|
16
|
+
grep -q 'close_parent' "$CLOSE" || { printf 'FAIL: close.md does not mention close_parent\n' >&2; exit 1; }
|
|
17
|
+
|
|
18
|
+
run_case() {
|
|
19
|
+
local parent_state="$1" already_posted="$2" log
|
|
20
|
+
log=$(mktemp)
|
|
21
|
+
gh() {
|
|
22
|
+
case "$1 $2" in
|
|
23
|
+
"issue view") echo "$parent_state" ;;
|
|
24
|
+
"api repos/"*) echo "$already_posted" ;;
|
|
25
|
+
*) echo "gh $*" >>"$log" ;;
|
|
26
|
+
esac
|
|
27
|
+
}
|
|
28
|
+
set_workflow_state() { echo "set_workflow_state $*" >>"$log"; }
|
|
29
|
+
# Render the spec placeholders.
|
|
30
|
+
local rendered
|
|
31
|
+
rendered=$(printf '%s\n' "$BLOCK" | sed -e 's/{GH_FLAG}/-R o\/r/g' -e 's/{GH_REPO}/o\/r/g' -e 's/{NUMBER}/7/g')
|
|
32
|
+
OPEN_SUBS=0 PARENT_REF=5 PR_NUMBER=9 PARENT_CLOSED=false
|
|
33
|
+
eval "$rendered"
|
|
34
|
+
cat "$log"; rm -f "$log"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fail() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
|
|
38
|
+
|
|
39
|
+
OUT=$(run_case CLOSED 0)
|
|
40
|
+
printf '%s\n' "$OUT" | grep -q 'gh issue comment 5 .*All sub-issues complete' || fail "closed parent: completion comment missing"
|
|
41
|
+
printf '%s\n' "$OUT" | grep -q 'set_workflow_state 5 merged' || fail "closed parent: merged not set"
|
|
42
|
+
printf '%s\n' "$OUT" | grep -q 'gh issue close' && fail "closed parent: must not be closed again"
|
|
43
|
+
printf '%s\n' "$OUT" | grep -qE 'reopen|--state open' && fail "closed parent: must not be reopened"
|
|
44
|
+
|
|
45
|
+
OUT=$(run_case CLOSED 1)
|
|
46
|
+
printf '%s\n' "$OUT" | grep -q 'gh issue comment' && fail "closed parent: completion comment must not repeat"
|
|
47
|
+
printf '%s\n' "$OUT" | grep -q 'set_workflow_state 5 merged' || fail "closed parent resume: merged not set"
|
|
48
|
+
|
|
49
|
+
OUT=$(run_case OPEN 0)
|
|
50
|
+
printf '%s\n' "$OUT" | grep -q 'gh issue close 5 .*All sub-issues complete' || fail "open parent: must be auto-closed"
|
|
51
|
+
printf '%s\n' "$OUT" | grep -q 'set_workflow_state 5 merged' || fail "open parent: merged not set"
|
|
52
|
+
printf '%s\n' "$OUT" | grep -q 'gh issue comment' && fail "open parent: no separate comment expected"
|
|
53
|
+
|
|
54
|
+
printf 'PASS: close-parent-c3\n'
|