fix(tools): write Gitea reviews/comments via REST POST and verify by exact created id (#865)
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
Replace the tea-based write + boundary/author read-back with a direct Gitea
REST POST that returns the created record's id, and verify that exact record.
BLOCKER 2 (credential ordering): resolve the acting identity, the write token,
and the read-back token from the SAME effective login. A --login override now
selects the credential used for the POST, GET /user, and the GET-by-id
read-back, so an overridden write is verified against the identity that
performed it -- not the host default. Login-name resolution is best-effort and
non-fatal (the override always wins; otherwise fall back to the host
credential), so exotic/ported hosts still resolve a token.
BLOCKER 1+3 (attribution + tautological tests): the write is now
POST /issues/{n}/comments or POST /pulls/{n}/reviews (event + body + commit_id
== PR head), parsing the provider-returned created id. Verification GETs that
exact id and checks author == acting identity and body (comments) or state +
commit_id (reviews). Keying on the created id closes the concurrency window:
a no-op create yields no id and fails closed with no list-scan fallback, and a
concurrent same-identity record has a different id. The review body travels in
the review submit, removing the separate detached comment.
Tests: the curl stub now models a real server with persistent on-disk
review/comment state -- a POST actually creates+persists a record and returns
its id, and the read-back reads that same state (no fabricated record for the
wrapper to find). Adds same-identity no-op-concurrent and author-mismatch
fail-closed cases for both comments and reviews, and >page-1 pagination
coverage for both. README "Durable review provenance" refreshed for the REST
mechanism.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,28 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regression harness for issue-comment.sh top-level `tea comment` invocation and
|
||||
# its BOUNDED, INVOCATION-ATTRIBUTED, PAGINATED read-back verification (#865).
|
||||
# Regression harness for issue-comment.sh's Gitea comment write + 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. 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. 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 #865 defect class: tea 0.11.1's `tea issue comment ...` (a nonexistent
|
||||
# subcommand) silently no-ops yet exits 0, and tea cannot emit the id of a
|
||||
# record it created — so an exit code is worthless as proof of a durable write.
|
||||
# The wrapper therefore does NOT write via tea at all. It POSTs the comment to
|
||||
# the Gitea REST API (which returns the created comment object, including its
|
||||
# id), then GETs THAT EXACT id back and requires it to match on id, author
|
||||
# (acting identity), body, and issue. Because verification is keyed to the id
|
||||
# the create returned, no concurrent comment can masquerade as this write, and a
|
||||
# suppressed/no-op create yields no id and fails closed.
|
||||
#
|
||||
# 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, correctly-attributed id.
|
||||
# This harness models a REAL server: the curl stub keeps persistent comment
|
||||
# state on disk, the POST actually CREATES and PERSISTS a record and returns its
|
||||
# id, and the read-back GET reads that same state. There is no independently
|
||||
# fabricated record for the wrapper to "find" — the only way verification
|
||||
# passes is if the POST genuinely created the record the read-back retrieves.
|
||||
# It proves the wrapper:
|
||||
# 1. never shells out to tea to write (no `tea comment` / `tea issue comment`);
|
||||
# 2. creates the comment via REST POST and learns the provider-returned id;
|
||||
# 3. verifies THAT EXACT id by direct GET, attributed to the acting identity;
|
||||
# 4. fails closed when the write is a no-op even though a concurrent
|
||||
# SAME-IDENTITY comment with the same body already exists (the closed
|
||||
# concurrency window — no fallback list scan can rescue a no-op);
|
||||
# 5. fails closed when the created record is not authored by the acting
|
||||
# identity;
|
||||
# 6. enumerates the created id in the issue's FULLY PAGINATED comment list,
|
||||
# finding it even when it lands beyond page 1.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -30,22 +35,24 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/issue-comment-readback}"
|
||||
REPO_DIR="$WORK_DIR/repo"
|
||||
BIN_DIR="$WORK_DIR/bin"
|
||||
XDG_DIR="$WORK_DIR/xdg"
|
||||
TEA_LOG="$WORK_DIR/tea.log"
|
||||
CURL_LOG="$WORK_DIR/curl.log"
|
||||
OUTPUT_FILE="$WORK_DIR/output.log"
|
||||
CREDENTIALS_FILE="$WORK_DIR/credentials.json"
|
||||
CALLS_FILE="$WORK_DIR/comment_calls"
|
||||
STATE_FILE="$WORK_DIR/comments.json"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$REPO_DIR" "$BIN_DIR"
|
||||
mkdir -p "$REPO_DIR" "$BIN_DIR" "$XDG_DIR"
|
||||
git -C "$REPO_DIR" init -q
|
||||
git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
||||
|
||||
ISSUE_NUMBER=7
|
||||
REPO_SLUG="mosaicstack/stack"
|
||||
API_BASE="https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack"
|
||||
API_ROOT="https://git.mosaicstack.dev/api/v1"
|
||||
BODY='durable "note" -- marker'
|
||||
@@ -68,8 +75,8 @@ with open(sys.argv[1], "w", encoding="utf-8") as credentials:
|
||||
}, credentials)
|
||||
PY
|
||||
|
||||
# tea stub: resolves the login list, and treats `tea comment ...` as a silent
|
||||
# no-op (exit 0 without creating anything) to mimic the real failure mode.
|
||||
# tea stub: only ever answers the login list (used to resolve the default login
|
||||
# name). It must NEVER be asked to write a comment — the wrapper writes via REST.
|
||||
cat > "$BIN_DIR/tea" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
@@ -81,29 +88,16 @@ if [[ "$*" == "login list --output json" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# The wrapper must use the TOP-LEVEL `tea comment` form; the broken
|
||||
# `tea issue comment` subcommand must never be invoked.
|
||||
if [[ "$*" == issue\ comment* ]]; then
|
||||
echo "wrapper invoked nonexistent 'tea issue comment' subcommand" >&2
|
||||
exit 90
|
||||
fi
|
||||
|
||||
if [[ "$*" == comment\ * ]]; then
|
||||
# Mimic tea v0.11.1: exit 0. Whether a comment actually lands is modeled
|
||||
# by the curl stub's post-write read-back response, not here.
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Unexpected tea command: $*" >&2
|
||||
echo "Unexpected tea command (wrapper must not write via tea): $*" >&2
|
||||
exit 92
|
||||
SH
|
||||
chmod +x "$BIN_DIR/tea"
|
||||
|
||||
# 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.
|
||||
# curl stub: a small REST server backed by persistent on-disk comment state.
|
||||
# GET /user -> acting identity
|
||||
# POST /issues/7/comments -> CREATE + PERSIST, return created object
|
||||
# GET /issues/comments/{id} -> read the persisted record by exact id
|
||||
# GET /issues/7/comments?page=&.. -> paginated listing of persisted state
|
||||
cat > "$BIN_DIR/curl" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
@@ -111,22 +105,22 @@ set -euo pipefail
|
||||
output_file=""
|
||||
method="GET"
|
||||
url=""
|
||||
data=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-o) output_file="$2"; shift 2 ;;
|
||||
-w|-H) shift 2 ;;
|
||||
-X) method="$2"; shift 2 ;;
|
||||
-d|--data) shift 2 ;;
|
||||
-d|--data) data="$2"; shift 2 ;;
|
||||
-s|-S|-sS) shift ;;
|
||||
http://*|https://*) url="$1"; shift ;;
|
||||
*) shift ;;
|
||||
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%%\?*}"
|
||||
query="${url#*\?}"
|
||||
[[ "$query" == "$url" ]] && query=""
|
||||
printf '%s %s\n' "$method" "$url" >> "$ISSUE_COMMENT_CURL_LOG"
|
||||
|
||||
write_response() {
|
||||
@@ -143,55 +137,82 @@ 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
|
||||
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'
|
||||
elif [[ "$method" == "POST" && "$path" == "$ISSUE_COMMENT_API_BASE/issues/7/comments" ]]; then
|
||||
result=$(ISSUE_COMMENT_DATA="$data" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
body = os.environ["ISSUE_COMMENT_BODY"]
|
||||
state_path = os.environ["ISSUE_COMMENT_STATE"]
|
||||
mode = os.environ["ISSUE_COMMENT_TEST_MODE"]
|
||||
acting = os.environ["ISSUE_COMMENT_ACTING_LOGIN"]
|
||||
foreign = os.environ["ISSUE_COMMENT_FOREIGN_LOGIN"]
|
||||
mode = os.environ["ISSUE_COMMENT_TEST_MODE"]
|
||||
repo = os.environ["ISSUE_COMMENT_REPO_SLUG"]
|
||||
body = json.loads(os.environ["ISSUE_COMMENT_DATA"]).get("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))
|
||||
with open(state_path, encoding="utf-8") as handle:
|
||||
comments = json.load(handle)
|
||||
|
||||
# no-op-concurrent: the wrapper's own write is SUPPRESSED (returns 200 with no
|
||||
# created object) even though a concurrent same-identity comment already exists
|
||||
# in state. Nothing is persisted; there is no created id to verify.
|
||||
if mode == "no-op-concurrent":
|
||||
print("200")
|
||||
print(json.dumps({}))
|
||||
raise SystemExit(0)
|
||||
|
||||
author = foreign if mode == "author-mismatch" else acting
|
||||
new_id = (max((c["id"] for c in comments), default=0)) + 1
|
||||
record = {
|
||||
"id": new_id,
|
||||
"body": body,
|
||||
"user": {"login": author},
|
||||
"issue_url": f"https://git.mosaicstack.dev/api/v1/repos/{repo}/issues/7",
|
||||
}
|
||||
comments.append(record)
|
||||
with open(state_path, "w", encoding="utf-8") as handle:
|
||||
json.dump(comments, handle)
|
||||
print("201")
|
||||
print(json.dumps(record))
|
||||
PY
|
||||
)
|
||||
else
|
||||
: > "$calls_file"
|
||||
response=$(ISSUE_COMMENT_BODY="$ISSUE_COMMENT_EXPECTED_BODY" \
|
||||
ISSUE_COMMENT_ACTING_LOGIN="$ISSUE_COMMENT_ACTING_LOGIN" python3 - <<'PY'
|
||||
write_response "$(printf '%s' "$result" | head -n1)" "$(printf '%s' "$result" | tail -n +2)"
|
||||
elif [[ "$method" == "GET" && "$path" == "$ISSUE_COMMENT_API_BASE"/issues/comments/* ]]; then
|
||||
result=$(ISSUE_COMMENT_GET_ID="${path##*/}" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
body = os.environ["ISSUE_COMMENT_BODY"]
|
||||
acting = os.environ["ISSUE_COMMENT_ACTING_LOGIN"]
|
||||
print(json.dumps([{"id": 50, "body": body, "user": {"login": acting}}]))
|
||||
|
||||
state_path = os.environ["ISSUE_COMMENT_STATE"]
|
||||
wanted = int(os.environ["ISSUE_COMMENT_GET_ID"])
|
||||
with open(state_path, encoding="utf-8") as handle:
|
||||
comments = json.load(handle)
|
||||
match = next((c for c in comments if c["id"] == wanted), None)
|
||||
if match is None:
|
||||
print("404")
|
||||
print(json.dumps({"message": "not found"}))
|
||||
else:
|
||||
print("200")
|
||||
print(json.dumps(match))
|
||||
PY
|
||||
)
|
||||
fi
|
||||
write_response 200 "$response"
|
||||
write_response "$(printf '%s' "$result" | head -n1)" "$(printf '%s' "$result" | tail -n +2)"
|
||||
elif [[ "$method" == "GET" && "$path" == "$ISSUE_COMMENT_API_BASE/issues/7/comments" ]]; then
|
||||
result=$(ISSUE_COMMENT_QUERY="$query" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
state_path = os.environ["ISSUE_COMMENT_STATE"]
|
||||
params = parse_qs(os.environ["ISSUE_COMMENT_QUERY"])
|
||||
limit = int(params.get("limit", ["50"])[0])
|
||||
page = int(params.get("page", ["1"])[0])
|
||||
with open(state_path, encoding="utf-8") as handle:
|
||||
comments = json.load(handle)
|
||||
start = (page - 1) * limit
|
||||
print("200")
|
||||
print(json.dumps(comments[start:start + limit]))
|
||||
PY
|
||||
)
|
||||
write_response "$(printf '%s' "$result" | head -n1)" "$(printf '%s' "$result" | tail -n +2)"
|
||||
else
|
||||
echo "Unexpected curl request: $method $url" >&2
|
||||
exit 97
|
||||
@@ -199,67 +220,112 @@ fi
|
||||
SH
|
||||
chmod +x "$BIN_DIR/curl"
|
||||
|
||||
# Seed persistent server state for a mode, then run the wrapper against it.
|
||||
seed_state() {
|
||||
local mode="$1"
|
||||
ISSUE_COMMENT_SEED_MODE="$mode" ISSUE_COMMENT_SEED_BODY="$BODY" \
|
||||
ISSUE_COMMENT_SEED_ACTING="$ACTING_LOGIN" ISSUE_COMMENT_SEED_REPO="$REPO_SLUG" \
|
||||
python3 - "$STATE_FILE" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
mode = os.environ["ISSUE_COMMENT_SEED_MODE"]
|
||||
body = os.environ["ISSUE_COMMENT_SEED_BODY"]
|
||||
acting = os.environ["ISSUE_COMMENT_SEED_ACTING"]
|
||||
repo = os.environ["ISSUE_COMMENT_SEED_REPO"]
|
||||
issue_url = f"https://git.mosaicstack.dev/api/v1/repos/{repo}/issues/7"
|
||||
|
||||
if mode == "fresh-success":
|
||||
# 50 pre-existing comments fill page 1 (limit 50); the comment this run
|
||||
# creates becomes id 51 and lands ALONE on page 2, exercising >page-1
|
||||
# pagination in the enumeration check.
|
||||
comments = [
|
||||
{"id": i, "body": f"prior {i}", "user": {"login": acting}, "issue_url": issue_url}
|
||||
for i in range(1, 51)
|
||||
]
|
||||
elif mode == "no-op-concurrent":
|
||||
# A concurrent SAME-IDENTITY comment with the IDENTICAL body already exists.
|
||||
# The wrapper's own write will be a no-op; it must still fail closed because
|
||||
# no created id is returned — it must not scan and accept this record.
|
||||
comments = [
|
||||
{"id": 55, "body": body, "user": {"login": acting}, "issue_url": issue_url}
|
||||
]
|
||||
else: # author-mismatch
|
||||
comments = []
|
||||
|
||||
with open(sys.argv[1], "w", encoding="utf-8") as handle:
|
||||
json.dump(comments, handle)
|
||||
PY
|
||||
}
|
||||
|
||||
run_comment() {
|
||||
local mode="$1"
|
||||
: > "$TEA_LOG"
|
||||
: > "$CURL_LOG"
|
||||
: > "$OUTPUT_FILE"
|
||||
rm -f "$CALLS_FILE"
|
||||
seed_state "$mode"
|
||||
(
|
||||
cd "$REPO_DIR"
|
||||
PATH="$BIN_DIR:$PATH" \
|
||||
XDG_CONFIG_HOME="$XDG_DIR" \
|
||||
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
||||
ISSUE_COMMENT_TEA_LOG="$TEA_LOG" \
|
||||
ISSUE_COMMENT_CURL_LOG="$CURL_LOG" \
|
||||
ISSUE_COMMENT_CALLS="$CALLS_FILE" \
|
||||
ISSUE_COMMENT_STATE="$STATE_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_REPO_SLUG="$REPO_SLUG" \
|
||||
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
|
||||
}
|
||||
|
||||
# Case 1: silent no-op with a pre-existing identical body must FAIL CLOSED.
|
||||
if run_comment noop-preexisting; then
|
||||
echo "FAIL: wrapper reported success when tea no-opped but an identical body pre-existed" >&2
|
||||
cat "$OUTPUT_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -q 'Added and verified comment' "$OUTPUT_FILE"; then
|
||||
echo "FAIL: read-back matched a pre-existing comment by body only" >&2
|
||||
exit 1
|
||||
fi
|
||||
# The wrapper must have used the top-level form and read comments back twice
|
||||
# (boundary + post-write).
|
||||
grep -q "^comment 7 " "$TEA_LOG"
|
||||
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" ]]
|
||||
# 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 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.
|
||||
# Case 1: a genuine REST create (id 51) is verified end to end via its exact
|
||||
# provider-returned id and enumerated on page 2 of the paginated listing.
|
||||
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"
|
||||
grep -q 'Added and verified comment on Gitea issue #7 (comment ID 51)' "$OUTPUT_FILE"
|
||||
# The write is a REST POST, never a tea comment.
|
||||
grep -q "^POST $API_BASE/issues/7/comments$" "$CURL_LOG"
|
||||
if grep -Eq '^comment |^issue comment ' "$TEA_LOG"; then
|
||||
echo "FAIL: wrapper wrote a comment via tea instead of REST" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Read-back is a DIRECT GET of the exact created id.
|
||||
grep -q "^GET $API_BASE/issues/comments/51$" "$CURL_LOG"
|
||||
# Acting identity resolved via GET /user.
|
||||
grep -q "^GET $API_ROOT/user$" "$CURL_LOG"
|
||||
# Enumeration paginated beyond page 1 to find the created comment.
|
||||
grep -q "^GET $API_BASE/issues/7/comments?limit=[0-9]*&page=2$" "$CURL_LOG"
|
||||
|
||||
echo "issue-comment.sh bounded + attributed read-back regression passed"
|
||||
# Case 2: a no-op write with a concurrent SAME-IDENTITY, same-body comment
|
||||
# already present must FAIL CLOSED — the closed concurrency window.
|
||||
if run_comment no-op-concurrent; then
|
||||
echo "FAIL: wrapper reported success when its write no-opped but a concurrent same-identity comment existed" >&2
|
||||
cat "$OUTPUT_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -q 'Added and verified comment' "$OUTPUT_FILE"; then
|
||||
echo "FAIL: wrapper accepted a concurrent record for a no-op write (window not closed)" >&2
|
||||
exit 1
|
||||
fi
|
||||
# It must NOT have fallen back to a list scan that could find the concurrent id.
|
||||
if grep -q "^GET $API_BASE/issues/comments/55$" "$CURL_LOG"; then
|
||||
echo "FAIL: wrapper read back the concurrent comment id 55 (illegitimate fallback)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Case 3: a created record NOT authored by the acting identity must FAIL CLOSED.
|
||||
if run_comment author-mismatch; then
|
||||
echo "FAIL: wrapper accepted a created 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 did not enforce acting-identity authorship" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "issue-comment.sh REST create + exact-id read-back regression passed"
|
||||
|
||||
Reference in New Issue
Block a user