All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
Round 2 remediation for the read-back verification in issue-comment.sh and pr-review.sh. BLOCKER A (invocation attribution): id-above-boundary + content/state match only proves temporal ordering — a concurrent write from a different identity could satisfy it while this tea invocation created nothing. Both wrappers now resolve the acting identity once via curl GET /api/v1/user and additionally require the accepted record's author login to equal that identity. Residual same-identity same-body/state concurrency is documented in-code (tea 0.11.1 emits no reliable created-record id to close it further). BLOCKER B (pagination): the comments and reviews list reads now walk every page (?limit=&page=1,2,… until a short/empty page) for both the pre-write boundary and the post-write read-back, so a record beyond page 1 is still found. Adds regressions: concurrent different-identity write fails closed (comments and reviews); a matching review beyond page 1 is still found. README updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
448 lines
18 KiB
Bash
448 lines
18 KiB
Bash
#!/usr/bin/env bash
|
|
# Regression harness for durable Gitea PR review comments (#812) and for the
|
|
# approve/reject `--comment` flag removal (#835). The `tea` stub below rejects
|
|
# any `-comment`/`--comment` flag on `pr approve`/`pr reject` exactly like real
|
|
# `tea` v0.11.1 does ("flag provided but not defined: -comment"), so this
|
|
# harness fails RED against the pre-#835 wrapper (which passed that flag) and
|
|
# only passes once the wrapper routes the review body through the durable
|
|
# comment REST API instead.
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-review-gitea-comment}"
|
|
REPO_DIR="$WORK_DIR/repo"
|
|
BIN_DIR="$WORK_DIR/bin"
|
|
TEA_LOG="$WORK_DIR/tea.log"
|
|
CURL_LOG="$WORK_DIR/curl.log"
|
|
OUTPUT_FILE="$WORK_DIR/output.log"
|
|
CREDENTIALS_FILE="$WORK_DIR/credentials.json"
|
|
|
|
cleanup() {
|
|
rm -rf "$WORK_DIR"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
ACTING_LOGIN="review-bot"
|
|
FOREIGN_LOGIN="other-writer"
|
|
|
|
mkdir -p "$REPO_DIR" "$BIN_DIR"
|
|
git -C "$REPO_DIR" init -q
|
|
git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
|
|
|
write_credentials() {
|
|
local configured_url="$1"
|
|
CONFIGURED_GITEA_URL="$configured_url" python3 - "$CREDENTIALS_FILE" <<'PY'
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
with open(sys.argv[1], "w", encoding="utf-8") as credentials:
|
|
json.dump({
|
|
"gitea": {
|
|
"mosaicstack": {
|
|
"url": os.environ["CONFIGURED_GITEA_URL"],
|
|
"token": "test-only-placeholder",
|
|
}
|
|
}
|
|
}, credentials)
|
|
PY
|
|
}
|
|
|
|
cat > "$BIN_DIR/tea" <<'SH'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
printf '%s\n' "$*" >> "$PR_REVIEW_TEA_LOG"
|
|
|
|
if [[ "$*" == "login list --output json" ]]; then
|
|
printf '%s\n' '[{"name":"mosaicstack","url":"https://git.mosaicstack.dev"}]'
|
|
exit 0
|
|
fi
|
|
|
|
# tea v0.11.1 defines no --comment/-comment flag on `pr approve` or `pr
|
|
# reject`; it fails closed with this exact message and a nonzero exit. Any
|
|
# regression that reintroduces the flag on those subcommands must hit this
|
|
# branch and fail RED (#835).
|
|
if [[ "$*" == *" -comment "* || "$*" == *" --comment "* || "$*" == *" -comment" || "$*" == *" --comment" ]]; then
|
|
echo "flag provided but not defined: -comment" >&2
|
|
exit 1
|
|
fi
|
|
|
|
case "${PR_REVIEW_TEST_MODE:-}" in
|
|
approve|paginated-approve|foreign-review)
|
|
[[ "$*" == "pr approve 123 --repo mosaicstack/stack --login mosaicstack" ]] || exit 90
|
|
;;
|
|
request-changes)
|
|
[[ "$*" == "pr reject 123 --repo mosaicstack/stack --login mosaicstack" ]] || exit 91
|
|
;;
|
|
legacy-fallback|comment-success|http-success|prefix-success|subpath-success|port-success|scp-ssh-success|url-ssh-success|ssh-transport-port-success|explicit-default-port-success|write-transport-failure|write-http-failure|readback-failure)
|
|
if [[ "$*" == pr\ comment* ]]; then
|
|
# tea v0.11.1 treats the nonexistent subcommand as `tea pr list` and exits 0.
|
|
printf '%s\n' 'INDEX TITLE STATE'
|
|
exit 0
|
|
fi
|
|
echo "Unexpected tea command: $*" >&2
|
|
exit 92
|
|
;;
|
|
*)
|
|
exit 95
|
|
;;
|
|
esac
|
|
SH
|
|
chmod +x "$BIN_DIR/tea"
|
|
|
|
cat > "$BIN_DIR/curl" <<'SH'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
output_file=""
|
|
method="GET"
|
|
payload=""
|
|
url=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-o)
|
|
output_file="$2"
|
|
shift 2
|
|
;;
|
|
-w|-H)
|
|
shift 2
|
|
;;
|
|
-X)
|
|
method="$2"
|
|
shift 2
|
|
;;
|
|
-d|--data)
|
|
payload="$2"
|
|
shift 2
|
|
;;
|
|
-s|-S|-sS)
|
|
shift
|
|
;;
|
|
http://*|https://*)
|
|
url="$1"
|
|
shift
|
|
;;
|
|
*)
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Strip any query string so pagination params (?limit=&page=) don't defeat
|
|
# path matching, but keep the full URL in the log so tests can assert that the
|
|
# read-back paginated.
|
|
path="${url%%\?*}"
|
|
page="${url##*page=}"
|
|
[[ "$page" == "$url" ]] && page=1
|
|
printf '%s %s\n' "$method" "$url" >> "$PR_REVIEW_CURL_LOG"
|
|
|
|
write_response() {
|
|
local status="$1" body="$2"
|
|
[[ -n "$output_file" ]] || exit 96
|
|
printf '%s' "$body" > "$output_file"
|
|
printf '%s' "$status"
|
|
}
|
|
|
|
case "${PR_REVIEW_TEST_MODE:-}" in
|
|
legacy-fallback|write-transport-failure)
|
|
echo "simulated transport failure" >&2
|
|
exit 7
|
|
;;
|
|
write-http-failure)
|
|
write_response 500 '{"message":"simulated rejection"}'
|
|
;;
|
|
approve|request-changes|paginated-approve|foreign-review|comment-success|http-success|prefix-success|subpath-success|port-success|scp-ssh-success|url-ssh-success|ssh-transport-port-success|explicit-default-port-success|readback-failure)
|
|
if [[ "$method" == "GET" && "$path" == "$PR_REVIEW_API_ROOT/user" ]]; then
|
|
# Acting reviewer identity used for invocation attribution.
|
|
write_response 200 "$(PR_REVIEW_LOGIN="$PR_REVIEW_ACTING_LOGIN" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
print(json.dumps({"login": os.environ["PR_REVIEW_LOGIN"]}))
|
|
PY
|
|
)"
|
|
elif [[ "$method" == "GET" && "$path" == "$PR_REVIEW_EXPECTED_API_BASE/pulls/123/reviews" ]]; then
|
|
# First (boundary) call precedes the write; later calls are the
|
|
# post-write read-back that must surface a strictly-newer review
|
|
# created by THIS action (id 200 > boundary 100), authored by the
|
|
# acting identity, with the expected state and pinned to the PR's
|
|
# current head commit. The list is served PAGINATED so a target
|
|
# beyond page 1 is only found by a fully-paginating read-back.
|
|
calls_file="${PR_REVIEW_REVIEW_CALLS:-/dev/null}"
|
|
if [[ -f "$calls_file" ]]; then
|
|
phase="post"
|
|
else
|
|
: > "$calls_file"
|
|
phase="boundary"
|
|
fi
|
|
state="APPROVED"
|
|
[[ "$PR_REVIEW_TEST_MODE" == "request-changes" ]] && state="REQUEST_CHANGES"
|
|
response=$(PR_REVIEW_PHASE="$phase" PR_REVIEW_PAGE="$page" \
|
|
PR_REVIEW_MODE="$PR_REVIEW_TEST_MODE" PR_REVIEW_STATE="$state" \
|
|
PR_REVIEW_ACTING_LOGIN="$PR_REVIEW_ACTING_LOGIN" \
|
|
PR_REVIEW_FOREIGN_LOGIN="$PR_REVIEW_FOREIGN_LOGIN" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
|
|
phase = os.environ["PR_REVIEW_PHASE"]
|
|
page = int(os.environ["PR_REVIEW_PAGE"])
|
|
mode = os.environ["PR_REVIEW_MODE"]
|
|
state = os.environ["PR_REVIEW_STATE"]
|
|
acting = os.environ["PR_REVIEW_ACTING_LOGIN"]
|
|
foreign = os.environ["PR_REVIEW_FOREIGN_LOGIN"]
|
|
|
|
|
|
def review(review_id, review_state, commit, login):
|
|
return {
|
|
"id": review_id,
|
|
"state": review_state,
|
|
"commit_id": commit,
|
|
"user": {"login": login},
|
|
}
|
|
|
|
|
|
if phase == "boundary":
|
|
records = [review(100, "COMMENT", "oldsha0000", acting)] if page == 1 else []
|
|
elif mode == "paginated-approve":
|
|
# A full first page (50 non-matching records) forces the read-back to
|
|
# request page 2, where the genuine matching review lives.
|
|
if page == 1:
|
|
records = [review(101 + i, "COMMENT", "oldsha0000", acting) for i in range(50)]
|
|
elif page == 2:
|
|
records = [review(200, state, "HEADSHA_FEEDFACE", acting)]
|
|
else:
|
|
records = []
|
|
elif mode == "foreign-review":
|
|
# A concurrent APPROVED review at the current head, but authored by a
|
|
# DIFFERENT identity. tea created nothing; attribution must reject this.
|
|
records = [review(200, state, "HEADSHA_FEEDFACE", foreign)] if page == 1 else []
|
|
else:
|
|
if page == 1:
|
|
records = [
|
|
review(100, "COMMENT", "oldsha0000", acting),
|
|
review(200, state, "HEADSHA_FEEDFACE", acting),
|
|
]
|
|
else:
|
|
records = []
|
|
print(json.dumps(records))
|
|
PY
|
|
)
|
|
write_response 200 "$response"
|
|
elif [[ "$method" == "GET" && "$path" == "$PR_REVIEW_EXPECTED_API_BASE/pulls/123" ]]; then
|
|
write_response 200 '{"head":{"sha":"HEADSHA_FEEDFACE"}}'
|
|
elif [[ "$method" == "POST" && "$url" == "$PR_REVIEW_EXPECTED_API_BASE/issues/123/comments" ]]; then
|
|
PR_REVIEW_PAYLOAD="$payload" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
|
|
assert json.loads(os.environ["PR_REVIEW_PAYLOAD"]) == {"body": os.environ["PR_REVIEW_EXPECTED_BODY"]}
|
|
PY
|
|
response=$(python3 - <<'PY'
|
|
import json
|
|
import os
|
|
|
|
print(json.dumps({"id": 456, "body": os.environ["PR_REVIEW_EXPECTED_BODY"]}))
|
|
PY
|
|
)
|
|
write_response 201 "$response"
|
|
elif [[ "$method" == "GET" && "$url" == "$PR_REVIEW_EXPECTED_API_BASE/issues/comments/456" ]]; then
|
|
if [[ "$PR_REVIEW_TEST_MODE" == "readback-failure" ]]; then
|
|
body="different-body"
|
|
else
|
|
body="$PR_REVIEW_EXPECTED_BODY"
|
|
fi
|
|
response=$(PR_REVIEW_BODY="$body" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
|
|
print(json.dumps({
|
|
"id": 456,
|
|
"body": os.environ["PR_REVIEW_BODY"],
|
|
"issue_url": os.environ["PR_REVIEW_EXPECTED_API_BASE"] + "/issues/123",
|
|
}))
|
|
PY
|
|
)
|
|
write_response 200 "$response"
|
|
else
|
|
echo "Unexpected curl request: $method $url" >&2
|
|
exit 97
|
|
fi
|
|
;;
|
|
*)
|
|
exit 98
|
|
;;
|
|
esac
|
|
SH
|
|
chmod +x "$BIN_DIR/curl"
|
|
|
|
run_review() {
|
|
local mode="$1" action="$2" comment="${3:-}"
|
|
local configured_url="${4:-https://git.mosaicstack.dev}"
|
|
local remote_url="${5:-https://git.mosaicstack.dev/mosaicstack/stack.git}"
|
|
local expected_repo="${6:-mosaicstack/stack}"
|
|
local expected_api_base="${configured_url%/}/api/v1/repos/$expected_repo"
|
|
local expected_api_root="${configured_url%/}/api/v1"
|
|
git -C "$REPO_DIR" remote set-url origin "$remote_url"
|
|
write_credentials "$configured_url"
|
|
: > "$TEA_LOG"
|
|
: > "$CURL_LOG"
|
|
: > "$OUTPUT_FILE"
|
|
rm -f "$WORK_DIR/review_calls"
|
|
(
|
|
cd "$REPO_DIR"
|
|
PATH="$BIN_DIR:$PATH" \
|
|
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
|
PR_REVIEW_TEA_LOG="$TEA_LOG" \
|
|
PR_REVIEW_CURL_LOG="$CURL_LOG" \
|
|
PR_REVIEW_REVIEW_CALLS="$WORK_DIR/review_calls" \
|
|
PR_REVIEW_TEST_MODE="$mode" \
|
|
PR_REVIEW_EXPECTED_BODY="$comment" \
|
|
PR_REVIEW_EXPECTED_API_BASE="$expected_api_base" \
|
|
PR_REVIEW_API_ROOT="$expected_api_root" \
|
|
PR_REVIEW_ACTING_LOGIN="$ACTING_LOGIN" \
|
|
PR_REVIEW_FOREIGN_LOGIN="$FOREIGN_LOGIN" \
|
|
"$SCRIPT_DIR/pr-review.sh" -n 123 -a "$action" ${comment:+-c "$comment"}
|
|
) > "$OUTPUT_FILE" 2>&1
|
|
}
|
|
|
|
run_review approve approve
|
|
grep -q '^pr approve 123 --repo mosaicstack/stack --login mosaicstack$' "$TEA_LOG"
|
|
grep -q 'Approved and verified Gitea PR #123 (review ID 200)' "$OUTPUT_FILE"
|
|
# #865: the approval STATE itself is read back — a pre-write boundary GET and a
|
|
# post-write read-back GET on the (paginated) reviews endpoint, plus a PR head
|
|
# lookup and an acting-identity (GET /user) resolution for attribution.
|
|
grep -q '^GET https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/pulls/123/reviews?limit=[0-9]*&page=1$' "$CURL_LOG"
|
|
grep -q '^GET https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/pulls/123$' "$CURL_LOG"
|
|
grep -q '^GET https://git.mosaicstack.dev/api/v1/user$' "$CURL_LOG"
|
|
if grep -q 'comment' "$TEA_LOG"; then
|
|
echo "Plain approve (no review body) unexpectedly touched comment persistence" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# #865 (invocation attribution): a concurrent APPROVED review at the current
|
|
# head, authored by a DIFFERENT identity while tea created nothing, must NOT
|
|
# satisfy verification — id-above-boundary + state + head is only temporal
|
|
# ordering, not proof THIS reviewer wrote it.
|
|
if run_review foreign-review approve; then
|
|
echo "FAIL: approve accepted a review authored by a different identity" >&2
|
|
cat "$OUTPUT_FILE" >&2
|
|
exit 1
|
|
fi
|
|
if grep -q 'Approved and verified' "$OUTPUT_FILE"; then
|
|
echo "FAIL: read-back matched a different-identity review (attribution bypassed)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# #865 (pagination): a genuine matching review that lands beyond page 1 of the
|
|
# reviews list must still be found by a fully-paginating read-back.
|
|
run_review paginated-approve approve
|
|
grep -q 'Approved and verified Gitea PR #123 (review ID 200)' "$OUTPUT_FILE"
|
|
grep -q '^GET https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/pulls/123/reviews?limit=[0-9]*&page=2$' "$CURL_LOG"
|
|
|
|
# #835: tea v0.11.1 defines no --comment/-comment flag on `pr approve`. A
|
|
# review body supplied alongside approve must be routed through the durable
|
|
# comment REST API instead of being passed to `tea` directly.
|
|
run_review approve approve approve-note
|
|
grep -q '^pr approve 123 --repo mosaicstack/stack --login mosaicstack$' "$TEA_LOG"
|
|
grep -q 'Approved and verified Gitea PR #123 (review ID 200)' "$OUTPUT_FILE"
|
|
grep -q '^POST https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/123/comments$' "$CURL_LOG"
|
|
grep -q '^GET https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/comments/456$' "$CURL_LOG"
|
|
grep -q 'Added and verified review comment on Gitea PR #123 (comment ID 456)' "$OUTPUT_FILE"
|
|
|
|
# #835: same for `pr reject` (request-changes), where a comment is required.
|
|
run_review request-changes request-changes changes-required
|
|
grep -q '^pr reject 123 --repo mosaicstack/stack --login mosaicstack$' "$TEA_LOG"
|
|
grep -q 'Requested changes and verified on Gitea PR #123 (review ID 200)' "$OUTPUT_FILE"
|
|
grep -q '^GET https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/pulls/123/reviews?limit=[0-9]*&page=1$' "$CURL_LOG"
|
|
grep -q '^POST https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/123/comments$' "$CURL_LOG"
|
|
grep -q '^GET https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/comments/456$' "$CURL_LOG"
|
|
grep -q 'Added and verified review comment on Gitea PR #123 (comment ID 456)' "$OUTPUT_FILE"
|
|
|
|
if run_review legacy-fallback comment durable-body; then
|
|
echo "The old nonexistent tea pr comment fallback returned success" >&2
|
|
cat "$OUTPUT_FILE" >&2
|
|
exit 1
|
|
fi
|
|
if grep -q '^pr comment ' "$TEA_LOG"; then
|
|
echo "Wrapper invoked unsupported tea pr comment" >&2
|
|
exit 1
|
|
fi
|
|
if grep -q 'Added comment to Gitea PR' "$OUTPUT_FILE"; then
|
|
echo "Wrapper reported success without durable persistence" >&2
|
|
exit 1
|
|
fi
|
|
|
|
complex_body=$'durable "body"\n-- marker'
|
|
run_review comment-success comment "$complex_body"
|
|
grep -q '^POST https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/123/comments$' "$CURL_LOG"
|
|
grep -q '^GET https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/comments/456$' "$CURL_LOG"
|
|
grep -q 'Added and verified comment on Gitea PR #123' "$OUTPUT_FILE"
|
|
if [[ -s "$TEA_LOG" ]]; then
|
|
echo "REST comment path unexpectedly invoked tea" >&2
|
|
cat "$TEA_LOG" >&2
|
|
exit 1
|
|
fi
|
|
|
|
run_review http-success comment durable-body http://git.mosaicstack.dev
|
|
grep -q '^POST http://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/123/comments$' "$CURL_LOG"
|
|
grep -q '^GET http://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/comments/456$' "$CURL_LOG"
|
|
|
|
run_review prefix-success comment durable-body https://git.mosaicstack.dev/gitea/
|
|
grep -q '^POST https://git.mosaicstack.dev/gitea/api/v1/repos/mosaicstack/stack/issues/123/comments$' "$CURL_LOG"
|
|
grep -q '^GET https://git.mosaicstack.dev/gitea/api/v1/repos/mosaicstack/stack/issues/comments/456$' "$CURL_LOG"
|
|
|
|
run_review subpath-success comment durable-body https://git.example/gitea https://git.example/gitea/owner/repo.git owner/repo
|
|
grep -q '^POST https://git.example/gitea/api/v1/repos/owner/repo/issues/123/comments$' "$CURL_LOG"
|
|
grep -q '^GET https://git.example/gitea/api/v1/repos/owner/repo/issues/comments/456$' "$CURL_LOG"
|
|
if grep -q '/repos/gitea/owner/repo/' "$CURL_LOG"; then
|
|
echo "Configured Gitea path prefix leaked into the repository slug" >&2
|
|
exit 1
|
|
fi
|
|
|
|
run_review port-success comment durable-body http://git.example:3000 http://git.example:3000/owner/repo.git owner/repo
|
|
grep -q '^POST http://git.example:3000/api/v1/repos/owner/repo/issues/123/comments$' "$CURL_LOG"
|
|
grep -q '^GET http://git.example:3000/api/v1/repos/owner/repo/issues/comments/456$' "$CURL_LOG"
|
|
|
|
run_review scp-ssh-success comment durable-body https://git.example git@git.example:owner/repo.git owner/repo
|
|
grep -q '^POST https://git.example/api/v1/repos/owner/repo/issues/123/comments$' "$CURL_LOG"
|
|
|
|
run_review url-ssh-success comment durable-body https://git.example ssh://git@git.example/owner/repo.git owner/repo
|
|
grep -q '^POST https://git.example/api/v1/repos/owner/repo/issues/123/comments$' "$CURL_LOG"
|
|
|
|
# #850 (follow-up to #812): an SSH remote's transport port (e.g. `ssh://
|
|
# git@host:2222/...`) must NOT be compared against the configured HTTP(S) API
|
|
# URL's port -- they identify unrelated properties (SSH daemon port vs. HTTP(S)
|
|
# provider port) of the same Gitea host. Before the fix, host-match required
|
|
# the configured URL to carry the identical port, so this failed closed even
|
|
# though both remote and configured URL name the same host.
|
|
run_review ssh-transport-port-success comment durable-body https://git.example ssh://git@git.example:2222/owner/repo.git owner/repo
|
|
grep -q '^POST https://git.example/api/v1/repos/owner/repo/issues/123/comments$' "$CURL_LOG"
|
|
|
|
# #850 (follow-up to #812): an explicit default HTTP(S) port on the remote
|
|
# (`https://host:443/...`) must be treated as equal to an implicit
|
|
# (portless) configured URL on BOTH sides -- the pre-fix comparison only
|
|
# normalized the default port when the REMOTE side was portless, so the
|
|
# inverse (explicit remote, implicit configured) form failed closed.
|
|
run_review explicit-default-port-success comment durable-body https://git.example https://git.example:443/owner/repo.git owner/repo
|
|
grep -q '^POST https://git.example/api/v1/repos/owner/repo/issues/123/comments$' "$CURL_LOG"
|
|
|
|
if run_review write-transport-failure comment durable-body; then
|
|
echo "Expected provider transport failure to return nonzero" >&2
|
|
exit 1
|
|
fi
|
|
if run_review write-http-failure comment durable-body; then
|
|
echo "Expected non-201 provider write to return nonzero" >&2
|
|
exit 1
|
|
fi
|
|
if run_review readback-failure comment durable-body; then
|
|
echo "Expected mismatched provider read-back to return nonzero" >&2
|
|
exit 1
|
|
fi
|
|
if grep -q 'Added and verified comment' "$OUTPUT_FILE"; then
|
|
echo "Read-back mismatch reported durable success" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "pr-review.sh durable Gitea comment regression passed"
|