forgedock 1.14.0 → 1.15.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 +11 -0
- package/bin/engine/phases.mjs +115 -14
- package/bin/engine/phases.test.mjs +24 -0
- package/bin/engine.mjs +41 -7
- package/bin/forgedock.mjs +1 -1
- package/commands/issue.md +17 -2
- package/commands/orchestrate/phase-4-execution.md +10 -3
- package/commands/review-pr-staging.md +278 -28
- package/commands/review-pr.md +71 -22
- package/commands/work-on/build/implement.md +15 -1
- package/commands/work-on/build.md +7 -4
- package/commands/work-on/investigate.md +5 -3
- package/commands/work-on/remediate.md +215 -35
- package/docs/CONFIG.md +2 -2
- package/docs/RELEASE-NOTES.md +12 -0
- package/package.json +1 -1
- package/packages/protocol/src/phases.js +6 -0
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/classify-finding.sh +14 -7
- package/scripts/classify-finding.test.sh +9 -0
- package/scripts/forge-root.test.sh +41 -4
- package/scripts/remediate-approved-head.test.sh +43 -0
- package/scripts/remediation-marker-match.test.sh +6 -0
- package/scripts/resolve-deploy-gate.sh +51 -0
- package/scripts/resolve-deploy-gate.test.sh +59 -0
- package/scripts/review-pr-delta-wiring.test.sh +8 -0
- package/scripts/review-pr-domain-map.test.sh +10 -0
- package/scripts/review-pr-staging-scope.test.sh +99 -0
|
@@ -51,7 +51,9 @@
|
|
|
51
51
|
# earlier head of this PR or by a bundle's member PRs, one per line, columns
|
|
52
52
|
# file<TAB>line-start<TAB>line-end<TAB>title<TAB>prior-disposition<TAB>issue#.
|
|
53
53
|
# prior-disposition is `filed` (its issue is still open), `filed-closed` (issue closed:
|
|
54
|
-
# the row is IGNORED so a regressed defect is classified normally)
|
|
54
|
+
# the row is IGNORED so a regressed defect is classified normally), `note` (a recorded NOTE) or
|
|
55
|
+
# `unknown` (a reviewer-body row whose issue linkage failed: it is IGNORED for a MEDIUM
|
|
56
|
+
# CONFIRMED/LIKELY finding so it can never suppress one, and behaves like `note` otherwise).
|
|
55
57
|
# Line columns may be empty (NOTE-sourced rows carry no range). A finding re-flags a row
|
|
56
58
|
# when it is the same file AND ((line IoU >= 0.6 and >= 1 shared title token), or >= 3 shared title tokens). Output is
|
|
57
59
|
# `NOTE reflag-filed #N` (row has an issue number) or `NOTE reflag-note`; this gate never
|
|
@@ -180,7 +182,7 @@ if [ -n "$PRIOR_FINDINGS" ] && [ -n "$FINDING_FILE" ]; then
|
|
|
180
182
|
case "$_rls" in ''|*[!0-9]*) _rls=""; _rle="" ;; esac
|
|
181
183
|
case "$_rle" in ''|*[!0-9]*) _rle="$_rls" ;; esac
|
|
182
184
|
_dmap="$DIFF_MAP"; { [ -n "$_dmap" ] && [ -r "$_dmap" ]; } || _dmap=""
|
|
183
|
-
_reflag=$(awk -F '\t' -v file="$_rpath" -v ls="$_rls" -v le="$_rle" -v title="$TITLE" -v dmap="$_dmap" '
|
|
185
|
+
_reflag=$(awk -F '\t' -v file="$_rpath" -v ls="$_rls" -v le="$_rle" -v title="$TITLE" -v dmap="$_dmap" -v sev="$SEV" -v conf="$CONF" '
|
|
184
186
|
function toks(str, arr, n, i, parts, t, c) {
|
|
185
187
|
c = 0; str = tolower(str); gsub(/[^a-z0-9]+/, " ", str)
|
|
186
188
|
n = split(str, parts, " ")
|
|
@@ -202,18 +204,23 @@ if [ -n "$PRIOR_FINDINGS" ] && [ -n "$FINDING_FILE" ]; then
|
|
|
202
204
|
}
|
|
203
205
|
close(dmap)
|
|
204
206
|
}
|
|
207
|
+
# Generic words count toward the token set but never as topical similarity (#3719).
|
|
208
|
+
ns = split("the and with for from before after write read when that this into not are but can has have via use using does should may all any only also than then over under without within while where which what its it was were been being each both one two", sw, " ")
|
|
209
|
+
for (i = 1; i <= ns; i++) stop[sw[i]] = 1
|
|
205
210
|
ntitle = toks(title, ft)
|
|
206
211
|
}
|
|
207
212
|
{
|
|
208
213
|
pf = $1; sub(/^\.\//, "", pf)
|
|
209
214
|
if (pf != file) next
|
|
210
215
|
if ($5 == "filed-closed") next
|
|
216
|
+
if ($5 == "unknown" && sev == "MEDIUM" && (conf == "CONFIRMED" || conf == "LIKELY")) next
|
|
211
217
|
hit = 0
|
|
212
|
-
delete pt; toks($4, pt); shared = 0
|
|
213
|
-
for (t in pt) if (t in ft) shared++
|
|
218
|
+
delete pt; toks($4, pt); shared = 0; cshared = 0
|
|
219
|
+
for (t in pt) if (t in ft) { shared++; if (!(t in stop)) cshared++ }
|
|
214
220
|
# Line overlap alone is not a match (an unrelated finding can land on the same line): it also
|
|
215
|
-
# needs at least one shared title token. Title-only matching
|
|
216
|
-
|
|
221
|
+
# needs at least one shared non-stopword title token. Title-only matching needs >= 3 shared
|
|
222
|
+
# tokens, of which >= 2 are non-stopword (generic words never count as topical similarity).
|
|
223
|
+
if (cshared >= 1 && ls != "" && $2 ~ /^[0-9]+$/) {
|
|
217
224
|
ps = $2 + 0; pe = ($3 ~ /^[0-9]+$/) ? $3 + 0 : ps
|
|
218
225
|
if (pe < ps) pe = ps
|
|
219
226
|
d = shift(pf, ps, pe); ps += d; pe += d
|
|
@@ -222,7 +229,7 @@ if [ -n "$PRIOR_FINDINGS" ] && [ -n "$FINDING_FILE" ]; then
|
|
|
222
229
|
uni = (pe - ps + 1) + (le - ls + 1) - inter
|
|
223
230
|
if (uni > 0 && inter / uni >= 0.6) hit = 1
|
|
224
231
|
}
|
|
225
|
-
if (!hit && ntitle >= 3 && shared >= 3) hit = 1
|
|
232
|
+
if (!hit && ntitle >= 3 && shared >= 3 && cshared >= 2) hit = 1
|
|
226
233
|
if (!hit) next
|
|
227
234
|
if ($5 == "filed" && $6 ~ /^#?[0-9]+$/) { n = $6; sub(/^#/, "", n); print "NOTE reflag-filed #" n; found = 1; exit }
|
|
228
235
|
if (!note) note = "NOTE reflag-note"
|
|
@@ -166,6 +166,7 @@ DMAP=$(mktemp "${TMPDIR:-/tmp}/classify-finding-dmap.XXXXXX")
|
|
|
166
166
|
printf 'src/api.py\t10\t20\tmissing retry on timeout handler\tfiled\t123\n'
|
|
167
167
|
printf 'src/closed.py\t5\t15\tstale cache entry never evicted\tfiled-closed\t77\n'
|
|
168
168
|
printf 'src/notes.py\t\t\tduplicated predicate counted twice\tnote\t\n'
|
|
169
|
+
printf 'src/unlinked.py\t30\t40\tunvalidated redirect target in login handler\tunknown\t\n'
|
|
169
170
|
} > "$PRIOR"
|
|
170
171
|
PF=(--prior-findings "$PRIOR")
|
|
171
172
|
M=(--severity MEDIUM --confidence CONFIRMED)
|
|
@@ -182,9 +183,17 @@ expect_out "NOTE reflag-note" "NOTE row with empty line columns matches by title
|
|
|
182
183
|
expect ISSUE "same line, different topic MEDIUM CONFIRMED is not swallowed (SEC-1)" -- "${M[@]}" --agent security "${PF[@]}" --file src/sync.sh --line-start 42 --line-end 50 --title "SQL injection via unsanitized token"
|
|
183
184
|
expect ISSUE "same line with no title at all is not swallowed" -- "${M[@]}" "${PF[@]}" --file src/sync.sh --line-start 42 --line-end 50
|
|
184
185
|
expect_out "NOTE reflag-note" "same line, same topic (one shared token) still NOTE" -- "${M[@]}" "${PF[@]}" --file src/sync.sh --line-start 42 --line-end 50 --title "Lock handling is racy"
|
|
186
|
+
expect ISSUE "same line, only a stopword-class word shared is not swallowed (#3719)" -- "${M[@]}" "${PF[@]}" --file src/sync.sh --line-start 42 --line-end 50 --title "Path traversal in the write target"
|
|
187
|
+
expect ISSUE "far line, only generic words shared is not swallowed (#3719)" -- "${M[@]}" "${PF[@]}" --file src/sync.sh --line-start 300 --title "Command injection before write with the lock"
|
|
185
188
|
expect ISSUE "non-overlapping same-file finding is classified normally" -- "${M[@]}" "${PF[@]}" --file src/sync.sh --line-start 200 --line-end 210 --title "missing bounds check"
|
|
186
189
|
expect ISSUE "two shared tokens are not enough" -- "${M[@]}" "${PF[@]}" --file src/sync.sh --line-start 200 --title "lock released late"
|
|
187
190
|
expect ISSUE "same title on a different file is classified normally" -- "${M[@]}" "${PF[@]}" --file src/other.sh --line-start 45 --title "lock released before write completes"
|
|
191
|
+
expect ISSUE "unknown row does not demote MEDIUM CONFIRMED (unlinked prior)" -- "${M[@]}" "${PF[@]}" --file src/unlinked.py --line-start 300 --title "Unvalidated redirect target in the login handler"
|
|
192
|
+
expect ISSUE "unknown row does not demote MEDIUM CONFIRMED by line IoU" -- "${M[@]}" "${PF[@]}" --file src/unlinked.py --line-start 32 --line-end 40 --title "redirect target unchecked"
|
|
193
|
+
expect ISSUE "unknown row does not demote MEDIUM LIKELY" -- --severity MEDIUM --confidence LIKELY "${PF[@]}" --file src/unlinked.py --line-start 300 --title "Unvalidated redirect target in the login handler"
|
|
194
|
+
expect_out "NOTE reflag-note" "unknown row still demotes LOW" -- --severity LOW --confidence CONFIRMED "${PF[@]}" --file src/unlinked.py --line-start 300 --title "Unvalidated redirect target in the login handler"
|
|
195
|
+
expect_out "NOTE reflag-note" "unknown row still demotes MEDIUM POSSIBLE" -- --severity MEDIUM --confidence POSSIBLE "${PF[@]}" --file src/unlinked.py --line-start 300 --title "Unvalidated redirect target in the login handler"
|
|
196
|
+
expect ISSUE "HIGH unaffected by unknown row" -- --severity HIGH --confidence CONFIRMED "${PF[@]}" --file src/unlinked.py --line-start 300 --title "Unvalidated redirect target in the login handler"
|
|
188
197
|
expect_out "NOTE reflag-filed #123" "re-flag of an already-filed issue links it" -- "${M[@]}" "${PF[@]}" --file src/api.py --line-start 12 --line-end 18 --title "retry x"
|
|
189
198
|
expect_out "NOTE reflag-filed #123" "filed re-flag by title tokens" -- "${M[@]}" "${PF[@]}" --file src/api.py --line-start 99 --title "Missing retry on timeout"
|
|
190
199
|
expect ISSUE "filed-closed row is ignored (regression not swallowed)" -- "${M[@]}" "${PF[@]}" --file src/closed.py --line-start 5 --line-end 15 --title "stale cache entry never evicted"
|
|
@@ -13,7 +13,7 @@ ok() { PASS=$((PASS+1)); }
|
|
|
13
13
|
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
14
14
|
expect() { [ "$2" = "$3" ] && ok || bad "$1 (got '$3' want '$2')"; }
|
|
15
15
|
|
|
16
|
-
SITES="commands/work-on.md commands/work-on/build/validate.md commands/quality-gate.md commands/work-on/review.md commands/work-on/investigate.md commands/work-on/decompose.md commands/work-on/build.md commands/work-on/close.md commands/review-pr.md commands/orchestrate/phase-1-resolve.md commands/orchestrate/phase-4-execution.md"
|
|
16
|
+
SITES="commands/work-on.md commands/work-on/build/validate.md commands/quality-gate.md commands/work-on/review.md commands/work-on/investigate.md commands/work-on/decompose.md commands/work-on/build.md commands/work-on/close.md commands/review-pr.md commands/work-on/remediate.md commands/orchestrate/phase-1-resolve.md commands/orchestrate/phase-4-execution.md"
|
|
17
17
|
|
|
18
18
|
# extract the bootstrap block (comment line .. closing top-level fi), indentation stripped
|
|
19
19
|
# Block ends at the first line (after the marker) that starts with `fi` at the marker's own indentation.
|
|
@@ -111,6 +111,23 @@ else
|
|
|
111
111
|
echo "SKIP: symlink cases (this platform cannot create symlinks)"; SKIPPED=$((SKIPPED+1))
|
|
112
112
|
fi
|
|
113
113
|
|
|
114
|
+
# forge#3758 (hgate is a fixture of its own: the loop runs once per shell and h14 is reused below with other versions): deploy-gate script resolves on a plugin-only install (cache only, no symlink, no FORGE_* env).
|
|
115
|
+
# The gate-script line is taken verbatim from the spec so the test exercises exactly what ships.
|
|
116
|
+
mkscripts "$T/hgate/.claude/plugins/cache/forgedock/forgedock/1.0.0"; : > "$T/hgate/.claude/plugins/cache/forgedock/forgedock/1.0.0/scripts/resolve-deploy-gate.sh"
|
|
117
|
+
grep -hE '^[[:space:]]*GATE_SCRIPT=""; \[ -n "\$FORGE_ROOT" \]' "$ROOT/commands/work-on/remediate.md" "$ROOT/commands/review-pr.md" | sed 's/^[[:space:]]*//' | sort -u > "$T/gs"
|
|
118
|
+
expect "gate-script resolver line is identical across sites" 1 "$(wc -l < "$T/gs" | tr -d ' ')"
|
|
119
|
+
gate_run() { ( cd "$T/consumer" && env -i PATH="$PATH" HOME="$1" "$SH" -c "$(cat "$T/canon"); $(cat "$T/gs"); printf %s \"\$GATE_SCRIPT\"" ); }
|
|
120
|
+
expect "plugin-only install resolves resolve-deploy-gate.sh" "$T/hgate/.claude/plugins/cache/forgedock/forgedock/1.0.0/scripts/resolve-deploy-gate.sh" "$(gate_run "$T/hgate")"
|
|
121
|
+
expect "unresolved gate script stays empty (strict)" "" "$(gate_run "$T/empty")"
|
|
122
|
+
# cwd/consumer repo is never a candidate even when it carries the gate script
|
|
123
|
+
: > "$T/consumer/scripts/resolve-deploy-gate.sh"
|
|
124
|
+
expect "consumer repo gate script never used" "" "$(gate_run "$T/empty")"
|
|
125
|
+
rm -f "$T/consumer/scripts/resolve-deploy-gate.sh"
|
|
126
|
+
# no spec iterates CLAUDE_PLUGIN_ROOT as an env var for the gate script
|
|
127
|
+
for f in commands/work-on/remediate.md commands/review-pr.md; do
|
|
128
|
+
grep -qF '"${CLAUDE_PLUGIN_ROOT:-}" "${FORGE_ROOT:-}"' "$ROOT/$f" && bad "env-style CLAUDE_PLUGIN_ROOT gate loop in $f" || ok
|
|
129
|
+
done
|
|
130
|
+
|
|
114
131
|
# newest cached version wins (1.10.0 must beat 1.9.0)
|
|
115
132
|
for v in 1.9.0 1.10.0 1.2.0; do mkscripts "$T/h3/.claude/plugins/cache/forgedock/forgedock/$v"; done
|
|
116
133
|
expect "newest cached version wins" "$T/h3/.claude/plugins/cache/forgedock/forgedock/1.10.0" "$(run "$T/h3")"
|
|
@@ -325,6 +342,8 @@ hr_check_block() { # label file var script
|
|
|
325
342
|
hr_check_block review-pr "$HR_CLASSIFY" DERIVE_MILESTONE_SCRIPT derive-finding-milestone.sh
|
|
326
343
|
hr_check_block review-pr "$HR_CLASSIFY" PRIORITY_SCRIPT severity-to-priority.sh
|
|
327
344
|
hr_check_block review-pr "$HR_CLASSIFY" CODE_INDEX_SCRIPT code-index.sh
|
|
345
|
+
hr_check_block review-pr "$HR_CLASSIFY" DEDUP_SCRIPT issue-dedup.sh
|
|
346
|
+
hr_check_block issue "$ROOT/commands/issue.md" DEDUP_SCRIPT issue-dedup.sh
|
|
328
347
|
hr_check_block review-pr-staging "$HR_STG" DERIVE_MILESTONE_SCRIPT derive-finding-milestone.sh
|
|
329
348
|
hr_check_block review-pr-staging "$HR_STG" PRIORITY_SCRIPT severity-to-priority.sh
|
|
330
349
|
extract_helper_block "$HR_CLASSIFY" DERIVE_MILESTONE_SCRIPT > "$T/hr_dm.sh"
|
|
@@ -334,6 +353,9 @@ extract_helper_block "$HR_STG" PRIORITY_SCRIPT > "$T/hr_pr_stg.sh"
|
|
|
334
353
|
# only the variable-name-specific text may differ, and review-pr/review-pr-staging must agree
|
|
335
354
|
cmp -s "$T/hr_dm.sh" "$T/hr_dm_stg.sh" && ok || bad 'DERIVE_MILESTONE_SCRIPT resolver differs between review-pr and review-pr-staging'
|
|
336
355
|
cmp -s "$T/hr_pr.sh" "$T/hr_pr_stg.sh" && ok || bad 'PRIORITY_SCRIPT resolver differs between review-pr and review-pr-staging'
|
|
356
|
+
extract_helper_block "$HR_CLASSIFY" DEDUP_SCRIPT > "$T/hr_dd.sh"
|
|
357
|
+
extract_helper_block "$ROOT/commands/issue.md" DEDUP_SCRIPT > "$T/hr_dd_issue.sh"
|
|
358
|
+
cmp -s "$T/hr_dd.sh" "$T/hr_dd_issue.sh" && ok || bad 'DEDUP_SCRIPT resolver differs between review-pr and issue'
|
|
337
359
|
# no bare cwd-relative helper call in either review spec
|
|
338
360
|
if grep -nE 'bash scripts/(derive-finding-milestone|severity-to-priority)\.sh' "$HR_CLASSIFY" "$HR_STG" >/dev/null 2>&1; then bad 'review spec still calls a helper by a bare scripts/ path'; else ok; fi
|
|
339
361
|
|
|
@@ -342,10 +364,25 @@ if grep -nE 'bash scripts/(derive-finding-milestone|severity-to-priority)\.sh' "
|
|
|
342
364
|
# design-bench.md, design-render-critique-loop.md, pipeline-health/phase-2-metrics.md, phase-3-dependency.md:
|
|
343
365
|
# node scripts/*.mjs run inside the ForgeDock repo itself or printed as user hints (not run under a PR checkout cwd).
|
|
344
366
|
# work-on/investigate.md, work-on/build/architect.md: {REPO_PATH}/scripts/code-index.sh, deferred to #3670.
|
|
367
|
+
# orchestrate/phase-3-dependency.md: scripts/worktree-lifecycle.sh lines sit in a roadmap/proposal fence, not executed.
|
|
345
368
|
# deploy-info.md: read-only grep of decrypt-secrets.sh, never executed.
|
|
346
|
-
SCRIPTS_ALLOW='commands/(design-bench|design-render-critique-loop|deploy-info|pipeline-health/phase-2-metrics|pipeline-health/phase-3-dependency|work-on/investigate|work-on/build/architect)\.md'
|
|
347
|
-
|
|
348
|
-
|
|
369
|
+
SCRIPTS_ALLOW='commands/(design-bench|design-render-critique-loop|deploy-info|pipeline-health/phase-2-metrics|pipeline-health/phase-3-dependency|orchestrate/phase-3-dependency|work-on/investigate|work-on/build/architect)\.md'
|
|
370
|
+
# Executed forms: a bash/sh/source word, a command substitution `$(scripts/x.sh`, or a line-start/after-;&&| direct `scripts/x.sh` / `./scripts/x.sh`.
|
|
371
|
+
GUARD_RE='(^|[^A-Za-z0-9_-])(bash|sh|source) +(\./)?scripts/[A-Za-z0-9_.-]+|REPO_PATH\}?/scripts/|\$\( *(\./)?scripts/[A-Za-z0-9_.-]+\.sh|(^|:[0-9]+:|[;&(] *)[[:space:]]*(\./)?scripts/[A-Za-z0-9_.-]+\.sh'
|
|
372
|
+
guard_scan() { # dir -> offending file:line:text rows (comment lines are prose)
|
|
373
|
+
grep -rnE "$GUARD_RE" "$1" 2>/dev/null | grep -vE '^[^:]+:[0-9]+:[[:space:]]*#' || true
|
|
374
|
+
}
|
|
375
|
+
guard_hits=$(guard_scan "$ROOT/commands" | sed "s#^$ROOT/##" | grep -vE "^($SCRIPTS_ALLOW):" || true)
|
|
376
|
+
# Negative fixtures (#3679): a bare direct call must be flagged; backticked prose and comments must not.
|
|
377
|
+
mkdir -p "$T/guardfx/bad1" "$T/guardfx/bad2" "$T/guardfx/bad3" "$T/guardfx/good"
|
|
378
|
+
printf 'X=$(scripts/foo.sh a b)\n' > "$T/guardfx/bad1/x.md"
|
|
379
|
+
printf 'scripts/foo.sh arg\n' > "$T/guardfx/bad2/x.md"
|
|
380
|
+
printf 'ok && ./scripts/foo.sh\n' > "$T/guardfx/bad3/x.md"
|
|
381
|
+
printf 'See `scripts/foo.sh` for details, forwarded to scripts/foo.sh --exclude.\n# $(scripts/foo.sh) in a comment\n' > "$T/guardfx/good/x.md"
|
|
382
|
+
for f in bad1 bad2 bad3; do
|
|
383
|
+
[ -n "$(guard_scan "$T/guardfx/$f")" ] && ok || bad "widened guard missed direct execution fixture $f"
|
|
384
|
+
done
|
|
385
|
+
[ -z "$(guard_scan "$T/guardfx/good")" ] && ok || bad 'widened guard flags prose/comment fixture'
|
|
349
386
|
if [ -n "$guard_hits" ]; then bad "commands/ has a cwd-relative or REPO_PATH helper call outside the allowlist: $(printf '%s' "$guard_hits" | head -5 | tr '\n' '|')"; else ok; fi
|
|
350
387
|
|
|
351
388
|
# Behavior (#3596): planted cwd copies of the two review helpers must never be selected.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# remediate-approved-head.test.sh — the M7 CI-gated merge is bound to APPROVED_HEAD on every path (forge#3686)
|
|
3
|
+
# Extracts the guard from commands/work-on/remediate.md and exercises it. No network. Usage: bash scripts/remediate-approved-head.test.sh
|
|
4
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
10
|
+
SPEC="$ROOT/commands/work-on/remediate.md"
|
|
11
|
+
|
|
12
|
+
PASS=0; FAILN=0
|
|
13
|
+
ok() { PASS=$((PASS+1)); echo "ok - $1"; }
|
|
14
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL - $1"; }
|
|
15
|
+
eq() { if [ "$2" = "$3" ]; then ok "$1"; else bad "$1 (got '$2', want '$3')"; fi; }
|
|
16
|
+
|
|
17
|
+
grep -qE 'GATED_HEAD" != "\$\{?APPROVED_HEAD' "$SPEC" && ok "spec compares GATED_HEAD to APPROVED_HEAD" || bad "spec compares GATED_HEAD to APPROVED_HEAD"
|
|
18
|
+
grep -qF 'if [ "${RESUME_AT_M7:-false}" = "true" ] && [ "$CI_GATE_RC" -eq 0 ]' "$SPEC" && bad "guard is no longer resume-only" || ok "guard is no longer resume-only"
|
|
19
|
+
|
|
20
|
+
# Extract the guard fence body (from the scratch reload through the closing fi) and run it per case.
|
|
21
|
+
GUARD=$(awk '/^_SD="\$\{FORGE_SCRATCHPAD/ && !g {g=1} g {print} g && /^fi$/ {exit}' "$SPEC")
|
|
22
|
+
[ -n "$GUARD" ] && ok "guard block extracted" || bad "guard block extracted"
|
|
23
|
+
|
|
24
|
+
# run_guard <persisted head or NONE> <GATED_HEAD> <CI_GATE_RC> <RESUME_AT_M7>
|
|
25
|
+
run_guard() {
|
|
26
|
+
local d; d=$(mktemp -d)
|
|
27
|
+
[ "$1" = NONE ] || printf '%s\n' "$1" > "$d/remediate_approved_head_7"
|
|
28
|
+
FORGE_SCRATCHPAD="$d" GATED_HEAD="$2" CI_GATE_RC="$3" RESUME_AT_M7="$4" CI_GATE_OUT="" \
|
|
29
|
+
bash -c "${GUARD//\{PR_NUMBER\}/7}"$'\nprintf %s "$CI_GATE_RC"'
|
|
30
|
+
rm -rf "$d"
|
|
31
|
+
}
|
|
32
|
+
A=$(printf 'a%.0s' {1..40}); B=$(printf 'b%.0s' {1..40})
|
|
33
|
+
for r in true false; do
|
|
34
|
+
eq "match merges (resume=$r)" "$(run_guard "$A" "$A" 0 $r)" 0
|
|
35
|
+
eq "mismatch refuses (resume=$r)" "$(run_guard "$A" "$B" 0 $r)" 4
|
|
36
|
+
eq "missing approved head refuses (resume=$r)" "$(run_guard NONE "$A" 0 $r)" 4
|
|
37
|
+
eq "malformed approved head refuses (resume=$r)" "$(run_guard "zz" "$A" 0 $r)" 4
|
|
38
|
+
eq "empty gated head refuses (resume=$r)" "$(run_guard "$A" "" 0 $r)" 4
|
|
39
|
+
eq "nonzero rc is unchanged (resume=$r)" "$(run_guard "$A" "$B" 3 $r)" 3
|
|
40
|
+
done
|
|
41
|
+
|
|
42
|
+
echo "passed=$PASS failed=$FAILN"
|
|
43
|
+
[ "$FAILN" -eq 0 ]
|
|
@@ -28,6 +28,8 @@ INTERIM=$'<!-- FORGE:REMEDIATION -->\n## Remediation (in progress)'
|
|
|
28
28
|
QUOTED_REVIEW=$'## Review\nThe prior run posted FORGE:REMEDIATION:COMPLETE on this PR.\n<!-- FORGE:REMEDIATION:COMPLETE -->'
|
|
29
29
|
QUOTED_WORK_ORDER=$'<!-- FORGE:INPR_FIX: head=abc1234 -->\nquoting <!-- FORGE:REMEDIATION -->\n<!-- FORGE:REMEDIATION:COMPLETE -->'
|
|
30
30
|
|
|
31
|
+
HELD=$'<!-- FORGE:REMEDIATION_HELD -->\n**Re-gate outcome**: HELD-AWAITING-MERGE'
|
|
32
|
+
eq "held-resume record matches neither COMPLETE nor TRAIL (forge#3685)" "$(complete_n "[$(c NONE Bot b "$HELD")]")/$(trail_n "[$(c NONE Bot b "$HELD")]")" "0/0"
|
|
31
33
|
eq "genuine M8 trail by bot matches COMPLETE" "$(complete_n "[$(c NONE Bot 'app[bot]' "$REAL")]")" 1
|
|
32
34
|
eq "review comment quoting COMPLETE mid-body does not match" "$(complete_n "[$(c OWNER User u "$QUOTED_REVIEW")]")" 0
|
|
33
35
|
eq "INPR_FIX work order quoting the markers does not match COMPLETE" "$(complete_n "[$(c NONE Bot b "$QUOTED_WORK_ORDER")]")" 0
|
|
@@ -47,5 +49,9 @@ for f in commands/work-on/remediate.md commands/work-on.md commands/orchestrate/
|
|
|
47
49
|
done
|
|
48
50
|
grep -qF "count '$COMPLETE_RE'" "$ROOT/commands/work-on/remediate.md" && ok "remediate.md uses the tested COMPLETE regex" || bad "remediate.md COMPLETE regex drifted from this test"
|
|
49
51
|
|
|
52
|
+
grep -qF 'FORGE:REMEDIATION_HELD' "$ROOT/commands/work-on/remediate.md" && ok "remediate.md documents FORGE:REMEDIATION_HELD" || bad "remediate.md lacks FORGE:REMEDIATION_HELD"
|
|
53
|
+
! grep -qE 'PR_OPEN_STATE = .?MERGED.? → EXIT .REMEDIATE_RESULT: status: ALREADY_DONE. \(nothing to remediate' "$ROOT/commands/work-on/remediate.md" && ok "M0 has no unconditional merged ALREADY_DONE exit" || bad "M0 still exits ALREADY_DONE unconditionally on MERGED"
|
|
54
|
+
grep -qiE 'merged.*(skip|exclusive|do not fall through)' "$ROOT/commands/work-on/remediate.md" && ok "M7 merged-first branch is exclusive" || bad "M7 merged-first exclusivity missing"
|
|
55
|
+
|
|
50
56
|
echo "passed=$PASS failed=$FAILN"
|
|
51
57
|
[ "$FAILN" -eq 0 ]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# resolve-deploy-gate.sh — trusted deploy-gate resolver (forge#3753)
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
#
|
|
6
|
+
# Usage: resolve-deploy-gate.sh <base> <owner/repo>
|
|
7
|
+
#
|
|
8
|
+
# Prints exactly one line: `true` (deploy gate, strict human bar) or `false`
|
|
9
|
+
# (fast lane). Always exits 0; every failure path prints `true` (fail closed).
|
|
10
|
+
#
|
|
11
|
+
# A base is the deploy gate when it is empty/`null`, the literal `main` (hard
|
|
12
|
+
# floor), the repository's GitHub default branch (`default_branch`), or the
|
|
13
|
+
# `branches.default` of the default branch's COMMITTED forge.yaml
|
|
14
|
+
# (`origin/<default>:forge.yaml`, falling back to the contents API) or of an
|
|
15
|
+
# explicit trusted config (FORGE_TRUSTED_CONFIG, orchestrator-supplied).
|
|
16
|
+
#
|
|
17
|
+
# It NEVER reads forge.yaml from $PWD: the PR worktree is author-controlled, so
|
|
18
|
+
# a PR could otherwise redefine which branch counts as the deploy gate.
|
|
19
|
+
# Config sources can only ADD gate branches; main and the GitHub default are
|
|
20
|
+
# unconditional, so no source relaxes the bar below the literal-main floor.
|
|
21
|
+
set -uo pipefail
|
|
22
|
+
|
|
23
|
+
BASE="${1:-}"
|
|
24
|
+
REPO="${2:-}"
|
|
25
|
+
|
|
26
|
+
emit() { printf '%s\n' "$1"; exit 0; }
|
|
27
|
+
|
|
28
|
+
{ [ -z "$BASE" ] || [ "$BASE" = "null" ] || [ "$BASE" = "main" ]; } && emit true
|
|
29
|
+
[ -n "$REPO" ] || emit true
|
|
30
|
+
|
|
31
|
+
GH_DEFAULT=$(gh api "repos/${REPO}" --jq .default_branch 2>/dev/null || echo "")
|
|
32
|
+
{ [ -z "$GH_DEFAULT" ] || [ "$GH_DEFAULT" = "null" ]; } && emit true
|
|
33
|
+
[ "$BASE" = "$GH_DEFAULT" ] && emit true
|
|
34
|
+
|
|
35
|
+
_cfg_default() { yq '.branches.default // ""' 2>/dev/null || true; }
|
|
36
|
+
|
|
37
|
+
CFG_DEFAULT=""
|
|
38
|
+
# Committed default-branch config only (a ref, never the working tree).
|
|
39
|
+
CFG_DEFAULT=$(git show "origin/${GH_DEFAULT}:forge.yaml" 2>/dev/null | _cfg_default)
|
|
40
|
+
if [ -z "$CFG_DEFAULT" ]; then
|
|
41
|
+
CFG_DEFAULT=$(gh api "repos/${REPO}/contents/forge.yaml?ref=${GH_DEFAULT}" --jq .content 2>/dev/null \
|
|
42
|
+
| base64 -d 2>/dev/null | _cfg_default)
|
|
43
|
+
fi
|
|
44
|
+
[ -n "$CFG_DEFAULT" ] && [ "$CFG_DEFAULT" != "null" ] && [ "$BASE" = "$CFG_DEFAULT" ] && emit true
|
|
45
|
+
|
|
46
|
+
if [ -n "${FORGE_TRUSTED_CONFIG:-}" ] && [ -f "$FORGE_TRUSTED_CONFIG" ]; then
|
|
47
|
+
TRUSTED=$(_cfg_default < "$FORGE_TRUSTED_CONFIG")
|
|
48
|
+
[ -n "$TRUSTED" ] && [ "$TRUSTED" != "null" ] && [ "$BASE" = "$TRUSTED" ] && emit true
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
emit false
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# resolve-deploy-gate.test.sh — cases for scripts/resolve-deploy-gate.sh (forge#3753).
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
S="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/resolve-deploy-gate.sh"
|
|
7
|
+
PASS=0; FAILN=0
|
|
8
|
+
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
9
|
+
ok() { PASS=$((PASS+1)); }
|
|
10
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
11
|
+
|
|
12
|
+
command -v yq >/dev/null 2>&1 || { echo "SKIP: yq not installed"; exit 0; }
|
|
13
|
+
|
|
14
|
+
# Stub gh: GH_DEFAULT_STUB drives `repos/<r> --jq .default_branch`; contents API unused (fails).
|
|
15
|
+
mkdir -p "$TMP/bin"
|
|
16
|
+
cat > "$TMP/bin/gh" <<'EOF'
|
|
17
|
+
#!/usr/bin/env bash
|
|
18
|
+
case "$*" in
|
|
19
|
+
*"--jq .default_branch"*) [ -n "${GH_FAIL:-}" ] && exit 1; printf '%s\n' "${GH_DEFAULT_STUB:-}" ;;
|
|
20
|
+
*) exit 1 ;;
|
|
21
|
+
esac
|
|
22
|
+
EOF
|
|
23
|
+
chmod +x "$TMP/bin/gh"
|
|
24
|
+
|
|
25
|
+
# Repo fixture: origin/<default> committed forge.yaml, plus a PR worktree dir with its own forge.yaml.
|
|
26
|
+
ORIGIN="$TMP/origin.git"; REPO="$TMP/repo"
|
|
27
|
+
git init -q --bare "$ORIGIN"
|
|
28
|
+
git init -q "$REPO"; git -C "$REPO" config user.email t@t; git -C "$REPO" config user.name t
|
|
29
|
+
printf 'branches:\n default: develop\n' > "$REPO/forge.yaml"
|
|
30
|
+
git -C "$REPO" add forge.yaml; git -C "$REPO" commit -qm init
|
|
31
|
+
git -C "$REPO" branch -M trunk
|
|
32
|
+
git -C "$REPO" remote add origin "$ORIGIN"; git -C "$REPO" push -q origin trunk 2>/dev/null
|
|
33
|
+
git -C "$REPO" fetch -q origin
|
|
34
|
+
# PR worktree content: attacker redefines the deploy gate.
|
|
35
|
+
printf 'branches:\n default: release\n' > "$REPO/forge.yaml"
|
|
36
|
+
|
|
37
|
+
run() { # base default_stub [env...]
|
|
38
|
+
local base="$1" def="$2"; shift 2
|
|
39
|
+
(cd "$REPO" && env PATH="$TMP/bin:$PATH" GH_DEFAULT_STUB="$def" "$@" bash "$S" "$base" owner/repo)
|
|
40
|
+
}
|
|
41
|
+
expect() { [ "$1" = "$2" ] && ok || bad "$3: got '$1' want '$2'"; }
|
|
42
|
+
|
|
43
|
+
expect "$(run main trunk)" true "main with worktree release"
|
|
44
|
+
expect "$(run release trunk)" false "worktree-declared branch is not a gate"
|
|
45
|
+
expect "$(run staging trunk)" false "staging is fast lane"
|
|
46
|
+
expect "$(run trunk trunk)" true "github default"
|
|
47
|
+
expect "$(run master master)" true "github default master"
|
|
48
|
+
expect "$(run develop trunk)" true "committed origin config default"
|
|
49
|
+
expect "$(run '' trunk)" true "empty base"
|
|
50
|
+
expect "$(run null trunk)" true "null base"
|
|
51
|
+
expect "$(run staging '')" true "empty gh default fails closed"
|
|
52
|
+
expect "$(run staging trunk GH_FAIL=1)" true "gh failure fails closed"
|
|
53
|
+
expect "$(run main '' GH_FAIL=1)" true "main on gh failure"
|
|
54
|
+
printf 'branches:\n default: prod\n' > "$TMP/trusted.yaml"
|
|
55
|
+
expect "$(run prod trunk FORGE_TRUSTED_CONFIG="$TMP/trusted.yaml")" true "trusted config default"
|
|
56
|
+
expect "$(run prod trunk)" false "prod without trusted config"
|
|
57
|
+
|
|
58
|
+
echo "resolve-deploy-gate.test.sh: $PASS passed, $FAILN failed"
|
|
59
|
+
[ "$FAILN" -eq 0 ]
|
|
@@ -120,8 +120,16 @@ expect "round 2 narrows to security by default" "security" "$(roster 2 'security
|
|
|
120
120
|
expect "round 2 adds a path-triggered domain" "security auth" "$(roster 2 'security auth billing' $'README.md\nsrc/auth/login.py')"
|
|
121
121
|
expect "round 3 cap holds" "security" "$(roster 3 'security auth billing database' 'docs/notes.txt')"
|
|
122
122
|
expect "claim/lease paths trigger concurrency" "security concurrency" "$(roster 2 'security' 'engine/claim-lease.txt')"
|
|
123
|
+
export REPO_PATH="$ROOT" # ForgeDock itself: the persona file exists
|
|
123
124
|
expect "commands/** triggers spec-cli" "security spec-cli" "$(roster 2 'security' 'commands/foo.md')"
|
|
124
125
|
expect "bin/** triggers spec-cli" "security spec-cli" "$(roster 2 'security' 'bin/foo.mjs')"
|
|
126
|
+
# consumer repo layout: no persona file, so no spec-cli
|
|
127
|
+
mkdir -p "$TMP/consumer/bin" "$TMP/consumer/commands"
|
|
128
|
+
export REPO_PATH="$TMP/consumer"
|
|
129
|
+
expect "consumer bin/ gets no spec-cli" "security" "$(roster 2 'security' 'bin/foo.mjs')"
|
|
130
|
+
expect "consumer commands/ gets no spec-cli" "security" "$(roster 2 'security' 'commands/foo.md')"
|
|
131
|
+
expect "consumer with another trigger keeps it, no spec-cli" "security auth" "$(roster 2 'security' $'bin/foo.mjs\nsrc/auth/login.py')"
|
|
132
|
+
unset REPO_PATH
|
|
125
133
|
expect "non-numeric rounds fail to the 3B roster" "security auth" "$(roster 'x' 'security auth' 'README.md')"
|
|
126
134
|
|
|
127
135
|
# ---- 3. mode resolution through the real fence ----------------------------------------------------------------------
|
|
@@ -83,5 +83,15 @@ done
|
|
|
83
83
|
# Test 6: the protocol defines the marker domain through the mapping
|
|
84
84
|
if grep -qF 'marker domain' "$PERSONAS/protocols.md"; then ok; else bad "protocols.md does not define the marker domain"; fi
|
|
85
85
|
|
|
86
|
+
# Test 7: the roster regexes in delta_roster must equal the DOMAIN_FILES_* regexes, and every domain needs an empty-slice fallback
|
|
87
|
+
for pair in AUTH:auth BILLING:billing CONCURRENCY:concurrency DATABASE:database INFRA:infra SCRAPING:scraper FRONTEND:frontend API:api; do
|
|
88
|
+
var=${pair%%:*}; dom=${pair#*:}
|
|
89
|
+
a=$(grep -E "^DOMAIN_FILES_$var=" "$SPEC" | head -n 1 | sed -n 's/.*grep -iE "\([^"]*\)".*/\1/p')
|
|
90
|
+
b=$(grep -F "&& out=\"\$out $dom\"" "$SPEC" | grep 'grep -qiE' | head -n 1 | sed -n 's/.*grep -qiE "\([^"]*\)".*/\1/p')
|
|
91
|
+
if [ -z "$a" ] || [ -z "$b" ]; then bad "could not extract $var pattern (DOMAIN_FILES='$a' delta_roster='$b')"
|
|
92
|
+
elif [ "$a" = "$b" ]; then ok; else bad "$var regex drifted: DOMAIN_FILES='$a' delta_roster='$b'"; fi
|
|
93
|
+
if grep -qF "[ -z \"\$DOMAIN_FILES_$var\" ] && DOMAIN_FILES_$var=\"\$FILES\"" "$SPEC"; then ok; else bad "no empty-slice fallback for DOMAIN_FILES_$var"; fi
|
|
94
|
+
done
|
|
95
|
+
|
|
86
96
|
echo "review-pr-domain-map: $pass passed, $fail failed"
|
|
87
97
|
[ "$fail" -eq 0 ]
|
|
@@ -99,6 +99,20 @@ done
|
|
|
99
99
|
awk '/^[^ `#].* # FENCE_BOOTSTRAP/ && !seen[$0]++' "$SPEC" > "$TMP/boot.sh"
|
|
100
100
|
[ "$(wc -l < "$TMP/boot.sh")" -eq 8 ] && ok || bad "FENCE_BOOTSTRAP block expected 8 distinct lines (got $(wc -l < "$TMP/boot.sh"))"
|
|
101
101
|
[ "$(grep -c '^# Fence bootstrap' "$SPEC")" -ge 3 ] && ok || bad "bootstrap must be carried by 0B.1 and both 7B.5 fences"
|
|
102
|
+
# forge#3681: EVERY bash fence after Phase 0B that reads cross-fence state must carry the bootstrap (a fresh Bash call loses it)
|
|
103
|
+
UNCOVERED=$(awk '
|
|
104
|
+
/^```bash/ { infence=1; body=""; start=NR; next }
|
|
105
|
+
/^```/ && infence { infence=0
|
|
106
|
+
if (start > 400 && body ~ /PR_NUMBER|ALL_PR_NUMBERS|GH_REPO|DEFAULT_BRANCH|STAGING_BRANCH/ && body !~ /# FENCE_BOOTSTRAP/) printf "%d ", start
|
|
107
|
+
next }
|
|
108
|
+
infence { body = body "\n" $0 }' "$SPEC")
|
|
109
|
+
[ -z "$UNCOVERED" ] && ok || bad "bash fences starting at line(s) ${UNCOVERED}consume cross-fence state without the FENCE_BOOTSTRAP block"
|
|
110
|
+
# the persisted Phase 6.5 / 6.75 values: producers write them, consumers reload them validated
|
|
111
|
+
grep -q 'test_gate_verdict' "$SPEC" && grep -q 'launched_review_agents' "$SPEC" && ok || bad "TEST_GATE_VERDICT and LAUNCHED_REVIEW_AGENTS must be persisted to the scratchpad"
|
|
112
|
+
# PR-keyed (concurrent runs in one cwd must not share state) and reset by an executable command, not prose
|
|
113
|
+
if grep -qE '\$_SD/(test_gate_verdict|test_gate_reason|launched_review_agents)|\.forge-scratch\}/launched_review_agents' "$SPEC"; then bad "gate/launched-agent scratch files must be keyed by \${PR_NUMBER}_"; else ok; fi
|
|
114
|
+
grep -qF ': > "$_SD/${PR_NUMBER}_launched_review_agents"' "$SPEC" && grep -qF 'rm -f "$_SD/${PR_NUMBER}_test_gate_verdict" "$_SD/${PR_NUMBER}_test_gate_reason"' "$SPEC" && ok || bad "Phase 2 must truncate and Phase 6.5 must rm -f the PR-keyed scratch files in executable commands"
|
|
115
|
+
if awk '/^### 7E:/,/^### 7F:/' "$SPEC" | grep -qF '{GH_REPO}'; then bad "7E must not use the {GH_REPO} template token"; else ok; fi
|
|
102
116
|
mkdir -p "$TMP/sd8/full" "$TMP/sd8/empty" "$TMP/sd8/bad" "$TMP/bin"
|
|
103
117
|
printf '#!/bin/sh\necho stub-value\n' > "$TMP/bin/yq"; chmod +x "$TMP/bin/yq"
|
|
104
118
|
printf '42\n' > "$TMP/sd8/full/staging_pr_number"; printf '7\n8\n' > "$TMP/sd8/full/42_bundle-prs.txt"
|
|
@@ -155,5 +169,90 @@ run_dup() { # finding-file finding-line -> "DISPOSITION|FILED|REFLAG|LISTED"
|
|
|
155
169
|
[ "$(run_dup src/other.py:12)" = "ISSUE MEDIUM-CONFIRMED|1|0|0" ] && ok || bad "a non-duplicate on another file must classify normally (got '$(run_dup src/other.py:12)')"
|
|
156
170
|
run_dup src/api.py:12 >/dev/null
|
|
157
171
|
grep -q 'reflag-filed #123' "$TMP/dup-notes.md" && ok || bad "a re-flag NOTE must be listed, not dropped"
|
|
172
|
+
# (9) stale-head delta credit (forge#3638): the spec's bundle_delta_credit runs against real git fixtures and the REAL review-delta.sh / trusted-comments.sh.
|
|
173
|
+
# Fail-closed cases first: every FULL / error / untrusted path must leave the PR's commits UNREVIEWED (empty credit).
|
|
174
|
+
if command -v git >/dev/null 2>&1; then
|
|
175
|
+
awk '/^# BEGIN bundle-delta-credit$/{f=1;next} /^# END bundle-delta-credit$/{f=0} f' "$SPEC" > "$TMP/delta-fn.sh"
|
|
176
|
+
[ -s "$TMP/delta-fn.sh" ] && ok || bad "bundle-delta-credit block not found in the spec"
|
|
177
|
+
RDS="$ROOT/scripts/review-delta.sh"
|
|
178
|
+
export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_NOSYSTEM=1
|
|
179
|
+
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@example.invalid GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@example.invalid
|
|
180
|
+
g9() { git -C "$W9" -c commit.gpgsign=false -c core.hooksPath=/dev/null "$@"; }
|
|
181
|
+
commit9() { mkdir -p "$W9/$(dirname "$1")"; printf '%s\n' "$2" > "$W9/$1"; g9 add -A >/dev/null; g9 commit -q -m "$3" >/dev/null; }
|
|
182
|
+
C9=0
|
|
183
|
+
mkrepo9() { # W9 = clone with origin/main, branch feat with one PR commit
|
|
184
|
+
C9=$((C9+1)); local o="$TMP/do$C9"; W9="$TMP/dw$C9"
|
|
185
|
+
git init -q --bare -b main "$o"; git clone -q "$o" "$W9" 2>/dev/null
|
|
186
|
+
g9 checkout -q -b main 2>/dev/null; commit9 a.txt base base; g9 push -q origin main; g9 checkout -q feat 2>/dev/null || g9 checkout -q -b feat; commit9 pr.txt pr1 pr1
|
|
187
|
+
}
|
|
188
|
+
agent9() { printf '<!-- FORGE:REVIEW-AGENT:%s -->\nReviewed-SHA: %s\n\n<!-- REVIEW-FINDINGS-START -->\nNo findings.\n<!-- REVIEW-FINDINGS-END -->' "$1" "$2"; }
|
|
189
|
+
synth9() { printf '<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->\nReviewed-SHA: %s\n\nsynth' "$1"; }
|
|
190
|
+
carried9() { printf '<!-- FORGE:REVIEW-CARRIED -->\nReviewed-SHA: %s\nCarried-From-SHA: %s\n' "$1" "$2"; }
|
|
191
|
+
cm9() { jq -n --arg b "$1" --arg t "${2:-Bot}" '{"body":$b,"author_association":"NONE","user":{"type":$t,"login":"x"}}'; }
|
|
192
|
+
panel9() { CM9=$(printf '%s\n' "$(cm9 "$(agent9 security "$1")")" "$(cm9 "$(agent9 api "$1")")" "$(cm9 "$(synth9 "$1")")" | jq -s '.'); }
|
|
193
|
+
credit9() { # args: head, labels-csv (default none); needs W9 CM9; prints "<DELTA_FULL>|<credited oids, short>"
|
|
194
|
+
local head="$1" labels="${2:-}" J
|
|
195
|
+
J=$(git -C "$W9" log --format=%H origin/main..HEAD | jq -R . | jq -s --arg h "$head" --arg l "$labels" '{headRefOid:$h,baseRefName:"main",mergeCommit:null,labels:($l|split(",")|map(select(.!="")|{name:.})),commits:(map({oid:.})|reverse)}')
|
|
196
|
+
( cd "$W9"; pr=7; PR_NUMBER=9; GH_REPO=o/r; _SD="$TMP"; _HEAD="$head"; _J="$J"; _CM="$CM9"; TRUSTED_SCRIPT="${T9_TRUSTED:-$TC}"; REVIEW_DELTA_SCRIPT="${T9_RD-$RDS}"
|
|
197
|
+
. "$TMP/delta-fn.sh"; bundle_delta_credit; echo "$DELTA_FULL|$(for o in $DELTA_COMMITS; do printf '%s ' "${o:0:7}"; done)" )
|
|
198
|
+
}
|
|
199
|
+
short9() { git -C "$W9" rev-parse --short=7 "$1"; }
|
|
200
|
+
# stale-SHA PR, but no trusted panel: stays UNREVIEWED
|
|
201
|
+
mkrepo9; R=$(git -C "$W9" rev-parse HEAD); commit9 pr.txt pr2 pr2; H=$(git -C "$W9" rev-parse HEAD); CM9='[]'
|
|
202
|
+
[ "$(credit9 "$H")" = "0|" ] && ok || bad "stale head with no trusted panel must credit nothing (got '$(credit9 "$H")')"
|
|
203
|
+
# untrusted reviewer comments (User type): stays UNREVIEWED
|
|
204
|
+
CM9=$(printf '%s\n' "$(cm9 "$(agent9 security "$R")" User)" "$(cm9 "$(agent9 api "$R")" User)" "$(cm9 "$(synth9 "$R")" User)" | jq -s '.')
|
|
205
|
+
[ "$(credit9 "$H")" = "0|" ] && ok || bad "untrusted panel comments must credit nothing (got '$(credit9 "$H")')"
|
|
206
|
+
# resolver script missing / garbage / error: UNREVIEWED
|
|
207
|
+
panel9 "$R"
|
|
208
|
+
[ "$(T9_RD="" credit9 "$H")" = "0|" ] && ok || bad "unresolved review-delta.sh must credit nothing"
|
|
209
|
+
printf '#!/bin/sh\necho "DELTA garbage"\necho FULL_ROUNDS=1\n' > "$TMP/rd-garbage.sh"
|
|
210
|
+
[ "$(T9_RD="$TMP/rd-garbage.sh" credit9 "$H")" = "0|" ] && ok || bad "malformed resolver output must credit nothing"
|
|
211
|
+
printf '#!/bin/sh\nexit 3\n' > "$TMP/rd-error.sh"
|
|
212
|
+
[ "$(T9_RD="$TMP/rd-error.sh" credit9 "$H")" = "0|" ] && ok || bad "a failing resolver must credit nothing"
|
|
213
|
+
printf '#!/bin/sh\necho FULL\necho FULL_ROUNDS=0\n' > "$TMP/rd-full.sh"
|
|
214
|
+
[ "$(T9_RD="$TMP/rd-full.sh" credit9 "$H")" = "0|" ] && ok || bad "resolver FULL must credit nothing"
|
|
215
|
+
printf '#!/bin/sh\necho "DELTA %s..%s"\necho FULL_ROUNDS=1\n' "$H" "$H" > "$TMP/rd-same.sh"
|
|
216
|
+
[ "$(T9_RD="$TMP/rd-same.sh" credit9 "$H")" = "0|" ] && ok || bad "a resolver DELTA whose from equals head must credit nothing"
|
|
217
|
+
printf '#!/bin/sh\necho "DELTA %s..%s"\necho FULL_ROUNDS=1\n' "$R" "$R" > "$TMP/rd-wrongto.sh"
|
|
218
|
+
[ "$(T9_RD="$TMP/rd-wrongto.sh" credit9 "$H")" = "0|" ] && ok || bad "a resolver DELTA whose to is not the PR head must credit nothing"
|
|
219
|
+
# degraded label: the resolver forces FULL
|
|
220
|
+
[ "$(credit9 "$H" review-degraded)" = "0|" ] && ok || bad "a review-degraded PR must credit nothing"
|
|
221
|
+
# force-pushed PR (reviewed sha no longer an ancestor): all UNREVIEWED
|
|
222
|
+
mkrepo9; R=$(git -C "$W9" rev-parse HEAD); panel9 "$R"; g9 reset -q --hard HEAD~1; commit9 pr.txt rewritten rewritten; H=$(git -C "$W9" rev-parse HEAD)
|
|
223
|
+
[ "$(credit9 "$H")" = "0|" ] && ok || bad "force-pushed PR must keep every commit UNREVIEWED (got '$(credit9 "$H")')"
|
|
224
|
+
# resolver says DELTA from a sha that is not an ancestor of the PR commits (stub): per-commit ancestor test credits nothing
|
|
225
|
+
mkrepo9; R=$(git -C "$W9" rev-parse HEAD); commit9 pr.txt pr2 pr2; H=$(git -C "$W9" rev-parse HEAD); panel9 "$R"
|
|
226
|
+
g9 checkout -q -b other main; commit9 o.txt o other; O=$(git -C "$W9" rev-parse HEAD); g9 checkout -q feat
|
|
227
|
+
printf '#!/bin/sh\necho "DELTA %s..%s"\necho FULL_ROUNDS=1\n' "$O" "$H" > "$TMP/rd-unrelated.sh"
|
|
228
|
+
[ "$(T9_RD="$TMP/rd-unrelated.sh" credit9 "$H")" = "0|" ] && ok || bad "a DELTA from an unrelated sha must credit no PR commit"
|
|
229
|
+
# carried base-sync: untrusted / absent record stays UNREVIEWED
|
|
230
|
+
mkrepo9; R=$(git -C "$W9" rev-parse HEAD); panel9 "$R"; g9 checkout -q main; commit9 b.txt b1 b1; g9 push -q origin main; g9 checkout -q feat; g9 fetch -q origin
|
|
231
|
+
g9 merge -q --no-ff -m "merge main" origin/main >/dev/null 2>&1; H=$(git -C "$W9" rev-parse HEAD)
|
|
232
|
+
PANEL9=$CM9
|
|
233
|
+
[ "$(credit9 "$H")" = "0|" ] && ok || bad "BASE_SYNC_ONLY with no carried record must credit nothing (got '$(credit9 "$H")')"
|
|
234
|
+
CM9=$(printf '%s\n%s\n' "$(printf '%s' "$PANEL9" | jq -c '.[]')" "$(cm9 "$(carried9 "$H" "$R")" User)" | jq -s '.')
|
|
235
|
+
[ "$(credit9 "$H")" = "0|" ] && ok || bad "an untrusted REVIEW-CARRIED record must credit nothing (got '$(credit9 "$H")')"
|
|
236
|
+
CM9=$(printf '%s\n%s\n' "$(printf '%s' "$PANEL9" | jq -c '.[]')" "$(cm9 "$(carried9 "$H" "$(printf 'c%.0s' $(seq 1 40))")")" | jq -s '.')
|
|
237
|
+
[ "$(credit9 "$H")" = "0|" ] && ok || bad "a carried record naming another reviewed sha must credit nothing"
|
|
238
|
+
# --- happy paths
|
|
239
|
+
CM9=$(printf '%s\n%s\n' "$(printf '%s' "$PANEL9" | jq -c '.[]')" "$(cm9 "$(carried9 "$H" "$R")")" | jq -s '.')
|
|
240
|
+
[ "$(credit9 "$H")" = "1|" ] && ok || bad "carried base-sync with a trusted record must credit the whole PR (got '$(credit9 "$H")')"
|
|
241
|
+
# stale-SHA PR: older trusted complete panel at R, one real new commit -> only the old commit is credited
|
|
242
|
+
mkrepo9; R=$(git -C "$W9" rev-parse HEAD); R7=$(short9 "$R"); commit9 pr.txt pr2 pr2; H=$(git -C "$W9" rev-parse HEAD); H7=$(short9 "$H"); panel9 "$R"
|
|
243
|
+
OUT9=$(credit9 "$H")
|
|
244
|
+
[ "$OUT9" = "0|$R7 " ] && ok || bad "stale-SHA PR must credit only commits up to the reviewed sha, not the new one (got '$OUT9', new commit $H7)"
|
|
245
|
+
# partially reviewed PR: two commits reviewed, two new -> only the reviewed two credited
|
|
246
|
+
mkrepo9; commit9 pr.txt pr1b pr1b; R=$(git -C "$W9" rev-parse HEAD); panel9 "$R"; commit9 pr.txt pr3 pr3; commit9 pr.txt pr4 pr4; H=$(git -C "$W9" rev-parse HEAD)
|
|
247
|
+
OUT9=$(credit9 "$H"); N9=$(printf '%s' "${OUT9#*|}" | wc -w)
|
|
248
|
+
[ "${OUT9%%|*}" = 0 ] && [ "$N9" -eq 2 ] && case "$OUT9" in *"$(short9 HEAD)"*|*"$(short9 HEAD~1)"*) false ;; *) true ;; esac && ok || bad "partial review must credit exactly the 2 reviewed commits (got '$OUT9')"
|
|
249
|
+
# spec text guards: the resolver is reused, never copied; carried record accepted; the unchanged cross-PR and merge-resolution paths are still present
|
|
250
|
+
grep -q 'review-delta.sh' "$SPEC" && grep -q 'REVIEW-CARRIED' "$SPEC" && ok || bad "spec must call review-delta.sh and accept the REVIEW-CARRIED record"
|
|
251
|
+
if grep -qE 'merge-base --is-ancestor.*REVIEWED_SHA_STALE|diff_shape\(\)' "$SPEC"; then bad "spec must not carry a second copy of the resolver logic"; else ok; fi
|
|
252
|
+
grep -qF 'printf '"'"'%s\n'"'"' "$_FILES" | sort -u | sed "s|^|$pr |" >> "$_PF"' "$SPEC" && grep -qF 'git diff-tree --no-commit-id --name-only -r --cc "$c"' "$SPEC" && ok || bad "CROSS_PR_FILES accumulation and --cc merge scoping must stay intact"
|
|
253
|
+
[ "$(grep -c '# FENCE_BOOTSTRAP' "$SPEC" | head -1)" -ge 8 ] && ok || bad "FENCE_BOOTSTRAP lines must remain"
|
|
254
|
+
# a stale-head PR must not credit its own merge commit (it contains the post-review commits): the DELTA branch only appends DELTA_COMMITS
|
|
255
|
+
if awk '/^bundle_delta_credit\(\)/,/^\}/' "$SPEC" | grep -q 'mergeCommit'; then bad "the DELTA path must not credit the PR merge commit"; else ok; fi
|
|
256
|
+
else echo "review-pr-staging-scope.test.sh: git missing, delta section skipped"; fi
|
|
158
257
|
echo "review-pr-staging-scope.test.sh: passed=$PASS failed=$FAILN"
|
|
159
258
|
[ "$FAILN" -eq 0 ]
|