#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SCRIPT="$ROOT_DIR/scripts/pr-state" pass() { printf 'ok - %s\n' "$1" } fail() { printf 'not ok - %s\n' "$1" >&2 exit 1 } TMP_DIRS=() cleanup() { if [ "${#TMP_DIRS[@]}" -gt 0 ]; then rm -rf "${TMP_DIRS[@]}" fi } trap cleanup EXIT make_tmp_dir() { local __out=$1 local __tmp __tmp="$(mktemp -d)" TMP_DIRS+=("$__tmp") printf -v "$__out" '%s' "$__tmp" } assert_jq() { local name=$1 local expr=$2 local json=$3 jq -e "$expr" <<<"$json" >/dev/null || fail "$name" } make_mock_gh() { local dir=$1 mkdir -p "$dir" cat >"$dir/gh" <<'EOF' #!/usr/bin/env bash set -euo pipefail if [[ "${GH_FAIL_REVIEWS:-0}" == "1" && "$*" == *"pulls/123/reviews"* ]]; then echo "reviews api failed" >&2 exit 1 fi if [[ "${GH_FAIL_GRAPHQL:-0}" == "1" && "$1" == "api" && "$2" == "graphql" ]]; then echo "graphql failed" >&2 exit 1 fi if [[ "${GH_FAIL_PR_VIEW:-0}" == "1" && "$1" == "pr" && "$2" == "view" ]]; then echo "pr view failed" >&2 exit 1 fi if [[ "${GH_FAIL_REPO:-0}" == "1" && "$1" == "repo" && "$2" == "view" ]]; then echo "repo view failed" >&2 exit 1 fi if [[ "${GH_FAIL_COMMENT:-0}" == "1" && "$1" == "api" && "$2" == repos/kdlbs/kandev/pulls/comments/* ]]; then echo "comment api failed" >&2 exit 1 fi if [[ "${GH_FAIL_APPROVAL_RUNS:-0}" == "1" && "$*" == *"repos/kdlbs/kandev/actions/runs?event=pull_request&head_sha=abc123&per_page=100"* ]]; then echo "workflow runs api failed" >&2 exit 1 fi if [[ "$1" == "repo" && "$2" == "view" ]]; then printf '{"owner":{"login":"kdlbs"},"name":"kandev"}\n' exit 0 fi if [[ "$1" == "api" && "$2" == repos/kdlbs/kandev/compare/* ]]; then merge_base="${GH_MERGE_BASE:-base-branch-sha}" if [[ "${GH_BASE_SEQUENCE:-stable}" == "race" && "$2" == *"late-base-head-sha..."* ]]; then merge_base="late-merge-base-sha" fi cat <"$log_file" if [[ "${GH_JOB_LOG_FORMAT:-plain}" == "zip" ]]; then base64 -d <<'ZIP' UEsDBBQAAAAAALip/lxqPXj2qgAAAKoAAAAHAAAAam9iLmxvZ3NldHVwIGNvbXBsZXRlClRlc3QgdGltZW91dDogZXhwZWN0ZWQgcmVhZHkgc3RhdGUKc3RhY2sgZnJhbWUgb25lCnN0YWNrIGZyYW1lIHR3bwpzdGFjayBmcmFtZSB0aHJlZQpzdGFjayBmcmFtZSBmb3VyCnN0YWNrIGZyYW1lIGZpdmUKc3RhY2sgZnJhbWUgc2l4CnVucmVsYXRlZCB0YWlsIGxpbmUKUEsBAhQDFAAAAAAAuKn+XGo9ePaqAAAAqgAAAAcAAAAAAAAAAAAAAIABAAAAAGpvYi5sb2dQSwUGAAAAAAEAAQA1AAAAzwAAAAAA ZIP else cat "$log_file" fi rm -f "$log_file" exit 0 fi if [[ "$*" == *"actions/workflows/opencode-code-review.yml/runs"* ]]; then if [[ "${GH_FAIL_TRUSTED_WORKFLOW:-0}" == "1" ]]; then echo "workflow api failed" >&2 exit 1 fi if [[ "${GH_HEAD_SEQUENCE:-stable}" == "after-workflow" ]]; then : >"${GH_HEAD_COUNTER_FILE:?}" fi if [[ "${GH_TRUSTED_MULTI:-0}" == "1" ]]; then cat <<'JSON' {"workflow_runs":[{"id":901,"path":".github/workflows/opencode-code-review.yml","event":"pull_request_target","status":"completed","conclusion":"failure","run_number":11,"run_attempt":1,"pull_requests":[{"number":123,"head":{"sha":"abc123"}}]},{"id":900,"path":".github/workflows/opencode-code-review.yml","event":"pull_request_target","status":"completed","conclusion":"success","run_number":10,"run_attempt":2,"pull_requests":[{"number":123,"head":{"sha":"abc123"}}]}]} JSON exit 0 fi conclusion="${GH_TRUSTED_WORKFLOW_CONCLUSION:-success}" status="${GH_TRUSTED_WORKFLOW_STATUS:-completed}" cat <", "createdAt": "2026-06-01T10:00:00Z" }, { "author": { "login": "github-actions" }, "body": "**Claude finished review**\\n| Blocker | 1 |\\n| Suggestion | 2 |\\n**Verdict:** Ready with suggestions", "createdAt": "2026-06-01T13:00:00Z", "url": "https://github.com/kdlbs/kandev/pull/123#issuecomment-2" }, { "author": { "login": "github-actions" }, "body": "\\n**OpenCode review complete**\\n\\nOpenCode model: \`opencode-go/minimax-m3\`\\n\\nNo suggestions found for commit \`abc1234\`.", "createdAt": "2026-06-01T13:01:00Z", "url": "https://github.com/kdlbs/kandev/pull/123#issuecomment-3" }, { "author": { "login": "other-bot[bot]" }, "body": "\\n**OpenCode review complete**\\n\\nOpenCode model: \`opencode-go/minimax-m3\`\\n\\nNo suggestions found for commit \`abc1234\`.", "createdAt": "2026-06-01T13:02:00Z" }, { "author": { "login": "github-actions" }, "body": "Review quotes later", "createdAt": "2026-06-01T13:03:00Z" }, { "author": { "login": "github-actions" }, "body": "\\n**OpenCode review complete**\\nBlocker: real issue", "createdAt": "2026-06-01T13:04:00Z" }, { "author": { "login": "github-actions" }, "body": " failure", "createdAt": "2026-06-01T13:05:00Z" }, { "author": { "login": "github-actions" }, "body": " finding", "createdAt": "2026-06-01T13:06:00Z" } ], "statusCheckRollup": [ { "__typename": "CheckRun", "name": "web lint", "status": "COMPLETED", "conclusion": "CANCELLED", "detailsUrl": "https://github.com/kdlbs/kandev/actions/runs/27340000000/job/55150000000" }, { "__typename": "CheckRun", "name": "web lint", "status": "COMPLETED", "conclusion": "SUCCESS", "detailsUrl": "https://github.com/kdlbs/kandev/actions/runs/27340000001/job/55150000001" }, { "__typename": "CheckRun", "name": "e2e", "status": "COMPLETED", "conclusion": "FAILURE", "detailsUrl": "https://github.com/kdlbs/kandev/actions/runs/27340000002/job/55150000002", "checkSuite": { "workflowRun": { "workflow": { "name": "CI" } } } }, { "__typename": "CheckRun", "name": "claude-review", "status": "IN_PROGRESS", "conclusion": "", "detailsUrl": "https://github.com/kdlbs/kandev/actions/runs/27340000003/job/55150000003" }, { "__typename": "CheckRun", "name": "opencode-review-same-repo", "workflowName": "OpenCode Code Review", "status": "COMPLETED", "conclusion": "SKIPPED", "detailsUrl": "https://github.com/kdlbs/kandev/actions/runs/27340000004/job/55150000004" }, { "__typename": "CheckRun", "name": "opencode-review-same-repo", "workflowName": "OpenCode Code Review", "status": "COMPLETED", "conclusion": "SUCCESS", "detailsUrl": "https://github.com/kdlbs/kandev/actions/runs/27340000005/job/55150000005" }, { "__typename": "CheckRun", "name": "opencode-review-fork", "workflowName": "OpenCode Code Review", "status": "COMPLETED", "conclusion": "SKIPPED", "detailsUrl": "https://github.com/kdlbs/kandev/actions/runs/27340000006/job/55150000006" }, { "__typename": "CheckRun", "name": "opencode-review-fork", "workflowName": "OpenCode Code Review", "status": "COMPLETED", "conclusion": "FAILURE", "detailsUrl": "https://github.com/kdlbs/kandev/actions/runs/27340000007/job/55150000007" }, { "__typename": "CheckRun", "name": "late-cancelled-failure", "workflowName": "CI", "status": "COMPLETED", "conclusion": "FAILURE", "detailsUrl": "https://github.com/kdlbs/kandev/actions/runs/27340000008/job/55150000008" }, { "__typename": "CheckRun", "name": "late-cancelled-failure", "workflowName": "CI", "status": "COMPLETED", "conclusion": "CANCELLED", "detailsUrl": "https://github.com/kdlbs/kandev/actions/runs/27340000009/job/55150000009" }, { "__typename": "StatusContext", "context": "external pending", "state": "PENDING", "targetUrl": "https://ci.example.test/build/1" } ] } JSON exit 0 fi if [[ "$1" == "api" && "$2" == "--paginate" && "$3" == "-X" && "$4" == "GET" && "$5" == "repos/kdlbs/kandev/pulls/123/reviews" ]]; then if [[ "${GH_REVIEW_SCENARIO:-default}" == "old-head" ]]; then printf '%s\n' '[ {"user":{"login":"greptile-apps[bot]"},"state":"APPROVED","commit_id":"old-head-sha","submitted_at":"2026-06-01T13:30:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "exact-head" ]]; then printf '%s\n' '[ {"user":{"login":"greptile-apps[bot]"},"state":"APPROVED","commit_id":"abc123","body":"Reviewed and approved.","submitted_at":"2026-06-01T13:30:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "github-actions-clean" ]]; then printf '%s\n' '[ {"user":{"login":"github-actions[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"\\n**OpenCode review complete**","submitted_at":"2026-06-01T13:30:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "dedicated-app-clean" ]]; then printf '%s\n' '[ {"user":{"login":"opencode-review-app[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"\\n\\n**OpenCode review complete**","submitted_at":"2026-06-01T13:30:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "dedicated-app-wrong-run" ]]; then printf '%s\n' '[ {"user":{"login":"opencode-review-app[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"\\n","submitted_at":"2026-06-01T13:30:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "github-actions-old-head" ]]; then printf '%s\n' '[ {"user":{"login":"github-actions[bot]"},"state":"COMMENTED","commit_id":"old-head-sha","body":"\\n**OpenCode review complete**","submitted_at":"2026-06-01T13:30:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "exact-dismissed" ]]; then printf '%s\n' '[ {"user":{"login":"greptile-apps[bot]"},"state":"DISMISSED","commit_id":"abc123","body":"Dismissed after update.","submitted_at":"2026-06-01T13:30:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "approved-blocker" ]]; then printf '%s\n' '[ {"user":{"login":"greptile-apps[bot]"},"state":"APPROVED","commit_id":"abc123","body":"Blocker: the approval does not waive this required fix.","submitted_at":"2026-06-01T13:30:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "comment-blocker" ]]; then printf '%s\n' '[ {"user":{"login":"greptile-apps[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"Blocker: please fix the race before merge.","submitted_at":"2026-06-01T13:30:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "plain-selected-blocker" ]]; then printf '%s\n' '[ {"user":{"login":"greptile-apps[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"Blocker: race condition in the retry loop.","submitted_at":"2026-06-01T13:30:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "plain-nonselected-blocker" ]]; then printf '%s\n' '[ {"user":{"login":"greptile-apps[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"\nNo blockers.","submitted_at":"2026-06-01T13:30:00Z"}, {"user":{"login":"cubic-dev-ai[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"No blockers in unrelated files.\nBlockers: race condition in this path.","submitted_at":"2026-06-01T13:31:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "negated-colon-labels" ]]; then printf '%s\n' '[ {"user":{"login":"one[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"Blocker: 0","submitted_at":"2026-06-01T13:30:00Z"}, {"user":{"login":"two[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"Blockers: 0","submitted_at":"2026-06-01T13:31:00Z"}, {"user":{"login":"three[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"Blocker: none","submitted_at":"2026-06-01T13:32:00Z"}, {"user":{"login":"four[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"Blockers: no issues","submitted_at":"2026-06-01T13:33:00Z"}, {"user":{"login":"five[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"No blocker: all clear","submitted_at":"2026-06-01T13:34:00Z"}, {"user":{"login":"six[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"No blockers: all clear","submitted_at":"2026-06-01T13:35:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "negated-then-positive-label" ]]; then printf '%s\n' '[ {"user":{"login":"greptile-apps[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"No blockers: all clear\nBlocker: race condition","submitted_at":"2026-06-01T13:30:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "formatted-negative-labels" ]]; then printf '%s\n' '[ {"user":{"login":"one[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"**Blocker:** none","submitted_at":"2026-06-01T13:30:00Z"}, {"user":{"login":"two[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"> **Blockers:** 0","submitted_at":"2026-06-01T13:31:00Z"}, {"user":{"login":"three[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"_Blocker:_ no issue","submitted_at":"2026-06-01T13:32:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "formatted-positive-labels" ]]; then printf '%s\n' '[ {"user":{"login":"one[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"**Blocker:** race condition","submitted_at":"2026-06-01T13:30:00Z"}, {"user":{"login":"two[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"> **Blockers:** retry race","submitted_at":"2026-06-01T13:31:00Z"}, {"user":{"login":"three[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"- _Blocker:_ missing lock","submitted_at":"2026-06-01T13:32:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "single-asterisk-labels" ]]; then printf '%s\n' '[ {"user":{"login":"one[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"*Blocker:* race condition","submitted_at":"2026-06-01T13:30:00Z"}, {"user":{"login":"two[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"*Blocker:* none","submitted_at":"2026-06-01T13:31:00Z"}, {"user":{"login":"three[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"*Blockers:* 0","submitted_at":"2026-06-01T13:32:00Z"}, {"user":{"login":"four[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"* Blocker: race condition","submitted_at":"2026-06-01T13:33:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "comment-clean" ]]; then printf '%s\n' '[ {"user":{"login":"greptile-apps[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"\nNo findings.","submitted_at":"2026-06-01T13:30:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "comment-zero-table" ]]; then printf '%s\n' '[ {"user":{"login":"greptile-apps[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"\n| Blocker | 0 |\nNo findings.","submitted_at":"2026-06-01T13:30:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "comment-no-blockers" ]]; then printf '%s\n' '[ {"user":{"login":"greptile-apps[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"\nNo blockers. No findings.","submitted_at":"2026-06-01T13:30:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "comment-positive-table" ]]; then printf '%s\n' '[ {"user":{"login":"greptile-apps[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"\n| Blocker | 2 |\nMust fix before merge.","submitted_at":"2026-06-01T13:30:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "clean-plus-current-blocker" ]]; then printf '%s\n' '[ {"user":{"login":"greptile-apps[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"\nNo findings.","submitted_at":"2026-06-01T13:30:00Z"}, {"user":{"login":"cubic-dev-ai[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"Must fix this race before merge.","submitted_at":"2026-06-01T13:31:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "clean-plus-old-blocker" ]]; then printf '%s\n' '[ {"user":{"login":"greptile-apps[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"\nNo findings.","submitted_at":"2026-06-01T13:30:00Z"}, {"user":{"login":"cubic-dev-ai[bot]"},"state":"COMMENTED","commit_id":"old-head-sha","body":"Must fix this old issue before merge.","submitted_at":"2026-06-01T13:31:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "changes-dismissed" ]]; then printf '%s\n' '[ {"user":{"login":"cubic-dev-ai[bot]"},"state":"CHANGES_REQUESTED","commit_id":"abc123","submitted_at":"2026-06-01T13:30:00Z"}, {"user":{"login":"cubic-dev-ai[bot]"},"state":"DISMISSED","commit_id":"abc123","submitted_at":"2026-06-01T13:45:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "changes-current-clean" ]]; then printf '%s\n' '[ {"user":{"login":"cubic-dev-ai[bot]"},"state":"CHANGES_REQUESTED","commit_id":"abc123","submitted_at":"2026-06-01T13:30:00Z"}, {"user":{"login":"cubic-dev-ai[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"\nNo findings.","submitted_at":"2026-06-01T13:45:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "old-change-current-clean" ]]; then printf '%s\n' '[ {"user":{"login":"cubic-dev-ai[bot]"},"state":"CHANGES_REQUESTED","commit_id":"old-head-sha","submitted_at":"2026-06-01T13:30:00Z"}, {"user":{"login":"cubic-dev-ai[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"\\nNo findings.","submitted_at":"2026-06-01T13:45:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "current-change-old-approval" ]]; then printf '%s\n' '[ {"user":{"login":"cubic-dev-ai[bot]"},"state":"CHANGES_REQUESTED","commit_id":"abc123","submitted_at":"2026-06-01T13:30:00Z"}, {"user":{"login":"cubic-dev-ai[bot]"},"state":"APPROVED","commit_id":"old-head-sha","submitted_at":"2026-06-01T13:45:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "quoted-blocker" ]]; then printf '%s\n' '[ {"user":{"login":"greptile-apps[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"\\nPrior review quoted: please fix the race before merge.","submitted_at":"2026-06-01T13:30:00Z"} ]' exit 0 fi if [[ "${GH_REVIEW_SCENARIO:-default}" == "fenced-blocker-example" ]]; then printf '%s\n' '[ {"user":{"login":"greptile-apps[bot]"},"state":"COMMENTED","commit_id":"abc123","body":"\nNo findings.\n\n```text\nplease fix the example\nBlocker: illustrative only\n```","submitted_at":"2026-06-01T13:30:00Z"} ]' exit 0 fi printf '%s\n' '[ { "user": { "login": "greptile-apps[bot]" }, "state": "COMMENTED", "commit_id": "old-head-sha", "submitted_at": "2026-06-01T10:30:00Z" } ]' printf '%s\n' '[ { "user": { "login": "cubic-dev-ai[bot]" }, "state": "CHANGES_REQUESTED", "commit_id": "abc123", "submitted_at": "2026-06-01T13:30:00Z" } ]' exit 0 fi if [[ "$1" == "api" && "$2" == "graphql" ]]; then if [[ "$*" == *"baseRefOid"* && "$*" != *"headRefOid"* ]]; then base_head="${GH_BASE_HEAD:-base-head-sha}" if [[ "${GH_BASE_SEQUENCE:-stable}" == "race" ]]; then if [[ -f "${GH_BASE_COUNTER_FILE:?}" ]]; then base_head="late-base-head-sha" else : >"$GH_BASE_COUNTER_FILE" fi fi printf '%s\n' '{ "data": { "repository": { "pullRequest": { "baseRefName": "main", "baseRefOid": "'"$base_head"'" } } } }' exit 0 fi if [[ "$*" == *"headRefOid"* ]]; then head_oid="abc123" if [[ "${GH_HEAD_SEQUENCE:-stable}" == "race" ]]; then if [[ -f "${GH_HEAD_COUNTER_FILE:?}" ]]; then head_oid="def456" else : >"$GH_HEAD_COUNTER_FILE" fi elif [[ "${GH_HEAD_SEQUENCE:-stable}" == "after-workflow" && -f "${GH_HEAD_COUNTER_FILE:?}" ]]; then head_oid="def456" fi printf '%s\n' '{ "data": { "repository": { "pullRequest": { "headRefOid": "'"$head_oid"'", "commits": { "nodes": [ { "commit": { "oid": "'"$head_oid"'", "committedDate": "2026-06-01T12:00:00Z" } } ] } } } } }' exit 0 fi if [[ "${GH_GRAPHQL_TWO_PAGES:-0}" == "1" && "$*" != *"cursor=CURSOR1"* ]]; then printf '%s\n' '{ "data": { "repository": { "pullRequest": { "reviewThreads": { "pageInfo": { "hasNextPage": true, "endCursor": "CURSOR1" }, "nodes": [ { "id": "PRRT_1", "isResolved": false, "path": "apps/web/file.ts", "comments": { "nodes": [ { "databaseId": 111, "createdAt": "2026-06-01T13:00:00Z", "author": { "login": "greptile-apps[bot]" }, "body": "Please rename this helper" } ] } } ] } } } } }' exit 0 fi if [[ "${GH_GRAPHQL_TWO_PAGES:-0}" == "1" && "$*" == *"cursor=CURSOR1"* ]]; then printf '%s\n' '{ "data": { "repository": { "pullRequest": { "reviewThreads": { "pageInfo": { "hasNextPage": false, "endCursor": null }, "nodes": [ { "id": "PRRT_2", "isResolved": true, "path": "apps/web/other.ts", "comments": { "nodes": [ { "databaseId": 222, "createdAt": "2026-06-01T14:00:00Z", "author": { "login": "cubic-dev-ai[bot]" }, "body": "resolved" } ] } } ] } } } } }' exit 0 fi printf '%s\n' '{ "data": { "repository": { "pullRequest": { "reviewThreads": { "pageInfo": { "hasNextPage": false, "endCursor": null }, "nodes": [ { "id": "PRRT_1", "isResolved": false, "path": "apps/web/file.ts", "comments": { "nodes": [ { "databaseId": 111, "createdAt": "2026-06-01T10:00:00Z", "author": { "login": "greptile-apps[bot]" }, "body": "Please rename this helper" }, { "databaseId": 112, "createdAt": "2026-06-01T13:00:00Z", "author": { "login": "greptile-apps[bot]" }, "body": "Please rename this helper after the latest commit" } ] } }, { "id": "PRRT_2", "isResolved": false, "path": "apps/web/other.ts", "comments": { "nodes": [ { "databaseId": 222, "createdAt": "2026-06-01T11:00:00Z", "author": { "login": "cubic-dev-ai[bot]" }, "body": "resolved" } ] } } ] } } } } }' exit 0 fi echo "unexpected gh args: $*" >&2 exit 1 EOF chmod +x "$dir/gh" } test_snapshot_happy_path() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "pr number" '.pr.number == 123' "$json" assert_jq "branch" '.pr.branch == "feat/pr-state"' "$json" assert_jq "head delivery target" '.pr.head_repository_owner == "kdlbs" and .pr.head_repository_name == "kandev" and .pr.head_ref_name == "feat/pr-state" and .pr.head_ref_oid == "abc123" and .pr.maintainer_can_modify == true' "$json" assert_jq "base divergence fields" '.pr.base_ref_name == "main" and .pr.base_head_oid == "base-head-sha" and .pr.merge_base_oid == "base-branch-sha" and .pr.base_advanced_since_head == true' "$json" assert_jq "since timestamp" '.since.committed_at == "2026-06-01T12:00:00Z"' "$json" assert_jq "checks collapse duplicate workflow attempts" '.checks | length < 10' "$json" assert_jq "latest duplicate check uses newest attempt" '[.checks[] | select(.name == "web lint")][0] | .conclusion == "success" and .run_id == "27340000001"' "$json" assert_jq "failed check preserved" '.checks[] | select(.name == "e2e") | .conclusion == "failure"' "$json" assert_jq "check run id" '.checks[] | select(.name == "e2e") | .run_id == "27340000002"' "$json" assert_jq "nested workflow name" '.checks[] | select(.name == "e2e") | .workflow == "CI"' "$json" assert_jq "duplicate check prefers non-skipped first" '[.checks[] | select(.name == "opencode-review-same-repo")][0] | .conclusion == "success" and .run_id == "27340000005"' "$json" assert_jq "duplicate cancellation retains prior failure" '[.checks[] | select(.name == "late-cancelled-failure")][0] | .conclusion == "failure" and .run_id == "27340000008"' "$json" assert_jq "pending status context conclusion normalized" '.checks[] | select(.name == "external pending") | .status == "pending" and .conclusion == null' "$json" assert_jq "threads count" '.review_threads | length == 1' "$json" assert_jq "total unresolved count includes historical unresolved thread" '.unresolved_review_thread_count == 2' "$json" assert_jq "filtered thread count" '.filtered_review_thread_count == 1' "$json" assert_jq "thread comment id" '.review_threads[] | select(.thread_id == "PRRT_1") | .comment_id == 112' "$json" assert_jq "thread comment timestamp" '.review_threads[] | select(.thread_id == "PRRT_1") | .comment_created_at == "2026-06-01T13:00:00Z"' "$json" assert_jq "reviews count" '.reviews | length == 1' "$json" assert_jq "review author" '.reviews[] | select(.author == "cubic-dev-ai[bot]") | .author == "cubic-dev-ai[bot]"' "$json" assert_jq "issue comments count" '.issue_comments | length == 7' "$json" assert_jq "issue comment author" 'any(.issue_comments[]; .author == "github-actions" and (.body | contains("Verdict")))' "$json" assert_jq "only GitHub Actions noncanonical output is actionable" '([.issue_comments[] | select(.actionable == false)] | length) == 2 and .actionable_issue_comment_count == 5' "$json" assert_jq "non-GitHub-Actions canonical marker is nonactionable while workflow diagnostic and fallback remain actionable" 'any(.issue_comments[]; .author == "other-bot[bot]" and .actionable == false) and all(.issue_comments[] | select(.body | test("Blocker: real|diagnostic|fallback"; "i")); .actionable == true)' "$json" assert_jq "no errors" '.errors == []' "$json" assert_jq "raw review evidence is complete" '.review_evidence.complete == true and .review_evidence.current_head_sha == "abc123"' "$json" assert_jq "active change request surfaced" '.review_evidence.active_changes_requested_count == 1' "$json" pass "snapshot happy path" } test_old_head_review_does_not_qualify() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json GH_REVIEW_SCENARIO=old-head PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "old head review evidence is complete but not exact" '.review_evidence.complete == true and .review_evidence.exact_current_head_reviews == []' "$json" pass "old-head review does not qualify" } test_exact_head_selected_review_qualifies() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json GH_REVIEW_SCENARIO=exact-head PATH="$tmp/bin:$PATH" "$SCRIPT" --summary 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "summary exact selected review evidence" '.review_evidence.complete == true and .review_evidence.exact_current_head_reviews[0].author == "greptile-apps[bot]" and .review_evidence.exact_current_head_reviews[0].body == "Reviewed and approved." and .review_evidence.exact_current_head_reviews[0].eligibility == "eligible"' "$json" pass "exact-head selected review qualifies" } test_clean_marker_requires_configured_dedicated_app_provenance() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local exact_json GH_REVIEW_SCENARIO=github-actions-clean PATH="$tmp/bin:$PATH" "$SCRIPT" --summary 123 >"$tmp/unconfigured.json" local unconfigured_json unconfigured_json="$(<"$tmp/unconfigured.json")" assert_jq "unconfigured marker is not trusted evidence" '.review_evidence.exact_current_head_reviews[0].trusted_default_producer == false and .review_evidence.trusted_default_producer_exact_current_head_reviews == []' "$unconfigured_json" GH_REVIEW_SCENARIO=dedicated-app-clean PATH="$tmp/bin:$PATH" "$SCRIPT" --summary --trusted-reviewer 'opencode-review-app[bot]' 123 >"$tmp/exact.json" exact_json="$(<"$tmp/exact.json")" assert_jq "matching dedicated App has authenticated OpenCode provenance" '.review_evidence.exact_current_head_reviews[0].author == "opencode-review-app[bot]" and .review_evidence.exact_current_head_reviews[0].eligibility == "eligible" and .review_evidence.exact_current_head_reviews[0].trusted_default_producer == true and .review_evidence.trusted_workflow_run.id == 900 and .review_evidence.trusted_workflow_run.complete == true and .review_evidence.trusted_producer == "true"' "$exact_json" local old_json GH_REVIEW_SCENARIO=github-actions-old-head PATH="$tmp/bin:$PATH" "$SCRIPT" --summary --trusted-reviewer 'github-actions[bot]' 123 >"$tmp/old.json" old_json="$(<"$tmp/old.json")" assert_jq "old head actions review never becomes trusted evidence" '.review_evidence.exact_current_head_reviews == [] and .review_evidence.trusted_default_producer_exact_current_head_reviews == []' "$old_json" pass "clean marker requires configured dedicated app provenance" } test_trusted_workflow_requires_latest_success_and_matching_marker() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json for conclusion in failure neutral cancelled skipped; do GH_TRUSTED_WORKFLOW_CONCLUSION="$conclusion" GH_REVIEW_SCENARIO=dedicated-app-clean PATH="$tmp/bin:$PATH" "$SCRIPT" --summary --trusted-reviewer 'opencode-review-app[bot]' 123 >"$tmp/$conclusion.json" json="$(<"$tmp/$conclusion.json")" assert_jq "latest $conclusion workflow invalidates old clean review" '.review_evidence.trusted_workflow_run.complete == false and .review_evidence.trusted_producer == "false" and .review_evidence.trusted_default_producer_exact_current_head_reviews == []' "$json" done GH_TRUSTED_WORKFLOW_STATUS=in_progress GH_TRUSTED_WORKFLOW_CONCLUSION=null GH_REVIEW_SCENARIO=dedicated-app-clean PATH="$tmp/bin:$PATH" "$SCRIPT" --summary --trusted-reviewer 'opencode-review-app[bot]' 123 >"$tmp/pending.json" json="$(<"$tmp/pending.json")" assert_jq "latest pending workflow invalidates old clean review" '.review_evidence.trusted_workflow_run.complete == false and .review_evidence.trusted_default_producer_exact_current_head_reviews == []' "$json" GH_REVIEW_SCENARIO=dedicated-app-wrong-run PATH="$tmp/bin:$PATH" "$SCRIPT" --summary --trusted-reviewer 'opencode-review-app[bot]' 123 >"$tmp/wrong-marker.json" json="$(<"$tmp/wrong-marker.json")" assert_jq "wrong workflow marker is untrusted" '.review_evidence.trusted_workflow_run.complete == true and .review_evidence.trusted_default_producer_exact_current_head_reviews == []' "$json" GH_TRUSTED_MULTI=1 GH_REVIEW_SCENARIO=dedicated-app-clean PATH="$tmp/bin:$PATH" "$SCRIPT" --summary --trusted-reviewer 'opencode-review-app[bot]' 123 >"$tmp/reversed-generations.json" json="$(<"$tmp/reversed-generations.json")" assert_jq "newer authentic failure beats older success despite reverse API order" '.review_evidence.trusted_workflow_run.id == 901 and .review_evidence.trusted_workflow_run.complete == false and .review_evidence.trusted_default_producer_exact_current_head_reviews == []' "$json" GH_FAIL_TRUSTED_WORKFLOW=1 GH_REVIEW_SCENARIO=dedicated-app-clean PATH="$tmp/bin:$PATH" "$SCRIPT" --summary --trusted-reviewer 'opencode-review-app[bot]' 123 >"$tmp/api-failure.json" json="$(<"$tmp/api-failure.json")" assert_jq "workflow API failure is unknown and untrusted" '.review_evidence.trusted_workflow_run.known == false and .review_evidence.trusted_producer == "unknown" and .review_evidence.trusted_default_producer_exact_current_head_reviews == []' "$json" pass "trusted workflow requires latest success and matching marker" } test_fork_and_unknown_provenance_are_untrusted() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local fork_json unknown_json GH_CROSS_REPOSITORY=true GH_REVIEW_SCENARIO=dedicated-app-clean PATH="$tmp/bin:$PATH" "$SCRIPT" --summary --trusted-reviewer 'opencode-review-app[bot]' 123 >"$tmp/fork.json" fork_json="$(<"$tmp/fork.json")" assert_jq "fork App marker is untrusted" '.pr.is_cross_repository == true and .review_evidence.is_cross_repository == true and .review_evidence.trusted_default_producer_exact_current_head_reviews == []' "$fork_json" GH_CROSS_REPOSITORY=null GH_REVIEW_SCENARIO=dedicated-app-clean PATH="$tmp/bin:$PATH" "$SCRIPT" --summary --trusted-reviewer 'opencode-review-app[bot]' 123 >"$tmp/unknown.json" unknown_json="$(<"$tmp/unknown.json")" assert_jq "unknown repository provenance is untrusted" '.pr.is_cross_repository == null and .review_evidence.is_cross_repository == null and .review_evidence.trusted_default_producer_exact_current_head_reviews == []' "$unknown_json" pass "fork and unknown provenance are untrusted" } test_head_change_during_collection_invalidates_review_evidence() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json GH_HEAD_SEQUENCE=race GH_HEAD_COUNTER_FILE="$tmp/head-calls" GH_REVIEW_SCENARIO=exact-head PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "head race is incomplete and cannot qualify" '.review_evidence.known == false and .review_evidence.complete == false and .review_evidence.opening_head_sha == "abc123" and .review_evidence.closing_head_sha == "def456" and .review_evidence.current_head_sha == "def456" and .review_evidence.evidence_head_sha == null and .review_evidence.exact_current_head_reviews == []' "$json" pass "head change during collection invalidates review evidence" } test_head_change_during_authenticated_workflow_collection_invalidates_trust() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json GH_HEAD_SEQUENCE=after-workflow GH_HEAD_COUNTER_FILE="$tmp/workflow-called" GH_REVIEW_SCENARIO=dedicated-app-clean PATH="$tmp/bin:$PATH" "$SCRIPT" --summary --trusted-reviewer 'opencode-review-app[bot]' 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "post-workflow head race invalidates authenticated trust" '.review_evidence.complete == false and .review_evidence.opening_head_sha == "abc123" and .review_evidence.closing_head_sha == "def456" and .review_evidence.trusted_default_producer_exact_current_head_reviews == []' "$json" pass "head change during authenticated workflow collection invalidates trust" } test_checks_head_mismatch_invalidates_combined_evidence() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json GH_CHECKS_HEAD=old-checks-sha GH_REVIEW_SCENARIO=exact-head PATH="$tmp/bin:$PATH" "$SCRIPT" --summary 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "checks and reviews cannot be paired across heads" '.checks_head_sha == "old-checks-sha" and .checks_snapshot_complete == false and .review_evidence.known == false and .review_evidence.complete == false and .review_evidence.checks_head_sha == "old-checks-sha" and .review_evidence.exact_current_head_reviews == []' "$json" pass "checks head mismatch invalidates combined evidence" } test_exact_head_dismissed_review_is_not_eligible() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json GH_REVIEW_SCENARIO=exact-dismissed PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "dismissed exact review is ineligible" '.review_evidence.exact_current_head_reviews[0].state == "dismissed" and .review_evidence.exact_current_head_reviews[0].eligibility == "ineligible"' "$json" pass "exact-head dismissed review is not eligible" } test_approved_review_with_blocker_is_not_eligible() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json GH_REVIEW_SCENARIO=approved-blocker PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "approved review body blocker wins" '.review_evidence.exact_current_head_reviews[0].state == "approved" and .review_evidence.exact_current_head_reviews[0].eligibility == "blocked" and .review_evidence.exact_current_head_reviews[0].verdict == "blocker"' "$json" pass "approved review with blocker is not eligible" } test_commented_blocker_is_not_eligible() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json GH_REVIEW_SCENARIO=comment-blocker PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "commented blocker is blocked" '(.review_evidence.exact_current_head_reviews[0].body | contains("Blocker")) and .review_evidence.exact_current_head_reviews[0].eligibility == "blocked" and .review_evidence.exact_current_head_reviews[0].verdict == "blocker"' "$json" pass "commented blocker is not eligible" } test_plain_blocker_labels_are_blocked_and_aggregated() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local selected_json GH_REVIEW_SCENARIO=plain-selected-blocker PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/selected.json" selected_json="$(<"$tmp/selected.json")" assert_jq "plain selected blocker is blocked" '.review_evidence.exact_current_head_reviews[0].eligibility == "blocked" and .review_evidence.blocked_exact_current_head_review_count == 1' "$selected_json" local nonselected_json GH_REVIEW_SCENARIO=plain-nonselected-blocker PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/nonselected.json" nonselected_json="$(<"$tmp/nonselected.json")" assert_jq "plain nonselected blocker aggregates despite no blockers text" '.review_evidence.exact_current_head_reviews[0].eligibility == "eligible" and .review_evidence.blocked_exact_current_head_review_count == 1 and .review_evidence.blocked_exact_current_head_reviews[0].author == "cubic-dev-ai[bot]"' "$nonselected_json" pass "plain blocker labels are blocked and aggregated" } test_colon_blocker_labels_are_line_anchored_and_scan_all_lines() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local negative_json GH_REVIEW_SCENARIO=negated-colon-labels PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/negative.json" negative_json="$(<"$tmp/negative.json")" assert_jq "zero and negated colon labels are nonblocking" '.review_evidence.blocked_exact_current_head_review_count == 0 and ([.review_evidence.exact_current_head_reviews[].eligibility] | all(. != "blocked"))' "$negative_json" local mixed_json GH_REVIEW_SCENARIO=negated-then-positive-label PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/mixed.json" mixed_json="$(<"$tmp/mixed.json")" assert_jq "later positive label wins" '.review_evidence.blocked_exact_current_head_review_count == 1 and .review_evidence.exact_current_head_reviews[0].eligibility == "blocked"' "$mixed_json" pass "colon blocker labels are line anchored and scan all lines" } test_formatted_colon_labels_handle_emphasis_after_colon() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local negative_json GH_REVIEW_SCENARIO=formatted-negative-labels PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/negative.json" negative_json="$(<"$tmp/negative.json")" assert_jq "formatted negative labels are nonblocking" '.review_evidence.blocked_exact_current_head_review_count == 0 and ([.review_evidence.exact_current_head_reviews[].eligibility] | all(. != "blocked"))' "$negative_json" local positive_json GH_REVIEW_SCENARIO=formatted-positive-labels PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/positive.json" positive_json="$(<"$tmp/positive.json")" assert_jq "formatted positive labels block" '.review_evidence.blocked_exact_current_head_review_count == 3 and ([.review_evidence.exact_current_head_reviews[].eligibility] | all(. == "blocked"))' "$positive_json" pass "formatted colon labels handle emphasis after colon" } test_single_asterisk_emphasis_and_list_markers_are_safe() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json GH_REVIEW_SCENARIO=single-asterisk-labels PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "single asterisk forms classify correctly" '.review_evidence.blocked_exact_current_head_review_count == 2 and .review_evidence.exact_current_head_reviews[0].eligibility == "blocked" and .review_evidence.exact_current_head_reviews[1].eligibility != "blocked" and .review_evidence.exact_current_head_reviews[2].eligibility != "blocked" and .review_evidence.exact_current_head_reviews[3].eligibility == "blocked"' "$json" pass "single asterisk emphasis and list markers are safe" } test_explicit_clean_commented_review_is_eligible() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json GH_REVIEW_SCENARIO=comment-clean PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "explicit clean comment is eligible" '.review_evidence.exact_current_head_reviews[0].eligibility == "eligible" and .review_evidence.exact_current_head_reviews[0].verdict == "clean"' "$json" pass "explicit clean commented review is eligible" } test_zero_blocker_summaries_remain_clean_with_explicit_marker() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local table_json GH_REVIEW_SCENARIO=comment-zero-table PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/table.json" table_json="$(<"$tmp/table.json")" assert_jq "zero blocker table is clean" '.review_evidence.exact_current_head_reviews[0].eligibility == "eligible" and .review_evidence.exact_current_head_reviews[0].verdict == "clean"' "$table_json" local no_blockers_json GH_REVIEW_SCENARIO=comment-no-blockers PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/no-blockers.json" no_blockers_json="$(<"$tmp/no-blockers.json")" assert_jq "no blockers text is clean" '.review_evidence.exact_current_head_reviews[0].eligibility == "eligible" and .review_evidence.exact_current_head_reviews[0].verdict == "clean"' "$no_blockers_json" pass "zero blocker summaries remain clean with explicit marker" } test_positive_blocker_evidence_overrides_clean_marker() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json GH_REVIEW_SCENARIO=comment-positive-table PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "positive blocker overrides clean marker" '.review_evidence.exact_current_head_reviews[0].eligibility == "blocked" and .review_evidence.exact_current_head_reviews[0].verdict == "blocker"' "$json" pass "positive blocker evidence overrides clean marker" } test_exact_head_blockers_from_any_author_are_aggregated() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local current_json GH_REVIEW_SCENARIO=clean-plus-current-blocker PATH="$tmp/bin:$PATH" "$SCRIPT" --summary 123 >"$tmp/current.json" current_json="$(<"$tmp/current.json")" assert_jq "other author's current blocker is aggregated" '.review_evidence.blocked_exact_current_head_review_count == 1 and .review_evidence.blocked_exact_current_head_reviews[0].author == "cubic-dev-ai[bot]" and .review_evidence.blocked_exact_current_head_reviews[0].eligibility == "blocked"' "$current_json" local old_json GH_REVIEW_SCENARIO=clean-plus-old-blocker PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/old.json" old_json="$(<"$tmp/old.json")" assert_jq "old-head blocker is excluded" '.review_evidence.blocked_exact_current_head_review_count == 0 and .review_evidence.exact_current_head_reviews[0].eligibility == "eligible"' "$old_json" pass "exact-head blockers from any author are aggregated" } test_latest_decisive_review_state_controls_active_change_requests() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local blocked_json PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/blocked.json" blocked_json="$(<"$tmp/blocked.json")" assert_jq "latest change request blocks" '.review_evidence.active_changes_requested_count == 1 and .review_evidence.active_changes_requested[0].author == "cubic-dev-ai[bot]"' "$blocked_json" local cleared_json GH_REVIEW_SCENARIO=changes-dismissed PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/cleared.json" cleared_json="$(<"$tmp/cleared.json")" assert_jq "later decisive dismissal clears change request" '.review_evidence.active_changes_requested_count == 0' "$cleared_json" local clean_json GH_REVIEW_SCENARIO=changes-current-clean PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/clean.json" clean_json="$(<"$tmp/clean.json")" assert_jq "later clean current-head review clears change request" '.review_evidence.active_changes_requested_count == 0' "$clean_json" local old_change_current_clean_json GH_REVIEW_SCENARIO=old-change-current-clean PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/old-change-current-clean.json" old_change_current_clean_json="$(<"$tmp/old-change-current-clean.json")" assert_jq "old-head change request does not block a clean current head" '.review_evidence.exact_current_head_reviews[0].eligibility == "eligible" and .review_evidence.active_changes_requested_count == 0' "$old_change_current_clean_json" local current_change_old_approval_json GH_REVIEW_SCENARIO=current-change-old-approval PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/current-change-old-approval.json" current_change_old_approval_json="$(<"$tmp/current-change-old-approval.json")" assert_jq "old-head approval cannot clear a current-head change request" '.review_evidence.active_changes_requested_count == 1 and .review_evidence.active_changes_requested[0].commit_id == "abc123"' "$current_change_old_approval_json" pass "latest decisive review state controls active change requests" } test_quoted_blocker_language_does_not_block_clean_review() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json GH_REVIEW_SCENARIO=quoted-blocker PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "quoted blocker text preserves explicit clean review" '.review_evidence.exact_current_head_reviews[0].eligibility == "eligible" and .review_evidence.blocked_exact_current_head_review_count == 0' "$json" pass "quoted blocker language does not block clean review" } test_fenced_blocker_examples_do_not_block_clean_review() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json GH_REVIEW_SCENARIO=fenced-blocker-example PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "fenced blocker examples preserve explicit clean review" '.review_evidence.exact_current_head_reviews[0].eligibility == "eligible" and .review_evidence.blocked_exact_current_head_review_count == 0' "$json" pass "fenced blocker examples do not block clean review" } test_partial_failure_records_error_but_keeps_other_data() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json GH_FAIL_REVIEWS=1 PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "reviews empty on failure" '.reviews == []' "$json" assert_jq "checks still present" '.checks | length < 9' "$json" assert_jq "new issue comments still present" '.issue_comments | length == 7' "$json" assert_jq "partial failure recorded" '.errors | length == 1' "$json" assert_jq "partial failure source" '.errors[0].source == "reviews"' "$json" pass "partial failure records error but keeps other data" } test_pr_view_failure_with_non_numeric_ref_keeps_schema() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json GH_FAIL_PR_VIEW=1 PATH="$tmp/bin:$PATH" "$SCRIPT" feat/pr-state >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "pr number is null on ref-based invocation" '.pr.number == null' "$json" assert_jq "pr branch preserved" '.pr.branch == "feat/pr-state"' "$json" assert_jq "pr_view error recorded" '.errors[] | select(.source == "pr_view") | .message == "gh pr view failed"' "$json" pass "pr_view failure keeps schema stable for non-numeric refs" } test_repo_failure_skips_review_threads() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json GH_FAIL_REPO=1 GH_NO_PR_URL=1 PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "checks still present on repo failure" '.checks | length < 9' "$json" assert_jq "review threads empty on repo failure" '.review_threads == []' "$json" assert_jq "unresolved count unknown on repo failure" '.unresolved_review_thread_count == null' "$json" assert_jq "repo failure recorded" '.errors[] | select(.source == "repo") | .message == "gh repo view failed"' "$json" assert_jq "review thread skip recorded" '.errors[] | select(.source == "review_threads") | .message == "skipped: repo lookup failed"' "$json" pass "repo failure skips review threads" } test_graphql_failure_records_error_but_keeps_other_data() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json GH_FAIL_GRAPHQL=1 PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "checks still present on graphql failure" '.checks | length < 9' "$json" assert_jq "reviews still present on graphql failure" '.reviews | length == 2' "$json" assert_jq "review threads empty on graphql failure" '.review_threads == []' "$json" assert_jq "unresolved count unknown on graphql failure" '.unresolved_review_thread_count == null' "$json" assert_jq "graphql failure records base, opening, and closing errors" '.errors | length == 5' "$json" assert_jq "graphql failure records base lookup error" '.errors[] | select(.source == "base") | .message == "gh api graphql base ref lookup failed"' "$json" assert_jq "graphql failure records since error" '.errors[] | select(.source == "since") | .message == "gh api graphql head commit failed; including historical comments"' "$json" assert_jq "graphql failure records review_threads error" '.errors[] | select(.source == "review_threads") | .message == "gh api graphql reviewThreads failed"' "$json" assert_jq "graphql failure records closing head error" '.errors[] | select(.source == "closing_head") | .message == "gh api graphql closing head commit failed"' "$json" assert_jq "graphql failure records closing base error" '.errors[] | select(.source == "closing_base") | .message == "gh api graphql base ref lookup failed after evidence collection"' "$json" assert_jq "since fallback includes all historical comments" '.issue_comments | length == 8' "$json" pass "graphql failure records error but keeps other data" } test_graphql_pagination_collects_all_threads() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json GH_GRAPHQL_TWO_PAGES=1 PATH="$tmp/bin:$PATH" "$SCRIPT" 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "paginated threads count" '.review_threads | length == 2' "$json" assert_jq "paginated first thread" '.review_threads[] | select(.thread_id == "PRRT_1") | .is_resolved == false' "$json" assert_jq "paginated second thread" '.review_threads[] | select(.thread_id == "PRRT_2") | .is_resolved == true' "$json" assert_jq "paginated unresolved count" '.unresolved_review_thread_count == 1' "$json" assert_jq "paginated filtered thread count" '.filtered_review_thread_count == 2' "$json" assert_jq "paginated no errors" '.errors == []' "$json" pass "graphql pagination collects all threads" } test_all_flag_includes_historical_comments_and_reviews() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json PATH="$tmp/bin:$PATH" "$SCRIPT" --all 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "since omitted in all mode" '.since == null' "$json" assert_jq "all issue comments present" '.issue_comments | length == 8' "$json" assert_jq "all reviews present" '.reviews | length == 2' "$json" assert_jq "all review threads present" '.review_threads | length == 2' "$json" assert_jq "all mode keeps historical thread comment" '.review_threads[] | select(.thread_id == "PRRT_1") | .comment_id == 111' "$json" pass "--all includes historical comments and reviews" } test_summary_mode_returns_compact_fixup_state() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json PATH="$tmp/bin:$PATH" "$SCRIPT" --summary 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "summary keeps pr" '.pr.number == 123' "$json" assert_jq "summary keeps authoritative head delivery target" '.pr.head_repository_owner == "kdlbs" and .pr.head_repository_name == "kandev" and .pr.head_ref_name == "feat/pr-state" and .pr.head_ref_oid == "abc123" and .pr.maintainer_can_modify == true' "$json" assert_jq "summary keeps base divergence fields" '.pr.base_ref_name == "main" and .pr.base_head_oid == "base-head-sha" and .pr.merge_base_oid == "base-branch-sha" and .pr.base_advanced_since_head == true' "$json" assert_jq "summary keeps since" '.since.committed_at == "2026-06-01T12:00:00Z"' "$json" assert_jq "summary has no approval-required runs by default" '.approval_required_runs == []' "$json" assert_jq "summary failed check count" '.failed_checks | length == 3' "$json" assert_jq "summary failed check" '.failed_checks[] | select(.name == "e2e") | .conclusion == "failure" and .run_id == "27340000002"' "$json" assert_jq "summary reports failed duplicate over skipped" '.failed_checks[] | select(.name == "opencode-review-fork") | .conclusion == "failure" and .run_id == "27340000007"' "$json" assert_jq "summary retains failure before a duplicate cancellation" '.failed_checks[] | select(.name == "late-cancelled-failure") | .conclusion == "failure" and .run_id == "27340000008"' "$json" assert_jq "summary ignores cancelled duplicate check" '[.failed_checks[] | select(.name == "web lint")] | length == 0' "$json" assert_jq "summary dedupes skipped duplicate check" '[.failed_checks[], .pending_checks[] | select(.name == "opencode-review-same-repo")] | length == 0' "$json" assert_jq "summary pending check count" '.pending_checks | length == 2' "$json" assert_jq "summary pending check" '.pending_checks[0] | .name == "claude-review" and .status == "in_progress" and .run_id == "27340000003"' "$json" assert_jq "summary pending status context keeps target url" '.pending_checks[] | select(.name == "external pending") | .status == "pending" and .details_url == null and .target_url == "https://ci.example.test/build/1"' "$json" assert_jq "summary exposes normalized passing checks" '(.passed_check_count == (.successful_checks | length)) and any(.successful_checks[]; .name == "web lint" and .conclusion == "success") and any(.successful_checks[]; .name == "opencode-review-same-repo" and .conclusion == "success")' "$json" assert_jq "summary unresolved count" '.unresolved_review_thread_count == 2' "$json" assert_jq "summary filtered thread count" '.filtered_review_thread_count == 1' "$json" assert_jq "summary unresolved threads" '.unresolved_threads | length == 1' "$json" assert_jq "summary unresolved thread fields" '.unresolved_threads[0] | .thread_id == "PRRT_1" and .comment_id == 112 and .author == "greptile-apps[bot]"' "$json" assert_jq "summary hidden unresolved threads" '.hidden_unresolved_threads | length == 1' "$json" assert_jq "summary hidden unresolved thread fields" '.hidden_unresolved_threads[0] | .thread_id == "PRRT_2" and .comment_id == 222 and .author == "cubic-dev-ai[bot]"' "$json" assert_jq "summary omits raw arrays" 'has("checks") | not' "$json" assert_jq "summary no errors" '.errors == []' "$json" pass "--summary returns compact fixup state" } test_summary_reports_current_head_fork_approval_runs() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json GH_CROSS_REPOSITORY=true GH_APPROVAL_REQUIRED=1 PATH="$tmp/bin:$PATH" "$SCRIPT" --summary 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "summary reports only current-head approval run" \ '.approval_required_runs | length == 1 and .[0].run_id == "444" and .[0].head_sha == "abc123" and .[0].conclusion == "action_required"' \ "$json" pass "--summary reports current-head fork approval runs" } test_summary_reports_base_not_advanced_when_head_matches_merge_base() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json GH_BASE_HEAD=base-branch-sha GH_MERGE_BASE=base-branch-sha PATH="$tmp/bin:$PATH" "$SCRIPT" --summary 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "summary reports base not advanced" '.pr.base_advanced_since_head == false' "$json" pass "summary reports base not advanced when head matches merge base" } test_summary_revalidates_base_at_closing_head() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json GH_BASE_SEQUENCE=race GH_BASE_COUNTER_FILE="$tmp/base-calls" GH_MERGE_BASE=base-head-sha PATH="$tmp/bin:$PATH" "$SCRIPT" --summary 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "summary uses closing base ref" '.pr.base_head_oid == "late-base-head-sha" and .pr.merge_base_oid == "late-merge-base-sha" and .pr.base_advanced_since_head == true' "$json" pass "summary revalidates base at closing head" } test_summary_reports_approval_run_fetch_failure() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json GH_CROSS_REPOSITORY=true GH_FAIL_APPROVAL_RUNS=1 PATH="$tmp/bin:$PATH" "$SCRIPT" --summary 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "summary reports approval-run fetch failure" \ '.approval_required_runs == [] and any(.errors[]; .source == "approval_required_runs" and .message == "workflow runs API failed")' \ "$json" pass "--summary reports approval-run fetch failure" } test_job_log_mode_emits_bounded_failure_context() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local output output="$(PATH="$tmp/bin:$PATH" "$SCRIPT" --job-log 55150000002)" if ! grep -q 'Test timeout: expected ready state' <<<"$output"; then fail "--job-log emits matching failure context" fi if grep -q 'unrelated tail line' <<<"$output"; then fail "--job-log keeps output bounded" fi pass "--job-log emits bounded failure context" } test_job_log_mode_unpacks_zip_responses() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local output output="$(GH_JOB_LOG_FORMAT=zip PATH="$tmp/bin:$PATH" "$SCRIPT" --job-log 55150000002)" if ! grep -q 'Test timeout: expected ready state' <<<"$output"; then fail "--job-log unpacks zip responses" fi pass "--job-log unpacks zip responses" } test_summary_all_flag_includes_historical_unresolved_threads() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json PATH="$tmp/bin:$PATH" "$SCRIPT" --summary --all 123 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "summary all since omitted" '.since == null' "$json" assert_jq "summary all unresolved count" '.unresolved_review_thread_count == 2' "$json" assert_jq "summary all filtered thread count" '.filtered_review_thread_count == 2' "$json" assert_jq "summary all keeps historical first comment" '.unresolved_threads[] | select(.thread_id == "PRRT_1") | .comment_id == 111' "$json" assert_jq "summary all unresolved threads count" '.unresolved_threads | length == 2' "$json" assert_jq "summary all has no hidden unresolved threads" '.hidden_unresolved_threads == []' "$json" pass "--summary --all includes historical unresolved thread comments" } test_comment_mode_returns_full_review_comment() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" local json PATH="$tmp/bin:$PATH" "$SCRIPT" --comment 111 >"$tmp/out.json" json="$(<"$tmp/out.json")" assert_jq "comment id" '.comment_id == 111' "$json" assert_jq "comment body is full" '.body | contains("Full rationale here.")' "$json" assert_jq "comment path" '.path == "apps/web/file.ts"' "$json" assert_jq "comment line" '.line == 42' "$json" assert_jq "comment author" '.author == "greptile-apps[bot]"' "$json" pass "--comment returns full review comment" } test_comment_mode_reports_fetch_failure() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" if GH_FAIL_COMMENT=1 PATH="$tmp/bin:$PATH" "$SCRIPT" --comment 999 >"$tmp/out.json" 2>"$tmp/err.log"; then fail "--comment failure exits non-zero" fi if [ -s "$tmp/out.json" ]; then fail "--comment failure does not emit JSON" fi if ! grep -q "failed to fetch PR comment 999" "$tmp/err.log"; then fail "--comment failure reports clear error" fi pass "--comment reports fetch failure" } test_comment_mode_rejects_incompatible_flags() { local tmp make_tmp_dir tmp make_mock_gh "$tmp/bin" if PATH="$tmp/bin:$PATH" "$SCRIPT" --summary --comment 111 >"$tmp/out.log" 2>&1; then fail "--comment rejects --summary" fi if ! grep -q "scripts/pr-state --comment " "$tmp/out.log"; then fail "--comment --summary prints usage" fi if PATH="$tmp/bin:$PATH" "$SCRIPT" --all --comment 111 >"$tmp/out.log" 2>&1; then fail "--comment rejects --all" fi if PATH="$tmp/bin:$PATH" "$SCRIPT" --comment 111 --job-log 55150000002 >"$tmp/out.log" 2>&1; then fail "--comment rejects --job-log" fi if ! grep -q "scripts/pr-state --comment " "$tmp/out.log"; then fail "--comment --job-log prints usage" fi if ! grep -q "scripts/pr-state --comment " "$tmp/out.log"; then fail "--comment --all prints usage" fi if PATH="$tmp/bin:$PATH" "$SCRIPT" --comment --summary >"$tmp/out.log" 2>&1; then fail "--comment rejects flag-shaped id" fi if ! grep -q "scripts/pr-state --comment " "$tmp/out.log"; then fail "--comment flag-shaped id prints usage" fi if PATH="$tmp/bin:$PATH" "$SCRIPT" --comment abc >"$tmp/out.log" 2>&1; then fail "--comment rejects non-numeric id" fi if ! grep -q "scripts/pr-state --comment " "$tmp/out.log"; then fail "--comment non-numeric id prints usage" fi pass "--comment rejects incompatible flags" } join_line_continuations() { local file=$1 local line buffer='' lineno=0 start=1 while IFS= read -r line || [[ -n "$line" ]]; do lineno=$((lineno + 1)) [[ -n "$buffer" ]] || start=$lineno if [[ "$line" == *\\ ]]; then buffer+="${line%\\} " continue fi printf '%s:%s\n' "$start" "$buffer$line" buffer='' done <"$file" [[ -z "$buffer" ]] || printf '%s:%s\n' "$start" "$buffer" } test_jq_file_args_avoid_process_substitution() { local pattern='(--slurpfile|--rawfile|--argfile)[[:space:]]+[^[:space:]]+[[:space:]]+<\(' local tmp make_tmp_dir tmp printf '%s\n' "jq -cn --slurpfile page <(printf x) '.'" >"$tmp/same_line.sh" if ! join_line_continuations "$tmp/same_line.sh" | grep -qE "$pattern"; then fail "guard misses process substitution on a single line" fi printf '%s\n' 'jq -cn \' ' --slurpfile page \' ' <(printf x)' >"$tmp/continued.sh" if ! join_line_continuations "$tmp/continued.sh" | grep -qE "$pattern"; then fail "guard misses process substitution split across a line continuation" fi local offenders offenders="$(join_line_continuations "$SCRIPT" | grep -E "$pattern" || true)" if [[ -n "$offenders" ]]; then printf '%s\n' "$offenders" >&2 fail "jq file arguments must not come from process substitution (native jq cannot open /proc//fd/)" fi pass "jq file arguments avoid process substitution" } test_jq_file_args_avoid_process_substitution test_snapshot_happy_path test_old_head_review_does_not_qualify test_exact_head_selected_review_qualifies test_clean_marker_requires_configured_dedicated_app_provenance test_trusted_workflow_requires_latest_success_and_matching_marker test_fork_and_unknown_provenance_are_untrusted test_head_change_during_collection_invalidates_review_evidence test_head_change_during_authenticated_workflow_collection_invalidates_trust test_checks_head_mismatch_invalidates_combined_evidence test_exact_head_dismissed_review_is_not_eligible test_approved_review_with_blocker_is_not_eligible test_commented_blocker_is_not_eligible test_plain_blocker_labels_are_blocked_and_aggregated test_colon_blocker_labels_are_line_anchored_and_scan_all_lines test_formatted_colon_labels_handle_emphasis_after_colon test_single_asterisk_emphasis_and_list_markers_are_safe test_explicit_clean_commented_review_is_eligible test_zero_blocker_summaries_remain_clean_with_explicit_marker test_positive_blocker_evidence_overrides_clean_marker test_exact_head_blockers_from_any_author_are_aggregated test_fenced_blocker_examples_do_not_block_clean_review test_latest_decisive_review_state_controls_active_change_requests test_quoted_blocker_language_does_not_block_clean_review test_partial_failure_records_error_but_keeps_other_data test_pr_view_failure_with_non_numeric_ref_keeps_schema test_repo_failure_skips_review_threads test_graphql_failure_records_error_but_keeps_other_data test_graphql_pagination_collects_all_threads test_all_flag_includes_historical_comments_and_reviews test_summary_mode_returns_compact_fixup_state test_summary_reports_current_head_fork_approval_runs test_summary_reports_base_not_advanced_when_head_matches_merge_base test_summary_revalidates_base_at_closing_head test_summary_reports_approval_run_fetch_failure test_summary_all_flag_includes_historical_unresolved_threads test_job_log_mode_emits_bounded_failure_context test_job_log_mode_unpacks_zip_responses test_comment_mode_returns_full_review_comment test_comment_mode_reports_fetch_failure test_comment_mode_rejects_incompatible_flags