forgedock 1.49.0 → 1.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/bin/engine/outcome-codes.mjs +2 -0
  2. package/bin/engine-cli.mjs +123 -20
  3. package/bin/forgedock.mjs +31 -3
  4. package/bin/lean/batch-health.mjs +219 -0
  5. package/bin/lean/batch-health.test.mjs +156 -0
  6. package/bin/lean/batch.mjs +12 -3
  7. package/bin/lean/batch.test.mjs +304 -8
  8. package/bin/lean/conformance.mjs +79 -7
  9. package/bin/lean/conformance.test.mjs +63 -1
  10. package/bin/lean/context.mjs +8 -2
  11. package/bin/lean/context.test.mjs +11 -1
  12. package/bin/lean/credentials.mjs +18 -0
  13. package/bin/lean/execution.mjs +23 -9
  14. package/bin/lean/execution.test.mjs +93 -0
  15. package/bin/lean/fixclosereasons.mjs +52 -0
  16. package/bin/lean/fixclosereasons.test.mjs +64 -0
  17. package/bin/lean/heavy.mjs +14 -4
  18. package/bin/lean/heavy.test.mjs +55 -1
  19. package/bin/lean/lean.mjs +93 -36
  20. package/bin/lean/lean.test.mjs +143 -19
  21. package/bin/lean/live-execution.mjs +136 -0
  22. package/bin/lean/live-execution.test.mjs +83 -0
  23. package/bin/lean/orchestrate-blocked.test.mjs +47 -0
  24. package/bin/lean/orchestrate.mjs +135 -17
  25. package/bin/lean/prefilter.mjs +4 -3
  26. package/bin/lean/prefilter.test.mjs +5 -4
  27. package/bin/lean/prompts.mjs +4 -2
  28. package/bin/lean/recovery.test.mjs +5 -5
  29. package/bin/lean/sandbox-provider.contract.test.mjs +36 -1
  30. package/bin/lean/sandbox-remote.mjs +6 -2
  31. package/bin/lean/schedd.mjs +9 -0
  32. package/bin/pool/agent.mjs +3 -1
  33. package/bin/pool/router.mjs +72 -7
  34. package/bin/review-staging.mjs +2 -0
  35. package/bin/runner.mjs +24 -0
  36. package/commands/orchestrate.md +2 -1
  37. package/commands/review-pr-staging.md +2 -2
  38. package/docs/CONFIG.md +39 -1
  39. package/docs/OUTCOME-CODES.md +2 -1
  40. package/package.json +1 -1
  41. package/pool/README.md +2 -0
  42. package/pool/image/cs-cred +8 -1
  43. package/scripts/review-staging-post.sh +70 -25
  44. package/scripts/review-staging-post.test.sh +40 -8
@@ -10,12 +10,15 @@
10
10
  # delivery counts trusted, current-head FORGE:REVIEW-AGENT comments (findings block required) for every domain listed in
11
11
  # DIR/<N>_launched_review_agents. A shortfall labels the PR review-degraded + needs-human and posts
12
12
  # FORGE:GATE_FAILURE:TYPE=review-panel-integrity; an empty/unreadable list or a missing domain is a delivery failure.
13
- # Exit 0 = every launched reviewer delivered, 1 = not.
13
+ # The comments are listed once per attempt; a missing domain or an API error re-lists after each
14
+ # FORGE_REVIEW_DELIVERY_RETRY_DELAYS wait (seconds, default "5 15 45"). An API error on the last attempt is
15
+ # REVIEW DELIVERY UNVERIFIED (exit 2, nothing labelled or posted), never "missing" (forge#5166).
16
+ # Exit 0 = every launched reviewer delivered, 1 = not, 2 = could not be verified.
14
17
  # test-gate reads the saved /test-gate output FILE (the last FORGE:TEST_GATE:RESULT= marker; an absent marker stays empty and
15
18
  # fails closed as BLOCK, never SKIP), applies the trusted override, the posture and the FORGE:GATE_PASS /
16
19
  # FORGE:GATE_FAILURE test-gate comments, and persists DIR/<N>_test_gate_verdict and _reason.
17
20
  # Exit 0 = continue, 1 = RESULT: BLOCK DEPLOY.
18
- # Exit: 2 on a usage error.
21
+ # Exit: 2 on a usage error (and delivery UNVERIFIED).
19
22
  #
20
23
  # The `# BEGIN <name>` ... `# END <name>` blocks below are the spec's former fences, copied without their forge_resolve locator
21
24
  # stubs (this script resolves helpers once, below). Helpers (trusted-comments.sh) resolve through forge_resolve from trusted
@@ -59,24 +62,77 @@ type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; _S
59
62
 
60
63
  if [ "$CMD" = delivery ]; then
61
64
  echo "=== Incomplete-panel guard and Phase 6.75: verify agent delivery ==="
65
+ # BEGIN delivery-fetch
66
+ # The PR's comments are listed ONCE per attempt into a cache file; the panel guard and the per-domain check both read it (forge#5166).
67
+ # An attempt is an API error unless gh exits 0 and every page is a JSON array: an error is never evidence that a comment is absent.
68
+ DELIVERY_CACHE="$_SD/${PR_NUMBER}_delivery_comments.json"
69
+ delivery_fetch() {
70
+ rm -f "$DELIVERY_CACHE" "$DELIVERY_CACHE.tmp"
71
+ gh api --paginate "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" > "$DELIVERY_CACHE.tmp" 2>/dev/null || { rm -f "$DELIVERY_CACHE.tmp"; return 1; }
72
+ jq -se 'length > 0 and all(.[]; type == "array")' "$DELIVERY_CACHE.tmp" >/dev/null 2>&1 || { rm -f "$DELIVERY_CACHE.tmp"; return 1; }
73
+ mv -f "$DELIVERY_CACHE.tmp" "$DELIVERY_CACHE"
74
+ }
75
+ # Unique trusted current-head reviewer domains in the cached listing (findings block not required), one per line.
76
+ delivery_domains() {
77
+ bash "$TRUSTED_SCRIPT" bodies "^<!-- FORGE:REVIEW-AGENT:[a-z0-9-]+ -->[\\s\\S]*(^|\\n)Reviewed-SHA:[ ]${REVIEW_SHA_FULL}(\\r?\\n|$)" < "$DELIVERY_CACHE" 2>/dev/null \
78
+ | jq -r 'scan("^<!-- FORGE:REVIEW-AGENT:([a-z0-9-]+) -->") | .[0]' 2>/dev/null | sort -u
79
+ }
80
+ # Trusted current-head comments for one domain that carry a findings block, in the cached listing.
81
+ delivery_count() {
82
+ local AGENT_DOMAIN="$1" n
83
+ n=$(bash "$TRUSTED_SCRIPT" count "^(?=[\\s\\S]*<!-- REVIEW-FINDINGS-START -->[\\s\\S]*<!-- REVIEW-FINDINGS-END -->)<!-- FORGE:REVIEW-AGENT:${AGENT_DOMAIN} -->[\\s\\S]*(^|\\n)Reviewed-SHA:[ ]${REVIEW_SHA_FULL}(\\r?\\n|$)" < "$DELIVERY_CACHE" 2>/dev/null)
84
+ case "$n" in ''|*[!0-9]*) n=0 ;; esac # fail closed: counts as missing
85
+ printf '%s\n' "$n"
86
+ }
87
+ # END delivery-fetch
62
88
  # BEGIN panel-guard
63
89
  # Each Bash call is a fresh shell: re-resolve the full head SHA if it is not set, and refuse when it is not a commit id
64
90
  # (an empty SHA would make the Reviewed-SHA match accept any head).
65
91
  [ -n "${REVIEW_SHA_FULL:-}" ] || REVIEW_SHA_FULL=$(gh pr view "$PR_NUMBER" -R "$GH_REPO" --json headRefOid --jq '.headRefOid' 2>/dev/null || echo "")
66
92
  case "$REVIEW_SHA_FULL" in *[!0-9a-f]*|"") REVIEW_SHA_FULL="" ;; *) [ "${#REVIEW_SHA_FULL}" -eq 40 ] || [ "${#REVIEW_SHA_FULL}" -eq 64 ] || REVIEW_SHA_FULL="" ;; esac
67
93
  TRUSTED_SCRIPT=$(forge_resolve trusted-comments.sh)
68
- ACTUAL_AGENT_COUNT=0
69
- if [ -n "$REVIEW_SHA_FULL" ] && [ -n "$TRUSTED_SCRIPT" ]; then
70
- # Only TRUSTED, current-head reviewer comments count, over every page of comments. Unique domains, so a duplicate does not inflate the count.
71
- ACTUAL_AGENT_COUNT=$(gh api --paginate "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null \
72
- | bash "$TRUSTED_SCRIPT" bodies "^<!-- FORGE:REVIEW-AGENT:[a-z0-9-]+ -->[\\s\\S]*(^|\\n)Reviewed-SHA:[ ]${REVIEW_SHA_FULL}(\\r?\\n|$)" 2>/dev/null \
73
- | jq -r 'scan("^<!-- FORGE:REVIEW-AGENT:([a-z0-9-]+) -->") | .[0]' 2>/dev/null | sort -u | grep -c . || echo 0)
74
- fi
75
- case "$ACTUAL_AGENT_COUNT" in ''|*[!0-9]*) ACTUAL_AGENT_COUNT=0 ;; esac # fail closed: unreadable or untrusted counts as not posted
76
94
  # Fresh shell: SELECTED_AGENT_COUNT is not carried over. Reload it from the PR-keyed launched-agent file, then fail closed when it is empty, non-integer or zero (a staging review always selects at least one reviewer).
77
95
  [ -n "${SELECTED_AGENT_COUNT:-}" ] || { [ -n "${PR_NUMBER:-}" ] && [ -r "$_SD/${PR_NUMBER}_launched_review_agents" ] && SELECTED_AGENT_COUNT=$(grep -E '^[a-z0-9-]+$' "$_SD/${PR_NUMBER}_launched_review_agents" | sort -u | grep -c .); }
78
96
  SELECTED_COUNT_INVALID=false
79
97
  case "${SELECTED_AGENT_COUNT:-}" in ''|*[!0-9]*|0) SELECTED_COUNT_INVALID=true; SELECTED_AGENT_COUNT=0 ;; esac
98
+ # Reload the launched-agent list persisted at dispatch (validated: lowercase marker domains only).
99
+ [ -n "${LAUNCHED_REVIEW_AGENTS:-}" ] || { [ -r "$_SD/${PR_NUMBER}_launched_review_agents" ] && LAUNCHED_REVIEW_AGENTS=$(grep -E '^[a-z0-9-]+$' "$_SD/${PR_NUMBER}_launched_review_agents" | sort -u | tr '\n' ' '); }
100
+ ACTUAL_AGENT_COUNT=0
101
+ DELIVERY_LAST=skipped # ok = the last attempt was a successful listing, error = it was an API error
102
+ DELIVERY_FOUND=" " # launched domains with a findings block, found on any attempt (found stays found)
103
+ DELIVERY_SEEN="" # union of trusted current-head reviewer domains over the successful listings
104
+ if [ -n "$REVIEW_SHA_FULL" ] && [ -n "$TRUSTED_SCRIPT" ] && [ "${DISPATCH_FAILED:-}" != "true" ]; then
105
+ if [ "$SELECTED_COUNT_INVALID" = "true" ] || [ -z "${LAUNCHED_REVIEW_AGENTS:-}" ]; then
106
+ # Nothing to wait for: one listing for the report only, no retry.
107
+ if delivery_fetch; then DELIVERY_LAST=ok; DELIVERY_SEEN=$(delivery_domains); else DELIVERY_LAST=error; fi
108
+ else
109
+ # Retry on listing lag or an API error, after each FORGE_REVIEW_DELIVERY_RETRY_DELAYS wait (seconds; non-integers ignored).
110
+ _ATTEMPT=0
111
+ for _DELAY in 0 ${FORGE_REVIEW_DELIVERY_RETRY_DELAYS-5 15 45}; do
112
+ case "$_DELAY" in ''|*[!0-9]*) continue ;; esac
113
+ _ATTEMPT=$((_ATTEMPT + 1))
114
+ [ "$_ATTEMPT" -gt 1 ] && [ "$_DELAY" -gt 0 ] && sleep "$_DELAY"
115
+ if ! delivery_fetch; then DELIVERY_LAST=error; echo "DELIVERY: attempt ${_ATTEMPT}: API error listing PR comments"; continue; fi
116
+ DELIVERY_LAST=ok
117
+ DELIVERY_SEEN=$(printf '%s\n%s\n' "$DELIVERY_SEEN" "$(delivery_domains)" | grep -E '^[a-z0-9-]+$' | sort -u)
118
+ _PENDING=""
119
+ for _D in $LAUNCHED_REVIEW_AGENTS; do
120
+ case "$DELIVERY_FOUND" in *" $_D "*) continue ;; esac
121
+ if [ "$(delivery_count "$_D")" -ge 1 ]; then DELIVERY_FOUND="${DELIVERY_FOUND}${_D} "; echo "DELIVERY: ${_D} found on attempt ${_ATTEMPT}"; else _PENDING="${_PENDING} ${_D}"; fi
122
+ done
123
+ [ -z "$_PENDING" ] && break
124
+ echo "DELIVERY: attempt ${_ATTEMPT}: missing:${_PENDING}"
125
+ done
126
+ fi
127
+ ACTUAL_AGENT_COUNT=$(printf '%s\n' "$DELIVERY_SEEN" | grep -c . || true)
128
+ fi
129
+ case "$ACTUAL_AGENT_COUNT" in ''|*[!0-9]*) ACTUAL_AGENT_COUNT=0 ;; esac # fail closed: unreadable or untrusted counts as not posted
130
+ if [ "${DISPATCH_FAILED:-}" != "true" ] && [ "$SELECTED_COUNT_INVALID" != "true" ] && [ -n "${LAUNCHED_REVIEW_AGENTS:-}" ] && [ "$DELIVERY_LAST" = error ]; then
131
+ # Never "missing": the comments could not be listed, so nothing is labelled or posted (forge#5166).
132
+ echo "REVIEW DELIVERY UNVERIFIED: could not list PR comments after ${_ATTEMPT:-1} attempt(s) (GitHub API error); found so far:${DELIVERY_FOUND% }"
133
+ echo "Do not re-dispatch reviewers: rerun the finalize step once GitHub answers."
134
+ exit 2
135
+ fi
80
136
  if [ "${DISPATCH_FAILED:-}" = "true" ] || [ "$SELECTED_COUNT_INVALID" = "true" ] || [ "$ACTUAL_AGENT_COUNT" -lt "$SELECTED_AGENT_COUNT" ]; then
81
137
  gh label create "review-degraded" --color "E4E669" --description "PR review panel was incomplete; re-review required before deployment. Managed by ForgeDock." --force -R "$GH_REPO" 2>/dev/null || true
82
138
  gh pr edit "$PR_NUMBER" -R "$GH_REPO" --add-label "review-degraded" --add-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
@@ -91,7 +147,7 @@ Re-run the full staging review in a fresh session before deployment."
91
147
  fi
92
148
  # END panel-guard
93
149
  # BEGIN delivery-check
94
- # Reload the launched-agent list persisted at dispatch (validated: lowercase marker domains only).
150
+ # Reload the launched-agent list persisted at dispatch (validated: lowercase marker domains only); a no-op when the panel guard loaded it.
95
151
  [ -n "${LAUNCHED_REVIEW_AGENTS:-}" ] || { [ -r "$_SD/${PR_NUMBER}_launched_review_agents" ] && LAUNCHED_REVIEW_AGENTS=$(grep -E '^[a-z0-9-]+$' "$_SD/${PR_NUMBER}_launched_review_agents" | sort -u | tr '\n' ' '); }
96
152
  # Fail closed: a staging review always launches at least the Phase 2 material-change agent, so an empty list means lost state or skipped bookkeeping, never "nothing to verify".
97
153
  if [ -z "${PR_NUMBER:-}" ] || [ -z "${LAUNCHED_REVIEW_AGENTS:-}" ]; then
@@ -100,21 +156,9 @@ if [ -z "${PR_NUMBER:-}" ] || [ -z "${LAUNCHED_REVIEW_AGENTS:-}" ]; then
100
156
  exit 1
101
157
  fi
102
158
  MISSING_AGENT_COMMENTS=""
103
- # Each Bash call is a fresh shell: re-resolve the full head SHA if it is not set, and refuse when it is not a commit id
104
- # (an empty SHA would make the Reviewed-SHA match accept any head).
105
- [ -n "${REVIEW_SHA_FULL:-}" ] || REVIEW_SHA_FULL=$(gh pr view "$PR_NUMBER" -R "$GH_REPO" --json headRefOid --jq '.headRefOid' 2>/dev/null || echo "")
106
- case "$REVIEW_SHA_FULL" in *[!0-9a-f]*|"") REVIEW_SHA_FULL="" ;; *) [ "${#REVIEW_SHA_FULL}" -eq 40 ] || [ "${#REVIEW_SHA_FULL}" -eq 64 ] || REVIEW_SHA_FULL="" ;; esac
107
- TRUSTED_SCRIPT=$(forge_resolve trusted-comments.sh)
159
+ # Counted once per attempt from the cached listing above (delivery_count); a domain not found on any attempt is missing.
108
160
  for AGENT_DOMAIN in $LAUNCHED_REVIEW_AGENTS; do
109
- AGENT_COMMENT_COUNT=0
110
- if [ -n "$REVIEW_SHA_FULL" ] && [ -n "$TRUSTED_SCRIPT" ]; then
111
- AGENT_COMMENT_COUNT=$(gh api --paginate "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null \
112
- | bash "$TRUSTED_SCRIPT" count "^(?=[\\s\\S]*<!-- REVIEW-FINDINGS-START -->[\\s\\S]*<!-- REVIEW-FINDINGS-END -->)<!-- FORGE:REVIEW-AGENT:${AGENT_DOMAIN} -->[\\s\\S]*(^|\\n)Reviewed-SHA:[ ]${REVIEW_SHA_FULL}(\\r?\\n|$)" 2>/dev/null || echo 0)
113
- fi
114
- case "$AGENT_COMMENT_COUNT" in ''|*[!0-9]*) AGENT_COMMENT_COUNT=0 ;; esac # fail closed: counts as missing
115
- if [ "${AGENT_COMMENT_COUNT:-0}" -lt 1 ]; then
116
- MISSING_AGENT_COMMENTS="${MISSING_AGENT_COMMENTS} ${AGENT_DOMAIN}"
117
- fi
161
+ case "$DELIVERY_FOUND" in *" ${AGENT_DOMAIN} "*) ;; *) MISSING_AGENT_COMMENTS="${MISSING_AGENT_COMMENTS} ${AGENT_DOMAIN}" ;; esac
118
162
  done
119
163
 
120
164
  if [ -n "$MISSING_AGENT_COMMENTS" ]; then
@@ -123,6 +167,7 @@ if [ -n "$MISSING_AGENT_COMMENTS" ]; then
123
167
  exit 1
124
168
  fi
125
169
  # END delivery-check
170
+ rm -f "$DELIVERY_CACHE"
126
171
  echo "DELIVERY: OK (${ACTUAL_AGENT_COUNT}/${SELECTED_AGENT_COUNT} launched reviewers posted a findings block for head ${REVIEW_SHA_FULL})"
127
172
  exit 0
128
173
  fi
@@ -19,12 +19,12 @@ OTHER=fedcba9876543210fedcba9876543210fedcba98
19
19
  blk() { awk -v n="$1" '$0 == "# BEGIN " n {f=1; next} $0 == "# END " n {f=0} f' "$POST"; }
20
20
 
21
21
  # (1) the fences now live only in the script: the blocks exist, the spec no longer carries them
22
- for b in panel-guard delivery-check test-gate-extract test-gate-verdict; do
22
+ for b in delivery-fetch panel-guard delivery-check test-gate-extract test-gate-verdict; do
23
23
  [ -n "$(blk "$b")" ] && ok || bad "block $b missing from review-staging-post.sh"
24
24
  done
25
25
  if grep -qE 'ACTUAL_AGENT_COUNT=\$\(gh api|MISSING_AGENT_COMMENTS=""|GATE_VERDICT_INVALID=0' "$SPEC"; then bad "the spec must not carry a copy of the panel-guard, delivery or verdict-handling fence"; else ok; fi
26
- # 6.75 carries the findings-block lookahead; the 6.75 empty-list guard precedes the per-domain loop
27
- blk delivery-check | grep -qF 'count "^(?=[\\s\\S]*<!-- REVIEW-FINDINGS-START -->[\\s\\S]*<!-- REVIEW-FINDINGS-END -->)<!-- FORGE:REVIEW-AGENT:${AGENT_DOMAIN} -->' && ok || bad "6.75 count must require the findings block"
26
+ # 6.75 carries the findings-block lookahead (in the shared cached-listing counter); the 6.75 empty-list guard precedes the per-domain loop
27
+ blk delivery-fetch | grep -qF 'count "^(?=[\\s\\S]*<!-- REVIEW-FINDINGS-START -->[\\s\\S]*<!-- REVIEW-FINDINGS-END -->)<!-- FORGE:REVIEW-AGENT:${AGENT_DOMAIN} -->' && ok || bad "6.75 count must require the findings block"
28
28
  G=$(blk delivery-check | grep -n 'launched-agent list is empty or unreadable' | head -1 | cut -d: -f1); L=$(blk delivery-check | grep -n '^for AGENT_DOMAIN in' | head -1 | cut -d: -f1)
29
29
  [ -n "$G" ] && [ -n "$L" ] && [ "$G" -lt "$L" ] && ok || bad "6.75 empty-list guard must precede the for AGENT_DOMAIN loop"
30
30
  # the producer never coerces an absent marker to SKIP; the catch-all arm never sets SKIP
@@ -38,7 +38,8 @@ bash "$POST" delivery --repo o/r --pr 1 >/dev/null 2>&1; [ $? -eq 2 ] && ok || b
38
38
  bash "$POST" test-gate --repo o/r --pr 1 --sd "$TMP/u" --output x --prs-file "$TMP/none" >/dev/null 2>&1; [ $? -eq 2 ] && ok || bad "unreadable --prs-file must exit 2"
39
39
  bash "$POST" test-gate --repo o/r --pr 1 --sd "$TMP/u" --output x --prs-file /dev/null --posture lax >/dev/null 2>&1; [ $? -eq 2 ] && ok || bad "unknown --posture must exit 2"
40
40
 
41
- # gh stub: $FIX/comments.json is the PR's comment list; posted bodies and edits go to $FIX/posted / $FIX/edits
41
+ # gh stub: $FIX/comments.json is the PR's comment list ($FIX/comments.<k>.json, when present, answers the k-th listing; no file = API error);
42
+ # each listing is logged to $FIX/api_calls; posted bodies and edits go to $FIX/posted / $FIX/edits
42
43
  mkdir -p "$TMP/bin"
43
44
  cat > "$TMP/bin/gh" <<'STUB'
44
45
  #!/usr/bin/env bash
@@ -49,17 +50,21 @@ case "$1 $2" in
49
50
  "label create") exit 0 ;;
50
51
  "pr view") [ -f "$F/head" ] && { cat "$F/head"; exit 0; }; exit 1 ;;
51
52
  esac
52
- if [ "$1" = api ]; then case "$a" in *"/comments"*) cat "$F/comments.json" 2>/dev/null || exit 1; exit 0 ;; esac; fi
53
+ if [ "$1" = api ]; then case "$a" in *"/comments"*)
54
+ echo x >> "$F/api_calls"; k=$(wc -l < "$F/api_calls" | tr -d ' ')
55
+ if [ -f "$F/comments.$k.json" ]; then cat "$F/comments.$k.json"; else cat "$F/comments.json" 2>/dev/null || exit 1; fi; exit 0 ;; esac; fi
53
56
  echo "gh stub: unhandled: $a" >&2; exit 1
54
57
  STUB
55
58
  printf '#!/bin/sh\nexit 1\n' > "$TMP/bin/yq" # no yq: defaults apply
56
59
  chmod +x "$TMP/bin/gh" "$TMP/bin/yq"
57
60
  export PATH="$TMP/bin:$PATH"
61
+ export FORGE_REVIEW_DELIVERY_RETRY_DELAYS="0 0 0" # retries without waiting (4 attempts)
58
62
  newfix() { FIX="$TMP/fix.$1"; rm -rf "$FIX"; mkdir -p "$FIX/sd"; export FIX; }
59
63
  FB=$'<!-- REVIEW-FINDINGS-START -->\n<!-- REVIEW-FINDINGS-END -->'
60
64
  cm() { jq -n --arg b "$1" --arg t "${2:-Bot}" '{body:$b, author_association:"NONE", user:{type:$t, login:"x"}}'; }
61
65
  agent() { printf '<!-- FORGE:REVIEW-AGENT:%s -->\nReviewed-SHA: %s\n\n%s' "$1" "${2:-$HEAD}" "${3-$FB}"; }
62
- comments() { jq -s '.' > "$FIX/comments.json"; }
66
+ comments() { jq -s '.' > "$FIX/comments${1:+.$1}.json"; }
67
+ calls() { [ -f "$FIX/api_calls" ] && wc -l < "$FIX/api_calls" | tr -d ' ' || echo 0; }
63
68
  deliver() { bash "$POST" delivery --repo o/r --pr 42 --sd "$FIX/sd" --head "${1:-$HEAD}" 2>&1; echo "rc=$?"; }
64
69
 
65
70
  # (3) delivery: a complete panel passes and posts nothing
@@ -67,6 +72,7 @@ newfix full; printf 'security\nmaterial-change\n' > "$FIX/sd/42_launched_review_
67
72
  { cm "$(agent security)"; cm "$(agent material-change)"; } | comments
68
73
  OUT=$(deliver); case "$OUT" in *"DELIVERY: OK (2/2"*"rc=0") ok ;; *) bad "complete panel must pass (got: $OUT)" ;; esac
69
74
  [ ! -s "$FIX/posted" ] && ok || bad "a complete panel must post nothing"
75
+ [ "$(calls)" = 1 ] && ok || bad "delivery: complete panel lists comments once (got $(calls) listings)"
70
76
  # a duplicate domain line does not inflate the selected count
71
77
  printf 'security\nsecurity\nmaterial-change\n' > "$FIX/sd/42_launched_review_agents"
72
78
  OUT=$(deliver); case "$OUT" in *"rc=0") ok ;; *) bad "duplicate launched lines must count once (got: $OUT)" ;; esac
@@ -94,9 +100,35 @@ newfix empty; : > "$FIX/sd/42_launched_review_agents"; cm "$(agent security)" |
94
100
  OUT=$(deliver); case "$OUT" in *"rc=1") ok ;; *) bad "empty launched list must fail (got: $OUT)" ;; esac
95
101
  newfix none; cm "$(agent security)" | comments
96
102
  OUT=$(deliver); case "$OUT" in *"rc=1") ok ;; *) bad "absent launched list must fail (got: $OUT)" ;; esac
97
- # unreadable comments count as 0 delivered
103
+ # unreadable comments (an API error on every attempt) fail closed as UNVERIFIED, never as missing (forge#5166)
98
104
  newfix unreadable; printf 'security\n' > "$FIX/sd/42_launched_review_agents"
99
- OUT=$(deliver); case "$OUT" in *"rc=1") ok ;; *) bad "unreadable comments must fail closed (got: $OUT)" ;; esac
105
+ OUT=$(deliver); case "$OUT" in *"REVIEW DELIVERY UNVERIFIED"*"rc=2") ok ;; *) bad "delivery: API error on every attempt is UNVERIFIED (got: $OUT)" ;; esac
106
+ case "$OUT" in *"REVIEW DELIVERY FAILURE"*) bad "an API error must not be reported as missing" ;; *) ok ;; esac
107
+ [ ! -s "$FIX/posted" ] && [ ! -s "$FIX/edits" ] && ok || bad "UNVERIFIED must post no GATE_FAILURE and add no label"
108
+ [ "$(calls)" = 4 ] && ok || bad "UNVERIFIED must come after every retry (got $(calls) listings)"
109
+ # delivery: lagging listing passes on retry -- the first listing lacks a comment, the second shows it
110
+ newfix lag; printf 'security\nmaterial-change\n' > "$FIX/sd/42_launched_review_agents"
111
+ cm "$(agent security)" | comments 1
112
+ { cm "$(agent security)"; cm "$(agent material-change)"; } | comments
113
+ OUT=$(deliver); case "$OUT" in *"DELIVERY: OK (2/2"*"rc=0") ok ;; *) bad "delivery: lagging listing passes on retry (got: $OUT)" ;; esac
114
+ case "$OUT" in *"material-change found on attempt 2"*) ok ;; *) bad "lag: must print the attempt a domain was found on (got: $OUT)" ;; esac
115
+ case "$OUT" in *"security found on attempt 1"*) ok ;; *) bad "lag: security must be found on attempt 1 (got: $OUT)" ;; esac
116
+ [ ! -s "$FIX/posted" ] && [ ! -s "$FIX/edits" ] && ok || bad "lag: a panel delivered on retry must post and label nothing"
117
+ [ "$(calls)" = 2 ] && ok || bad "lag: retries stop once every domain is found (got $(calls) listings)"
118
+ # a transient API error, then a full listing, passes
119
+ newfix transient; printf 'security\n' > "$FIX/sd/42_launched_review_agents"
120
+ echo 'not json' > "$FIX/comments.1.json"; cm "$(agent security)" | comments
121
+ OUT=$(deliver); case "$OUT" in *"attempt 1: API error"*"security found on attempt 2"*"rc=0") ok ;; *) bad "a transient API error must be retried (got: $OUT)" ;; esac
122
+ # a real absence after every retry is still missing (listed and absent), with the panel-integrity failure
123
+ newfix absent; printf 'security\nmaterial-change\n' > "$FIX/sd/42_launched_review_agents"; cm "$(agent security)" | comments
124
+ OUT=$(deliver); case "$OUT" in *"rc=1") ok ;; *) bad "absent after retries must fail as missing (got: $OUT)" ;; esac
125
+ case "$OUT" in *UNVERIFIED*) bad "a listed absence is not UNVERIFIED" ;; *) ok ;; esac
126
+ grep -qF 'review-panel-integrity' "$FIX/posted" 2>/dev/null && ok || bad "absent: must post GATE_FAILURE:TYPE=review-panel-integrity"
127
+ [ "$(calls)" = 4 ] && ok || bad "absent: must retry before declaring missing (got $(calls) listings)"
128
+ # an API error on the LAST attempt after a lacking listing is UNVERIFIED, not missing
129
+ newfix lasterr; printf 'security\nmaterial-change\n' > "$FIX/sd/42_launched_review_agents"
130
+ cm "$(agent security)" | comments 1; cm "$(agent security)" | comments 2; cm "$(agent security)" | comments 3; echo '{}' > "$FIX/comments.4.json"
131
+ OUT=$(deliver); case "$OUT" in *"REVIEW DELIVERY UNVERIFIED"*"rc=2") ok ;; *) bad "an error on the last attempt must be UNVERIFIED (got: $OUT)" ;; esac
100
132
  # a malformed --head with no resolvable head refuses
101
133
  newfix badhead; printf 'security\n' > "$FIX/sd/42_launched_review_agents"; cm "$(agent security)" | comments
102
134
  OUT=$(deliver "n/a"); case "$OUT" in *"rc=1") ok ;; *) bad "an unresolvable head must fail closed (got: $OUT)" ;; esac