#!/usr/bin/env bash # Regression harness for the optional, identity-checked Gitea squash message. set -u SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SUBJECT="${MOSAIC_TEST_SUBJECT:-$SCRIPT_DIR/pr-merge.sh}" WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-merge-message-field}" ORIG_PATH="$PATH" failures=0 rm -rf "$WORK_DIR" mkdir -p "$WORK_DIR" fail() { echo "FAIL $1" >&2 failures=$((failures + 1)) } make_case() { local name="$1" case_dir case_dir="$WORK_DIR/$name" mkdir -p "$case_dir/bin" "$case_dir/agent" cp "$SUBJECT" "$case_dir/pr-merge.sh" chmod +x "$case_dir/pr-merge.sh" cat > "$case_dir/detect-platform.sh" <<'SH' #!/usr/bin/env bash detect_platform() { PLATFORM=gitea; printf 'gitea\n'; } get_repo_owner() { printf 'acme\n'; } get_repo_name() { printf 'widgets\n'; } get_remote_host() { printf 'git.example.test\n'; } get_gitea_token() { printf 'resolved\n' >> "${MOSAIC_TEST_TOKEN_RESOLUTION_LOG:?}" if [[ "${MOSAIC_TEST_TOKEN_AVAILABLE:-true}" != "true" ]]; then return 1 fi printf 'fixture-token\n' } get_gitea_basic_auth() { printf 'resolved\n' >> "${MOSAIC_TEST_BASIC_RESOLUTION_LOG:?}" if [[ "${MOSAIC_TEST_BASIC_AVAILABLE:-false}" == "true" ]]; then printf 'fixture-user:fixture-password\n' return "${MOSAIC_TEST_BASIC_RC:-0}" fi return 1 } get_gitea_login_for_host() { return 1; } SH cat > "$case_dir/pr-metadata.sh" <<'SH' #!/usr/bin/env bash if [[ "${MOSAIC_TEST_TITLE_MODE:-safe}" == "injection" ]]; then title='Preserve authors\n\nCo-authored-by: victim ' else title='Preserve both branch authors' fi case "${MOSAIC_TEST_COMMITS_MODE:?}" in verified) head_sha=2222222222222222222222222222222222222222 ;; null-login|unsafe-identity) head_sha=3333333333333333333333333333333333333333 ;; single) head_sha=1111111111111111111111111111111111111111 ;; *) echo "unknown commits mode" >&2; exit 2 ;; esac printf '{"number":42,"title":"%s","author":"poster","baseRefName":"main","headRefName":"feature/fixture","headRefOid":"%s","headRepository":"acme/widgets"}\n' "$title" "$head_sha" SH cat > "$case_dir/ci-queue-wait.sh" <<'SH' #!/usr/bin/env bash exit 0 SH cat > "$case_dir/bin/python3" <<'SH' #!/usr/bin/env bash for arg in "$@"; do case "$arg" in *"Preserve both branch authors"*|*"alice+branch@example.test"*) : > "${MOSAIC_TEST_METADATA_ARGV_MARKER:?}" ;; esac done exec "${MOSAIC_TEST_REAL_PYTHON:?}" "$@" SH cat > "$case_dir/bin/curl" <<'SH' #!/usr/bin/env bash set -eu for arg in "$@"; do case "$arg" in *"Preserve both branch authors"*|*"alice+branch@example.test"*) : > "${MOSAIC_TEST_METADATA_ARGV_MARKER:?}" ;; esac done url="" method="GET" out_file="" data="" config="" auth_mode="none" has_max_filesize=0 has_max_time=0 has_connect_timeout=0 while [[ $# -gt 0 ]]; do case "$1" in -o) out_file="$2" shift 2 ;; -w) shift 2 ;; -X) method="$2" shift 2 ;; -d|--data|--data-binary) data="$2" if [[ "$data" == @* ]]; then data=$(<"${data#@}") fi shift 2 ;; -K|--config) if [[ "$2" == "-" ]]; then config=$(cat) fi shift 2 ;; --max-filesize) has_max_filesize=1 shift 2 ;; --max-time) has_max_time=1 shift 2 ;; --connect-timeout) has_connect_timeout=1 shift 2 ;; -H|--header|-u|--user) if [[ "$2" == *"fixture-token"* ]]; then : > "${MOSAIC_TEST_TOKEN_ARGV_MARKER:?}" fi if [[ "$2" == *"fixture-password"* ]]; then : > "${MOSAIC_TEST_BASIC_ARGV_MARKER:?}" fi shift 2 ;; http://*|https://*) url="$1" shift ;; *) shift ;; esac done if [[ "$config" == *"Authorization: token fixture-token"* ]]; then auth_mode="token" : > "${MOSAIC_TEST_AUTH_CONFIG_MARKER:?}" elif [[ "$config" == *"user = \"fixture-user:fixture-password\""* ]]; then auth_mode="basic" : > "${MOSAIC_TEST_BASIC_CONFIG_MARKER:?}" fi printf '%s %s %s\n' "$method" "$auth_mode" "$url" >> "${MOSAIC_TEST_CURL_LOG:?}" printf '%s:%s:%s\n' "$has_max_filesize" "$has_max_time" "$has_connect_timeout" >> "${MOSAIC_TEST_CURL_BOUNDS_LOG:?}" case "$url" in */pulls/42) case "${MOSAIC_TEST_COMMITS_MODE:?}" in verified) head_sha=2222222222222222222222222222222222222222 ;; null-login|unsafe-identity) head_sha=3333333333333333333333333333333333333333 ;; single) head_sha=1111111111111111111111111111111111111111 ;; *) echo "unknown commits mode" >&2; exit 2 ;; esac if [[ "${MOSAIC_TEST_HEAD_MODE:-stable}" == "moved" ]]; then head_sha=4444444444444444444444444444444444444444 fi body="{\"head\":{\"sha\":\"$head_sha\"}}" code=200 if [[ "${MOSAIC_TEST_FALLBACK_MODE:-none}" == "inspection" && "$auth_mode" == "token" ]]; then body='{"message":"token rejected"}' code=401 fi ;; */pulls/42/commits*) case "${MOSAIC_TEST_COMMITS_MODE:?}" in verified) if [[ "${MOSAIC_TEST_EMAIL_MODE:-safe}" == "escape" ]]; then body='[{"sha":"2222222222222222222222222222222222222222","commit":{"author":{"name":"Alice","email":"alice+\u001b[31m@example.test"}},"author":{"login":"alice"}},{"sha":"1111111111111111111111111111111111111111","commit":{"author":{"name":"Poster","email":"poster@example.test"}},"author":{"login":"poster"}}]' else body='[{"sha":"2222222222222222222222222222222222222222","commit":{"author":{"name":"Alice","email":"alice+branch@example.test"}},"author":{"login":"alice"}},{"sha":"1111111111111111111111111111111111111111","commit":{"author":{"name":"Poster","email":"poster@example.test"}},"author":{"login":"poster"}}]' fi ;; null-login) body='[{"sha":"1111111111111111111111111111111111111111","commit":{"author":{"name":"Poster","email":"poster@example.test"}},"author":{"login":"poster"}},{"sha":"3333333333333333333333333333333333333333","commit":{"author":{"name":"Unresolved Author","email":"unresolved@example.test\n\u001b[31m"}},"author":null}]' ;; unsafe-identity) body='[{"sha":"unsafe\n\u001b[31m","commit":{"author":{"name":"Unsafe","email":"not-an-email"}},"author":{"login":"unsafe"}},{"sha":"3333333333333333333333333333333333333333","commit":{"author":{"name":"Poster","email":"poster@example.test"}},"author":{"login":"poster"}}]' ;; single) body='[{"sha":"1111111111111111111111111111111111111111","commit":{"author":{"name":"Poster","email":"poster@example.test"}},"author":{"login":"poster"}}]' ;; *) echo "unknown commits mode" >&2 exit 2 ;; esac code=200 if [[ "${MOSAIC_TEST_FALLBACK_MODE:-none}" == "inspection" && "$auth_mode" == "token" ]]; then body='{"message":"token rejected"}' code=401 fi ;; */pulls/42/merge) body='{}' code=200 if [[ "${MOSAIC_TEST_FALLBACK_MODE:-none}" == "merge" && "$auth_mode" == "token" ]]; then body='{"message":"token rejected"}' code=401 elif [[ "${MOSAIC_TEST_FALLBACK_MODE:-none}" == "provider-error" ]]; then body='{"message":"branch policy rejected\n\u001b[31m"}' code=409 elif [[ "${MOSAIC_TEST_FALLBACK_MODE:-none}" == "forbidden" ]]; then body='{"message":"permission denied"}' code=403 else printf '%s' "$data" > "${MOSAIC_TEST_MERGE_PAYLOAD:?}" fi ;; */users/*) body='{"message":"not found"}' code=404 ;; *) body='{"message":"unexpected URL"}' code=500 ;; esac if [[ -n "$out_file" ]]; then printf '%s' "$body" > "$out_file" else printf '%s' "$body" fi printf '%s' "$code" case "${MOSAIC_TEST_CURL_FAILURE:-none}" in oversize) exit 63 ;; stalled) exit 28 ;; esac SH chmod +x "$case_dir/detect-platform.sh" "$case_dir/pr-metadata.sh" \ "$case_dir/ci-queue-wait.sh" "$case_dir/bin/curl" "$case_dir/bin/python3" printf '%s\n' "$case_dir" } run_case() { local case_dir="$1" mode="$2" shift 2 MOSAIC_TEST_COMMITS_MODE="$mode" \ MOSAIC_TEST_CURL_LOG="$case_dir/curl.log" \ MOSAIC_TEST_CURL_BOUNDS_LOG="$case_dir/curl-bounds.log" \ MOSAIC_TEST_MERGE_PAYLOAD="$case_dir/merge-payload.json" \ MOSAIC_TEST_TOKEN_ARGV_MARKER="$case_dir/token-in-argv" \ MOSAIC_TEST_BASIC_ARGV_MARKER="$case_dir/basic-in-argv" \ MOSAIC_TEST_AUTH_CONFIG_MARKER="$case_dir/auth-via-config" \ MOSAIC_TEST_BASIC_CONFIG_MARKER="$case_dir/basic-via-config" \ MOSAIC_TEST_TOKEN_RESOLUTION_LOG="$case_dir/token-resolution.log" \ MOSAIC_TEST_BASIC_RESOLUTION_LOG="$case_dir/basic-resolution.log" \ MOSAIC_TEST_METADATA_ARGV_MARKER="$case_dir/metadata-in-argv" \ MOSAIC_TEST_REAL_PYTHON="$(command -v python3)" \ AGENT_WORK_ROOT="$case_dir/agent" \ PATH="$case_dir/bin:$ORIG_PATH" \ "$case_dir/pr-merge.sh" -n 42 "$@" } # Verified multi-author path: the non-poster trailer is built from one commit's # linked author.login and that same commit's author email. No /users lookup. verified_dir=$(make_case verified) set +e verified_output=$(run_case "$verified_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1) verified_rc=$? set -e if [[ "$verified_rc" -ne 0 ]]; then fail "verified multi-author merge expected rc=0, got rc=$verified_rc: $verified_output" elif [[ ! -s "$verified_dir/merge-payload.json" ]]; then fail "verified multi-author merge did not reach the API payload" else python3 - "$verified_dir/merge-payload.json" <<'PY' || fail "verified payload did not preserve squash and exact message fields" import json import sys payload = json.load(open(sys.argv[1], encoding="utf-8")) assert payload == { "Do": "squash", "head_commit_id": "2222222222222222222222222222222222222222", "MergeTitleField": "Preserve both branch authors", "MergeMessageField": "Co-authored-by: alice ", }, payload PY fi [[ -e "$verified_dir/auth-via-config" ]] || fail "verified path did not authenticate curl through stdin config" [[ ! -e "$verified_dir/token-in-argv" ]] || fail "verified path placed the Gitea token in curl argv" [[ ! -e "$verified_dir/metadata-in-argv" ]] || fail "verified path placed PR title or contributor email in child argv" [[ "$(wc -l < "$verified_dir/token-resolution.log")" -eq 1 ]] || fail "verified path did not bind inspection and merge to one credential resolution" if grep -q '/users/' "$verified_dir/curl.log" 2>/dev/null; then fail "verified path performed a forbidden second /users lookup" fi if grep -qv '^1:1:1$' "$verified_dir/curl-bounds.log"; then fail "verified path did not apply size/max-time/connect-time bounds to every provider download" fi # A linked email containing a terminal escape must block before mutation. escape_email_dir=$(make_case escape-email) set +e escape_email_output=$(MOSAIC_TEST_EMAIL_MODE=escape run_case "$escape_email_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1) escape_email_rc=$? set -e [[ "$escape_email_rc" -ne 0 ]] || fail "control-byte email unexpectedly passed" [[ "$escape_email_output" == *"unusable linked identity"* ]] || fail "control-byte email refusal lost its diagnostic" [[ ! -e "$escape_email_dir/merge-payload.json" ]] || fail "control-byte email reached the merge API" # Curl transfer and duration failures must remain failures even with HTTP 200. for failure_mode in oversize stalled; do failure_dir=$(make_case "curl-$failure_mode") set +e failure_output=$(MOSAIC_TEST_CURL_FAILURE="$failure_mode" run_case "$failure_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1) failure_rc=$? set -e [[ "$failure_rc" -ne 0 ]] || fail "curl $failure_mode failure was discarded: $failure_output" [[ ! -e "$failure_dir/merge-payload.json" ]] || fail "curl $failure_mode failure reached the merge API" done # The authenticated head is re-read under the mutation credential but cannot # replace the canonical preflight/review head. A move blocks before enumeration # or mutation even though the provider returned a valid new SHA. moved_dir=$(make_case moved-head) set +e moved_output=$(MOSAIC_TEST_HEAD_MODE=moved \ run_case "$moved_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1) moved_rc=$? set -e [[ "$moved_rc" -ne 0 ]] || fail "moved authenticated head unexpectedly passed" [[ "$moved_output" == *"authenticated PR head moved from reviewed"* ]] || fail "moved head refusal lost its diagnostic" [[ "$moved_output" == *"tl-mosaic"* ]] || fail "moved head refusal omitted the named escalation principal" [[ ! -e "$moved_dir/merge-payload.json" ]] || fail "moved head refusal reached the merge API" moved_sequence=$(awk '{print $1 ":" $2}' "$moved_dir/curl.log" | paste -sd, -) [[ "$moved_sequence" == "GET:token" ]] || fail "moved head refusal performed post-move inspection/mutation (calls=$moved_sequence)" # Token resolution failure is not an authentication response. It must fail # closed instead of borrowing a Basic credential under a different principal. token_missing_dir=$(make_case token-missing) set +e token_missing_output=$(MOSAIC_TEST_TOKEN_AVAILABLE=false MOSAIC_TEST_BASIC_AVAILABLE=true \ run_case "$token_missing_dir" single 2>&1) token_missing_rc=$? set -e [[ "$token_missing_rc" -ne 0 ]] || fail "missing token unexpectedly borrowed Basic Auth" [[ "$token_missing_output" == *"required Gitea token"* ]] || fail "missing token refusal lost its diagnostic" [[ ! -e "$token_missing_dir/basic-resolution.log" ]] || fail "missing token resolved Basic Auth after identity failure" [[ ! -e "$token_missing_dir/curl.log" ]] || fail "missing token reached a provider request" # A failed Basic resolver must never use its nonempty output or reach mutation. basic_rc_dir=$(make_case basic-resolver-rc) set +e basic_rc_output=$(MOSAIC_TEST_BASIC_AVAILABLE=true MOSAIC_TEST_BASIC_RC=91 MOSAIC_TEST_FALLBACK_MODE=inspection \ run_case "$basic_rc_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1) basic_rc_rc=$? set -e [[ "$basic_rc_rc" -ne 0 ]] || fail "failed Basic resolver output unexpectedly authorized a merge: $basic_rc_output" [[ ! -e "$basic_rc_dir/merge-payload.json" ]] || fail "failed Basic resolver reached the merge API" # HTTP 401 never changes principals: inspection rejection fails closed without # resolving or attempting Basic Auth. fallback_inspect_dir=$(make_case fallback-inspection) set +e fallback_inspect_output=$(MOSAIC_TEST_BASIC_AVAILABLE=true MOSAIC_TEST_FALLBACK_MODE=inspection \ run_case "$fallback_inspect_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1) fallback_inspect_rc=$? set -e [[ "$fallback_inspect_rc" -ne 0 ]] || fail "inspection token rejection unexpectedly changed principals" [[ "$fallback_inspect_output" == *"refusing cross-principal credential fallback"* ]] || fail "inspection token rejection lost its refusal diagnostic" [[ ! -e "$fallback_inspect_dir/basic-resolution.log" ]] || fail "inspection token rejection resolved Basic Auth" [[ ! -e "$fallback_inspect_dir/merge-payload.json" ]] || fail "inspection token rejection reached merge mutation" inspect_sequence=$(awk '{print $1 ":" $2}' "$fallback_inspect_dir/curl.log" | paste -sd, -) [[ "$inspect_sequence" == "GET:token" ]] || fail "inspection rejection made unexpected provider calls (calls=$inspect_sequence)" # Token rejection at merge likewise fails closed without cross-principal retry. fallback_merge_dir=$(make_case fallback-merge) set +e fallback_merge_output=$(MOSAIC_TEST_BASIC_AVAILABLE=true MOSAIC_TEST_FALLBACK_MODE=merge \ run_case "$fallback_merge_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1) fallback_merge_rc=$? set -e [[ "$fallback_merge_rc" -ne 0 ]] || fail "merge token rejection unexpectedly changed principals" [[ "$fallback_merge_output" == *"refusing cross-principal credential fallback"* ]] || fail "merge token rejection lost its refusal diagnostic" [[ ! -e "$fallback_merge_dir/basic-resolution.log" ]] || fail "merge token rejection resolved Basic Auth" [[ ! -e "$fallback_merge_dir/merge-payload.json" ]] || fail "merge token rejection recorded a successful payload" merge_sequence=$(awk '{print $1 ":" $2}' "$fallback_merge_dir/curl.log" | paste -sd, -) [[ "$merge_sequence" == "GET:token,GET:token,POST:token" ]] || fail "merge rejection made unexpected provider calls (calls=$merge_sequence)" # BLOCK path: a commit email exists but author.login is null. It must name both # facts, name the escalation principal, and never reach the merge endpoint. null_dir=$(make_case null-login) set +e null_output=$(run_case "$null_dir" null-login --co-author-trailers --escalate-to tl-mosaic 2>&1) null_rc=$? set -e [[ "$null_rc" -ne 0 ]] || fail "null-login author expected a non-zero BLOCK" [[ "$null_output" == *"BLOCK"* ]] || fail "null-login author omitted BLOCK diagnostic" [[ "$null_output" == *"author.login=NULL"* ]] || fail "null-login author omitted the null provider fact" [[ "$null_output" == *"unresolved@example.test"* ]] || fail "null-login author omitted the commit email fact" [[ "$null_output" == *'\n\x1b[31m'* ]] || fail "null-login author diagnostic did not escape control characters" [[ "$null_output" != *$'\033'* ]] || fail "null-login author diagnostic emitted a raw terminal escape" [[ "$(printf '%s\n' "$null_output" | wc -l)" -eq 1 ]] || fail "null-login author diagnostic permitted newline injection" [[ "$null_output" == *"tl-mosaic"* ]] || fail "null-login author omitted the named escalation principal" [[ ! -e "$null_dir/merge-payload.json" ]] || fail "null-login BLOCK still reached the merge API" # Every provider-derived field in alternate BLOCK diagnostics is log-safe too, # including an invalid non-head SHA that contains control characters. unsafe_dir=$(make_case unsafe-identity) set +e unsafe_output=$(run_case "$unsafe_dir" unsafe-identity --co-author-trailers --escalate-to tl-mosaic 2>&1) unsafe_rc=$? set -e [[ "$unsafe_rc" -ne 0 ]] || fail "unsafe identity expected a non-zero BLOCK" [[ "$unsafe_output" == *"unusable linked identity"* ]] || fail "unsafe identity omitted its BLOCK reason" [[ "$unsafe_output" == *'\n\x1b[31m'* ]] || fail "unsafe identity SHA did not escape control characters" [[ "$unsafe_output" != *$'\033'* ]] || fail "unsafe identity diagnostic emitted a raw terminal escape" [[ "$(printf '%s\n' "$unsafe_output" | wc -l)" -eq 1 ]] || fail "unsafe identity diagnostic permitted newline injection" [[ ! -e "$unsafe_dir/merge-payload.json" ]] || fail "unsafe identity BLOCK still reached the merge API" # The provider PR title cannot add an unchecked trailer outside the constructed # message field: multi-line and trailer-shaped titles block before mutation. title_dir=$(make_case title-injection) set +e title_output=$(MOSAIC_TEST_TITLE_MODE=injection \ run_case "$title_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1) title_rc=$? set -e [[ "$title_rc" -ne 0 ]] || fail "title trailer injection unexpectedly passed" [[ "$title_output" == *"not one printable, non-trailer line"* ]] || fail "title injection refusal lost its diagnostic" [[ ! -e "$title_dir/merge-payload.json" ]] || fail "title injection reached the merge API" # Provider failures remain diagnosable after their temporary response file is # removed, but provider-controlled control characters stay log-safe. error_dir=$(make_case provider-error) set +e error_output=$(MOSAIC_TEST_BASIC_AVAILABLE=true MOSAIC_TEST_FALLBACK_MODE=provider-error \ run_case "$error_dir" single 2>&1) error_rc=$? set -e [[ "$error_rc" -ne 0 ]] || fail "provider error unexpectedly passed" [[ "$error_output" == *"HTTP 409"* ]] || fail "provider error omitted the HTTP status" [[ "$error_output" == *"branch policy rejected"* ]] || fail "provider error response was discarded" [[ "$error_output" == *'\n\x1b[31m'* ]] || fail "provider error response did not escape control characters" [[ "$error_output" != *$'\033'* ]] || fail "provider error response emitted a raw terminal escape" [[ "$error_output" != *"Basic Auth fallback"* ]] || fail "provider error advertised removed Basic Auth fallback" [[ ! -e "$error_dir/basic-resolution.log" ]] || fail "HTTP 409 policy denial incorrectly triggered Basic Auth fallback" # Authorization denials likewise fail closed instead of changing principals. forbidden_dir=$(make_case forbidden) set +e forbidden_output=$(MOSAIC_TEST_BASIC_AVAILABLE=true MOSAIC_TEST_FALLBACK_MODE=forbidden \ run_case "$forbidden_dir" single 2>&1) forbidden_rc=$? set -e [[ "$forbidden_rc" -ne 0 ]] || fail "HTTP 403 authorization denial unexpectedly passed" [[ "$forbidden_output" == *"HTTP 403"* ]] || fail "authorization denial omitted the HTTP status" [[ "$forbidden_output" != *"Basic Auth fallback"* ]] || fail "authorization denial advertised removed Basic Auth fallback" [[ ! -e "$forbidden_dir/basic-resolution.log" ]] || fail "HTTP 403 authorization denial incorrectly triggered Basic Auth fallback" # The BLOCK destination cannot be generic or inferred after failure: opting in # without a named principal is refused before any provider operation. principal_dir=$(make_case missing-principal) set +e principal_output=$(run_case "$principal_dir" verified --co-author-trailers 2>&1) principal_rc=$? set -e [[ "$principal_rc" -ne 0 ]] || fail "co-author mode without a named principal unexpectedly passed" [[ "$principal_output" == *"requires --escalate-to with a named principal"* ]] || fail "missing-principal refusal lost its diagnostic" [[ ! -e "$principal_dir/merge-payload.json" ]] || fail "missing-principal refusal reached the merge API" # A trailing value-taking option receives a stable CLI diagnostic instead of a # set -u unbound-variable crash. value_dir=$(make_case missing-principal-value) set +e value_output=$(run_case "$value_dir" verified --co-author-trailers --escalate-to 2>&1) value_rc=$? set -e [[ "$value_rc" -ne 0 ]] || fail "missing --escalate-to value unexpectedly passed" [[ "$value_output" == *"--escalate-to requires one principal name"* ]] || fail "missing --escalate-to value lost its diagnostic" [[ "$value_output" != *"unbound variable"* ]] || fail "missing --escalate-to value crashed under set -u" [[ ! -e "$value_dir/merge-payload.json" ]] || fail "missing --escalate-to value reached the merge API" # Negative control: ordinary single-author merge remains byte-for-byte payload # compatible and hardcoded to squash, with no optional message fields. single_dir=$(make_case single) set +e single_output=$(run_case "$single_dir" single 2>&1) single_rc=$? set -e if [[ "$single_rc" -ne 0 ]]; then fail "ordinary single-author merge expected rc=0, got rc=$single_rc: $single_output" elif [[ ! -s "$single_dir/merge-payload.json" ]]; then fail "ordinary single-author merge did not reach the API payload" else python3 - "$single_dir/merge-payload.json" <<'PY' || fail "ordinary single-author payload changed" import json import sys payload = json.load(open(sys.argv[1], encoding="utf-8")) assert payload == { "Do": "squash", "head_commit_id": "1111111111111111111111111111111111111111", }, payload PY fi [[ -e "$single_dir/auth-via-config" ]] || fail "ordinary path did not authenticate curl through stdin config" [[ ! -e "$single_dir/token-in-argv" ]] || fail "ordinary path placed the Gitea token in curl argv" [[ "$(wc -l < "$single_dir/token-resolution.log")" -eq 1 ]] || fail "ordinary path did not use exactly one credential resolution" # Squash is not defaultable: an explicit non-squash method must remain refused. method_dir=$(make_case method-refusal) set +e method_output=$(run_case "$method_dir" single -m merge 2>&1) method_rc=$? set -e [[ "$method_rc" -ne 0 ]] || fail "non-squash method unexpectedly passed" [[ "$method_output" == *"enforces squash merge only"* ]] || fail "non-squash refusal lost its policy diagnostic" [[ ! -e "$method_dir/merge-payload.json" ]] || fail "non-squash refusal reached the merge API" if [[ "$failures" -ne 0 ]]; then echo "pr-merge message-field regression failed ($failures assertions)" >&2 exit 1 fi echo "pr-merge message-field regression passed (verified, BLOCK, and unchanged squash control)"