fix(scripts): pr-reviews reads all three review surfaces (improvise-xgra)

It printed review BODIES only and was blind to inline review comments --
the surface a human reviewer is most likely to use. On PR #122 the user
reviewed with the body "Generally good, added comments inline." and five
substantive rulings underneath it; pr-reviews printed the one-liner, and
an agent reported the PR as awaiting review while the answers sat on it.

pulls/{n}/comments and issues/{n}/comments join pulls/{n}/reviews, all
three interleaved in time order, each inline comment carrying its path
and line (falling back to original_line, which is what an outdated
comment has left).

The exit contract is unchanged: 0 means something exists against the
current head, 1 means the wait elapsed, 4 is usage. An inline comment at
the head now satisfies --wait -- it carries a commit_id and is exactly
the case that failed. An issue-level comment does not: it names no
commit and so makes no claim about which tree was read.

The usage's exit-code warning is widened from "do not pipe into
head/tail" to the real rule: nothing may follow the script in the same
`;` list either, because a trailing `echo "exit=$?"` makes the status
the echo's.

scripts/pr-reviews-test.sh is the check_the_checker, fixture-driven and
needing no network, wired into CI's kb job beside pr-watch-test.sh and
release-preflight-test.sh. Five faults were injected and watched to
fail; context/test-evidence/tooling.md records which case caught each.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
This commit is contained in:
Edward Langley
2026-09-22 13:49:36 -07:00
parent d672985b07
commit a47310d50f
5 changed files with 420 additions and 26 deletions
+4
View File
@@ -273,6 +273,10 @@ jobs:
# a tag is already public and immutable, so its refusals are checked here.
- name: The release preflight refuses what it claims to
run: ./scripts/release-preflight-test.sh
# pr-reviews is what stands between a merge and an unread review, and it
# was blind to inline comments for as long as it existed (improvise-xgra).
- name: The PR review reader refuses what it claims to
run: ./scripts/pr-reviews-test.sh
# ---------------------------------------------------------------------------
# Lints the one crate the `test` job cannot reach.
+55
View File
@@ -9,6 +9,61 @@ PR, every one a P1.
`scripts/pr-reviews <n>` prints them. `--wait <sec>` blocks until one lands
against the current head.
## Three surfaces, because a reviewer writes on whichever one is nearest
GitHub has three places to put a review, and they are three different API
endpoints:
| printed as | endpoint | what it is |
|---|---|---|
| `[REVIEW]` | `pulls/{n}/reviews` | the review's own body |
| `[INLINE]` | `pulls/{n}/comments` | a comment on a line of the diff |
| `[COMMENT]` | `issues/{n}/comments` | a comment on the PR thread |
**`pr-reviews` read only the first for as long as it existed** (improvise-xgra),
and the surface it could not see is the one a human is most likely to use. On
PR #122 the user reviewed with the body *"Generally good, added comments
inline."* and put five substantive rulings underneath it as inline comments.
`pr-reviews` printed the one-line body faithfully and nothing else, and an
agent reported the PR as awaiting review while the answers sat on it. The user
had to say so.
`scripts/pr-watch` does not cover the gap: that one watches **checks**. Nothing
in `scripts/` surfaced the surface a human writes on.
The three are **interleaved in time order**, not printed in three blocks,
because a review body and its inline comments are one act of reading and a
block-per-endpoint layout hides which body the rulings belong under.
## What satisfies `--wait`
An **inline comment at the current head does**, and that is the whole point of
the fix rather than an incidental widening. `--wait`'s question is *has anyone
read this tree yet*, and a `pulls/{n}/comments` entry carries a `commit_id`: it
is a review comment, written by a reviewer, about a specific commit. The case
that failed on #122 is exactly one where the substance was inline — treating it
as not-a-review would leave `--wait` blind to the reviews it most needs to
catch. Note that a single "Add single comment" on GitHub creates a review whose
**body is empty**, so under the old rule that reviewer's exit code was right
and their words were invisible; the surfaces and the wait have to move together.
An **issue-level comment does not**, because it carries no commit. It makes no
claim about which tree was read, and "I am rebasing this shortly" satisfying a
wait for a review would be the widening that looks like the fix.
A comment on an **earlier** head does not satisfy the wait either — same rule
the review bodies already had, and the same reason: it may be fixed or still
live, and only reading it tells you which. An outdated comment has `line: null`
and only `original_line` left, which is what is printed.
## `$?` must be this script's own
The usage text used to warn against piping into `head`/`tail`. That is narrower
than the real rule: **nothing may follow the script in the same `;` list
either**, because a trailing `echo "exit=$?"` makes the status the echo's. An
agent nearly reported a review that did not exist that way. Redirect to a file
and read the file in a later command.
## Why a review is easy to miss here
It arrives as a **review**, not a comment, so reading the comment thread does
+47
View File
@@ -219,3 +219,50 @@ The live tree was itself the first injection: before the three merges the check
exited 1 naming all three pairs with both line numbers; after them it exits 0.
The clean arms (two headings naming two tests, and prose repeated) are what
stop an always-refuse from satisfying the rest.
### The PR review reader's own suite — `scripts/pr-reviews-test.sh`
improvise-xgra. Like `kb-test.sh` and `pr-watch-test.sh`, its cases are already
injected faults and none of their names is a Rust `fn` the evidence checker
could resolve, so the case list is the evidence. What is recorded here is the
five faults it was watched to fail against, and which case caught each — a
suite that only asked "did it print something" passes all five.
Fault 1, **the inline surface dropped entirely**, which is the live bug as it
shipped: `inline: []` in both fetch paths. Eight cases fail, including both
`--wait` arms. This is the one that matters, because the old script's behaviour
*was* this fault and it exited 0 on PR #122's review body while printing none
of the five rulings underneath it.
Fault 2, **an inline comment counted rather than compared**:
`((.inline|length) > 0)` in place of the `commit_id == head` test. One case
fails, `an inline comment at an EARLIER head does not satisfy --wait`, and that
is the pair the two inline fixtures exist to discriminate — the at-head case
passes under this fault, so on its own it proves nothing.
Fault 3, **the time sort dropped** (`| .` for `| sort_by(.at)`). Only
`the three surfaces interleave in time order` fails: it reads the three bodies
back as `SECOND THIRD FIRST`, the per-endpoint concatenation order. Every
content assertion still passes, because each body is still printed.
Fault 4, **no `original_line` fallback**. Only the outdated-comment case fails.
`line` is null exactly when the line has moved under the comment, so a renderer
reading `.line` alone prints `draw.rs:null` on precisely the comments whose
location is hardest to recover by hand.
Fault 5, **`--wait` widened to any issue-level comment**. Only
`an issue-level comment does not satisfy --wait` fails. That is the fault a
well-meaning repair would introduce — "wait for anyone to say anything" — and
the fixture that separates it is a PR thread comment with no `commit_id` at
all.
Two cases are there for the contract rather than the bug:
`a review body at the current head still satisfies --wait` and
`--wait 0 over an empty PR is success`, because other briefs depend on exit 0
and exit 1 meaning what they did before. `no PR number is exit 4` pins the
third code, which is distinguishable from 1 only if nothing collapses them.
`the usage warns about a trailing $? reader` asserts a sentence rather than a
behaviour, which is unusual and deliberate: the `; echo "exit=$?"` trap costs a
wrong diagnosis every time it is hit, the warning is the only guard against it,
and a header is exactly the thing a later edit trims without noticing.
+99 -26
View File
@@ -1,28 +1,43 @@
#!/usr/bin/env bash
# Prints a PR's reviews, and optionally waits for one against the current head.
# Exists because twenty PRs merged with their reviews unread; see
# context/code/pr-review.md.
# Prints everything a reviewer may have written on a PR -- review bodies,
# INLINE review comments and issue-level comments -- and optionally waits for
# one against the current head. See context/code/pr-review.md.
set -euo pipefail
usage() {
cat >&2 <<'EOF'
usage: pr-reviews <pr-number> [--wait <sec>] [--interval <sec>]
--wait <sec> wait up to this long for a review whose commit is the
current head (default 0 = print what exists and exit)
--wait <sec> wait up to this long for a review or an inline review
comment whose commit is the current head
(default 0 = print what exists and exit)
--interval <sec> seconds between polls (default 30)
exit 0 a review against the current head exists (or --wait was 0)
exit 1 --wait elapsed with no review against the current head
exit 0 something against the current head exists (or --wait was 0)
exit 1 --wait elapsed with nothing against the current head
exit 4 usage or environment error
DO NOT PIPE THIS INTO head/tail. `$?` is then the pager's status, not this
script's, and a --wait that timed out reads as success. That trap has cost
this repo four separate diagnoses. Redirect to a file and read it instead.
THIS SCRIPT'S `$?` MUST BE THE ONE YOU READ. Nothing may follow it in the same
pipeline OR the same `;` list: `pr-reviews 1 --wait 60 | tail` gives you tail's
status, and so does `pr-reviews 1 --wait 60; echo "exit=$?"` -- the echo runs
first and $? is then the echo's. Either way a --wait that TIMED OUT reads as
success. That trap has cost this repo five separate diagnoses, the most recent
an agent that nearly reported a review which did not exist. Redirect to a file,
then read the file in a later command.
A review may target an EARLIER head, which is why the head is compared rather
than the count: a finding against an old commit may be fixed or still live, and
only reading it tells you which.
Three surfaces, because a reviewer writes on whichever one is nearest:
[REVIEW] the review's own body (pulls/{n}/reviews)
[INLINE] a comment on a line of the diff (pulls/{n}/comments)
[COMMENT] a comment on the PR thread (issues/{n}/comments)
They are interleaved in time order. INLINE is the one a human is most likely to
use and the one this script was blind to until improvise-xgra: PR #122 was
reviewed with the body "Generally good, added comments inline." and five
substantive rulings underneath it, of which this script printed none.
A review or comment may target an EARLIER head, which is why the head is
compared rather than the count: a finding against an old commit may be fixed or
still live, and only reading it tells you which. An issue-level [COMMENT] is
attached to no commit at all, so it can never satisfy --wait.
EOF
exit 4
}
@@ -48,24 +63,82 @@ gh_() {
fi
}
fetch() { gh_ pr view "$PR" --json headRefOid,reviews; }
# One poll reads all three surfaces, so the counter is bumped once per poll and
# not once per file. It lives on disk because fetch is called as "$(fetch)" and
# a command substitution is a SUBSHELL -- a variable incremented in it is lost,
# which is how pr-watch's fixtures could never reach their second poll.
fixture_poll() {
local c="$PR_REVIEWS_FIXTURE_DIR/.poll"
local n=$(( $(cat "$c" 2>/dev/null || echo 0) + 1 ))
echo "$n" > "$c"
echo "$n"
}
fixture_file() {
local n="$1" name="$2" f="$PR_REVIEWS_FIXTURE_DIR/$n.$name.json"
[ -f "$f" ] || f="$PR_REVIEWS_FIXTURE_DIR/last.$name.json"
[ -f "$f" ] && cat "$f" || echo 'null'
}
fetch() {
if [ -n "${PR_REVIEWS_FIXTURE_DIR:-}" ]; then
local n; n=$(fixture_poll)
jq -n \
--argjson pr "$(fixture_file "$n" pr)" \
--argjson inline "$(fixture_file "$n" inline)" \
--argjson issue "$(fixture_file "$n" issue)" \
'{head: ($pr.headRefOid // ""), reviews: ($pr.reviews // []),
inline: ($inline // []), issue: ($issue // [])}'
return
fi
local repo pr inline issue
repo=$(gh_ repo view --json nameWithOwner -q .nameWithOwner)
pr=$(gh_ pr view "$PR" --json headRefOid,reviews)
inline=$(gh_ api "repos/$repo/pulls/$PR/comments" --paginate)
issue=$(gh_ api "repos/$repo/issues/$PR/comments" --paginate)
jq -n --argjson pr "$pr" --argjson inline "$inline" --argjson issue "$issue" \
'{head: ($pr.headRefOid // ""), reviews: ($pr.reviews // []),
inline: ($inline // []), issue: ($issue // [])}'
}
# `line` is null once the line has moved under the comment; `original_line` is
# where it was written, and is all an outdated comment has left.
normalize='
[ (.reviews[]? | {at: .submittedAt, kind: "REVIEW", state: (.state // ""),
who: (.author.login // "?"), commit: (.commit.oid // ""),
at_line: "", body: (.body // "")}),
(.inline[]? | {at: .created_at, kind: "INLINE", state: "",
who: (.user.login // "?"), commit: (.commit_id // ""),
at_line: "\(.path // "?"):\(.line // .original_line // "?")",
body: (.body // "")}),
(.issue[]? | {at: .created_at, kind: "COMMENT", state: "",
who: (.user.login // "?"), commit: "",
at_line: "", body: (.body // "")}) ]
| sort_by(.at)'
report() {
local json="$1" head
head=$(printf '%s' "$json" | jq -r '.headRefOid')
head=$(printf '%s' "$json" | jq -r '.head')
printf 'pr-reviews: #%s head=%s\n' "$PR" "${head:0:8}"
printf '%s' "$json" | jq -r --arg head "$head" '
if (.reviews | length) == 0 then " (no reviews)"
else .reviews[] |
" [\(.state)] \(.submittedAt) commit=\(.commit.oid[0:8])" +
(if .commit.oid == $head then " <- CURRENT HEAD" else " <- an EARLIER head; verify whether it still applies" end) +
"\n" + (.body | split("\n") | map(" " + .) | join("\n"))
end'
printf '%s' "$json" | jq -r --arg head "$head" "$normalize"'
| if length == 0 then " (nothing written on any of the three surfaces)"
else .[] |
" [\(.kind)\(if .state == "" then "" else " " + .state end)] \(.at) @\(.who)" +
(if .at_line == "" then "" else " " + .at_line end) +
(if .commit == "" then " <- not attached to a commit"
elif .commit == $head then " commit=\(.commit[0:8]) <- CURRENT HEAD"
else " commit=\(.commit[0:8]) <- an EARLIER head; verify whether it still applies" end) +
"\n" + (.body | split("\n") | map(" " + .) | join("\n"))
end'
}
# An inline comment at the head counts, and that is the whole point of the fix:
# the case that failed was a reviewer whose substance was entirely inline. An
# issue-level comment carries no commit and so cannot count -- it makes no claim
# about which tree was read.
has_head_review() {
printf '%s' "$1" | jq -e --arg h "$(printf '%s' "$1" | jq -r .headRefOid)" \
'any(.reviews[]?; .commit.oid == $h)' >/dev/null 2>&1
printf '%s' "$1" | jq -e --arg h "$(printf '%s' "$1" | jq -r .head)" \
'any((.reviews[]?.commit.oid), (.inline[]?.commit_id); . == $h)' >/dev/null 2>&1
}
json=$(fetch)
@@ -80,12 +153,12 @@ while [ "$(date +%s)" -lt "$deadline" ]; do
sleep "$INTERVAL"
json=$(fetch)
if has_head_review "$json"; then
printf '\npr-reviews: a review against the current head arrived.\n\n'
printf '\npr-reviews: a review or inline comment against the current head arrived.\n\n'
report "$json"
exit 0
fi
done
printf '\npr-reviews: waited %ss; no review against the current head.\n' "$WAIT"
printf '\npr-reviews: waited %ss; nothing against the current head.\n' "$WAIT"
printf 'Reviews arrive as the owner quota allows, so absence is not approval.\n'
exit 1
+215
View File
@@ -0,0 +1,215 @@
#!/usr/bin/env bash
# check_the_checker for scripts/pr-reviews. Fixture-driven, no network.
# The case that matters is improvise-xgra: a reviewer whose substance is
# entirely inline. See context/test-evidence/tooling.md.
set -uo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REVIEWS="$here/pr-reviews"
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
pass=0; fail=0
ok() { printf ' ok %s\n' "$1"; pass=$((pass+1)); }
bad() { printf ' FAIL %s (%s)\n' "$1" "$2"; fail=$((fail+1)); }
# $1 name, $2 expected exit, $3 fixture dir, rest: extra args. Output goes to
# $work/out so a case may assert on what was PRINTED as well as on $?; nothing
# follows the script in its own pipeline, which is the rule its header states.
run_case() {
local name="$1" want="$2" dir="$3"; shift 3
rm -f "$dir/.poll"
PR_REVIEWS_FIXTURE_DIR="$dir" "$REVIEWS" 1 "$@" > "$work/out" 2>&1
local got=$?
[ "$got" = "$want" ] && ok "$name" || bad "$name" "exit $got, wanted $want"
}
# $1 name, $2 fixture dir, $3 grep -E pattern the output must contain
prints() {
local name="$1" dir="$2" pat="$3"
rm -f "$dir/.poll"
PR_REVIEWS_FIXTURE_DIR="$dir" "$REVIEWS" 1 > "$work/out" 2>&1
grep -Eq "$pat" "$work/out" && ok "$name" || bad "$name" "no /$pat/ in output"
}
mk() { mkdir -p "$1"; cat > "$1/$2"; }
HEAD=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
OLD=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
# ---------------------------------------------------------------------------
# The bug this script exists for. The review BODY is a one-liner at an older
# head; every ruling is an inline comment at the current head. A pr-reviews
# that reads only .reviews prints the one-liner, reports exit 1, and the
# rulings are invisible -- which is what happened on PR #122.
d="$work/inline-at-head"
mk "$d" last.pr.json <<EOF
{"headRefOid":"$HEAD","reviews":[
{"state":"COMMENTED","submittedAt":"2026-09-22T10:00:00Z","author":{"login":"owner"},
"commit":{"oid":"$OLD"},"body":"Generally good, added comments inline."}]}
EOF
mk "$d" last.inline.json <<EOF
[{"created_at":"2026-09-22T10:00:05Z","user":{"login":"owner"},"commit_id":"$HEAD",
"path":"crates/improvise-core/src/model/types.rs","line":412,
"body":"Use Aggregation::Exact here, not the none_cats list."}]
EOF
mk "$d" last.issue.json <<'EOF'
[]
EOF
run_case "an inline comment at the current head satisfies --wait" 0 "$d" --wait 1 --interval 1
prints "an inline comment's body is printed" "$d" 'Aggregation::Exact'
prints "an inline comment names its file and line" "$d" 'types\.rs:412'
prints "an inline comment at the head is marked CURRENT HEAD" "$d" 'types\.rs:412 commit=aaaaaaaa <- CURRENT HEAD'
prints "a review body at an earlier head still says so" "$d" 'commit=bbbbbbbb <- an EARLIER head'
# ---------------------------------------------------------------------------
# The mirror-image fault: an inline comment about a tree that no longer
# exists must NOT satisfy the wait. A version that counts inline comments
# rather than comparing their commit passes the case above and this one
# wrongly, so the pair is what discriminates.
d="$work/inline-at-old"
mk "$d" last.pr.json <<EOF
{"headRefOid":"$HEAD","reviews":[]}
EOF
mk "$d" last.inline.json <<EOF
[{"created_at":"2026-09-22T09:00:00Z","user":{"login":"owner"},"commit_id":"$OLD",
"path":"src/draw.rs","line":10,"body":"stale finding"}]
EOF
mk "$d" last.issue.json <<'EOF'
[]
EOF
run_case "an inline comment at an EARLIER head does not satisfy --wait" 1 "$d" --wait 1 --interval 1
prints "an earlier-head inline comment is still printed, flagged" "$d" \
'draw\.rs:10 commit=bbbbbbbb <- an EARLIER head'
# ---------------------------------------------------------------------------
# The contract that must not change: a review body at the head still counts.
d="$work/review-at-head"
mk "$d" last.pr.json <<EOF
{"headRefOid":"$HEAD","reviews":[
{"state":"APPROVED","submittedAt":"2026-09-22T11:00:00Z","author":{"login":"owner"},
"commit":{"oid":"$HEAD"},"body":"LGTM"}]}
EOF
mk "$d" last.inline.json <<'EOF'
[]
EOF
mk "$d" last.issue.json <<'EOF'
[]
EOF
run_case "a review body at the current head still satisfies --wait" 0 "$d" --wait 1 --interval 1
prints "a review prints its state" "$d" '\[REVIEW APPROVED\]'
# ---------------------------------------------------------------------------
# An issue-level comment is attached to no commit, so it makes no claim about
# which tree was read and cannot satisfy the wait. Widening --wait to "any
# text at all" would pass the first case and fail this one.
d="$work/issue-only"
mk "$d" last.pr.json <<EOF
{"headRefOid":"$HEAD","reviews":[]}
EOF
mk "$d" last.inline.json <<'EOF'
[]
EOF
mk "$d" last.issue.json <<'EOF'
[{"created_at":"2026-09-22T12:00:00Z","user":{"login":"owner"},
"body":"rebasing this shortly"}]
EOF
run_case "an issue-level comment does not satisfy --wait" 1 "$d" --wait 1 --interval 1
prints "an issue-level comment is still printed" "$d" '\[COMMENT\].*<- not attached to a commit'
# ---------------------------------------------------------------------------
# Time order across the three surfaces. A concatenation that prints each
# surface in its own block passes every case above and fails this one.
d="$work/interleaved"
mk "$d" last.pr.json <<EOF
{"headRefOid":"$HEAD","reviews":[
{"state":"COMMENTED","submittedAt":"2026-09-22T10:00:00Z","author":{"login":"owner"},
"commit":{"oid":"$HEAD"},"body":"SECOND"}]}
EOF
mk "$d" last.inline.json <<EOF
[{"created_at":"2026-09-22T11:00:00Z","user":{"login":"owner"},"commit_id":"$HEAD",
"path":"a.rs","line":1,"body":"THIRD"}]
EOF
mk "$d" last.issue.json <<'EOF'
[{"created_at":"2026-09-22T09:00:00Z","user":{"login":"owner"},"body":"FIRST"}]
EOF
rm -f "$d/.poll"
PR_REVIEWS_FIXTURE_DIR="$d" "$REVIEWS" 1 > "$work/out" 2>&1
order=$(grep -oE 'FIRST|SECOND|THIRD' "$work/out" | tr '\n' ' ')
[ "$order" = "FIRST SECOND THIRD " ] \
&& ok "the three surfaces interleave in time order" \
|| bad "the three surfaces interleave in time order" "got: $order"
# ---------------------------------------------------------------------------
# An outdated comment has `line: null` and only `original_line` left. A
# renderer that reads `.line` alone prints "a.rs:null" and loses the location.
d="$work/outdated-line"
mk "$d" last.pr.json <<EOF
{"headRefOid":"$HEAD","reviews":[]}
EOF
mk "$d" last.inline.json <<EOF
[{"created_at":"2026-09-22T10:00:00Z","user":{"login":"owner"},"commit_id":"$HEAD",
"path":"src/draw.rs","line":null,"original_line":77,"body":"moved since"}]
EOF
mk "$d" last.issue.json <<'EOF'
[]
EOF
prints "an outdated inline comment falls back to original_line" "$d" 'draw\.rs:77'
# ---------------------------------------------------------------------------
# Nothing on any surface, and the two shapes of that.
d="$work/empty"
mk "$d" last.pr.json <<EOF
{"headRefOid":"$HEAD","reviews":[]}
EOF
mk "$d" last.inline.json <<'EOF'
[]
EOF
mk "$d" last.issue.json <<'EOF'
[]
EOF
run_case "--wait 0 over an empty PR is success" 0 "$d"
run_case "--wait over an empty PR times out" 1 "$d" --wait 1 --interval 1
prints "an empty PR says so" "$d" 'nothing written on any of the three surfaces'
# ---------------------------------------------------------------------------
# An inline comment that arrives on a later poll. A script that fetched once
# and then only re-read the first response would sit here until the deadline.
d="$work/arrives-later"
mk "$d" 1.pr.json <<EOF
{"headRefOid":"$HEAD","reviews":[]}
EOF
mk "$d" 1.inline.json <<'EOF'
[]
EOF
mk "$d" 1.issue.json <<'EOF'
[]
EOF
mk "$d" last.pr.json <<EOF
{"headRefOid":"$HEAD","reviews":[]}
EOF
mk "$d" last.inline.json <<EOF
[{"created_at":"2026-09-22T10:00:00Z","user":{"login":"owner"},"commit_id":"$HEAD",
"path":"a.rs","line":3,"body":"arrived on the second poll"}]
EOF
mk "$d" last.issue.json <<'EOF'
[]
EOF
run_case "an inline comment arriving mid-wait is seen" 0 "$d" --wait 1 --interval 1
# ---------------------------------------------------------------------------
# Usage is exit 4 and must stay there: briefs distinguish it from 1.
rm -f "$work/empty/.poll"
PR_REVIEWS_FIXTURE_DIR="$work/empty" "$REVIEWS" > "$work/out" 2>&1
got=$?
[ "$got" = 4 ] && ok "no PR number is exit 4" || bad "no PR number is exit 4" "exit $got"
# The header must keep saying that nothing may follow this script, because the
# `; echo` half of that trap is the one an agent hit this week.
"$REVIEWS" --help > "$work/out" 2>&1
grep -q 'echo "exit=\$?"' "$work/out" \
&& ok "the usage warns about a trailing \$? reader" \
|| bad "the usage warns about a trailing \$? reader" "not in --help"
printf '\npr-reviews-test: %d passed, %d failed\n' "$pass" "$fail"
[ "$fail" -eq 0 ]