fix(tools): attribute read-back to acting identity and paginate fully (#865)
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
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>
This commit is contained in:
@@ -1,23 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regression harness for issue-comment.sh top-level `tea comment` invocation and
|
||||
# its BOUNDED read-back verification (#865).
|
||||
# its BOUNDED, INVOCATION-ATTRIBUTED, PAGINATED read-back verification (#865).
|
||||
#
|
||||
# The #865 bug: `tea issue comment ...` (a nonexistent subcommand on tea
|
||||
# v0.11.1) silently no-ops and exits 0, so a comment is never posted. A naive
|
||||
# read-back that matches ANY historical comment by body would falsely report
|
||||
# success whenever an identically-bodied comment already exists from a prior
|
||||
# run. This harness proves the wrapper:
|
||||
# run. Merely bounding by "id > pre-write max" is also insufficient: it accepts
|
||||
# ANY newer matching comment, including one a CONCURRENT DIFFERENT identity
|
||||
# posted while this tea invocation created nothing. This harness proves the
|
||||
# wrapper:
|
||||
# 1. uses the top-level `tea comment` form (never `tea issue comment`);
|
||||
# 2. records the pre-write maximum comment id as a boundary and requires a
|
||||
# strictly-newer comment on read-back, so a pre-existing identical body
|
||||
# does NOT satisfy verification (fails closed);
|
||||
# 3. reports success only when a genuinely new comment (id > boundary) with
|
||||
# the exact body appears.
|
||||
# 3. attributes the matched comment to the acting identity (GET /user login),
|
||||
# so a concurrent DIFFERENT-identity write does NOT satisfy verification;
|
||||
# 4. reports success only when a genuinely new comment (id > boundary) with
|
||||
# the exact body AND the acting author appears.
|
||||
#
|
||||
# The `tea` stub NEVER creates a comment (it mimics the silent no-op); the
|
||||
# "server" comment state is modeled entirely by the curl stub's responses, so
|
||||
# the fresh-success vs. no-op distinction is driven purely by whether the
|
||||
# post-write read-back surfaces a new id.
|
||||
# post-write read-back surfaces a new, correctly-attributed id.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -42,7 +47,10 @@ git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/sta
|
||||
|
||||
ISSUE_NUMBER=7
|
||||
API_BASE="https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack"
|
||||
API_ROOT="https://git.mosaicstack.dev/api/v1"
|
||||
BODY='durable "note" -- marker'
|
||||
ACTING_LOGIN="primary-reviewer"
|
||||
FOREIGN_LOGIN="other-writer"
|
||||
|
||||
CONFIGURED_GITEA_URL="https://git.mosaicstack.dev" python3 - "$CREDENTIALS_FILE" <<'PY'
|
||||
import json
|
||||
@@ -91,8 +99,9 @@ exit 92
|
||||
SH
|
||||
chmod +x "$BIN_DIR/tea"
|
||||
|
||||
# curl stub: serves GET .../issues/7/comments. First call = pre-write boundary,
|
||||
# second call = post-write read-back. The boundary always contains a
|
||||
# curl stub: serves GET /user (acting identity) and GET .../issues/7/comments
|
||||
# (paginated: ?limit=&page=). The comments endpoint's first call = pre-write
|
||||
# boundary, second call = post-write read-back. The boundary always contains a
|
||||
# pre-existing comment (id 50) whose body is IDENTICAL to the one under test,
|
||||
# which is exactly the condition a body-only match would trip over.
|
||||
cat > "$BIN_DIR/curl" <<'SH'
|
||||
@@ -114,6 +123,10 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
done
|
||||
|
||||
# Strip any query string so pagination params don't defeat path matching, but
|
||||
# still log the full URL (including ?limit=&page=) so the test can assert the
|
||||
# read-back paginated.
|
||||
path="${url%%\?*}"
|
||||
printf '%s %s\n' "$method" "$url" >> "$ISSUE_COMMENT_CURL_LOG"
|
||||
|
||||
write_response() {
|
||||
@@ -123,41 +136,58 @@ write_response() {
|
||||
printf '%s' "$status"
|
||||
}
|
||||
|
||||
if [[ "$method" == "GET" && "$url" == "$ISSUE_COMMENT_API_BASE/issues/7/comments" ]]; then
|
||||
if [[ "$method" == "GET" && "$path" == "$ISSUE_COMMENT_API_ROOT/user" ]]; then
|
||||
write_response 200 "$(ISSUE_COMMENT_LOGIN="$ISSUE_COMMENT_ACTING_LOGIN" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
print(json.dumps({"login": os.environ["ISSUE_COMMENT_LOGIN"]}))
|
||||
PY
|
||||
)"
|
||||
elif [[ "$method" == "GET" && "$path" == "$ISSUE_COMMENT_API_BASE/issues/7/comments" ]]; then
|
||||
calls_file="$ISSUE_COMMENT_CALLS"
|
||||
if [[ -f "$calls_file" ]]; then
|
||||
# post-write read-back
|
||||
if [[ "$ISSUE_COMMENT_TEST_MODE" == "fresh-success" ]]; then
|
||||
response=$(ISSUE_COMMENT_BODY="$ISSUE_COMMENT_EXPECTED_BODY" python3 - <<'PY'
|
||||
response=$(ISSUE_COMMENT_BODY="$ISSUE_COMMENT_EXPECTED_BODY" \
|
||||
ISSUE_COMMENT_ACTING_LOGIN="$ISSUE_COMMENT_ACTING_LOGIN" \
|
||||
ISSUE_COMMENT_FOREIGN_LOGIN="$ISSUE_COMMENT_FOREIGN_LOGIN" \
|
||||
ISSUE_COMMENT_TEST_MODE="$ISSUE_COMMENT_TEST_MODE" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
body = os.environ["ISSUE_COMMENT_BODY"]
|
||||
print(json.dumps([
|
||||
{"id": 50, "body": body},
|
||||
{"id": 60, "body": body},
|
||||
]))
|
||||
PY
|
||||
)
|
||||
else
|
||||
# no-op: nothing new landed; the pre-existing id-50 comment remains.
|
||||
response=$(ISSUE_COMMENT_BODY="$ISSUE_COMMENT_EXPECTED_BODY" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
acting = os.environ["ISSUE_COMMENT_ACTING_LOGIN"]
|
||||
foreign = os.environ["ISSUE_COMMENT_FOREIGN_LOGIN"]
|
||||
mode = os.environ["ISSUE_COMMENT_TEST_MODE"]
|
||||
|
||||
body = os.environ["ISSUE_COMMENT_BODY"]
|
||||
print(json.dumps([{"id": 50, "body": body}]))
|
||||
if mode == "fresh-success":
|
||||
# A genuinely new comment (id 60 > boundary 50) authored by the acting
|
||||
# identity.
|
||||
records = [
|
||||
{"id": 50, "body": body, "user": {"login": acting}},
|
||||
{"id": 60, "body": body, "user": {"login": acting}},
|
||||
]
|
||||
elif mode == "foreign-identity":
|
||||
# A concurrent new comment (id 60 > boundary 50) with the SAME body but a
|
||||
# DIFFERENT author. tea created nothing; attribution must reject this.
|
||||
records = [
|
||||
{"id": 50, "body": body, "user": {"login": acting}},
|
||||
{"id": 60, "body": body, "user": {"login": foreign}},
|
||||
]
|
||||
else:
|
||||
# no-op: nothing new landed; the pre-existing id-50 comment remains.
|
||||
records = [{"id": 50, "body": body, "user": {"login": acting}}]
|
||||
print(json.dumps(records))
|
||||
PY
|
||||
)
|
||||
fi
|
||||
else
|
||||
: > "$calls_file"
|
||||
response=$(ISSUE_COMMENT_BODY="$ISSUE_COMMENT_EXPECTED_BODY" python3 - <<'PY'
|
||||
response=$(ISSUE_COMMENT_BODY="$ISSUE_COMMENT_EXPECTED_BODY" \
|
||||
ISSUE_COMMENT_ACTING_LOGIN="$ISSUE_COMMENT_ACTING_LOGIN" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
body = os.environ["ISSUE_COMMENT_BODY"]
|
||||
print(json.dumps([{"id": 50, "body": body}]))
|
||||
acting = os.environ["ISSUE_COMMENT_ACTING_LOGIN"]
|
||||
print(json.dumps([{"id": 50, "body": body, "user": {"login": acting}}]))
|
||||
PY
|
||||
)
|
||||
fi
|
||||
@@ -184,7 +214,10 @@ run_comment() {
|
||||
ISSUE_COMMENT_CALLS="$CALLS_FILE" \
|
||||
ISSUE_COMMENT_TEST_MODE="$mode" \
|
||||
ISSUE_COMMENT_EXPECTED_BODY="$BODY" \
|
||||
ISSUE_COMMENT_ACTING_LOGIN="$ACTING_LOGIN" \
|
||||
ISSUE_COMMENT_FOREIGN_LOGIN="$FOREIGN_LOGIN" \
|
||||
ISSUE_COMMENT_API_BASE="$API_BASE" \
|
||||
ISSUE_COMMENT_API_ROOT="$API_ROOT" \
|
||||
"$SCRIPT_DIR/issue-comment.sh" -i "$ISSUE_NUMBER" -c "$BODY"
|
||||
) > "$OUTPUT_FILE" 2>&1
|
||||
}
|
||||
@@ -206,11 +239,27 @@ if grep -q '^issue comment' "$TEA_LOG"; then
|
||||
echo "FAIL: wrapper used the broken 'tea issue comment' subcommand" >&2
|
||||
exit 1
|
||||
fi
|
||||
[[ "$(grep -c "^GET $API_BASE/issues/7/comments$" "$CURL_LOG")" == "2" ]]
|
||||
[[ "$(grep -c "^GET $API_BASE/issues/7/comments?" "$CURL_LOG")" == "2" ]]
|
||||
# Read-back must be paginated (limit + page query params present).
|
||||
grep -q "^GET $API_BASE/issues/7/comments?limit=[0-9]*&page=1$" "$CURL_LOG"
|
||||
# Attribution must have resolved the acting identity via GET /user.
|
||||
grep -q "^GET $API_ROOT/user$" "$CURL_LOG"
|
||||
|
||||
# Case 2: a genuinely new comment (id 60 > boundary 50) verifies successfully.
|
||||
# Case 2: a concurrent DIFFERENT-identity write (id 60 > boundary, same body,
|
||||
# foreign author) must FAIL CLOSED — temporal ordering is not attribution.
|
||||
if run_comment foreign-identity; then
|
||||
echo "FAIL: wrapper accepted a concurrent comment authored by a different identity" >&2
|
||||
cat "$OUTPUT_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -q 'Added and verified comment' "$OUTPUT_FILE"; then
|
||||
echo "FAIL: read-back matched a different-identity comment (attribution bypassed)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Case 3: a genuinely new comment (id 60 > boundary 50, acting author) verifies.
|
||||
run_comment fresh-success
|
||||
grep -q 'Added and verified comment on Gitea issue #7 (comment ID 60)' "$OUTPUT_FILE"
|
||||
grep -q "^comment 7 " "$TEA_LOG"
|
||||
|
||||
echo "issue-comment.sh bounded read-back regression passed"
|
||||
echo "issue-comment.sh bounded + attributed read-back regression passed"
|
||||
|
||||
Reference in New Issue
Block a user