forgedock 1.12.0 → 1.13.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/phases-args.test.mjs +2 -1
- package/bin/engine/phases.mjs +349 -90
- package/bin/engine/runlog.mjs +25 -2
- package/bin/engine-cli.mjs +4 -1
- package/bin/engine.mjs +72 -19
- package/bin/hooks/interactive-engine.mjs +135 -209
- package/bin/hooks/pre-tool-use.mjs +16 -4
- package/bin/labels.json +10 -0
- package/bin/opencode-adapter.mjs +1 -1
- package/bin/orchestrate-preflight.mjs +1 -1
- package/bin/runner.mjs +258 -23
- package/bin/watch-utils.mjs +1 -0
- package/commands/autopilot.md +6 -6
- package/commands/cleanup.md +8 -5
- package/commands/orchestrate/phase-1-resolve.md +1 -1
- package/commands/orchestrate/phase-3-dependency.md +14 -19
- package/commands/orchestrate/phase-4-execution.md +80 -53
- package/commands/orchestrate/phase-6-report.md +6 -0
- package/commands/orchestrate/safety.md +1 -1
- package/commands/pipeline-health/phase-2-metrics.md +39 -0
- package/commands/pipeline-health/phase-5-report.md +1 -0
- package/commands/pipeline-resume.md +1 -1
- package/commands/pipeline-status.md +2 -2
- package/commands/recover-orphans.md +5 -5
- package/commands/review-pr-agents/api.md +1 -1
- package/commands/review-pr-agents/auth.md +1 -1
- package/commands/review-pr-agents/infra.md +3 -3
- package/commands/review-pr-agents/protocols.md +15 -6
- package/commands/review-pr-agents/scraper.md +1 -1
- package/commands/review-pr-agents/security.md +5 -5
- package/commands/review-pr-agents.md +12 -12
- package/commands/review-pr-staging.md +226 -8
- package/commands/review-pr.md +394 -53
- package/commands/work-on/build/architect.md +36 -0
- package/commands/work-on/build/implement.md +1 -1
- package/commands/work-on/build/validate.md +88 -4
- package/commands/work-on/build.md +106 -7
- package/commands/work-on/decompose.md +2 -2
- package/commands/work-on/investigate.md +14 -4
- package/commands/work-on/remediate.md +137 -25
- package/commands/work-on/review.md +25 -9
- package/commands/work-on.md +15 -15
- package/docs/CONFIG.md +29 -0
- package/docs/RELEASE-NOTES.md +45 -0
- package/docs/spec/forge-protocol-v1.md +22 -1
- package/docs/spec/label-state-machine.md +6 -1
- package/docs/spec/review-protocol.md +20 -5
- package/package.json +2 -1
- package/packages/protocol/src/phases.js +21 -0
- package/scripts/check-blast-radius.sh +164 -0
- package/scripts/check-blast-radius.test.sh +183 -0
- package/scripts/check-contract-scope.sh +137 -0
- package/scripts/check-contract-scope.test.sh +83 -0
- package/scripts/check-spec-bash.sh +60 -3
- package/scripts/check-spec-bash.test.sh +56 -0
- package/scripts/check-spec-markers.sh +29 -0
- package/scripts/check-test-wiring.sh +57 -0
- package/scripts/check-test-wiring.test.sh +70 -0
- package/scripts/classify-finding.sh +72 -8
- package/scripts/classify-finding.test.sh +77 -8
- package/scripts/close-dossier-no-main-commit.test.sh +12 -12
- package/scripts/diff-size.test.sh +3 -2
- package/scripts/doctor-pipeline-state.sh +35 -0
- package/scripts/forge-annotation.sh +3 -0
- package/scripts/forge-root.test.sh +103 -13
- package/scripts/forge-run.sh +2 -2
- package/scripts/recovery-claim-live.test.sh +10 -7
- package/scripts/remediate-basesync.test.sh +103 -0
- package/scripts/review-delta.sh +206 -0
- package/scripts/review-delta.test.sh +213 -0
- package/scripts/review-pr-domain-map.test.sh +87 -0
- package/scripts/review-pr-gate2.test.sh +84 -0
- package/scripts/review-pr-staging-scope.test.sh +99 -0
- package/scripts/run-shell-tests.sh +115 -0
- package/scripts/select-fix-targets.sh +17 -5
- package/scripts/transition-label.sh +3 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# review-delta.test.sh — fixture cases for scripts/review-delta.sh (forge#3636).
|
|
3
|
+
# Real git repos (local bare origin), synthetic comment JSON, and the REAL scripts/trusted-comments.sh.
|
|
4
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
8
|
+
RD="$ROOT/scripts/review-delta.sh"
|
|
9
|
+
TC="$ROOT/scripts/trusted-comments.sh"
|
|
10
|
+
PASS=0; FAILN=0
|
|
11
|
+
ok() { PASS=$((PASS+1)); }
|
|
12
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
13
|
+
command -v jq >/dev/null 2>&1 || { echo "review-delta.test.sh: jq missing, skipped"; exit 0; }
|
|
14
|
+
command -v git >/dev/null 2>&1 || { echo "review-delta.test.sh: git missing, skipped"; exit 0; }
|
|
15
|
+
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
16
|
+
|
|
17
|
+
# hermetic git: no user config, hooks or signing
|
|
18
|
+
export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_NOSYSTEM=1
|
|
19
|
+
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@example.invalid GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@example.invalid
|
|
20
|
+
g() { git -C "$W" -c commit.gpgsign=false -c core.hooksPath=/dev/null "$@"; }
|
|
21
|
+
commit() { # file content msg
|
|
22
|
+
mkdir -p "$W/$(dirname "$1")"; printf '%s\n' "$2" > "$W/$1"; g add -A >/dev/null; g commit -q -m "$3" >/dev/null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
CASE=0
|
|
26
|
+
mkrepo() { # sets W; main has a.txt, origin/main fetched, branch feat checked out with one PR commit
|
|
27
|
+
CASE=$((CASE+1)); local o="$TMP/o$CASE"; W="$TMP/w$CASE"
|
|
28
|
+
git init -q --bare -b main "$o"
|
|
29
|
+
git clone -q "$o" "$W" 2>/dev/null
|
|
30
|
+
g checkout -q -b main 2>/dev/null
|
|
31
|
+
commit a.txt "base" "base"
|
|
32
|
+
g push -q origin main
|
|
33
|
+
g checkout -q -b feat
|
|
34
|
+
commit pr.txt "pr1" "pr1"
|
|
35
|
+
}
|
|
36
|
+
sha() { g rev-parse "$1"; }
|
|
37
|
+
base_advance() { # file content -> new commit on origin/main, fetched
|
|
38
|
+
g checkout -q main; commit "$1" "$2" "base: $1"; g push -q origin main; g checkout -q feat; g fetch -q origin
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# --- comment fixtures
|
|
42
|
+
body_agent() { printf '<!-- FORGE:REVIEW-AGENT:%s -->\nReviewed-SHA: %s\n\n<!-- REVIEW-FINDINGS-START -->\nNo findings.\n<!-- REVIEW-FINDINGS-END -->' "$1" "$2"; }
|
|
43
|
+
body_synth() { printf '<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->\nReviewed-SHA: %s\n\nsynth' "$1"; }
|
|
44
|
+
body_summary() { printf '# PR Review Summary\n\n**Reviewed commit**: `%s` | **Current HEAD**: `%s` | **Status**: CURRENT\n**Domains**: x | **Agents**: %s (x)\n' "$1" "$1" "$2"; }
|
|
45
|
+
cm() { jq -n --arg b "$1" --arg t "${2:-Bot}" --arg a "${3:-NONE}" '{"body":$b,"author_association":$a,"user":{"type":$t,"login":"x"}}'; }
|
|
46
|
+
# comments <comment-json>... -> JSON array file path in $CF
|
|
47
|
+
comments() { CF="$TMP/c$CASE.json"; printf '%s\n' "$@" | jq -s '.' > "$CF"; }
|
|
48
|
+
panel() { # full proven panel for sha: 2 agents + synthesized body
|
|
49
|
+
comments "$(cm "$(body_agent security "$1")")" "$(cm "$(body_agent api "$1")")" "$(cm "$(body_synth "$1")")"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
run_rd() { # prints stdout; args after the fixed set are appended
|
|
53
|
+
(cd "$W" && bash "$RD" --pr 1 --head "$HEAD" --base main --trusted-script "$TC" --comments-file "$CF" --labels "" "$@" 2>&1)
|
|
54
|
+
}
|
|
55
|
+
expect() { # name expected-line1 [expected-rounds]
|
|
56
|
+
local out l1 l2; out=$(run_rd "${@:4}"); l1=$(printf '%s\n' "$out" | sed -n 1p); l2=$(printf '%s\n' "$out" | sed -n 2p)
|
|
57
|
+
[ "$l1" = "$2" ] && ok || bad "$1: expected '$2', got '$l1'"
|
|
58
|
+
if [ -n "${3-}" ]; then [ "$l2" = "FULL_ROUNDS=$3" ] && ok || bad "$1: expected FULL_ROUNDS=$3, got '$l2'"; fi
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# 1. ancestor delta -> DELTA
|
|
62
|
+
mkrepo; R=$(sha HEAD); commit pr.txt "pr2" "pr2"; HEAD=$(sha HEAD); panel "$R"
|
|
63
|
+
expect "ancestor delta" "DELTA $R..$HEAD" 1
|
|
64
|
+
|
|
65
|
+
# 2. head == reviewed -> empty delta, documented as DELTA
|
|
66
|
+
mkrepo; R=$(sha HEAD); HEAD=$R; panel "$R"
|
|
67
|
+
expect "head == reviewed" "DELTA $R..$R" 1
|
|
68
|
+
|
|
69
|
+
# 3. force-push / rebase -> FULL
|
|
70
|
+
mkrepo; R=$(sha HEAD); panel "$R"; g reset -q --hard HEAD~1; commit pr.txt "rewritten" "rewritten"; HEAD=$(sha HEAD)
|
|
71
|
+
expect "force-push (reviewed not an ancestor)" "FULL" 1
|
|
72
|
+
mkrepo; R=$(sha HEAD); panel "$R"; base_advance b.txt "b1"; g rebase -q origin/main >/dev/null 2>&1; HEAD=$(sha HEAD)
|
|
73
|
+
expect "rebase onto new base" "FULL" 1
|
|
74
|
+
|
|
75
|
+
# 4. clean base merge -> BASE_SYNC_ONLY
|
|
76
|
+
mkrepo; R=$(sha HEAD); panel "$R"; base_advance b.txt "b1"; g merge -q --no-ff -m "merge main" origin/main >/dev/null 2>&1; HEAD=$(sha HEAD)
|
|
77
|
+
expect "clean base merge" "BASE_SYNC_ONLY $R" 1
|
|
78
|
+
|
|
79
|
+
# 5. base merge with a conflict-resolution edit -> not BASE_SYNC_ONLY
|
|
80
|
+
mkrepo; commit shared.txt "line" "shared"; g push -q origin feat; R=$(sha HEAD); panel "$R"
|
|
81
|
+
g checkout -q main; commit shared.txt "base side" "base edits shared"; g push -q origin main; g checkout -q feat; g fetch -q origin
|
|
82
|
+
commit shared.txt "feat side" "feat edits shared"; R=$(sha HEAD); panel "$R"
|
|
83
|
+
g merge -q --no-ff origin/main >/dev/null 2>&1; printf 'resolved\n' > "$W/shared.txt"; g add -A >/dev/null; g commit -q -m "merge main (resolved)" >/dev/null; HEAD=$(sha HEAD)
|
|
84
|
+
out=$(run_rd | sed -n 1p); case "$out" in BASE_SYNC_ONLY*) bad "conflict-resolution merge must not be BASE_SYNC_ONLY (got $out)" ;; *) ok ;; esac
|
|
85
|
+
|
|
86
|
+
# 6. mixed: base merge plus a regular commit -> DELTA
|
|
87
|
+
mkrepo; R=$(sha HEAD); panel "$R"; base_advance b.txt "b1"; g merge -q --no-ff -m "merge main" origin/main >/dev/null 2>&1; commit pr.txt "pr2" "pr2"; HEAD=$(sha HEAD)
|
|
88
|
+
expect "base merge plus regular commit" "DELTA $R..$HEAD" 1
|
|
89
|
+
|
|
90
|
+
# 6b. merge of a non-base branch -> DELTA
|
|
91
|
+
mkrepo; R=$(sha HEAD); panel "$R"; g checkout -q -b other; commit o.txt "o" "other"; g checkout -q feat; g merge -q --no-ff -m "merge other" other >/dev/null 2>&1; HEAD=$(sha HEAD)
|
|
92
|
+
expect "merge of a non-base branch" "DELTA $R..$HEAD" 1
|
|
93
|
+
|
|
94
|
+
# 6c. crafted base "merge" whose second parent is an OLD base commit, reverting a binary file or a mode to that
|
|
95
|
+
# old version. diff-tree --cc is empty (result equals the second parent) and the 3-dot diffs have identical +/-
|
|
96
|
+
# lines, so only the per-path shape (binary blob ids, modes) can tell the PR change moved.
|
|
97
|
+
crafted_merge() { # path-from-old-base -> HEAD = merge(R, old base) taking that one path from the old base
|
|
98
|
+
local tree; g checkout -q "$OLD_B" -- "$1"; tree=$(g write-tree); HEAD=$(g commit-tree "$tree" -p "$R" -p "$OLD_B" -m "merge main")
|
|
99
|
+
g reset -q --hard "$HEAD"
|
|
100
|
+
}
|
|
101
|
+
mkrepo; g checkout -q main; printf '\000\001v1' > "$W/img.bin"; g add -A >/dev/null; g commit -q -m "img v1" >/dev/null; OLD_B=$(sha HEAD)
|
|
102
|
+
printf '\000\001v2' > "$W/img.bin"; g add -A >/dev/null; g commit -q -m "img v2" >/dev/null; g push -q origin main; g fetch -q origin
|
|
103
|
+
g checkout -q feat; g merge -q --no-ff -m "sync" origin/main >/dev/null 2>&1; R=$(sha HEAD); panel "$R"
|
|
104
|
+
crafted_merge img.bin
|
|
105
|
+
expect "crafted merge reverting a binary file" "DELTA $R..$HEAD" 1
|
|
106
|
+
# binary the PR already changes, swapped for the old base's bytes: same path set and modes, only the blob id differs
|
|
107
|
+
mkrepo; g checkout -q main; printf '\000\001v1' > "$W/img.bin"; g add -A >/dev/null; g commit -q -m "img v1" >/dev/null; OLD_B=$(sha HEAD)
|
|
108
|
+
printf '\000\001v2' > "$W/img.bin"; g add -A >/dev/null; g commit -q -m "img v2" >/dev/null; g push -q origin main; g fetch -q origin
|
|
109
|
+
g checkout -q feat; g merge -q --no-ff -m "sync" origin/main >/dev/null 2>&1
|
|
110
|
+
printf '\000\001pr' > "$W/img.bin"; g add -A >/dev/null; g commit -q -m "pr img" >/dev/null; R=$(sha HEAD); panel "$R"
|
|
111
|
+
crafted_merge img.bin
|
|
112
|
+
expect "crafted merge swapping a PR-changed binary" "DELTA $R..$HEAD" 1
|
|
113
|
+
mkrepo; g checkout -q main; chmod +x "$W/a.txt"; g add -A >/dev/null; g commit -q -m "a 755" >/dev/null; OLD_B=$(sha HEAD)
|
|
114
|
+
chmod -x "$W/a.txt"; g add -A >/dev/null; g commit -q -m "a 644" >/dev/null; g push -q origin main; g fetch -q origin
|
|
115
|
+
g checkout -q feat; g merge -q --no-ff -m "sync" origin/main >/dev/null 2>&1; R=$(sha HEAD); panel "$R"
|
|
116
|
+
crafted_merge a.txt
|
|
117
|
+
expect "crafted merge reverting a file mode" "DELTA $R..$HEAD" 1
|
|
118
|
+
|
|
119
|
+
# 7. spec / config changes in the delta -> FULL
|
|
120
|
+
for f in forge.yaml commands/review-pr.md commands/review-pr-agents.md commands/review-pr-agents/x.md scripts/review-delta.sh scripts/trusted-comments.sh; do
|
|
121
|
+
mkrepo; R=$(sha HEAD); panel "$R"; commit "$f" "changed" "touch $f"; HEAD=$(sha HEAD)
|
|
122
|
+
expect "$f changed in delta" "FULL" 1
|
|
123
|
+
done
|
|
124
|
+
|
|
125
|
+
# 8. checkpoint trust and shape
|
|
126
|
+
mkrepo; R=$(sha HEAD); commit pr.txt "pr2" "pr2"; HEAD=$(sha HEAD)
|
|
127
|
+
comments "$(cm "$(body_agent security "$R")" User NONE)" "$(cm "$(body_agent api "$R")" User NONE)" "$(cm "$(body_synth "$R")" User NONE)"
|
|
128
|
+
expect "untrusted reviewer comments" "FULL" 0
|
|
129
|
+
comments "$(cm "$(body_agent security "$R")" User MEMBER)" "$(cm "$(body_synth "$R")" User MEMBER)"
|
|
130
|
+
expect "trusted by author_association" "DELTA $R..$HEAD" 1
|
|
131
|
+
comments "$(cm "<!-- FORGE:REVIEW_ROUTE mode=single-pr spec=review-pr.md sha=${R:0:7} -->")"
|
|
132
|
+
expect "7-char ROUTE sha is never a checkpoint" "FULL" 0
|
|
133
|
+
comments "$(cm "$(body_agent security "${R:0:7}")")" "$(cm "$(body_synth "${R:0:7}")")"
|
|
134
|
+
expect "7-char Reviewed-SHA is never a checkpoint" "FULL" 0
|
|
135
|
+
comments "$(cm "$(body_agent security "$R")")" "$(cm "$(body_agent api "$R")")"
|
|
136
|
+
expect "agent bodies without the post-guard proof (partial panel)" "FULL" 0
|
|
137
|
+
comments "$(cm "$(body_agent security "$R")")" "$(cm "$(body_summary "$R" 2)")"
|
|
138
|
+
expect "summary Agents count above distinct domains" "FULL" 0
|
|
139
|
+
comments "$(cm "$(body_agent security "$R")")" "$(cm "$(body_agent api "$R")")" "$(cm "$(body_summary "$R" 2)")"
|
|
140
|
+
expect "summary Agents count equal to distinct domains" "DELTA $R..$HEAD" 1
|
|
141
|
+
comments "$(cm "<!-- FORGE:REVIEW-AGENT:security -->
|
|
142
|
+
Reviewed-SHA: $R
|
|
143
|
+
|
|
144
|
+
no findings block")" "$(cm "$(body_synth "$R")")"
|
|
145
|
+
expect "agent body without REVIEW-FINDINGS-START" "FULL" 0
|
|
146
|
+
comments "$(cm "$(body_agent security "$R")")" "$(cm "$(body_synth "$R")")" "$(cm "<!-- FORGE:GATE_FAILURE:TYPE=review-panel-integrity -->
|
|
147
|
+
panel degraded")"
|
|
148
|
+
expect "review-panel-integrity gate failure after the agents" "FULL" 0
|
|
149
|
+
comments "$(cm "<!-- FORGE:REVIEW_DEGRADED -->")" "$(cm "$(body_agent security "$R")")" "$(cm "$(body_synth "$R")")"
|
|
150
|
+
expect "degraded marker before the agents does not disqualify" "DELTA $R..$HEAD" 1
|
|
151
|
+
comments "$(cm "$(body_agent security "$R")")" "$(cm "$(body_synth "$R")")" "$(cm "<!-- FORGE:REVIEW_BLOCKED -->")"
|
|
152
|
+
expect "REVIEW_BLOCKED after the agents" "FULL" 0
|
|
153
|
+
panel "$R"
|
|
154
|
+
expect "review-degraded label" "FULL" 0 --labels "needs-human,review-degraded"
|
|
155
|
+
# --labels given twice: last wins in the parser, so use a fresh invocation without the fixed --labels
|
|
156
|
+
out=$(cd "$W" && bash "$RD" --pr 1 --head "$HEAD" --base main --trusted-script "$TC" --comments-file "$CF" --labels "bug,docs" 2>&1 | sed -n 1p)
|
|
157
|
+
[ "$out" = "DELTA $R..$HEAD" ] && ok || bad "unrelated labels must not force FULL (got $out)"
|
|
158
|
+
|
|
159
|
+
# 9. FULL_ROUNDS
|
|
160
|
+
mkrepo; R1=$(sha HEAD); commit pr.txt "pr2" "pr2"; R2=$(sha HEAD); commit pr.txt "pr3" "pr3"; HEAD=$(sha HEAD)
|
|
161
|
+
comments "$(cm "$(body_agent security "$R1")")" "$(cm "$(body_synth "$R1")")" \
|
|
162
|
+
"$(cm "$(body_agent security "$R2")")" "$(cm "$(body_synth "$R2")")"
|
|
163
|
+
expect "two complete SHAs: newest is the checkpoint, FULL_ROUNDS=2" "DELTA $R2..$HEAD" 2
|
|
164
|
+
comments "$(cm "$(body_agent security "$R1")")" "$(cm "$(body_synth "$R1")")" "$(cm "$(body_agent api "$R1")")" \
|
|
165
|
+
"$(cm "$(body_agent security "$R2")")" "$(cm "$(body_synth "$R2")")" "$(cm "$(body_synth "$R2")")"
|
|
166
|
+
expect "duplicate SHA counted once" "DELTA $R2..$HEAD" 2
|
|
167
|
+
comments "$(cm "$(body_agent security "$R1")")" "$(cm "$(body_synth "$R1")")" "$(cm "$(body_agent security "$R2")")"
|
|
168
|
+
expect "partial SHA ignored in FULL_ROUNDS" "DELTA $R1..$HEAD" 1
|
|
169
|
+
|
|
170
|
+
# 10. error paths all print FULL (+ FULL_ROUNDS line)
|
|
171
|
+
mkrepo; R=$(sha HEAD); commit pr.txt "pr2" "pr2"; HEAD=$(sha HEAD); panel "$R"
|
|
172
|
+
expect "relative --trusted-script" "FULL" "" --trusted-script "scripts/trusted-comments.sh"
|
|
173
|
+
out=$(run_rd --trusted-script "scripts/trusted-comments.sh" | sed -n 2p); case "$out" in FULL_ROUNDS=*) ok ;; *) bad "error path must print a FULL_ROUNDS line (got '$out')" ;; esac
|
|
174
|
+
expect "nonexistent --trusted-script" "FULL" "" --trusted-script "$TMP/nope.sh"
|
|
175
|
+
printf '#!/usr/bin/env bash\nexit 2\n' > "$TMP/tc-fail.sh"
|
|
176
|
+
expect "trust script exits 2" "FULL" 0 --trusted-script "$TMP/tc-fail.sh"
|
|
177
|
+
expect "unreadable comments file" "FULL" 0 --comments-file "$TMP/missing.json"
|
|
178
|
+
: > "$TMP/empty.json"
|
|
179
|
+
expect "empty comments file" "FULL" 0 --comments-file "$TMP/empty.json"
|
|
180
|
+
printf 'not json' > "$TMP/bad.json"
|
|
181
|
+
expect "malformed comments JSON" "FULL" 0 --comments-file "$TMP/bad.json"
|
|
182
|
+
BADHEAD=$HEAD; HEAD="${HEAD:0:7}"; expect "7-char --head" "FULL" ""; HEAD="ZZZ"; expect "malformed --head" "FULL" ""; HEAD=$BADHEAD
|
|
183
|
+
out=$(cd "$W" && bash "$RD" --pr x --head "$HEAD" --base main --trusted-script "$TC" --comments-file "$CF" --labels "" 2>&1 | sed -n 1p); [ "$out" = FULL ] && ok || bad "non-numeric --pr (got $out)"
|
|
184
|
+
out=$(cd "$W" && bash "$RD" --pr 1 --head "$HEAD" --base "--evil" --trusted-script "$TC" --comments-file "$CF" --labels "" 2>&1 | sed -n 1p); [ "$out" = FULL ] && ok || bad "option-like --base (got $out)"
|
|
185
|
+
out=$(cd "$W" && bash "$RD" --bogus 2>&1 | sed -n 1p); [ "$out" = FULL ] && ok || bad "unknown flag (got $out)"
|
|
186
|
+
out=$(cd "$W" && bash "$RD" 2>&1 | sed -n 1p); [ "$out" = FULL ] && ok || bad "no args (got $out)"
|
|
187
|
+
out=$(cd "$W" && bash "$RD" --pr 1 --head "$HEAD" --base main --trusted-script "$TC" --comments-file "$CF" 2>&1 | sed -n 1p); [ "$out" = FULL ] && ok || bad "labels neither given nor fetchable (got $out)"
|
|
188
|
+
out=$(cd "$W" && bash "$RD" --pr 1 --head "$HEAD" --base nosuchbranch --trusted-script "$TC" --comments-file "$CF" --labels "" 2>&1 | sed -n 1p); [ "$out" = FULL ] && ok || bad "missing origin/<base> (got $out)"
|
|
189
|
+
# unknown reviewed object: the checkpoint names a well-formed SHA that is not in the repo
|
|
190
|
+
GHOST=0123456789abcdef0123456789abcdef01234567; panel "$GHOST"
|
|
191
|
+
expect "unknown reviewed object" "FULL" 1
|
|
192
|
+
# not a git repo
|
|
193
|
+
panel "$R"; NOGIT="$TMP/nogit"; mkdir -p "$NOGIT"
|
|
194
|
+
out=$(bash "$RD" --pr 1 --head "$HEAD" --base main --trusted-script "$TC" --comments-file "$CF" --labels "" --repo-path "$NOGIT" 2>&1 | sed -n 1p); [ "$out" = FULL ] && ok || bad "not a git repo (got $out)"
|
|
195
|
+
|
|
196
|
+
# 11. pagination shape: two concatenated arrays
|
|
197
|
+
CF="$TMP/paged.json"; { printf '[%s]\n' "$(cm "$(body_agent security "$R")")"; printf '[%s]\n' "$(cm "$(body_synth "$R")")"; } > "$CF"
|
|
198
|
+
expect "concatenated pagination arrays" "DELTA $R..$HEAD" 1
|
|
199
|
+
|
|
200
|
+
# 12. 64-hex (sha256) checkpoints are accepted shape-wise
|
|
201
|
+
mkrepo; R=$(sha HEAD); commit pr.txt "pr2" "pr2"; HEAD=$(sha HEAD)
|
|
202
|
+
R64="${R}0123456789abcdef01234567"; R64=${R64:0:64}; panel "$R64"
|
|
203
|
+
expect "64-hex checkpoint not in repo -> FULL (validated, then unknown object)" "FULL" 1
|
|
204
|
+
|
|
205
|
+
# 13. trust script is never taken from the cwd: a planted copy must not change the verdict
|
|
206
|
+
mkrepo; R=$(sha HEAD); commit pr.txt "pr2" "pr2"; HEAD=$(sha HEAD)
|
|
207
|
+
comments "$(cm "$(body_agent security "$R")" User NONE)" "$(cm "$(body_synth "$R")" User NONE)"
|
|
208
|
+
mkdir -p "$W/scripts"; printf '#!/usr/bin/env bash\ncat >/dev/null; jq -c . <<< "\\"$1\\""\n' > "$W/scripts/trusted-comments.sh"
|
|
209
|
+
out=$(cd "$W" && bash "$RD" --pr 1 --head "$HEAD" --base main --trusted-script "$TC" --comments-file "$CF" --labels "" 2>&1 | sed -n 1p)
|
|
210
|
+
[ "$out" = FULL ] && ok || bad "planted cwd trust script must be ignored (got $out)"
|
|
211
|
+
|
|
212
|
+
echo "review-delta tests: pass=$PASS fail=$FAILN"
|
|
213
|
+
[ "$FAILN" -eq 0 ]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# Guards the review-pr roster-label to marker-domain mapping: every roster spelling collapses to one marker
|
|
6
|
+
# domain, the helper is identical wherever it is inlined, the roster dedups across spellings, and every
|
|
7
|
+
# persona file is covered by the table and the catalog. Run: bash scripts/review-pr-domain-map.test.sh
|
|
8
|
+
# (bash 3.2 compatible)
|
|
9
|
+
|
|
10
|
+
set -u
|
|
11
|
+
DIR=$(cd "$(dirname "$0")" && pwd)
|
|
12
|
+
ROOT=$(cd "$DIR/.." && pwd)
|
|
13
|
+
SPEC="$ROOT/commands/review-pr.md"
|
|
14
|
+
CATALOG="$ROOT/commands/review-pr-agents.md"
|
|
15
|
+
PERSONAS="$ROOT/commands/review-pr-agents"
|
|
16
|
+
pass=0; fail=0
|
|
17
|
+
ok() { pass=$((pass+1)); }
|
|
18
|
+
bad() { fail=$((fail+1)); echo "FAIL: $1"; }
|
|
19
|
+
|
|
20
|
+
WORK=$(mktemp -d "${TMPDIR:-/tmp}/review-pr-domain-map.XXXXXX")
|
|
21
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
22
|
+
|
|
23
|
+
# Test 1: helper copies. Every inline definition must be byte-identical (each Bash call is a fresh shell).
|
|
24
|
+
awk -v dir="$WORK" '
|
|
25
|
+
/^# BEGIN review-pr-agent-domain/ {n++; out=dir "/helper." n; on=1}
|
|
26
|
+
on {print > out}
|
|
27
|
+
/^# END review-pr-agent-domain/ {on=0; close(out)}
|
|
28
|
+
' "$SPEC"
|
|
29
|
+
copies=$(ls "$WORK"/helper.* 2>/dev/null | wc -l | tr -d ' ')
|
|
30
|
+
if [ "$copies" -ge 4 ]; then ok; else bad "expected >=4 inline agent_domain copies (roster, canonicalize, dispatch, phase 4), found $copies"; fi
|
|
31
|
+
for f in "$WORK"/helper.*; do
|
|
32
|
+
[ -f "$f" ] || continue
|
|
33
|
+
if cmp -s "$f" "$WORK/helper.1"; then ok; else bad "$(basename "$f") differs from helper.1"; fi
|
|
34
|
+
done
|
|
35
|
+
|
|
36
|
+
# Test 2: no site derives the marker by lowercasing the roster label directly
|
|
37
|
+
if grep -nE "AGENT_DOMAIN=.*tr '\[:upper:\]' '\[:lower:\]'" "$SPEC" >/dev/null; then bad "direct lowercase marker derivation remains"; else ok; fi
|
|
38
|
+
sites=$(grep -c 'AGENT_DOMAIN=$(agent_domain "$AGENT")' "$SPEC")
|
|
39
|
+
if [ "$sites" -ge 2 ]; then ok; else bad "expected >=2 agent_domain derivation sites, found $sites"; fi
|
|
40
|
+
|
|
41
|
+
# Test 3: spelling collapse
|
|
42
|
+
# shellcheck disable=SC1090
|
|
43
|
+
. "$WORK/helper.1"
|
|
44
|
+
check() { got=$(agent_domain "$1"); if [ "$got" = "$2" ]; then ok; else bad "agent_domain '$1' = '$got', want '$2'"; fi; }
|
|
45
|
+
for l in INFRA Infrastructure infra infrastructure; do check "$l" infra; done
|
|
46
|
+
for l in SCRAPING Scraping scraper Scraper scraping; do check "$l" scraper; done
|
|
47
|
+
for l in FRONTEND Frontend frontend Web; do check "$l" frontend; done
|
|
48
|
+
for l in Security SECURITY; do check "$l" security; done
|
|
49
|
+
for l in AUTH Auth; do check "$l" auth; done
|
|
50
|
+
for l in BILLING Billing; do check "$l" billing; done
|
|
51
|
+
for l in CONCURRENCY Concurrency; do check "$l" concurrency; done
|
|
52
|
+
for l in DATABASE Database; do check "$l" database; done
|
|
53
|
+
for l in API Api; do check "$l" api; done
|
|
54
|
+
check "CustomDomain" customdomain
|
|
55
|
+
|
|
56
|
+
# Test 4: add_agent dedups across spellings; canonicalization block yields one entry per persona
|
|
57
|
+
ADD=$(awk '/^add_agent\(\) \{/{on=1} on{print} on && /^\}/{exit}' "$SPEC")
|
|
58
|
+
if [ -n "$ADD" ]; then ok; else bad "add_agent not found in spec"; fi
|
|
59
|
+
eval "$ADD"
|
|
60
|
+
SELECTED_AGENTS="Security INFRA SCRAPING"
|
|
61
|
+
add_agent "Infrastructure"; add_agent "Scraping"; add_agent "Auth"; add_agent "AUTH"; add_agent "security"
|
|
62
|
+
want=$(printf '%s' "Security INFRA SCRAPING auth")
|
|
63
|
+
if [ "$SELECTED_AGENTS" = "$want" ]; then ok; else bad "add_agent roster '$SELECTED_AGENTS', want '$want'"; fi
|
|
64
|
+
|
|
65
|
+
CANON=$(awk '/^# END review-pr-agent-domain/{seen++} seen && /^CANON_ROSTER=/{on=1} on{print} on && /^SELECTED_AGENTS=/{exit}' "$SPEC" | head -20)
|
|
66
|
+
SELECTED_AGENTS="Security AUTH INFRA Infrastructure SCRAPING Scraping Frontend API api"
|
|
67
|
+
eval "$CANON"
|
|
68
|
+
want="security auth infra scraper frontend api"
|
|
69
|
+
if [ "$SELECTED_AGENTS" = "$want" ]; then ok; else bad "canonical roster '$SELECTED_AGENTS', want '$want'"; fi
|
|
70
|
+
cnt=$(printf '%s' "$SELECTED_AGENTS" | tr ' ' '\n' | grep -c '.')
|
|
71
|
+
if [ "$cnt" -eq 6 ]; then ok; else bad "roster count $cnt, want 6"; fi
|
|
72
|
+
|
|
73
|
+
# Test 5: every persona file (minus protocols and spec-cli) has a table row in the spec and a catalog row
|
|
74
|
+
for pf in "$PERSONAS"/*.md; do
|
|
75
|
+
name=$(basename "$pf" .md)
|
|
76
|
+
case "$name" in protocols|spec-cli) continue ;; esac
|
|
77
|
+
if grep -qE "^\| .*\| \`$name\` \| \`$name\.md\` \|" "$SPEC"; then ok; else bad "spec table has no row for persona $name"; fi
|
|
78
|
+
if grep -qE "^\| .*\| \`$name\` \| \`review-pr-agents/$name\.md\` \|" "$CATALOG"; then ok; else bad "catalog has no marker-domain row for persona $name"; fi
|
|
79
|
+
got=$(agent_domain "$name")
|
|
80
|
+
if [ "$got" = "$name" ]; then ok; else bad "persona $name maps to $got"; fi
|
|
81
|
+
done
|
|
82
|
+
|
|
83
|
+
# Test 6: the protocol defines the marker domain through the mapping
|
|
84
|
+
if grep -qF 'marker domain' "$PERSONAS/protocols.md"; then ok; else bad "protocols.md does not define the marker domain"; fi
|
|
85
|
+
|
|
86
|
+
echo "review-pr-domain-map: $pass passed, $fail failed"
|
|
87
|
+
[ "$fail" -eq 0 ]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# review-pr-gate2.test.sh — cases for the review-pr §6B.4 gate-2 sketch (forge#3597, #3598).
|
|
3
|
+
# Extracts the fenced bash block from commands/review-pr.md, runs its set-up against a stubbed
|
|
4
|
+
# `gh`, then applies the per-finding rules the block documents (gone, no patch, introduced, severity).
|
|
5
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
9
|
+
SPEC="$ROOT/commands/review-pr.md"
|
|
10
|
+
PASS=0; FAILN=0
|
|
11
|
+
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
12
|
+
ok() { PASS=$((PASS+1)); }
|
|
13
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
14
|
+
command -v jq >/dev/null 2>&1 || { echo "review-pr-gate2.test.sh: jq missing, skipped"; exit 0; }
|
|
15
|
+
|
|
16
|
+
# Extract the sketch: the fenced block that declares NOPATCH_FILES_FILE, minus comment-only lines.
|
|
17
|
+
awk '/^```bash$/{b="";in_b=1;next} /^```$/{if(in_b&&b~/NOPATCH_FILES_FILE=/)printf "%s",b; in_b=0;next} in_b{b=b $0 "\n"}' "$SPEC" \
|
|
18
|
+
| grep -v '^#' > "$TMP/sketch.sh"
|
|
19
|
+
if [ -s "$TMP/sketch.sh" ]; then ok; else bad "sketch block not found in review-pr.md"; echo "review-pr-gate2.test.sh: failed=$FAILN"; exit 1; fi
|
|
20
|
+
|
|
21
|
+
# Fixture files API: a.sh patch adds line 10; big.txt has no patch; gone.txt removed.
|
|
22
|
+
cat > "$TMP/files.json" <<'JSON'
|
|
23
|
+
[{"filename":"a.sh","status":"modified","patch":"@@ -9,2 +9,3 @@\n ctx\n+added\n ctx"},
|
|
24
|
+
{"filename":"big.txt","status":"modified"},
|
|
25
|
+
{"filename":"gone.txt","status":"removed"},
|
|
26
|
+
{"filename":"new.sh","status":"renamed","previous_filename":"old.sh","patch":"@@ -1,1 +1,1 @@\n+x"}]
|
|
27
|
+
JSON
|
|
28
|
+
mkdir "$TMP/bin"
|
|
29
|
+
printf '#!/bin/sh\ncat "%s/files.json"\n' "$TMP" > "$TMP/bin/gh"; chmod +x "$TMP/bin/gh"
|
|
30
|
+
|
|
31
|
+
# Spec's executable severity rule: the SEV= line and the case line from the sketch comments, comment marker stripped.
|
|
32
|
+
grep -E '^#[[:space:]]+(SEV=|case "\$SEV" in)' "$SPEC" | sed -e 's/^#[[:space:]]*//' -e 's/[[:space:]]*#.*$//' > "$TMP/rule.sh"
|
|
33
|
+
if [ "$(wc -l < "$TMP/rule.sh")" -eq 2 ]; then ok; else bad "severity rule lines not found in review-pr.md"; fi
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
printf 'export PATH="%s/bin:$PATH" FORGE_SCRATCHPAD="%s"\n' "$TMP" "$TMP"
|
|
37
|
+
printf 'SEVERITY_RULE=%q\n' "$(cat "$TMP/rule.sh")"
|
|
38
|
+
sed -e 's/{GH_REPO}/o\/r/; s/{PR_NUMBER}/1/; s/{REVIEW_SHA}/0123456789abcdef0123456789abcdef01234567/' "$TMP/sketch.sh"
|
|
39
|
+
cat <<'CLASSIFY'
|
|
40
|
+
# classify FILE LINE SEVERITY -> KEEP | pre-existing (mirrors the per-finding rules in the sketch comments)
|
|
41
|
+
classify() {
|
|
42
|
+
FILE="$1"; LINE="$2"; SEVERITY="$3"; INTRODUCED=0
|
|
43
|
+
grep -qxF -- "$FILE" "$GONE_FILES_FILE" && { echo KEEP; return; }
|
|
44
|
+
grep -qxF -- "$FILE" "$NOPATCH_FILES_FILE" && { echo KEEP; return; }
|
|
45
|
+
LO=$((LINE>5?LINE-5:1))
|
|
46
|
+
for L in $(seq "$LO" $((LINE+5))); do grep -qxF -- "${FILE}:${L}" "$ADDED_LINES_FILE" && INTRODUCED=1; done
|
|
47
|
+
SCOPE=changed
|
|
48
|
+
# The severity rule is the spec's own line (extracted below), not a re-implementation.
|
|
49
|
+
eval "$SEVERITY_RULE"
|
|
50
|
+
if [ "$SCOPE" = pre-existing ]; then echo pre-existing; else echo KEEP; fi
|
|
51
|
+
}
|
|
52
|
+
classify "$@"
|
|
53
|
+
CLASSIFY
|
|
54
|
+
} > "$TMP/run.sh"
|
|
55
|
+
|
|
56
|
+
check() { # name want file line severity
|
|
57
|
+
local got; got="$(bash "$TMP/run.sh" "$3" "$4" "$5" 2>&1 | tail -n1)"
|
|
58
|
+
if [ "$got" = "$2" ]; then ok; else bad "$1 (got '$got' want '$2')"; fi
|
|
59
|
+
}
|
|
60
|
+
check "added line intersects" KEEP a.sh 10 MEDIUM
|
|
61
|
+
check "MEDIUM far from added -> pre" pre-existing a.sh 100 MEDIUM
|
|
62
|
+
check "HIGH far from added kept" KEEP a.sh 100 HIGH
|
|
63
|
+
check "CRITICAL far from added kept" KEEP a.sh 100 CRITICAL
|
|
64
|
+
check "null-patch file MEDIUM kept" KEEP big.txt 3 MEDIUM
|
|
65
|
+
check "lowercase high kept" KEEP a.sh 100 high
|
|
66
|
+
check "padded HIGH kept" KEEP a.sh 100 "HIGH "
|
|
67
|
+
check "empty severity kept" KEEP a.sh 100 ""
|
|
68
|
+
check "unparseable severity kept" KEEP a.sh 100 "SEVERE"
|
|
69
|
+
check "lowercase medium -> pre" pre-existing a.sh 100 medium
|
|
70
|
+
check "INFO far from added -> pre" pre-existing a.sh 100 INFO
|
|
71
|
+
check "renamed file kept" KEEP old.sh 3 MEDIUM
|
|
72
|
+
check "patched file still pre w/ nopatch sibling" pre-existing a.sh 100 LOW
|
|
73
|
+
check "removed file kept" KEEP gone.txt 3 LOW
|
|
74
|
+
check "file absent from diff MEDIUM pre" pre-existing other.sh 5 MEDIUM
|
|
75
|
+
if grep -qF 'select(.patch == null' "$SPEC"; then ok; else bad "spec lacks null-patch selector"; fi
|
|
76
|
+
|
|
77
|
+
# Mirrored wording must stay in sync across the spec family.
|
|
78
|
+
for f in commands/review-pr-staging.md commands/review-pr-agents/protocols.md docs/spec/review-protocol.md; do
|
|
79
|
+
if grep -qiE 'CRITICAL/HIGH.*never routed as pre-existing' "$ROOT/$f"; then ok; else bad "$f lacks the CRITICAL/HIGH never-pre-existing rule"; fi
|
|
80
|
+
done
|
|
81
|
+
if grep -qF 'only known low severities' "$SPEC"; then ok; else bad "spec lacks inverted severity rule comment"; fi
|
|
82
|
+
|
|
83
|
+
echo "review-pr-gate2.test.sh: passed=$PASS failed=$FAILN"
|
|
84
|
+
[ "$FAILN" -eq 0 ]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# review-pr-staging-scope.test.sh — cases for the review-pr-staging scoped-review gates (forge#3451):
|
|
3
|
+
# (1) the 7B out-of-scope rule may only demote MEDIUM/LOW/INFO, never HIGH/CRITICAL or unknown severity;
|
|
4
|
+
# (2) the 0B.1 "reviewed" test needs the full-length Reviewed-SHA line, not a 7-char substring.
|
|
5
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
9
|
+
SPEC="$ROOT/commands/review-pr-staging.md"
|
|
10
|
+
TC="$ROOT/scripts/trusted-comments.sh"
|
|
11
|
+
PASS=0; FAILN=0
|
|
12
|
+
ok() { PASS=$((PASS+1)); }
|
|
13
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
14
|
+
command -v jq >/dev/null 2>&1 || { echo "review-pr-staging-scope.test.sh: jq missing, skipped"; exit 0; }
|
|
15
|
+
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
16
|
+
|
|
17
|
+
# (1) severity rule: the two SCOPE_RULE lines, marker stripped
|
|
18
|
+
grep -E '# SCOPE_RULE$' "$SPEC" | sed 's/[[:space:]]*# SCOPE_RULE$//; s/^[[:space:]]*//' > "$TMP/rule.sh"
|
|
19
|
+
if [ "$(wc -l < "$TMP/rule.sh")" -eq 2 ]; then ok; else bad "SCOPE_RULE lines not found (expected 2)"; fi
|
|
20
|
+
demote() { FINDING_SEVERITY="$1"; OOS_DEMOTE=; eval "$(cat "$TMP/rule.sh")"; echo "$OOS_DEMOTE"; }
|
|
21
|
+
chk() { [ "$(demote "$1")" = "$2" ] && ok || bad "severity '$1' expected demote=$2"; }
|
|
22
|
+
chk HIGH 0; chk high 0; chk " High " 0; chk CRITICAL 0; chk critical 0
|
|
23
|
+
chk MEDIUM 1; chk medium 1; chk LOW 1; chk INFO 1
|
|
24
|
+
chk "" 0; chk "SEVERE" 0; chk "HIGH/MEDIUM" 0
|
|
25
|
+
|
|
26
|
+
# (2) full-sha reviewed test: the _AGENT regex from the spec, run through the real trusted-comments predicate
|
|
27
|
+
LINE=$(grep -E '^[[:space:]]*_AGENT=' "$SPEC" | head -1)
|
|
28
|
+
RE=$(printf '%s' "$LINE" | sed -n 's/.*count "\(.*\)") ||.*/\1/p')
|
|
29
|
+
if [ -n "$RE" ]; then ok; else bad "_AGENT regex not found in review-pr-staging.md"; fi
|
|
30
|
+
FULL=0123456789abcdef0123456789abcdef01234567; SHORT=${FULL:0:7}
|
|
31
|
+
RE=${RE//\\\\/\\}; RE=${RE//\$\{_HEAD\}/$FULL}
|
|
32
|
+
FB=$'<!-- REVIEW-FINDINGS-START -->\nNo findings.\n<!-- REVIEW-FINDINGS-END -->'
|
|
33
|
+
mk() { jq -n --arg b "$1" --arg t "$2" '[{"body":$b,"author_association":"NONE","user":{"type":$t,"login":"x"}}]'; }
|
|
34
|
+
cnt() { bash "$TC" count "$RE"; }
|
|
35
|
+
[ "$(mk $'<!-- FORGE:REVIEW-AGENT:security -->\nReviewed-SHA: '"$FULL"$'\n\n'"$FB" Bot | cnt)" = 1 ] && ok || bad "full Reviewed-SHA line should count"
|
|
36
|
+
[ "$(mk $'<!-- FORGE:REVIEW-AGENT:security -->\nreviewed '"$SHORT"' only' Bot | cnt)" = 0 ] && ok || bad "7-char substring must not count"
|
|
37
|
+
[ "$(mk $'<!-- FORGE:REVIEW-AGENT:security -->\nReviewed-SHA: '"$SHORT"$'\n' Bot | cnt)" = 0 ] && ok || bad "short Reviewed-SHA must not count"
|
|
38
|
+
[ "$(mk $'<!-- FORGE:REVIEW-AGENT:security -->\nReviewed-SHA: '"${FULL}"$'ff\n'"$FB" Bot | cnt)" = 0 ] && ok || bad "longer sha must not count"
|
|
39
|
+
[ "$(mk $'<!-- FORGE:REVIEW-AGENT:security -->\nReviewed-SHA: '"$FULL"$'\n'"$FB" User | cnt)" = 0 ] && ok || bad "untrusted author must not count"
|
|
40
|
+
[ "$(mk $'<!-- FORGE:REVIEW-AGENT:security -->\nReviewed-SHA: '"$FULL"$'\n\nno findings block here' Bot | cnt)" = 0 ] && ok || bad "comment without a findings block must not count (panel integrity)"
|
|
41
|
+
|
|
42
|
+
# (3) _ROUTE regex: real mode on the final head counts, spec-evolution-blocked and a stale sha do not
|
|
43
|
+
RLINE=$(grep -E '^[[:space:]]*_ROUTE=' "$SPEC" | head -1)
|
|
44
|
+
RRE=$(printf '%s' "$RLINE" | sed -n 's/.*count "\(.*\)") ||.*/\1/p'); RRE=${RRE//\\\\/\\}; RRE=${RRE//\$\{_HEAD7\}/$SHORT}
|
|
45
|
+
rcnt() { bash "$TC" count "$RRE"; }
|
|
46
|
+
[ "$(mk '<!-- FORGE:REVIEW_ROUTE mode=single-pr spec=review-pr.md sha='"$SHORT"' -->' Bot | rcnt)" = 1 ] && ok || bad "route marker on the final head should count"
|
|
47
|
+
[ "$(mk '<!-- FORGE:REVIEW_ROUTE mode=spec-evolution-blocked spec=review-pr.md sha='"$SHORT"' -->' Bot | rcnt)" = 0 ] && ok || bad "spec-evolution-blocked route must not count"
|
|
48
|
+
[ "$(mk '<!-- FORGE:REVIEW_ROUTE mode=single-pr spec=review-pr.md sha=abcdef0 -->' Bot | rcnt)" = 0 ] && ok || bad "stale-sha route must not count"
|
|
49
|
+
|
|
50
|
+
# (4) spec text guards: agents may not be told to suppress findings; no $PWD tier in the classifier resolver; git reads are status-checked
|
|
51
|
+
if grep -q 'MUST NOT report a defect' "$SPEC"; then bad "Phase 2 must not tell agents to suppress findings (7B owns demotion)"; else ok; fi
|
|
52
|
+
if grep -n 'FINDING_TEXT_FILE=$(mktemp' "$SPEC" >/dev/null && grep -q '> "$FINDING_TEXT_FILE"' "$SPEC"; then ok; else bad "FINDING_TEXT_FILE must be written before the classifier call"; fi
|
|
53
|
+
if awk '/^CLASSIFY_SCRIPT=""/,/^done <<</' "$SPEC" | grep -q 'PWD'; then bad "classifier resolver must have no PWD tier"; else ok; fi
|
|
54
|
+
if grep -qE "^DEPLOY_WIRING_FILES=.*git diff" "$SPEC"; then bad "DEPLOY_WIRING_FILES must not pipe git diff directly"; else ok; fi
|
|
55
|
+
if grep -q 'git diff-tree .*|| { SCOPE_MODE=full' "$SPEC"; then ok; else bad "git diff-tree reads must fall back to full scope"; fi
|
|
56
|
+
if grep -q 'classify_one()' "$SPEC" && grep -q 'REVIEWED_MERGES' "$SPEC" && grep -q '_scope.env' "$SPEC"; then ok; else bad "7B.5 must be a per-finding function with persisted scope state"; fi
|
|
57
|
+
if grep -q 'DUPLICATE: #' "$SPEC" && grep -q 'grep -qF -- "$_SAFE_FILE"' "$SPEC"; then ok; else bad "7E exit 1 must be confirmed by file before skipping"; fi
|
|
58
|
+
# (5) run the spec's classify_one with a stub classifier: HIGH out of scope stays ISSUE; LOW out of scope is demoted and listed; multi-file in-scope is not demoted
|
|
59
|
+
awk '/^classify_one\(\) \{/,/^\}/' "$SPEC" > "$TMP/fn.sh"
|
|
60
|
+
printf '#!/bin/sh\necho "$STUB_DISP"\n' > "$TMP/classify.sh"
|
|
61
|
+
run1() { # severity stub-disposition paths -> prints DISPOSITION
|
|
62
|
+
( _SD="$TMP"; PR_NUMBER=9; CLASSIFY_SCRIPT="$TMP/classify.sh"; SCOPE_MODE=scoped; CROSS_PR_FILES="a/in.sh"; UNREVIEWED_FILES=""; DEPLOY_WIRING_FILES=""
|
|
63
|
+
NOTE_LIST_FILE="$TMP/n.md"; : > "$NOTE_LIST_FILE"; NOTES_LISTED=0; FINDINGS_FILED=0; OUT_OF_SCOPE=0
|
|
64
|
+
FINDING_ID=X1; FINDING_SEVERITY="$1"; FINDING_CONFIDENCE=CONFIRMED; FINDING_AGENT=Security; FINDING_TITLE=t; FINDING_BODY=b; FINDING_FILE=z/out.sh; FINDING_PATHS="$3"
|
|
65
|
+
export STUB_DISP="$2"; . "$TMP/fn.sh"; classify_one; echo "$DISPOSITION" ) 2>&1 | tail -1; }
|
|
66
|
+
[ "$(run1 HIGH 'ISSUE severity-HIGH' 'z/out.sh')" = "ISSUE severity-HIGH" ] && ok || bad "HIGH out of scope must stay ISSUE"
|
|
67
|
+
[ "$(run1 high 'ISSUE severity-HIGH' 'z/out.sh')" = "ISSUE severity-HIGH" ] && ok || bad "lowercase high out of scope must stay ISSUE"
|
|
68
|
+
[ "$(run1 LOW 'NOTE LOW-LIKELY' 'z/out.sh')" = "NOTE out_of_scope" ] && ok || bad "LOW out of scope should be demoted and listed"
|
|
69
|
+
[ "$(run1 MEDIUM 'ISSUE MEDIUM-CONFIRMED' 'z/out.sh')" = "NOTE out_of_scope" ] && ok || bad "MEDIUM out of scope should be demoted"
|
|
70
|
+
[ "$(run1 MEDIUM 'ISSUE MEDIUM-CONFIRMED' $'z/out.sh\na/in.sh')" = "ISSUE MEDIUM-CONFIRMED" ] && ok || bad "multi-file finding with an in-scope file must not be demoted"
|
|
71
|
+
[ "$(run1 LOW 'ISSUE safety-exemption-auth' 'z/out.sh')" = "ISSUE safety-exemption-auth" ] && ok || bad "safety-exempt finding must not be demoted"
|
|
72
|
+
# (6) behavioural --text-file: each classify_one call must hand the classifier its OWN finding text (a shared/stale text file would leak the first finding into the second)
|
|
73
|
+
cat > "$TMP/classify-textfile.sh" <<'STUB'
|
|
74
|
+
#!/bin/bash
|
|
75
|
+
tf=""
|
|
76
|
+
while [ $# -gt 0 ]; do case "$1" in --text-file) tf="$2"; shift ;; esac; shift; done
|
|
77
|
+
[ -n "$tf" ] && [ -r "$tf" ] || { echo "NOTE no-text-file"; exit 0; }
|
|
78
|
+
if grep -q 'MARKER_TOKEN' "$tf"; then echo "ISSUE saw-marker"; else echo "NOTE no-marker"; fi
|
|
79
|
+
STUB
|
|
80
|
+
mkdir -p "$TMP/sd6"
|
|
81
|
+
run2() { # prints the dispositions of two consecutive classify_one calls in ONE shell
|
|
82
|
+
( _SD="$TMP/sd6"; PR_NUMBER=9; CLASSIFY_SCRIPT="$TMP/classify-textfile.sh"; SCOPE_MODE=full; CROSS_PR_FILES=""; UNREVIEWED_FILES=""; DEPLOY_WIRING_FILES=""
|
|
83
|
+
NOTE_LIST_FILE="$TMP/n6.md"; : > "$NOTE_LIST_FILE"; NOTES_LISTED=0; FINDINGS_FILED=0; OUT_OF_SCOPE=0
|
|
84
|
+
FINDING_ID=X1; FINDING_SEVERITY=LOW; FINDING_CONFIDENCE=CONFIRMED; FINDING_AGENT=Security; FINDING_BODY=b; FINDING_FILE=z/a.sh; FINDING_PATHS=z/a.sh
|
|
85
|
+
. "$TMP/fn.sh"
|
|
86
|
+
FINDING_TITLE="has MARKER_TOKEN"; classify_one; echo "$DISPOSITION"
|
|
87
|
+
FINDING_TITLE="plain title"; classify_one; echo "$DISPOSITION"
|
|
88
|
+
FINDING_TITLE="has MARKER_TOKEN"; classify_one; echo "$DISPOSITION" ) 2>&1
|
|
89
|
+
}
|
|
90
|
+
OUT6=$(run2 | tr '\n' '|')
|
|
91
|
+
[ "$OUT6" = "ISSUE saw-marker|NOTE no-marker|ISSUE saw-marker|" ] && ok || bad "each classify_one call must classify its own text-file content (got '$OUT6')"
|
|
92
|
+
if ls "$TMP/sd6"/staging-finding.* >/dev/null 2>&1; then bad "classify_one must remove its per-finding temp file"; else ok; fi
|
|
93
|
+
# (7) fail-closed guard: BLOCKED when the classifier is unresolvable, no manual classifier mode, no classifier-unavailable fallback
|
|
94
|
+
SPEC_MAIN="$(dirname "$SPEC")/review-pr.md"
|
|
95
|
+
for _s in "$SPEC" "$SPEC_MAIN"; do
|
|
96
|
+
if grep -qE 'BLOCKED.*classifier unavailable' "$_s" && ! grep -qF 'echo script || echo manual' "$_s" && ! grep -qF 'classifier=manual' "$_s" && ! grep -qF 'classifier-unavailable' "$_s"; then ok; else bad "$(basename "$_s") must fail closed with BLOCKED classifier unavailable and have no manual fallback"; fi
|
|
97
|
+
done
|
|
98
|
+
echo "review-pr-staging-scope.test.sh: passed=$PASS failed=$FAILN"
|
|
99
|
+
[ "$FAILN" -eq 0 ]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# run-shell-tests.sh — glob runner for every scripts/*.test.sh suite.
|
|
6
|
+
#
|
|
7
|
+
# Usage: bash scripts/run-shell-tests.sh [suite-file ...]
|
|
8
|
+
# No args: runs every scripts/*.test.sh (sorted). New suites need no workflow edit.
|
|
9
|
+
# Args: runs only the given suite files (used to point at fixtures).
|
|
10
|
+
#
|
|
11
|
+
# Env:
|
|
12
|
+
# SHELL_TEST_TIMEOUT per-suite timeout in seconds (default 300)
|
|
13
|
+
#
|
|
14
|
+
# Exit: 0 when every suite passes; 1 on any FAIL/TIMEOUT or when no suite is found.
|
|
15
|
+
# Bash 3.2 portable: no mapfile, no associative arrays.
|
|
16
|
+
|
|
17
|
+
set -u
|
|
18
|
+
|
|
19
|
+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
20
|
+
REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
|
|
21
|
+
cd "$REPO_ROOT" || exit 1
|
|
22
|
+
|
|
23
|
+
TIMEOUT_SECS="${SHELL_TEST_TIMEOUT:-300}"
|
|
24
|
+
# WIRE:PROVEN — manual: SHELL_TEST_TIMEOUT=abc and SHELL_TEST_TIMEOUT=0 both print the error and exit 2
|
|
25
|
+
case "$TIMEOUT_SECS" in
|
|
26
|
+
''|*[!0-9]*) echo "run-shell-tests: SHELL_TEST_TIMEOUT must be a positive integer, got '$TIMEOUT_SECS'" >&2; exit 2 ;;
|
|
27
|
+
esac
|
|
28
|
+
if [ "$TIMEOUT_SECS" -eq 0 ]; then
|
|
29
|
+
echo "run-shell-tests: SHELL_TEST_TIMEOUT must be a positive integer, got '$TIMEOUT_SECS'" >&2
|
|
30
|
+
exit 2
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
TIMEOUT_BIN=""
|
|
34
|
+
# WIRE:PROVEN — manual: timeout branch runs on Linux; gtimeout branch is the macOS fallback (same invocation shape); neither-found branch only warns and runs suites unbounded
|
|
35
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
36
|
+
TIMEOUT_BIN="timeout"
|
|
37
|
+
elif command -v gtimeout >/dev/null 2>&1; then
|
|
38
|
+
TIMEOUT_BIN="gtimeout"
|
|
39
|
+
else
|
|
40
|
+
echo "run-shell-tests: no timeout/gtimeout found; suites run without a per-suite timeout" >&2
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
SUITES=""
|
|
44
|
+
if [ "$#" -gt 0 ]; then
|
|
45
|
+
for f in "$@"; do
|
|
46
|
+
SUITES="${SUITES}${f}
|
|
47
|
+
"
|
|
48
|
+
done
|
|
49
|
+
else
|
|
50
|
+
for f in scripts/*.test.sh; do
|
|
51
|
+
[ -e "$f" ] || continue
|
|
52
|
+
SUITES="${SUITES}${f}
|
|
53
|
+
"
|
|
54
|
+
done
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
if [ -z "$SUITES" ]; then
|
|
58
|
+
echo "run-shell-tests: no suites found (scripts/*.test.sh)" >&2
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# WIRE:PROVEN — manual: GITHUB_ACTIONS=true emits ::group::/::endgroup:: lines, unset prints "=== suite"
|
|
63
|
+
GROUPING=false
|
|
64
|
+
[ "${GITHUB_ACTIONS:-}" = "true" ] && GROUPING=true
|
|
65
|
+
|
|
66
|
+
TOTAL=0
|
|
67
|
+
FAILED=0
|
|
68
|
+
SUMMARY=""
|
|
69
|
+
|
|
70
|
+
while IFS= read -r suite; do
|
|
71
|
+
[ -n "$suite" ] || continue
|
|
72
|
+
TOTAL=$((TOTAL + 1))
|
|
73
|
+
# WIRE:PROVEN — manual: passing a nonexistent suite path prints FAIL (missing file) and exits 1
|
|
74
|
+
if [ ! -f "$suite" ]; then
|
|
75
|
+
SUMMARY="${SUMMARY}FAIL ${suite} (missing file)
|
|
76
|
+
"
|
|
77
|
+
FAILED=$((FAILED + 1))
|
|
78
|
+
continue
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
if $GROUPING; then echo "::group::${suite}"; else echo "=== ${suite}"; fi
|
|
82
|
+
start=$(date +%s)
|
|
83
|
+
if [ -n "$TIMEOUT_BIN" ]; then
|
|
84
|
+
"$TIMEOUT_BIN" "$TIMEOUT_SECS" bash "$suite"
|
|
85
|
+
else
|
|
86
|
+
bash "$suite"
|
|
87
|
+
fi
|
|
88
|
+
rc=$?
|
|
89
|
+
elapsed=$(( $(date +%s) - start ))
|
|
90
|
+
if $GROUPING; then echo "::endgroup::"; fi
|
|
91
|
+
|
|
92
|
+
if [ "$rc" -eq 0 ]; then
|
|
93
|
+
status="PASS"
|
|
94
|
+
# WIRE:PROVEN — manual: SHELL_TEST_TIMEOUT=1 against a `sleep 5` fixture suite yields rc 124 -> TIMEOUT
|
|
95
|
+
elif [ "$rc" -eq 124 ] && [ -n "$TIMEOUT_BIN" ]; then
|
|
96
|
+
status="TIMEOUT"
|
|
97
|
+
FAILED=$((FAILED + 1))
|
|
98
|
+
else
|
|
99
|
+
status="FAIL"
|
|
100
|
+
FAILED=$((FAILED + 1))
|
|
101
|
+
fi
|
|
102
|
+
[ "$status" = "PASS" ] || echo "${status}: ${suite} (exit ${rc})"
|
|
103
|
+
SUMMARY="${SUMMARY}$(printf '%-8s %s (%ss)' "$status" "$suite" "$elapsed")
|
|
104
|
+
"
|
|
105
|
+
done <<EOF_SUITES
|
|
106
|
+
${SUITES}
|
|
107
|
+
EOF_SUITES
|
|
108
|
+
|
|
109
|
+
echo
|
|
110
|
+
echo "=== Shell test summary ==="
|
|
111
|
+
printf '%s' "$SUMMARY"
|
|
112
|
+
echo "Total: ${TOTAL} Failed: ${FAILED}"
|
|
113
|
+
|
|
114
|
+
[ "$FAILED" -eq 0 ] || exit 1
|
|
115
|
+
exit 0
|