forgedock 1.30.0 → 1.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engine/outcome-codes.mjs +3 -2
- package/bin/engine/runlog.mjs +82 -12
- package/bin/engine/telemetry-seam.mjs +11 -4
- package/bin/engine/telemetry-seam.test.mjs +18 -0
- package/bin/engine-cli.mjs +409 -38
- package/bin/engine.mjs +47 -37
- package/bin/forgedock.mjs +35 -3
- package/bin/gc.mjs +50 -17
- package/bin/hooks/reread-nudge.mjs +65 -0
- package/bin/labels.json +5 -0
- package/bin/lean/apishape.mjs +170 -0
- package/bin/lean/apishape.test.mjs +187 -0
- package/bin/lean/batch.mjs +32 -0
- package/bin/lean/batch.test.mjs +338 -2
- package/bin/lean/budget.mjs +89 -0
- package/bin/lean/budget.test.mjs +106 -0
- package/bin/lean/checks.mjs +32 -9
- package/bin/lean/checks.test.mjs +78 -1
- package/bin/lean/conformance.e2e.test.mjs +27 -5
- package/bin/lean/conformance.mjs +150 -15
- package/bin/lean/conformance.test.mjs +175 -3
- package/bin/lean/context.mjs +128 -44
- package/bin/lean/context.test.mjs +157 -5
- package/bin/lean/decompose.mjs +90 -0
- package/bin/lean/decompose.test.mjs +105 -0
- package/bin/lean/depcache.mjs +11 -7
- package/bin/lean/depcache.test.mjs +81 -0
- package/bin/lean/e2e.mjs +102 -0
- package/bin/lean/e2e.test.mjs +86 -0
- package/bin/lean/effects.mjs +48 -15
- package/bin/lean/explore.mjs +47 -0
- package/bin/lean/explore.test.mjs +61 -0
- package/bin/lean/facts.mjs +84 -0
- package/bin/lean/facts.test.mjs +66 -0
- package/bin/lean/fixround.mjs +90 -0
- package/bin/lean/fixround.test.mjs +117 -0
- package/bin/lean/heavy.mjs +35 -2
- package/bin/lean/heavy.test.mjs +32 -2
- package/bin/lean/lean.mjs +712 -134
- package/bin/lean/lean.test.mjs +1626 -37
- package/bin/lean/ledger.mjs +5 -3
- package/bin/lean/ledger.test.mjs +18 -0
- package/bin/lean/light.mjs +135 -0
- package/bin/lean/light.test.mjs +173 -0
- package/bin/lean/lightlane.mjs +219 -0
- package/bin/lean/lightlane.test.mjs +237 -0
- package/bin/lean/limits.test.mjs +36 -1
- package/bin/lean/orchestrate.mjs +303 -41
- package/bin/lean/pitfalls.mjs +136 -0
- package/bin/lean/pitfalls.test.mjs +117 -0
- package/bin/lean/postmerge.mjs +85 -0
- package/bin/lean/postmerge.test.mjs +79 -0
- package/bin/lean/precheck.mjs +138 -13
- package/bin/lean/precheck.test.mjs +118 -1
- package/bin/lean/prefilter.mjs +220 -5
- package/bin/lean/prefilter.test.mjs +260 -4
- package/bin/lean/preflight.mjs +108 -3
- package/bin/lean/preflight.test.mjs +48 -1
- package/bin/lean/probe.mjs +297 -0
- package/bin/lean/probe.test.mjs +210 -0
- package/bin/lean/prompts.mjs +104 -13
- package/bin/lean/render.mjs +7 -3
- package/bin/lean/resume.mjs +67 -0
- package/bin/lean/resume.test.mjs +84 -0
- package/bin/lean/review-trigger.mjs +12 -0
- package/bin/lean/review-trigger.test.mjs +17 -1
- package/bin/lean/routing.mjs +74 -0
- package/bin/lean/routing.test.mjs +67 -0
- package/bin/lean/sched.mjs +16 -1
- package/bin/lean/sched.test.mjs +175 -1
- package/bin/lean/schedd.mjs +55 -9
- package/bin/lean/schedd.test.mjs +170 -3
- package/bin/lean/selfreview.mjs +37 -0
- package/bin/lean/selfreview.test.mjs +51 -0
- package/bin/lean/seqlease.mjs +40 -0
- package/bin/lean/seqlease.test.mjs +101 -0
- package/bin/lean/sequence.mjs +296 -13
- package/bin/lean/sequence.test.mjs +263 -2
- package/bin/lean/toolchain.mjs +60 -15
- package/bin/lean/toolchain.test.mjs +120 -3
- package/bin/lean/toolstats.mjs +72 -0
- package/bin/lean/toolstats.test.mjs +47 -0
- package/bin/lean/verifier.mjs +232 -0
- package/bin/lean/verifier.test.mjs +163 -0
- package/bin/lean/verify.mjs +72 -10
- package/bin/lean/verify.test.mjs +68 -3
- package/bin/review-staging.mjs +533 -0
- package/bin/runner.mjs +82 -13
- package/bin/stats/aggregate.mjs +87 -2
- package/bin/stats/efficiency.mjs +335 -0
- package/bin/stats/efficiency.test.mjs +266 -0
- package/bin/stats/github-source.mjs +8 -3
- package/bin/stats/index.mjs +91 -6
- package/bin/stats/local-source.mjs +145 -8
- package/bin/stats/query.mjs +181 -3
- package/bin/stats/render.mjs +22 -0
- package/bin/stats/stats.test.mjs +403 -8
- package/commands/orchestrate.md +3 -1
- package/commands/review-pr-agents/protocols.md +1 -1
- package/commands/review-pr-staging.md +84 -831
- package/commands/review-pr.md +1 -1
- package/commands/work-on/build/implement.md +1 -0
- package/commands/work-on/build/validate.md +11 -1
- package/docs/CONFIG.md +80 -5
- package/docs/OUTCOME-CODES.md +2 -1
- package/docs/STATS.md +66 -1
- package/docs/TELEMETRY.md +1 -1
- package/docs/eval/corpus/lightlane-v1.json +55 -0
- package/docs/eval/lightlane-bench.md +91 -0
- package/docs/spec/review-protocol.md +1 -1
- package/package.json +1 -1
- package/scripts/bench-lightlane.mjs +286 -0
- package/scripts/ci-local/lib.mjs +20 -0
- package/scripts/ci-local/plan.mjs +2 -12
- package/scripts/ci-local/scoping.mjs +10 -1
- package/scripts/ci-local.test.sh +2 -0
- package/scripts/fixtures/apishape/other/go-change/base/x.go +3 -0
- package/scripts/fixtures/apishape/other/go-change/head/x.go +3 -0
- package/scripts/fixtures/apishape/python/generator-function/base/seq.py +3 -0
- package/scripts/fixtures/apishape/python/generator-function/head/seq.py +2 -0
- package/scripts/fixtures/apishape/python/ichunked/base/more.py +17 -0
- package/scripts/fixtures/apishape/python/ichunked/head/more.py +20 -0
- package/scripts/fixtures/apishape/python/private-edit/base/calc.py +6 -0
- package/scripts/fixtures/apishape/python/private-edit/head/calc.py +9 -0
- package/scripts/fixtures/apishape/python/removed/base/ops.py +6 -0
- package/scripts/fixtures/apishape/python/removed/head/ops.py +2 -0
- package/scripts/fixtures/apishape/python/renamed/base/text.py +2 -0
- package/scripts/fixtures/apishape/python/renamed/head/text.py +2 -0
- package/scripts/fixtures/apishape/python/signature/base/shapes.py +2 -0
- package/scripts/fixtures/apishape/python/signature/head/shapes.py +2 -0
- package/scripts/fixtures/apishape/python/sync-async/base/client.py +7 -0
- package/scripts/fixtures/apishape/python/sync-async/head/client.py +7 -0
- package/scripts/fixtures/apishape/ts/generator-function/base/seq.js +3 -0
- package/scripts/fixtures/apishape/ts/generator-function/head/seq.js +5 -0
- package/scripts/fixtures/apishape/ts/private-edit/base/calc.ts +7 -0
- package/scripts/fixtures/apishape/ts/private-edit/head/calc.ts +9 -0
- package/scripts/fixtures/apishape/ts/removed/base/ops.ts +7 -0
- package/scripts/fixtures/apishape/ts/removed/head/ops.ts +3 -0
- package/scripts/fixtures/apishape/ts/renamed/base/text.ts +3 -0
- package/scripts/fixtures/apishape/ts/renamed/head/text.ts +3 -0
- package/scripts/fixtures/apishape/ts/signature/base/shapes.ts +3 -0
- package/scripts/fixtures/apishape/ts/signature/head/shapes.ts +3 -0
- package/scripts/fixtures/apishape/ts/sync-async/base/client.mjs +5 -0
- package/scripts/fixtures/apishape/ts/sync-async/head/client.mjs +5 -0
- package/scripts/forge-root.test.sh +2 -2
- package/scripts/publish-staging-sync.test.sh +62 -4
- package/scripts/review-finalize.sh +17 -6
- package/scripts/review-finalize.test.sh +47 -0
- package/scripts/review-pr-arguments.test.sh +13 -1
- package/scripts/review-pr-delta-wiring.test.sh +2 -2
- package/scripts/review-pr-gate2.test.sh +6 -14
- package/scripts/review-pr-staging-finding-filter.test.sh +4 -4
- package/scripts/review-pr-staging-scope.test.sh +39 -33
- package/scripts/review-prepare.sh +73 -6
- package/scripts/review-prepare.test.sh +85 -0
- package/scripts/review-staging-post.sh +293 -0
- package/scripts/review-staging-post.test.sh +143 -0
- package/scripts/review-staging-pre.sh +401 -0
- package/scripts/review-staging-pre.test.sh +150 -0
- package/scripts/symbol-index.mjs +26 -7
- package/scripts/symbol-index.py +55 -9
- package/scripts/trusted-comments.test.sh +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# publish-staging-sync.test.sh — guards the publish.yml "Sync version to staging" step (forge#3695)
|
|
3
|
-
# and the deploy-pr Phase 2A.5 version-sync pre-flight. Static assertions, no network.
|
|
2
|
+
# publish-staging-sync.test.sh — guards the publish.yml "Sync version to staging" step (forge#3695, forge#4357)
|
|
3
|
+
# and the deploy-pr Phase 2A.5 version-sync pre-flight. Static assertions plus a local-only run of the step (bare origin, stub gh); no network.
|
|
4
4
|
# Usage: bash scripts/publish-staging-sync.test.sh
|
|
5
5
|
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
6
6
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
@@ -29,14 +29,72 @@ else bad "sync step sits between Push version bump and Create GitHub Release"; f
|
|
|
29
29
|
|
|
30
30
|
stepHas() { if printf '%s\n' "$STEP" | grep -qF -e "$2"; then ok "$1"; else bad "$1 (missing '$2')"; fi; }
|
|
31
31
|
stepHas "step fetches origin/staging" "origin/staging"
|
|
32
|
-
stepHas "step pushes HEAD to staging" "git push origin HEAD:staging"
|
|
33
32
|
stepHas "step is non-fatal" "continue-on-error: true"
|
|
34
33
|
stepHas "step is guarded by SKIP_PUBLISH" "env.SKIP_PUBLISH != 'true'"
|
|
35
|
-
|
|
34
|
+
# forge#4357: staging is protected (GH006), so the sync goes through a PR, never a direct push.
|
|
35
|
+
stepHas "step pushes to a chore/sync-version-X.Y.Z branch" 'SYNC_BRANCH="chore/sync-version-${VERSION}"'
|
|
36
|
+
stepHas "step force-pushes the sync branch" 'git push -f origin "HEAD:refs/heads/$SYNC_BRANCH"'
|
|
37
|
+
stepHas "step looks up an existing sync PR" 'gh pr list --head "$SYNC_BRANCH" --base staging'
|
|
38
|
+
stepHas "step opens a PR to staging" 'gh pr create --base staging --head "$SYNC_BRANCH"'
|
|
39
|
+
stepHas "step authenticates gh" 'GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}'
|
|
40
|
+
stepHas "step skips when staging is at or above the version" "nothing to sync"
|
|
41
|
+
if printf '%s\n' "$STEP" | grep -E 'git push' | grep -qE 'HEAD:staging|refs/heads/staging|origin staging'; then
|
|
42
|
+
bad "step must not push directly to staging"; else ok "step does not push directly to staging"; fi
|
|
43
|
+
if printf '%s\n' "$STEP" | grep -E 'git commit' | grep -qF '[skip ci]'; then
|
|
44
|
+
bad "sync commit (PR head) must not carry [skip ci]"; else ok "sync commit does not carry [skip ci]"; fi
|
|
45
|
+
PERMS=$(awk '/^ permissions:/{f=1;next} f&&/^ [a-z]/{exit} f{print}' "$PUB")
|
|
46
|
+
if printf '%s\n' "$PERMS" | grep -qE '^ +pull-requests: write'; then ok "publish job grants pull-requests: write"; else bad "publish job grants pull-requests: write"; fi
|
|
36
47
|
stepHas "only version files are staged" "git add package.json .claude-plugin/plugin.json"
|
|
37
48
|
stepHas "failure emits a warning" "::warning::"
|
|
38
49
|
if printf '%s\n' "$STEP" | grep -qE 'git (merge|pull)'; then bad "step must not merge main (version-only commit)"; else ok "step does not merge main"; fi
|
|
39
50
|
|
|
51
|
+
# Behavioural: run the step's script against a local bare origin with a stub gh.
|
|
52
|
+
RUN=$(printf '%s\n' "$STEP" | awk '/^ run: \|/{f=1;next} f{sub(/^ /,"");print}')
|
|
53
|
+
SB=$(mktemp -d "${TMPDIR:-/tmp}/pubsync.XXXXXX")
|
|
54
|
+
trap 'rm -rf "$SB"' EXIT
|
|
55
|
+
(
|
|
56
|
+
set -e
|
|
57
|
+
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t
|
|
58
|
+
git init -q --bare "$SB/origin.git"
|
|
59
|
+
git init -q -b staging "$SB/seed"; cd "$SB/seed"
|
|
60
|
+
mkdir .claude-plugin
|
|
61
|
+
echo '{"name":"x","version":"1.9.0"}' > package.json
|
|
62
|
+
echo '{"name":"x","version":"1.9.0"}' > .claude-plugin/plugin.json
|
|
63
|
+
echo keep > other.txt
|
|
64
|
+
git add -A; git commit -qm seed; git push -q "$SB/origin.git" staging
|
|
65
|
+
git clone -q -b staging "$SB/origin.git" "$SB/work"
|
|
66
|
+
mkdir "$SB/bin"
|
|
67
|
+
printf '#!/bin/sh\necho "$*" >> "%s/gh.log"\ncase "$2" in create) echo https://example.test/pr/1;; list) [ -z "$GH_STUB_LIST" ] || echo "$GH_STUB_LIST";; esac\n' "$SB" > "$SB/bin/gh"
|
|
68
|
+
chmod +x "$SB/bin/gh"
|
|
69
|
+
) >/dev/null 2>&1 || bad "behavioural sandbox setup"
|
|
70
|
+
# The workflow's bump step sets git user.name/email; give the step that identity here
|
|
71
|
+
# (CI runners have no global git config).
|
|
72
|
+
runStep() { (cd "$SB/work" && PATH="$SB/bin:$PATH" RUNNER_TEMP="$SB/rt" VERSION="$1" \
|
|
73
|
+
GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t bash -c "$RUN") >"$SB/out" 2>&1; }
|
|
74
|
+
mkdir -p "$SB/rt"
|
|
75
|
+
STAGING_BEFORE=$(git -C "$SB/origin.git" rev-parse staging 2>/dev/null)
|
|
76
|
+
if runStep 1.10.0; then ok "step exits 0"; else bad "step exits 0"; fi
|
|
77
|
+
[ "$(git -C "$SB/origin.git" rev-parse staging 2>/dev/null)" = "$STAGING_BEFORE" ] && ok "staging is not pushed to" || bad "staging is not pushed to"
|
|
78
|
+
SYNCV=$(git -C "$SB/origin.git" show chore/sync-version-1.10.0:package.json 2>/dev/null | node -e "process.stdout.write(JSON.parse(require('fs').readFileSync(0,'utf8')).version)" 2>/dev/null)
|
|
79
|
+
[ "$SYNCV" = "1.10.0" ] && ok "sync branch carries the new version" || bad "sync branch carries the new version (got '$SYNCV')"
|
|
80
|
+
CHANGED=$(git -C "$SB/origin.git" diff --name-only staging chore/sync-version-1.10.0 2>/dev/null | sort | tr '\n' ' ')
|
|
81
|
+
[ "$CHANGED" = ".claude-plugin/plugin.json package.json " ] && ok "sync commit is version-only" || bad "sync commit is version-only (got '$CHANGED')"
|
|
82
|
+
grep -q -- 'pr create --base staging --head chore/sync-version-1.10.0' "$SB/gh.log" 2>/dev/null && ok "gh pr create targets staging" || bad "gh pr create targets staging"
|
|
83
|
+
: > "$SB/gh.log"
|
|
84
|
+
if GH_STUB_LIST=https://example.test/pr/7 runStep 1.10.0 && grep -q 'Updated existing staging sync PR https://example.test/pr/7' "$SB/out" \
|
|
85
|
+
&& ! grep -q 'pr create' "$SB/gh.log"; then
|
|
86
|
+
ok "an open sync PR is updated, not duplicated"; else bad "an open sync PR is updated, not duplicated"; fi
|
|
87
|
+
: > "$SB/gh.log"
|
|
88
|
+
if runStep 1.9.0 && grep -q 'nothing to sync' "$SB/out" && [ ! -s "$SB/gh.log" ]; then
|
|
89
|
+
ok "skipped when staging is already at the version"; else bad "skipped when staging is already at the version"; fi
|
|
90
|
+
# A failed commit (no identity) must not push an unchanged branch or open a PR.
|
|
91
|
+
: > "$SB/gh.log"
|
|
92
|
+
git -C "$SB/origin.git" branch -D chore/sync-version-1.11.0 >/dev/null 2>&1 || true
|
|
93
|
+
(cd "$SB/work" && PATH="$SB/bin:$PATH" RUNNER_TEMP="$SB/rt" VERSION=1.11.0 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 \
|
|
94
|
+
GIT_AUTHOR_NAME='' GIT_COMMITTER_NAME='' EMAIL='' bash -c "$RUN") >"$SB/out" 2>&1
|
|
95
|
+
if ! git -C "$SB/origin.git" rev-parse -q --verify chore/sync-version-1.11.0 >/dev/null && [ ! -s "$SB/gh.log" ]; then
|
|
96
|
+
ok "failed commit pushes nothing and opens no PR"; else bad "failed commit pushes nothing and opens no PR"; fi
|
|
97
|
+
|
|
40
98
|
has "deploy-pr has the 2A.5 pre-flight" "### 2A.5: Version Sync Pre-flight" "$DEP"
|
|
41
99
|
has "deploy-pr pre-flight compares package.json" "package.json" "$DEP"
|
|
42
100
|
|
|
@@ -364,12 +364,18 @@ do_extract() {
|
|
|
364
364
|
}
|
|
365
365
|
}' "$BODY_FILE" > "$SECTION_FILE" 2>/dev/null || : > "$SECTION_FILE"
|
|
366
366
|
# Failure scenario: present / absent when the section was found, unknown when the carrying comment or section is unreadable.
|
|
367
|
+
# Read from the prose (#4356): a `**Failure scenario**` anywhere on a line, or a line that starts (after list/quote/bold markup, any
|
|
368
|
+
# case) with a `failure scenario`, `scenario`, `repro`, `reproduction` or `steps to reproduce` label followed by `:` or a closing `**`.
|
|
369
|
+
# The content sits on that line or the next non-blank one; a mid-sentence use of the word is not a label.
|
|
367
370
|
SCENARIO="unknown"
|
|
368
371
|
if [ -s "$SECTION_FILE" ]; then
|
|
369
372
|
SCENARIO=$(awk '
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
+
want { t = $0; gsub(/[[:space:]]/, "", t); if (t != "") { print "present"; found = 1; exit } next }
|
|
374
|
+
{ l = tolower($0); s = "" }
|
|
375
|
+
l ~ /\*\*failure scenario\*\*/ { s = l; sub(/^.*\*\*failure scenario\*\*/, "", s) }
|
|
376
|
+
s == "" { m = l; sub(/^[[:space:]]*([-*+>_][[:space:]]*)*/, "", m)
|
|
377
|
+
if (match(m, /^(failure scenario|steps to reproduce|reproduction|repro|scenario)((\*\*|__)?[[:space:]]*:|\*\*|__)/)) s = substr(m, RLENGTH + 1); else next }
|
|
378
|
+
{ gsub(/[*_:[:space:]]/, "", s); if (s != "") { print "present"; found = 1; exit } else want = 1 }
|
|
373
379
|
END { if (!found) print "absent" }' "$SECTION_FILE")
|
|
374
380
|
fi
|
|
375
381
|
FIX_EFF=$(grep -iE '^[[:space:]]*(\*\*Fix efficacy\*\*:?|FIX_EFFICACY:)[[:space:]]*\**[[:space:]]*(fails|regresses)' "$SECTION_FILE" 2>/dev/null | head -n 1 \
|
|
@@ -531,10 +537,15 @@ SYMEOF
|
|
|
531
537
|
case "$SEV" in MEDIUM|LOW|INFO) ROUTE="pre-existing" ;; esac
|
|
532
538
|
fi
|
|
533
539
|
fi
|
|
534
|
-
# Gate 3 (concrete failure): MEDIUM or lower without a
|
|
535
|
-
# CRITICAL/HIGH are exempt
|
|
540
|
+
# Gate 3 (concrete failure): MEDIUM or lower without a failure scenario in its prose is speculative hardening, demoted to a NOTE.
|
|
541
|
+
# CRITICAL/HIGH (any confidence) and CONFIRMED MEDIUM are exempt (#4356): a confirmed finding is filed through the classifier with its
|
|
542
|
+
# agent and prose as evidence. An unreadable carrying comment keeps the finding.
|
|
536
543
|
if [ "$ROUTE" = "file" ] && [ "$SCEN" = "absent" ]; then
|
|
537
|
-
|
|
544
|
+
CONF=$(upper "$(printf '%s' "$F" | jq -r '.confidence // ""')")
|
|
545
|
+
case "$SEV" in
|
|
546
|
+
MEDIUM) [ "$CONF" = "CONFIRMED" ] || ROUTE="demoted-no-scenario" ;;
|
|
547
|
+
LOW|INFO) ROUTE="demoted-no-scenario" ;;
|
|
548
|
+
esac
|
|
538
549
|
fi
|
|
539
550
|
case "$ROUTE" in pre-existing) N_PREEXISTING=$((N_PREEXISTING + 1)) ;; demoted-no-scenario) N_NO_SCENARIO=$((N_NO_SCENARIO + 1)) ;; esac
|
|
540
551
|
N_KEPT=$((N_KEPT + 1))
|
|
@@ -286,6 +286,45 @@ want "scope: HIGH near an added line" '.findings[] | select(.id == "N-2") | .sco
|
|
|
286
286
|
want "scope: MEDIUM near an added line" '.findings[] | select(.id == "N-3") | .scope' findings.json "introduced"
|
|
287
287
|
want "scope: every finding carries a valid scope" '[.findings[].scope | select(. == "introduced" or . == "pre-existing" or . == "unknown")] | length' findings.json "$(jq '.findings | length' "$WORK/out/findings.json")"
|
|
288
288
|
want "scope: introduced count" '.counts.findings_introduced' findings.json "$(jq '[.findings[] | select(.scope == "introduced")] | length' "$WORK/out/findings.json")"
|
|
289
|
+
# Gate 3 reads the scenario from the prose and never demotes a CONFIRMED MEDIUM+ for a missing field (#4356); the damper stays for the rest
|
|
290
|
+
reset
|
|
291
|
+
for f in a b c d e f g h; do printf '{"filename":"%s.py","status":"modified","patch":"@@ -9,0 +10,1 @@\\n+new line"}\n' "$f"; done | jq -s . > "$FIX/pr-files.json"
|
|
292
|
+
cm MEMBER User "$(agent_body security "$SHA" '### C-1 confirmed
|
|
293
|
+
Real bug described in prose only.
|
|
294
|
+
### C-2 confirmed lower case
|
|
295
|
+
Real bug described in prose only.
|
|
296
|
+
### C-3 confirmed low
|
|
297
|
+
Prose only.
|
|
298
|
+
### C-4 plain label
|
|
299
|
+
Failure scenario: a missing key crashes the loader
|
|
300
|
+
### C-5 repro label
|
|
301
|
+
- **Repro**: call load() twice
|
|
302
|
+
### C-6 no scenario
|
|
303
|
+
Some prose without the field.
|
|
304
|
+
### C-7 mid-sentence word
|
|
305
|
+
no scenario written here
|
|
306
|
+
### C-8 label on next line
|
|
307
|
+
**Steps to reproduce**:
|
|
308
|
+
|
|
309
|
+
1. run it
|
|
310
|
+
' '<!-- FINDING:C-1|CONFIRMED|MEDIUM|a.py:10|Confirmed medium without a scenario field -->
|
|
311
|
+
<!-- FINDING:C-2|confirmed|medium|b.py:10|Lower case confirmed medium without a scenario field -->
|
|
312
|
+
<!-- FINDING:C-3|CONFIRMED|LOW|c.py:10|Confirmed low without a scenario field -->
|
|
313
|
+
<!-- FINDING:C-4|LIKELY|MEDIUM|d.py:10|Plain failure scenario label in prose -->
|
|
314
|
+
<!-- FINDING:C-5|POSSIBLE|MEDIUM|e.py:10|Bold repro label in a list item -->
|
|
315
|
+
<!-- FINDING:C-6|POSSIBLE|MEDIUM|f.py:10|Possible medium with no scenario anywhere -->
|
|
316
|
+
<!-- FINDING:C-7|LIKELY|MEDIUM|g.py:10|Scenario word mid sentence only -->
|
|
317
|
+
<!-- FINDING:C-8|LIKELY|MEDIUM|h.py:10|Scenario content on the next line -->')"
|
|
318
|
+
flush; run "$GOOD" extract; rc_is "gate3 prose" "$RC" 0
|
|
319
|
+
want "gate3: CONFIRMED MEDIUM without a scenario is filed" '.findings[] | select(.id == "C-1") | [.scenario, .route] | join(",")' findings.json "absent,file"
|
|
320
|
+
want "gate3: lower-case confirmed medium is filed" '.findings[] | select(.id == "C-2") | .route' findings.json "file"
|
|
321
|
+
want "gate3: CONFIRMED LOW without a scenario is still demoted" '.findings[] | select(.id == "C-3") | .route' findings.json "demoted-no-scenario"
|
|
322
|
+
want "gate3: plain Failure scenario label counts" '.findings[] | select(.id == "C-4") | [.scenario, .route] | join(",")' findings.json "present,file"
|
|
323
|
+
want "gate3: bold Repro label counts" '.findings[] | select(.id == "C-5") | [.scenario, .route] | join(",")' findings.json "present,file"
|
|
324
|
+
want "gate3: POSSIBLE MEDIUM with no scenario is demoted" '.findings[] | select(.id == "C-6") | .route' findings.json "demoted-no-scenario"
|
|
325
|
+
want "gate3: a mid-sentence scenario word is not a label" '.findings[] | select(.id == "C-7") | [.scenario, .route] | join(",")' findings.json "absent,demoted-no-scenario"
|
|
326
|
+
want "gate3: label content on the next line counts" '.findings[] | select(.id == "C-8") | .scenario' findings.json "present"
|
|
327
|
+
want "gate3: demoted count" '.counts.notes_demoted_no_scenario' findings.json "3"
|
|
289
328
|
# an unreadable files API classifies nothing as pre-existing
|
|
290
329
|
reset; echo '[]' > "$FIX/pr-files.json"
|
|
291
330
|
cm MEMBER User "$(agent_body security "$SHA" '### P-1 pre
|
|
@@ -625,6 +664,14 @@ want "lineage damps LIKELY medium" '.findings[] | select(.id == "L-1") | .class'
|
|
|
625
664
|
want "no-scenario note" '.findings[] | select(.id == "L-2") | .disposition' classified.json "NOTE no-scenario"
|
|
626
665
|
posted_has "no-scenario counter" "notes_demoted_no_scenario=1"
|
|
627
666
|
posted_has "no-scenario listed" "- L-2 a.py:9 [LIKELY] Demoted for lack of a scenario: no-scenario"
|
|
667
|
+
# a CONFIRMED MEDIUM that Gate 3 kept (scenario absent, route file) is classified, never a no-scenario NOTE (#4356); marker fields unchanged
|
|
668
|
+
reset
|
|
669
|
+
mkfind K-1 CONFIRMED MEDIUM a.py 5 "Confirmed medium kept without a scenario field"
|
|
670
|
+
FJ=$(printf '%s' "$FJ" | jq -c '.scenario = "absent"'); putfind 1
|
|
671
|
+
run "$GOOD" classify; rc_is "confirmed medium classify" "$RC" 0
|
|
672
|
+
want "confirmed medium without scenario is an issue" '.findings[] | select(.id == "K-1") | .class' classified.json "ISSUE"
|
|
673
|
+
has "confirmed medium without scenario is filed" "FINDINGS_FILED=1"
|
|
674
|
+
posted_has "confirmed medium: marker field set unchanged" "<!-- FORGE:NOTE_DISPOSITION: notes_fixed=0 notes_listed=0 notes_dropped=0 findings_filed=1 inpr_fix=0 contract_demoted=0 findings_dropped_stale=0 findings_preexisting=0 introduced=0 notes_demoted_no_scenario=0 lineage=none classifier=script notes_reflag=0 notes_capped=0 reviewed_sha=${SHA} -->"
|
|
628
675
|
# the PR-body issue in the context alone never sets lineage: only the invoker's --issue does (#3952 SEC-1/QA-2)
|
|
629
676
|
reset; echo review-finding > "$FIX/labels"; mkctx 1 FULL "" 12
|
|
630
677
|
mkfind L-1 LIKELY MEDIUM a.py 2 "Lineage damped medium finding"
|
|
@@ -151,6 +151,17 @@ touch $SENTINEL"; do
|
|
|
151
151
|
else bad "staging injection not rejected: '$got'"; fi
|
|
152
152
|
done
|
|
153
153
|
|
|
154
|
+
# Test 6b (forge#4360): `--delta <full sha>` is forwarded to the CLI; anything but a full lowercase hex SHA is dropped
|
|
155
|
+
DELTA_LINE=$(grep -m1 '^STAGING_DELTA=' "$STAGING")
|
|
156
|
+
if [ -n "$DELTA_LINE" ] && grep -qF '${STAGING_DELTA:+--delta "$STAGING_DELTA"}' "$STAGING"; then ok; else bad "staging --delta parse/forward not found"; fi
|
|
157
|
+
run_delta() { STAGING_ARGS_RAW="$1" "${BASH:-bash}" -c "$DELTA_LINE"$'\n''printf "%s" "$STAGING_DELTA"'; }
|
|
158
|
+
D40=0123456789abcdef0123456789abcdef01234567
|
|
159
|
+
got=$(run_delta "3401 --delta $D40"); if [ "$got" = "$D40" ]; then ok; else bad "staging --delta not parsed: '$got'"; fi
|
|
160
|
+
got=$(run_delta "3401 --delta $D40 --thorough"); if [ "$got" = "$D40" ]; then ok; else bad "staging --delta before a flag: '$got'"; fi
|
|
161
|
+
for v in "abc123" "${D40}0" "XYZ" ""; do
|
|
162
|
+
got=$(run_delta "3401 --delta $v"); if [ -z "$got" ]; then ok; else bad "staging --delta '$v' not dropped: '$got'"; fi
|
|
163
|
+
done
|
|
164
|
+
|
|
154
165
|
# Test 7: Phase -1 stops on an empty PR_NUMBER before any `gh pr view` (gh falls back to the current-branch PR)
|
|
155
166
|
GUARD=$(awk '/^ if \[ -z "\$PR_NUMBER" \]; then$/{p=1} p{print} p && /^ fi$/{exit}' "$SPEC")
|
|
156
167
|
if [ -n "$GUARD" ]; then ok; else bad "Phase -1 empty-PR_NUMBER guard not found"; fi
|
|
@@ -167,7 +178,8 @@ done
|
|
|
167
178
|
got=$(run_route "3401 --auto-merge --issue 3398"); if [ "$got" = REACHED ]; then ok; else bad "route stopped a valid PR number: '$got'"; fi
|
|
168
179
|
|
|
169
180
|
# Test 8: the whole staging fence stops (non-zero, no REVIEW_ROUTE post) on a rejected string
|
|
170
|
-
|
|
181
|
+
# Cut before the CLI resolution (#4359): the fence ends by running `forgedock review-staging`, which this test never runs.
|
|
182
|
+
STAGING_FENCE=$(awk '/^```bash/{buf=""; in_f=1; next} /^```$/{if (in_f && buf ~ /STAGING_ARGS_RAW/) {printf "%s", buf; exit}; in_f=0; next} in_f && /^FD_ROOT=/{if (buf ~ /STAGING_ARGS_RAW/) {printf "%s", buf; exit}} in_f{buf=buf $0 "\n"}' "$STAGING")
|
|
171
183
|
if [ -n "$STAGING_FENCE" ]; then ok; else bad "staging fence not found"; fi
|
|
172
184
|
run_stg_fence() { gen_script "$STAGING_FENCE" "$1" "$WORK/stgf.sh" 'echo REACHED'; : > "$ERR"; (cd "$WORK" && "${BASH:-bash}" "$WORK/stgf.sh" 2>"$ERR"); }
|
|
173
185
|
got=$(run_stg_fence "\$(touch $SENTINEL)"); rc=$?
|
|
@@ -235,8 +235,8 @@ case "$(p4 FULL 'security auth')" in "PANEL_STATUS: SHORTFALL"*) ok ;; *) bad "u
|
|
|
235
235
|
# a verdict without a findings block is undelivered (#3976): not complete, never counted
|
|
236
236
|
{ cm "$(printf '<!-- FORGE:REVIEW-AGENT:security -->\nReviewed-SHA: %s\n\nVerdict: APPROVE, no findings.' "$HEAD")" User MEMBER; cm "$(body_agent auth "$HEAD")" User MEMBER; } | jq -s '.' > "$FAKE_COMMENTS"
|
|
237
237
|
case "$(p4 FULL 'security auth')" in "PANEL_STATUS: SHORTFALL (ACTUAL_AGENT_COUNT=1)"*) ok ;; *) bad "a block-less reviewer comment must not satisfy Phase 4: $(p4 FULL 'security auth')" ;; esac
|
|
238
|
-
# the 3C pending-agent count and the staging 6.75 delivery count carry the same block lookahead as Phase 4
|
|
239
|
-
STAGING="$ROOT/
|
|
238
|
+
# the 3C pending-agent count and the staging 6.75 delivery count (scripts/review-staging-post.sh) carry the same block lookahead as Phase 4
|
|
239
|
+
STAGING="$ROOT/scripts/review-staging-post.sh" # owner of the staging 6.75 delivery count (forge#4359)
|
|
240
240
|
for spec in "$SPEC" "$STAGING"; do
|
|
241
241
|
n=$(grep -F 'count "^(?=[\\s\\S]*<!-- REVIEW-FINDINGS-START -->[\\s\\S]*<!-- REVIEW-FINDINGS-END -->)<!-- FORGE:REVIEW-AGENT:${AGENT_DOMAIN} -->' "$spec" | wc -l)
|
|
242
242
|
want=2; [ "$spec" = "$STAGING" ] && want=1
|
|
@@ -37,20 +37,12 @@ for p in infra security scraper; do
|
|
|
37
37
|
if [ "$reads" -ge 1 ] && [ "$reads" = "$decls" ]; then ok; else bad "$p.md: $reads fences read SLICE_FILE, $decls re-declare it"; fi
|
|
38
38
|
done
|
|
39
39
|
|
|
40
|
-
# Staging slice path:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if grep -qF '
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
got=$(REVIEW_SHA_FULL="$full"; eval "$SHORT_LINE"; printf '%s' "$REVIEW_SHA_SHORT")
|
|
47
|
-
case "$full" in
|
|
48
|
-
0123*) want="0123456" ;;
|
|
49
|
-
*) want="unresolved" ;;
|
|
50
|
-
esac
|
|
51
|
-
if [ "$got" = "$want" ]; then ok; else bad "REVIEW_SHA_SHORT for '$full' = '$got' (want '$want')"; fi
|
|
52
|
-
case "$got" in */*) bad "REVIEW_SHA_SHORT contains a slash for '$full'" ;; *) ok ;; esac
|
|
53
|
-
done
|
|
40
|
+
# Staging slice path (#4359): `forgedock review-staging` renders [REVIEW_SHA_SHORT] into the briefs. It derives it only from a validated
|
|
41
|
+
# full head SHA and stops before any brief when none resolves, so the slice path can never carry a / or an unresolved sentinel.
|
|
42
|
+
STAGING_CLI="$ROOT/bin/review-staging.mjs"
|
|
43
|
+
if grep -qF 'const SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;' "$STAGING_CLI" && grep -qF 'if (!SHA_RE.test(head)) throw new Stop(1' "$STAGING_CLI"; then ok; else bad "staging CLI does not refuse an unvalidated head SHA"; fi
|
|
44
|
+
if grep -qF 'const sha7 = head.slice(0, 7);' "$STAGING_CLI" && grep -qF 'REVIEW_SHA_SHORT: sha7' "$STAGING_CLI"; then ok; else bad "staging CLI does not bind [REVIEW_SHA_SHORT] to the validated head's first 7 hex"; fi
|
|
45
|
+
if grep -q 'REVIEW_SHA_SHORT' "$ROOT/commands/review-pr-staging.md"; then bad "staging spec still binds [REVIEW_SHA_SHORT] by hand (the CLI renders the briefs)"; else ok; fi
|
|
54
46
|
|
|
55
47
|
echo "review-pr-gate2.test.sh: passed=$PASS failed=$FAILN"
|
|
56
48
|
[ "$FAILN" -eq 0 ]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# review-pr-staging-finding-filter.test.sh — fixtures for the anchored open-finding filter (forge#3800).
|
|
3
|
-
# The
|
|
3
|
+
# The staging gate's `# FINDING_PR_FILTER` line (scripts/review-staging-pre.sh since #4359) is extracted and run against issue shapes: the gate must keep a
|
|
4
4
|
# hit only when its title suffix or `**Source**:` line names exactly PR #N; hidden markers never count.
|
|
5
5
|
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
6
6
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
@@ -12,10 +12,10 @@ bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
|
12
12
|
command -v jq >/dev/null 2>&1 || { echo "review-pr-staging-finding-filter.test.sh: jq missing, skipped"; exit 0; }
|
|
13
13
|
|
|
14
14
|
extract() { grep -E '# FINDING_PR_FILTER$' "$1" | sed 's/[[:space:]]*# FINDING_PR_FILTER$//; s/^[[:space:]]*//'; }
|
|
15
|
-
SPEC="$ROOT/
|
|
15
|
+
SPEC="$ROOT/scripts/review-staging-pre.sh" # owner of the staging 0A gate (forge#4359)
|
|
16
16
|
DEF="$(extract "$SPEC" | sort -u)"
|
|
17
|
-
[ "$(printf '%s\n' "$DEF" | grep -c .)" -eq 1 ] && ok || bad "review-
|
|
18
|
-
[ "$(extract "$ROOT/commands/deploy-info.md" | sort -u)" = "$DEF" ] && ok || bad "deploy-info.md filter differs from review-
|
|
17
|
+
[ "$(printf '%s\n' "$DEF" | grep -c .)" -eq 1 ] && ok || bad "review-staging-pre.sh must carry one identical FINDING_PR_FILTER definition"
|
|
18
|
+
[ "$(extract "$ROOT/commands/deploy-info.md" | sort -u)" = "$DEF" ] && ok || bad "deploy-info.md filter differs from review-staging-pre.sh"
|
|
19
19
|
[ "$(grep -c -F 'FINDING_PR_FILTER' "$SPEC")" -ge 3 ] && ok || bad "filter must be referenced by all three queries"
|
|
20
20
|
grep -q -F 'OVERRIDE: shipping with open findings' "$SPEC" && ok || bad "OVERRIDE text missing"
|
|
21
21
|
eval "$DEF"
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
set -uo pipefail
|
|
9
9
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
10
10
|
SPEC="$ROOT/commands/review-pr-staging.md"
|
|
11
|
+
# Since #4359 the spec calls `forgedock review-staging`: the 0A/0B.1 blocks live in review-staging-pre.sh, the panel guard,
|
|
12
|
+
# 6.75 delivery and 6.5 verdict handling in review-staging-post.sh, and the 7A-7C wiring in bin/review-staging.mjs.
|
|
13
|
+
PRE="$ROOT/scripts/review-staging-pre.sh"
|
|
14
|
+
POST="$ROOT/scripts/review-staging-post.sh"
|
|
15
|
+
CLI="$ROOT/bin/review-staging.mjs"
|
|
16
|
+
blk() { awk -v n="$2" '$0 == "# BEGIN " n {f=1; next} $0 == "# END " n {f=0} f' "$1"; }
|
|
11
17
|
TC="$ROOT/scripts/trusted-comments.sh"
|
|
12
18
|
PASS=0; FAILN=0
|
|
13
19
|
ok() { PASS=$((PASS+1)); }
|
|
@@ -24,10 +30,10 @@ chk HIGH 0; chk high 0; chk " High " 0; chk CRITICAL 0; chk critical 0
|
|
|
24
30
|
chk MEDIUM 1; chk medium 1; chk LOW 1; chk INFO 1
|
|
25
31
|
chk "" 0; chk "SEVERE" 0; chk "HIGH/MEDIUM" 0
|
|
26
32
|
|
|
27
|
-
# (2) full-sha reviewed test: the _AGENT regex from the
|
|
28
|
-
LINE=$(grep -E '^[[:space:]]*_AGENT=' "$
|
|
33
|
+
# (2) full-sha reviewed test: the _AGENT regex from the 0B.1 scope loop, run through the real trusted-comments predicate
|
|
34
|
+
LINE=$(grep -E '^[[:space:]]*_AGENT=' "$PRE" | head -1)
|
|
29
35
|
RE=$(printf '%s' "$LINE" | sed -n 's/.*count "\(.*\)") ||.*/\1/p')
|
|
30
|
-
if [ -n "$RE" ]; then ok; else bad "_AGENT regex not found in review-
|
|
36
|
+
if [ -n "$RE" ]; then ok; else bad "_AGENT regex not found in review-staging-pre.sh"; fi
|
|
31
37
|
FULL=0123456789abcdef0123456789abcdef01234567; SHORT=${FULL:0:7}
|
|
32
38
|
RE=${RE//\\\\/\\}; RE=${RE//\$\{_HEAD\}/$FULL}
|
|
33
39
|
FB=$'<!-- REVIEW-FINDINGS-START -->\nNo findings.\n<!-- REVIEW-FINDINGS-END -->'
|
|
@@ -41,7 +47,7 @@ cnt() { bash "$TC" count "$RE"; }
|
|
|
41
47
|
[ "$(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)"
|
|
42
48
|
|
|
43
49
|
# (3) _ROUTE regex: real mode on the final head counts, spec-evolution-blocked and a stale sha do not
|
|
44
|
-
RLINE=$(grep -E '^[[:space:]]*_ROUTE=' "$
|
|
50
|
+
RLINE=$(grep -E '^[[:space:]]*_ROUTE=' "$PRE" | head -1)
|
|
45
51
|
RRE=$(printf '%s' "$RLINE" | sed -n 's/.*count "\(.*\)") ||.*/\1/p'); RRE=${RRE//\\\\/\\}; RRE=${RRE//\$\{_HEAD7\}/$SHORT}
|
|
46
52
|
rcnt() { bash "$TC" count "$RRE"; }
|
|
47
53
|
[ "$(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"
|
|
@@ -50,14 +56,14 @@ rcnt() { bash "$TC" count "$RRE"; }
|
|
|
50
56
|
|
|
51
57
|
# (4) spec text guards: agents may not be told to suppress findings; no $PWD tier in the classifier resolver; git reads are status-checked
|
|
52
58
|
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
|
|
53
|
-
if grep -qE "^DEPLOY_WIRING_FILES=.*git diff" "$SPEC"; then bad "DEPLOY_WIRING_FILES must not pipe git diff directly"; else ok; fi
|
|
54
|
-
if grep -q 'git diff-tree .*|| { SCOPE_MODE=full' "$
|
|
59
|
+
if grep -qE "^DEPLOY_WIRING_FILES=.*git diff" "$SPEC" "$PRE"; then bad "DEPLOY_WIRING_FILES must not pipe git diff directly"; else ok; fi
|
|
60
|
+
if grep -q 'git diff-tree .*|| { SCOPE_MODE=full' "$PRE"; then ok; else bad "git diff-tree reads must fall back to full scope"; fi
|
|
55
61
|
# the staging triage is the shared script: scoped and prior-PR arguments reach `classify`, the scope file and member PR list are built from the persisted 0B.1 state, no classifier logic is copied into the spec
|
|
56
|
-
if grep -qF
|
|
62
|
+
if grep -qF '"--scope-files", scopeFile' "$CLI" && grep -qF '"--prior-prs", priorPrs' "$CLI" && grep -qF '_scope.json' "$CLI" && grep -qF '"review-finalize.sh"' "$CLI"; then ok; else bad "the staging CLI must call review-finalize.sh classify with --scope-files and --prior-prs from the persisted 0B.1 scope"; fi
|
|
57
63
|
if grep -qE "^(classify_one|cap_pass|prior_findings_tsv)\(\) \{" "$SPEC"; then bad "review-pr-staging.md must not carry a copy of the classifier, cap or re-flag logic"; else ok; fi
|
|
58
64
|
if grep -qF 'FINDING_TEXT_FILE="$WORKDIR/finding-text.$I"; jq -r' "$ROOT/scripts/review-finalize.sh" && grep -qF -- '--text-file "$FINDING_TEXT_FILE"' "$ROOT/scripts/review-finalize.sh"; then ok; else bad "FINDING_TEXT_FILE must be written before the classifier call in review-finalize.sh"; fi
|
|
59
65
|
grep -q "skip-dedup" "$ROOT/scripts/review-finalize.sh" && ok || bad "review-finalize.sh must skip a deduped finding"
|
|
60
|
-
grep -q "review-prepare.sh" "$
|
|
66
|
+
grep -q "review-prepare.sh" "$CLI" && grep -q -- "--no-delta" "$CLI" && ok || bad "staging must call review-prepare.sh --no-delta in 0C"
|
|
61
67
|
# (7) fail-closed guard: BLOCKED when the classifier is unresolvable, no manual classifier mode, no classifier-unavailable fallback
|
|
62
68
|
SPEC_FIN="$ROOT/scripts/review-finalize.sh" # review-pr.md delegates classification to review-finalize.sh, which owns the fail-closed BLOCKED line (#3841)
|
|
63
69
|
if grep -qE 'classifier=manual|echo script \|\| echo manual|classifier-unavailable' "$SPEC"; then bad "review-pr-staging.md must carry no manual classifier mode"; else ok; fi
|
|
@@ -68,7 +74,7 @@ done
|
|
|
68
74
|
# (forge#3778) the spec carries a byte-identical stub per fence; the body lives in scripts/lib/fence-bootstrap.sh, which is SOURCED
|
|
69
75
|
# in a fresh shell (the stub's locator + source line is executed for real, so the stub itself is covered too).
|
|
70
76
|
[ "$(grep '# FENCE_BOOTSTRAP$' "$SPEC" | sort -u | wc -l | tr -d ' ')" -eq 1 ] && ok || bad "FENCE_BOOTSTRAP stubs must be byte-identical"
|
|
71
|
-
[ "$(grep -c '# FENCE_BOOTSTRAP$' "$SPEC")" -ge 3 ] && ok || bad "bootstrap must be carried by
|
|
77
|
+
[ "$(grep -c '# FENCE_BOOTSTRAP$' "$SPEC")" -ge 3 ] && ok || bad "bootstrap must be carried by the 7A, 7C and Phase 8 fences"
|
|
72
78
|
grep '^type forge_resolve >/dev/null' "$SPEC" | sed 1q > "$TMP/boot.sh"
|
|
73
79
|
grep '# FENCE_BOOTSTRAP$' "$SPEC" | sed 1q >> "$TMP/boot.sh"
|
|
74
80
|
[ "$(wc -l < "$TMP/boot.sh")" -eq 2 ] && ok || bad "bootstrap stub (locator + source line) not found"
|
|
@@ -81,17 +87,17 @@ UNCOVERED=$(awk '
|
|
|
81
87
|
infence { body = body "\n" $0 }' "$SPEC")
|
|
82
88
|
[ -z "$UNCOVERED" ] && ok || bad "bash fences starting at line(s) ${UNCOVERED}consume cross-fence state without the FENCE_BOOTSTRAP block"
|
|
83
89
|
# the persisted Phase 6.5 / 6.75 values: producers write them, consumers reload them validated
|
|
84
|
-
grep -q 'test_gate_verdict' "$
|
|
90
|
+
grep -q 'test_gate_verdict' "$POST" && grep -q 'launched_review_agents' "$POST" && ok || bad "TEST_GATE_VERDICT and LAUNCHED_REVIEW_AGENTS must be persisted to the scratchpad"
|
|
85
91
|
# PR-keyed (concurrent runs in one cwd must not share state) and reset by an executable command, not prose
|
|
86
|
-
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
|
|
87
|
-
grep -qF '
|
|
92
|
+
if grep -qE '\$_SD/(test_gate_verdict|test_gate_reason|launched_review_agents)|\.forge-scratch\}/launched_review_agents' "$SPEC" "$POST"; then bad "gate/launched-agent scratch files must be keyed by \${PR_NUMBER}_"; else ok; fi
|
|
93
|
+
grep -qF 'io.writeFile(launchedFile, "")' "$CLI" && grep -qF 'rm -f "${_SD:?}/${PR_NUMBER:?}_test_gate_verdict" "${_SD:?}/${PR_NUMBER:?}_test_gate_reason"' "$POST" && ok || bad "the pre-dispatch must truncate the launched-agent file and the test gate must rm -f the PR-keyed verdict files"
|
|
88
94
|
if awk '/^### 7E:/,/^### 7F:/' "$SPEC" | grep -qF '{GH_REPO}'; then bad "7E must not use the {GH_REPO} template token"; else ok; fi
|
|
89
95
|
# (8b) forge#3733: Phase 6.75 fails closed on an empty/absent launched-agent list or empty PR_NUMBER; Phase 6 fails closed on a bad SELECTED_AGENT_COUNT
|
|
90
|
-
F675=$(
|
|
96
|
+
F675=$(blk "$POST" delivery-check)
|
|
91
97
|
GUARD_LN=$(printf '%s\n' "$F675" | grep -n 'launched-agent list is empty or unreadable' | head -1 | cut -d: -f1)
|
|
92
98
|
LOOP_LN=$(printf '%s\n' "$F675" | grep -n '^for AGENT_DOMAIN in' | head -1 | cut -d: -f1)
|
|
93
99
|
[ -n "$GUARD_LN" ] && [ -n "$LOOP_LN" ] && [ "$GUARD_LN" -lt "$LOOP_LN" ] && ok || bad "6.75 empty-list guard must precede the for AGENT_DOMAIN loop"
|
|
94
|
-
printf '%s\n' "$F675" | sed -n "1,$((LOOP_LN-1))p" | grep -v '^[[:space:]]*gh ' > "$TMP/f675.sh"
|
|
100
|
+
{ echo '_SD="$FORGE_SCRATCHPAD"'; printf '%s\n' "$F675" | sed -n "1,$((LOOP_LN-1))p" | grep -v '^[[:space:]]*gh '; } > "$TMP/f675.sh"
|
|
95
101
|
mkdir -p "$TMP/sd9/empty" "$TMP/sd9/full" "$TMP/bin9"
|
|
96
102
|
printf '#!/bin/sh\necho stub-value\n' > "$TMP/bin9/yq"; printf '#!/bin/sh\nexit 0\n' > "$TMP/bin9/gh"; chmod +x "$TMP/bin9/yq" "$TMP/bin9/gh"
|
|
97
103
|
: > "$TMP/sd9/empty/42_launched_review_agents"
|
|
@@ -104,7 +110,7 @@ done
|
|
|
104
110
|
OUT9=$(run675 "$TMP/sd9/full" 42)
|
|
105
111
|
case "$OUT9" in *"launched-agent list is empty"*) bad "6.75 guard must not fire for a non-empty list (got '$OUT9')" ;; *) ok ;; esac
|
|
106
112
|
# Phase 6 SELECTED_AGENT_COUNT validation: static order + behaviour of the reload/validate lines
|
|
107
|
-
F6=$(
|
|
113
|
+
F6=$(blk "$POST" panel-guard)
|
|
108
114
|
printf '%s' "$F6" | grep -qF 'SELECTED_COUNT_INVALID' && ok || bad "Phase 6 fence must validate SELECTED_AGENT_COUNT (SELECTED_COUNT_INVALID)"
|
|
109
115
|
printf '%s' "$F6" | grep -F 'SELECTED_AGENT_COUNT=' | grep -F 'launched_review_agents' > "$TMP/sel.sh"
|
|
110
116
|
printf '%s\n' 'SELECTED_COUNT_INVALID=false' "$(printf '%s' "$F6" | grep -F 'case "${SELECTED_AGENT_COUNT:-}"')" > "$TMP/selcase.sh"
|
|
@@ -140,20 +146,20 @@ case "$OUT8" in *"PR=|"*) ok ;; *) bad "non-numeric staging_pr_number must be re
|
|
|
140
146
|
OUT8=$( ( unset ALL_PR_NUMBERS; PR_NUMBER=99; export PATH="$TMP/bin:$PATH" FORGE_SCRATCHPAD="$TMP/sd8/full" FORGEDOCK_HOME="$ROOT"; . "$TMP/boot.sh" >/dev/null 2>&1; echo "PR=$PR_NUMBER|ALL=${ALL_PR_NUMBERS:-}" ) )
|
|
141
147
|
[ "$OUT8" = "PR=99|ALL=" ] && ok || bad "in-shell PR_NUMBER must not be overridden by the scratch file (got '$OUT8')"
|
|
142
148
|
# producers persist, and 0B.1 / 7B.5 no longer use the unsubstituted-template repo token or a bare unquoted PR number in gh pr comment
|
|
143
|
-
grep -q 'staging_pr_number' "$
|
|
144
|
-
if
|
|
149
|
+
grep -q 'staging_pr_number' "$CLI" && grep -q '_bundle-prs.txt' "$PRE" && ok || bad "Phase -1 / 0A must persist staging_pr_number and _bundle-prs.txt"
|
|
150
|
+
if grep -v '^[[:space:]]*#' "$PRE" "$POST" | grep -qE '(^|[^$]){GH_REPO}'; then bad "the 0A/0B.1 and post-dispatch scripts must not use the {GH_REPO} template token"; else ok; fi
|
|
145
151
|
if awk '/^### 7A:/,/^### 7D:/' "$SPEC" | grep -qE '\{GH_REPO\}|gh pr comment \$\{PR_NUMBER\}'; then bad "7A-7C must use quoted \$GH_REPO / \$PR_NUMBER"; else ok; fi
|
|
146
152
|
# (7) reviewer templates bind the full head SHA via the dispatch placeholder, not an orchestrator-only shell var
|
|
147
153
|
if grep -qE '^Reviewed-SHA: \$\{REVIEW_SHA_FULL\}$' "$SPEC"; then bad "reviewer template still uses \${REVIEW_SHA_FULL} (unbound in a fresh shell)"; else ok; fi
|
|
148
154
|
[ "$(grep -cE '^Reviewed-SHA: \[REVIEW_SHA\]$' "$SPEC")" -ge 5 ] && ok || bad "all five reviewer templates must use Reviewed-SHA: [REVIEW_SHA]"
|
|
149
155
|
if grep -E '^Reviewed-SHA:' "$SPEC" | grep -qvE '^Reviewed-SHA: \[REVIEW_SHA\]$'; then bad "every Reviewed-SHA template line must be [REVIEW_SHA]"; else ok; fi
|
|
150
|
-
grep -
|
|
156
|
+
grep -qiF 'substitute `[REVIEW_SHA]`' "$SPEC" && ok || bad "dispatch text must bind [REVIEW_SHA]"
|
|
151
157
|
# (8) cross-PR re-flag (forge#3641) and the per-review cap (forge#3642) now run inside review-finalize.sh classify: scripts/review-finalize.test.sh covers them (bundle re-flag, scope demotion, cap)
|
|
152
158
|
# (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.
|
|
153
159
|
# Fail-closed cases first: every FULL / error / untrusted path must leave the PR's commits UNREVIEWED (empty credit).
|
|
154
160
|
if command -v git >/dev/null 2>&1; then
|
|
155
|
-
awk '/^# BEGIN bundle-delta-credit$/{f=1;next} /^# END bundle-delta-credit$/{f=0} f' "$
|
|
156
|
-
[ -s "$TMP/delta-fn.sh" ] && ok || bad "bundle-delta-credit block not found in
|
|
161
|
+
awk '/^# BEGIN bundle-delta-credit$/{f=1;next} /^# END bundle-delta-credit$/{f=0} f' "$PRE" > "$TMP/delta-fn.sh"
|
|
162
|
+
[ -s "$TMP/delta-fn.sh" ] && ok || bad "bundle-delta-credit block not found in review-staging-pre.sh"
|
|
157
163
|
RDS="$ROOT/scripts/review-delta.sh"
|
|
158
164
|
export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_NOSYSTEM=1
|
|
159
165
|
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@example.invalid GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@example.invalid
|
|
@@ -227,25 +233,25 @@ mkrepo9; commit9 pr.txt pr1b pr1b; R=$(git -C "$W9" rev-parse HEAD); panel9 "$R"
|
|
|
227
233
|
OUT9=$(credit9 "$H"); N9=$(printf '%s' "${OUT9#*|}" | wc -w)
|
|
228
234
|
[ "${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')"
|
|
229
235
|
# spec text guards: the resolver is reused, never copied; carried record accepted; the unchanged cross-PR and merge-resolution paths are still present
|
|
230
|
-
grep -q 'review-delta.sh' "$
|
|
231
|
-
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
|
|
232
|
-
grep -qF 'printf '"'"'%s\n'"'"' "$_FILES" | sort -u | sed "s|^|$pr |" >> "$_PF"' "$
|
|
236
|
+
grep -q 'review-delta.sh' "$PRE" && grep -q 'REVIEW-CARRIED' "$PRE" && ok || bad "review-staging-pre.sh must call review-delta.sh and accept the REVIEW-CARRIED record"
|
|
237
|
+
if grep -qE 'merge-base --is-ancestor.*REVIEWED_SHA_STALE|diff_shape\(\)' "$SPEC" "$PRE"; then bad "spec must not carry a second copy of the resolver logic"; else ok; fi
|
|
238
|
+
grep -qF 'printf '"'"'%s\n'"'"' "$_FILES" | sort -u | sed "s|^|$pr |" >> "$_PF"' "$PRE" && grep -qF 'git diff-tree --no-commit-id --name-only -r --cc "$c"' "$PRE" && ok || bad "CROSS_PR_FILES accumulation and --cc merge scoping must stay intact"
|
|
233
239
|
[ "$(grep -c '# FENCE_BOOTSTRAP$' "$SPEC" | head -1)" -ge 3 ] && ok || bad "FENCE_BOOTSTRAP stubs must remain"
|
|
234
240
|
# an unresolvable helper fails the fence closed (exit 1), never an unsourced bootstrap
|
|
235
241
|
OUT8=$( ( unset FORGEDOCK_HOME FORGE_HOME FORGE_ROOT CLAUDE_PLUGIN_ROOT CLAUDE_CONFIG_DIR CODEX_HOME FORGE_SOURCED_ROOT FORGEDOCK_MARKETPLACE; export HOME="$TMP/nohome"; cd "$TMP" && bash -c "$(cat "$TMP/boot.sh"); echo reached" ) 2>&1 )
|
|
236
242
|
case "$OUT8" in *BLOCKED*) case "$OUT8" in *reached*) bad "unresolved bootstrap helper must stop the fence" ;; *) ok ;; esac ;; *) bad "unresolved bootstrap helper must print BLOCKED (got '$OUT8')" ;; esac
|
|
237
243
|
# a stale-head PR must not credit its own merge commit (it contains the post-review commits): the DELTA branch only appends DELTA_COMMITS
|
|
238
|
-
if awk '/^bundle_delta_credit\(\)/,/^\}/' "$
|
|
244
|
+
if awk '/^bundle_delta_credit\(\)/,/^\}/' "$PRE" | grep -q 'mergeCommit'; then bad "the DELTA path must not credit the PR merge commit"; else ok; fi
|
|
239
245
|
# (10) fail-closed gate verdict (forge#3734): an empty/missing/invalid persisted verdict must never become SKIP
|
|
240
|
-
|
|
246
|
+
blk "$POST" test-gate-verdict > "$TMP/vh.sh"
|
|
241
247
|
[ -s "$TMP/vh.sh" ] && ok || bad "could not extract the Phase 6.5 verdict-handling fence"
|
|
242
|
-
if awk '/^ \*\)/,/;;/' "$
|
|
243
|
-
grep -qF 'missing/invalid' "$SPEC" && ok || bad "spec must document missing/invalid gate-verdict handling"
|
|
244
|
-
if grep -F 'FORGE:TEST_GATE:RESULT=' "$
|
|
248
|
+
if awk '/^ \*\)/,/;;/' "$POST" | grep -q 'TEST_GATE_VERDICT="SKIP"'; then bad "catch-all arm must not set TEST_GATE_VERDICT=SKIP"; else ok; fi
|
|
249
|
+
grep -qF 'missing/invalid' "$SPEC" && grep -qF 'missing/invalid' "$POST" && ok || bad "spec must document missing/invalid gate-verdict handling"
|
|
250
|
+
if grep -F 'FORGE:TEST_GATE:RESULT=' "$POST" | grep -q 'tail -1 || echo "SKIP"'; then bad "producer must not coerce an absent marker to SKIP"; else ok; fi
|
|
245
251
|
vh10() { # $1 posture, $2 persisted verdict content ('-' = no file), $3 env verdict (optional)
|
|
246
252
|
local sd="$TMP/vh10.$RANDOM"; mkdir -p "$sd"
|
|
247
253
|
[ "$2" = "-" ] || printf '%s\n' "$2" > "$sd/5_test_gate_verdict"
|
|
248
|
-
( export _SD="$sd" PR_NUMBER=5 GH_REPO=o/r DEFAULT_BRANCH=main ALL_PR_NUMBERS="5" CONFIG_FILE=/dev/null P10="$1"
|
|
254
|
+
( export _SD="$sd" PR_NUMBER=5 GH_REPO=o/r DEFAULT_BRANCH=main ALL_PR_NUMBERS="5" CONFIG_FILE=/dev/null P10="$1" GATE_POSTURE="$1" OVERRIDE_PHRASE="OVERRIDE: x" GH_FLAG="-R o/r"
|
|
249
255
|
[ -z "${3:-}" ] || export TEST_GATE_VERDICT="$3"
|
|
250
256
|
forge_resolve() { :; }; yq() { case "$*" in *posture*) echo "$P10" ;; *) echo "OVERRIDE: x" ;; esac; }; gh() { return 0; }
|
|
251
257
|
. "$TMP/vh.sh" >"$sd/out" 2>&1; echo "rc=0" >> "$sd/out" ) ; local rc=$?
|
|
@@ -280,12 +286,12 @@ gh11() {
|
|
|
280
286
|
*) return 1 ;;
|
|
281
287
|
esac
|
|
282
288
|
}
|
|
283
|
-
[ "$(grep -c '^# BEGIN bundle-pr-kind$' "$
|
|
284
|
-
awk '/^# BEGIN bundle-pr-set$/{f=1;next} /^# END bundle-pr-set$/{f=0} f' "$
|
|
289
|
+
[ "$(grep -c '^# BEGIN bundle-pr-kind$' "$PRE")" -eq 2 ] && [ "$(awk '/^# BEGIN bundle-pr-kind$/{f=1;n++} f{print > ("'"$TMP"'/kind" n ".sh")} /^# END bundle-pr-kind$/{f=0}' "$PRE"; cmp -s "$TMP/kind1.sh" "$TMP/kind2.sh" && echo same)" = same ] && ok || bad "0A and 0B.1 must carry identical bundle_pr_kind blocks"
|
|
290
|
+
awk '/^# BEGIN bundle-pr-set$/{f=1;next} /^# END bundle-pr-set$/{f=0} f' "$PRE" | sed 's/{GH_REPO}/o\/r/g' > "$TMP/prset.sh"
|
|
285
291
|
[ -s "$TMP/prset.sh" ] && ok || bad "bundle-pr-set block not found in Phase 0A"
|
|
286
|
-
OUT11=$( gh() { gh11 "$@"; }; BUNDLE_PRS="5 6 28668 77 90"; MERGE_PRS="90"; . "$TMP/prset.sh"; echo $ALL_PR_NUMBERS "|$SKIPPED_ISSUES" )
|
|
292
|
+
OUT11=$( gh() { gh11 "$@"; }; GH_REPO=o/r; BUNDLE_PRS="5 6 28668 77 90"; MERGE_PRS="90"; . "$TMP/prset.sh"; echo $ALL_PR_NUMBERS "|$SKIPPED_ISSUES" )
|
|
287
293
|
[ "$OUT11" = "5 6 77 90 | 28668" ] && ok || bad "0A must drop the confirmed issue and keep PRs, merge PRs and unclassifiable numbers (got '$OUT11')"
|
|
288
|
-
awk '/^# BEGIN bundle-scope-loop$/{f=1;next} /^# END bundle-scope-loop$/{f=0} f' "$
|
|
294
|
+
awk '/^# BEGIN bundle-scope-loop$/{f=1;next} /^# END bundle-scope-loop$/{f=0} f' "$PRE" > "$TMP/loop.sh"
|
|
289
295
|
[ -s "$TMP/loop.sh" ] && ok || bad "bundle-scope-loop block not found in 0B.1"
|
|
290
296
|
scope11() { # $1 = ALL_PR_NUMBERS; prints "<SCOPE_MODE> <REVIEWED_PR_COUNT>" then the loop output
|
|
291
297
|
( gh() { gh11 "$@"; }; bundle_delta_credit() { DELTA_COMMITS=""; DELTA_FULL=0; }; . "$TMP/kind1.sh"
|