CtrlK
BlogDocsLog inGet started
Tessl Logo
PluginPublic

gamussa/coding-policy

Coding policy for Viktor Gamov's AI agents: language-agnostic quality rules, autonomous shipping discipline, and stack defaults for JVM, Swift, TypeScript, and Python

1.47x
Quality

93%

Does it follow best practices?

Impact

99%

1.47x

Average score across 10 eval scenarios

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

test_poll_pr_reviews.shskills/release/tests/

#!/usr/bin/env bash
# Outcome-based tests for poll-pr-reviews.sh, focused on the new
# `merge_state` output path: `fetch_merge_state` must return the
# `{status, mergeable}` envelope keyed off GitHub's `mergeStateStatus`
# and `mergeable` fields, and `main` must surface that envelope as a
# top-level field in the snapshot JSON.
#
# Approach: source the script (its main() guard prevents auto-run when
# sourced) and override `gh` with a shell function that returns
# fixture JSON for the two surfaces poll-pr-reviews.sh calls —
# `gh pr checks` and `gh pr view --json mergeStateStatus,mergeable` —
# plus the two `gh api` surfaces for reviews/comments. jq runs locally
# so the script's filter logic is exercised, not duplicated in the test.
#
# Run: bash skills/release/tests/test_poll_pr_reviews.sh
# Exit 0 on all-pass; non-zero with a per-test diagnostic on failure.

set -uo pipefail

# The runner's per-suite token binds this suite's summary line to this
# suite (scripts/run-tests.sh header). Unset it so a harness run from
# here prints an empty token and never stands in for this one.
SUMMARY_TOKEN="${TEST_SUMMARY_TOKEN:-}"
unset TEST_SUMMARY_TOKEN

# The github.com host pin: every gh call this suite's mock sees passes the
# gate first, with GH_HOST pointing at another server (gh_pin_gate.sh).
# shellcheck source=skills/release/tests/gh_pin_gate.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/gh_pin_gate.sh" \
  || { echo "fatal: cannot source gh_pin_gate.sh" >&2; exit 2; }
# gh_pin_gate.sh turns on -e; this harness counts failures itself and runs
# without it (rules/error-handling.md aggregate-reporting carve-out).
set +e

SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/poll-pr-reviews.sh"
[[ -x "$SCRIPT" ]] || { echo "fatal: poll-pr-reviews.sh not executable at $SCRIPT" >&2; exit 2; }

# shellcheck disable=SC1090
source "$SCRIPT" || true
set +e

FAIL_COUNT=0
PASS_COUNT=0

# The head SHA the mock's `gh pr view` returns. A review fixture whose
# commit_id equals this is "fresh" (bound to head); anything else is "stale"
# and its verdict resolves to state "none" (#186).
HEAD_SHA="head000000000000000000000000000000000000"
OLD_SHA="0ld00000000000000000000000000000000000000"

assert_eq() {
  local label="$1" expected="$2" actual="$3"
  if [[ "$expected" == "$actual" ]]; then
    return 0
  fi
  echo "    FAIL: ${label}: expected '${expected}', got '${actual}'" >&2
  return 1
}

run() {
  local name="$1"; shift
  if "$@"; then
    PASS_COUNT=$((PASS_COUNT + 1))
    echo "  pass: $name"
  else
    FAIL_COUNT=$((FAIL_COUNT + 1))
    echo "  FAIL: $name" >&2
  fi
}

# Mock `gh` — supports the four invocations poll-pr-reviews.sh issues.
# MOCK_MERGE_STATE selects the fixture for `gh pr view`; other surfaces
# return minimal fixtures so main() can complete.
_gh_mock() {
  case "$1" in
    pr)
      local subcmd="$2"
      shift 2
      case "$subcmd" in
        view)
          # Contract: `gh pr view <N> --repo <o/r> --json mergeStateStatus,mergeable`.
          # Validate the --json args explicitly so a regression that drops --json
          # or asks for the wrong fields surfaces as a loud mock failure rather
          # than passing silently against a permissive stub.
          local saw_json=0 json_args=""
          while [[ $# -gt 0 ]]; do
            case "$1" in
              --json) saw_json=1; json_args="${2:-}"; shift 2 ;;
              *)      shift ;;
            esac
          done
          [[ $saw_json -eq 1 ]] || { echo "mock gh pr view: missing --json flag (contract: --json mergeStateStatus,mergeable,headRefOid)" >&2; return 99; }
          [[ "$json_args" == "mergeStateStatus,mergeable,headRefOid" ]] || { echo "mock gh pr view: wrong --json args: '${json_args}' (expected 'mergeStateStatus,mergeable,headRefOid')" >&2; return 99; }
          # headRefOid is the head SHA review verdicts are bound to. Tests that
          # exercise verdict resolution set review fixtures' commit_id to
          # HEAD_SHA (fresh) or something else (stale) — see HEAD_SHA below.
          case "${MOCK_MERGE_STATE:-}" in
            clean)        echo '{"mergeStateStatus":"CLEAN","mergeable":"MERGEABLE","headRefOid":"'"$HEAD_SHA"'"}' ;;
            dirty)        echo '{"mergeStateStatus":"DIRTY","mergeable":"CONFLICTING","headRefOid":"'"$HEAD_SHA"'"}' ;;
            unknown)      echo '{"mergeStateStatus":"UNKNOWN","mergeable":"UNKNOWN","headRefOid":"'"$HEAD_SHA"'"}' ;;
            no-head)      echo '{"mergeStateStatus":"CLEAN","mergeable":"MERGEABLE","headRefOid":""}' ;;
            *) echo "mock gh: unknown MOCK_MERGE_STATE='${MOCK_MERGE_STATE:-}'" >&2; return 2 ;;
          esac
          ;;
        checks)
          # gh pr checks <N> --repo <o/r> --json name,bucket
          echo "${MOCK_CHECKS_BODY:-[]}"
          ;;
        *) echo "mock gh pr: unsupported subcommand: $subcmd" >&2; return 2 ;;
      esac
      ;;
    api)
      # `gh api graphql -f query=... --jq ...` — the pending review requests.
      # GraphQL, not REST: the REST endpoint omits bot reviewers entirely
      # (#276), so every bot lane would read "never requested" there (#369).
      #
      # Contract-checked, then answered the way GitHub answers: the query must
      # select the fields the filter reads and carry the PR as an `Int!`
      # variable, and the production `--jq` filter runs over a raw GraphQL
      # response. A wrong field selection therefore reads every lane
      # unrequested here too, instead of passing against a pre-shaped array.
      # MOCK_REQUESTED_BODY lists the pending logins; MOCK_REQUESTED_RAW
      # replaces the whole raw response.
      if [[ "${2:-}" == "graphql" ]]; then
        shift 2
        local query="" filter="" pr_var="" owner_var="" repo_var=""
        while [[ $# -gt 0 ]]; do
          case "$1" in
            -f) case "${2:-}" in
                  query=*) query="${2#query=}" ;;
                  owner=*) owner_var="${2#owner=}" ;;
                  repo=*)  repo_var="${2#repo=}" ;;
                esac; shift 2 ;;
            -F) case "${2:-}" in pr=*) pr_var="${2#pr=}" ;; esac; shift 2 ;;
            --jq) filter="${2:-}"; shift 2 ;;
            *) shift ;;
          esac
        done
        local needle
        # shellcheck disable=SC2016 # literal GraphQL variable names the query must carry
        for needle in 'reviewRequests(first: 50)' 'requestedReviewer' '... on Bot { login }' \
                      '... on User { login }' '... on Team { slug }' \
                      '$owner: String!' '$repo: String!' '$pr: Int!' 'pullRequest(number: $pr)'; do
          [[ "$query" == *"$needle"* ]] || { echo "mock gh api graphql: query lacks '${needle}'" >&2; return 99; }
        done
        [[ "$pr_var" =~ ^[1-9][0-9]*$ ]] || { echo "mock gh api graphql: -F pr must be a positive integer, got '${pr_var}'" >&2; return 99; }
        [[ -n "$owner_var" && -n "$repo_var" ]] || { echo "mock gh api graphql: owner and repo must travel as -f variables" >&2; return 99; }
        [[ -n "$filter" ]] || { echo "mock gh api graphql: missing --jq" >&2; return 99; }
        local raw
        raw="${MOCK_REQUESTED_RAW:-$(printf '%s' "${MOCK_REQUESTED_BODY:-[]}" | jq -c \
          '{data: {repository: {pullRequest: {reviewRequests: {nodes:
             [.[] | {requestedReviewer: {__typename: "Bot", login: .}}]}}}}}')}"
        printf '%s' "$raw" | jq "$filter"
        return $?
      fi
      # gh api --paginate repos/<o>/<r>/pulls/<N>/reviews?per_page=100
      # gh api --paginate repos/<o>/<r>/pulls/<N>/comments?per_page=100
      # The script pipes the raw paginated output through `jq -s` itself,
      # so this mock no longer forwards `--jq`. It echoes a fixture body
      # keyed off the path; tests can simulate multiple pages by setting
      # MOCK_REVIEWS_BODY / MOCK_COMMENTS_BODY to several concatenated
      # JSON arrays (what `gh api --paginate` actually emits across pages).
      shift  # consume "api"
      local path="" saw_paginate=0
      while [[ $# -gt 0 ]]; do
        case "$1" in
          --paginate) saw_paginate=1; shift ;;
          --jq)       echo "mock gh api: --jq is incompatible with --paginate here; the script should jq -s externally" >&2; return 99 ;;
          *)          [[ -z "$path" ]] && path="$1"; shift ;;
        esac
      done
      [[ $saw_paginate -eq 1 ]] || { echo "mock gh api: missing --paginate (required so the script never silently misses page 2+)" >&2; return 99; }
      case "$path" in
        *reviews*)  echo "${MOCK_REVIEWS_BODY:-[]}" ;;
        *comments*) echo "${MOCK_COMMENTS_BODY:-[]}" ;;
        *) echo "mock gh api: unsupported path: $path" >&2; return 2 ;;
      esac
      ;;
    *) echo "mock gh: unsupported invocation: $*" >&2; return 2 ;;
  esac
}
gh() { gh_pin_gate _gh_mock "$@"; }

# --- test bodies ---

# --- pending review requests (#369) ---
# A lane nobody requested is not a lane that is running late: a developer
# reading its own current-tip evidence must be able to tell the two apart
# without sitting out a pre-merge budget on a review it cannot itself request.

t_requested_among_matches_a_pending_bot() {
  local out
  out=$(requested_among '["github-actions"]' "github-actions[bot]")
  assert_eq "requested" "true" "$out"
}

t_requested_among_is_false_when_nobody_asked() {
  local out
  out=$(requested_among '[]' "github-actions[bot]")
  assert_eq "requested" "false" "$out"
}

# The raw GraphQL response, through the production filter: bot suffix and case
# normalized, a team read by slug, a reviewer GitHub could not resolve dropped.
t_fetch_requested_logins_reads_the_raw_graphql_shape() {
  local out
  MOCK_REQUESTED_RAW='{"data":{"repository":{"pullRequest":{"reviewRequests":{"nodes":[
    {"requestedReviewer":{"__typename":"Bot","login":"GitHub-Actions[bot]"}},
    {"requestedReviewer":{"__typename":"User","login":"Some-Human"}},
    {"requestedReviewer":{"__typename":"Team","slug":"Core-Team"}},
    {"requestedReviewer":null}]}}}}}'
  out=$(fetch_requested_logins owner repo 7)
  unset MOCK_REQUESTED_RAW
  assert_eq "logins" '["github-actions","some-human","core-team"]' "$out"
}

# A PR number is validated before any call: `-F` would read a file for an
# `@`-prefixed value, and anything else non-numeric is not a PR.
t_non_integer_pr_number_is_refused_before_any_call() {
  local candidate rc out
  for candidate in "1) { id } } #" "@/etc/hosts" "0" "-1" "1.5" "" "abc"; do
    rc=0
    out=$(fetch_requested_logins owner repo "$candidate" 2>&1) || rc=$?
    [[ "$rc" -eq 1 && "$out" == *"positive integer"* ]] \
      || { echo "    FAIL: fetch_requested_logins accepted '${candidate}' (rc=${rc}): ${out}" >&2; return 1; }
    rc=0
    out=$( (MOCK_MERGE_STATE=boom main owner repo "$candidate") 2>&1 ) || rc=$?
    [[ "$rc" -eq 2 && "$out" == *"positive integer"* ]] \
      || { echo "    FAIL: main accepted '${candidate}' (rc=${rc}): ${out}" >&2; return 1; }
  done
}

t_requested_among_ignores_another_reviewer() {
  local out
  out=$(requested_among '["some-human"]' "github-actions[bot]")
  assert_eq "requested" "false" "$out"
}

t_main_marks_an_unrequested_lane() {
  MOCK_MERGE_STATE=clean
  MOCK_REQUESTED_BODY='[]'
  local out
  out=$(main owner repo 1)
  assert_eq "codex state"     "none"  "$(echo "$out" | jq -r '.reviews.codex.state')" || return 1
  assert_eq "codex requested" "false" "$(echo "$out" | jq -r '.reviews.codex.requested')" || return 1
  assert_eq "codex stale"     "false" "$(echo "$out" | jq -r '.reviews.codex.stale')"
}

t_main_marks_a_pending_lane_requested() {
  MOCK_MERGE_STATE=clean
  MOCK_REQUESTED_BODY='["github-actions"]'
  local out
  out=$(main owner repo 1)
  assert_eq "codex state"     "none" "$(echo "$out" | jq -r '.reviews.codex.state')" || return 1
  assert_eq "codex requested" "true" "$(echo "$out" | jq -r '.reviews.codex.requested')"
}


t_fetch_merge_state_clean_returns_mergeable_envelope() {
  MOCK_MERGE_STATE=clean
  local out status mergeable
  out=$(fetch_merge_state "owner" "repo" "1")
  status=$(echo "$out" | jq -r '.status')
  mergeable=$(echo "$out" | jq -r '.mergeable')
  assert_eq "status"    "CLEAN"     "$status"    || return 1
  assert_eq "mergeable" "MERGEABLE" "$mergeable"
}

t_fetch_merge_state_dirty_returns_conflicting_envelope() {
  MOCK_MERGE_STATE=dirty
  local out status mergeable
  out=$(fetch_merge_state "owner" "repo" "1")
  status=$(echo "$out" | jq -r '.status')
  mergeable=$(echo "$out" | jq -r '.mergeable')
  assert_eq "status"    "DIRTY"       "$status"    || return 1
  assert_eq "mergeable" "CONFLICTING" "$mergeable"
}

t_fetch_merge_state_unknown_returns_unknown_envelope() {
  MOCK_MERGE_STATE=unknown
  local out status mergeable
  out=$(fetch_merge_state "owner" "repo" "1")
  status=$(echo "$out" | jq -r '.status')
  mergeable=$(echo "$out" | jq -r '.mergeable')
  assert_eq "status"    "UNKNOWN" "$status"    || return 1
  assert_eq "mergeable" "UNKNOWN" "$mergeable"
}

t_main_surfaces_merge_state_as_top_level_field() {
  MOCK_MERGE_STATE=clean
  local out keys
  out=$(main "owner" "repo" "1")
  keys=$(echo "$out" | jq -r '.merge_state | "\(.status)|\(.mergeable)"')
  assert_eq "merge_state in main output" "CLEAN|MERGEABLE" "$keys"
}

t_main_propagates_dirty_state() {
  MOCK_MERGE_STATE=dirty
  local out keys
  out=$(main "owner" "repo" "1")
  keys=$(echo "$out" | jq -r '.merge_state | "\(.status)|\(.mergeable)"')
  assert_eq "merge_state in main output" "DIRTY|CONFLICTING" "$keys"
}

# Issue #83: on PRs with > 1 page of reviews, gh api without --paginate
# returns only page 1. The pre-fix `| last` filter then picked the last
# entry on page 1 — not the actual newest review on the last page — and
# the gate could approve a merge against stale data.
#
# Build a fixture that mimics what `gh api --paginate` actually emits:
# two concatenated JSON arrays. Page 1's last entry is a COMMENTED review
# at 17:00; page 2's last entry is a CHANGES_REQUESTED review at 18:04.
# A correct implementation must report CHANGES_REQUESTED@18:04.
t_latest_review_by_picks_from_last_page() {
  MOCK_REVIEWS_BODY='[{"user":{"login":"github-actions[bot]"},"state":"APPROVED","submitted_at":"2026-05-18T16:00:00Z"},{"user":{"login":"github-actions[bot]"},"state":"COMMENTED","submitted_at":"2026-05-18T17:00:00Z"}][{"user":{"login":"github-actions[bot]"},"state":"CHANGES_REQUESTED","submitted_at":"2026-05-18T18:04:00Z"}]'
  local out state submitted_at
  out=$(latest_review_by "owner" "repo" "1" "github-actions[bot]")
  state=$(echo "$out" | jq -r '.state')
  submitted_at=$(echo "$out" | jq -r '.submitted_at')
  assert_eq "state from last page"        "CHANGES_REQUESTED"     "$state"        || return 1
  assert_eq "submitted_at from last page" "2026-05-18T18:04:00Z"  "$submitted_at"
}

t_latest_review_by_returns_none_when_no_reviews() {
  MOCK_REVIEWS_BODY='[]'
  local out state submitted_at body
  out=$(latest_review_by "owner" "repo" "1" "github-actions[bot]")
  state=$(echo "$out" | jq -r '.state')
  submitted_at=$(echo "$out" | jq -r '.submitted_at')
  body=$(echo "$out" | jq -r '.body')
  assert_eq "state for empty"        "none" "$state"        || return 1
  assert_eq "submitted_at for empty" "null" "$submitted_at" || return 1
  assert_eq "body for empty"         "null" "$body"
}

# A review's state classifies merge-gating, not whether its body must be read.
# A COMMENTED review with zero inline comments still carries a body the gate
# must surface so the agent reads it (rules/reviewer-feedback-reading.md).
t_latest_review_by_surfaces_body_text() {
  MOCK_REVIEWS_BODY='[{"user":{"login":"github-actions[bot]"},"state":"COMMENTED","submitted_at":"2026-05-18T16:00:00Z","body":"Non-blocking, but the rename in foo.py:42 drops the retry guard."}]'
  local out state body
  out=$(latest_review_by "owner" "repo" "1" "github-actions[bot]")
  state=$(echo "$out" | jq -r '.state')
  body=$(echo "$out" | jq -r '.body')
  assert_eq "state surfaced"  "COMMENTED" "$state" || return 1
  assert_eq "body surfaced verbatim" "Non-blocking, but the rename in foo.py:42 drops the retry guard." "$body"
}

t_latest_review_by_filters_other_logins_across_pages() {
  # Page 1: two human reviews + one bot review. Page 2: one human review
  # that's newer than the bot review. The bot's latest is still the page-1
  # bot review, even though the page-2 human is newer.
  MOCK_REVIEWS_BODY='[{"user":{"login":"alice"},"state":"COMMENTED","submitted_at":"2026-05-18T15:00:00Z"},{"user":{"login":"github-actions[bot]"},"state":"APPROVED","submitted_at":"2026-05-18T16:00:00Z"},{"user":{"login":"bob"},"state":"COMMENTED","submitted_at":"2026-05-18T16:30:00Z"}][{"user":{"login":"alice"},"state":"COMMENTED","submitted_at":"2026-05-18T17:00:00Z"}]'
  local out state submitted_at
  out=$(latest_review_by "owner" "repo" "1" "github-actions[bot]")
  state=$(echo "$out" | jq -r '.state')
  submitted_at=$(echo "$out" | jq -r '.submitted_at')
  assert_eq "bot state"        "APPROVED"             "$state"        || return 1
  assert_eq "bot submitted_at" "2026-05-18T16:00:00Z" "$submitted_at"
}

# Same shape for comments: counts must sum across pages, not pick page 1
# alone. Mix in a non-target login and an in_reply_to_id to confirm the
# filter still discards both.
t_toplevel_comments_by_sums_across_pages() {
  MOCK_COMMENTS_BODY='[{"user":{"login":"github-actions[bot]"},"in_reply_to_id":null},{"user":{"login":"github-actions[bot]"},"in_reply_to_id":null},{"user":{"login":"alice"},"in_reply_to_id":null}][{"user":{"login":"github-actions[bot]"},"in_reply_to_id":null},{"user":{"login":"github-actions[bot]"},"in_reply_to_id":12345}]'
  local count
  count=$(toplevel_comments_by "owner" "repo" "1" "github-actions[bot]")
  assert_eq "top-level bot comments across both pages" "3" "$count"
}

t_toplevel_comments_by_returns_zero_for_no_comments() {
  MOCK_COMMENTS_BODY='[]'
  local count
  count=$(toplevel_comments_by "owner" "repo" "1" "github-actions[bot]")
  assert_eq "comments count for empty" "0" "$count"
}

t_toplevel_comments_by_excludes_replies_and_other_logins() {
  MOCK_COMMENTS_BODY='[{"user":{"login":"github-actions[bot]"},"in_reply_to_id":null},{"user":{"login":"github-actions[bot]"},"in_reply_to_id":991},{"user":{"login":"some-human"},"in_reply_to_id":null}]'
  local count
  count=$(toplevel_comments_by "owner" "repo" "1" "${CODEX_COMMENT_LOGINS[@]}")
  assert_eq "replies and foreign logins excluded" "1" "$count"
}

# A degraded API response can put a non-object element (an error body, a bare
# string) in the array. Indexing it aborted jq mid-filter and failed the whole
# poll: watch-pr-reviews.sh then burned all 50 attempts and reported
# `pending_at_budget`, which reads as "no reviewer posted" rather than "the
# tool broke" (issue #300). Real comments around it must still be counted.
t_toplevel_comments_by_survives_non_object_element() {
  MOCK_COMMENTS_BODY='["Not Found",{"user":{"login":"github-actions[bot]"},"in_reply_to_id":null},{"user":{"login":"github-actions[bot]"},"in_reply_to_id":null}]'
  local count
  count=$(toplevel_comments_by "owner" "repo" "1" "${CODEX_COMMENT_LOGINS[@]}" 2>/dev/null)
  assert_eq "count survives a non-object element" "2" "$count"
}

# The drop is visible, never silent: best-effort work that continues past a
# failure warns (rules/error-handling.md Shell Error Handling).
t_toplevel_comments_by_warns_on_non_object_element() {
  MOCK_COMMENTS_BODY='["Not Found",{"user":{"login":"github-actions[bot]"},"in_reply_to_id":null}]'
  local err
  err=$( { toplevel_comments_by "owner" "repo" "1" "${CODEX_COMMENT_LOGINS[@]}" >/dev/null; } 2>&1 )
  case "$err" in
    *"1 non-object element"*) gh_hints_pinned "$err" ;;
    *) echo "    FAIL: expected a non-object-element warning on stderr, got: '${err}'" >&2; return 1 ;;
  esac
}

# Same guard on the reviews surface — the merge gate reads it, and a
# non-object element carries no verdict, so dropping it cannot mask one.
t_latest_review_by_survives_non_object_element() {
  MOCK_REVIEWS_BODY='["Not Found",{"user":{"login":"github-actions[bot]"},"state":"APPROVED","submitted_at":"2026-05-18T16:00:00Z","body":"ok","commit_id":"'"$HEAD_SHA"'"}]'
  local out state
  out=$(latest_review_by "owner" "repo" "1" "github-actions[bot]" 2>/dev/null)
  state=$(echo "$out" | jq -r '.state')
  assert_eq "verdict survives a non-object element" "APPROVED" "$state"
}

# End-to-end: the poll returns a snapshot instead of failing, which is what
# kept watch-pr-reviews.sh from stalling to budget on a degraded response.
t_main_survives_non_object_comment_element() {
  MOCK_MERGE_STATE=clean
  MOCK_COMMENTS_BODY='["Not Found",{"user":{"login":"github-actions[bot]"},"in_reply_to_id":null}]'
  local out codex
  out=$(main "owner" "repo" "1" 2>/dev/null) || return 1
  codex=$(echo "$out" | jq -r '.inline_comments.codex')
  assert_eq "snapshot still produced, Codex comment counted" "1" "$codex"
}

# End-to-end through main() — the path the Step 7 merge gate actually reads.
# Copilot has no lane (docs/adr/0014-retire-copilot-lane.md): its comments
# never count as Codex's, and land in `other` so each still needs a reply.
t_main_counts_copilot_comments_as_other() {
  MOCK_MERGE_STATE=clean
  MOCK_COMMENTS_BODY='[{"user":{"login":"Copilot"},"in_reply_to_id":null},{"user":{"login":"github-actions[bot]"},"in_reply_to_id":null},{"user":{"login":"github-actions[bot]"},"in_reply_to_id":null}]'
  local out counts
  out=$(main "owner" "repo" "1")
  counts=$(echo "$out" | jq -c '.inline_comments')
  assert_eq "inline_comments" '{"codex":2,"other":1}' "$counts"
}

# The resolver is variadic on login: a second policy-reviewer identity (a
# GitHub App, say) is resolved by passing it explicitly. These cases supply a
# hypothetical `policy-app[bot]` alongside the shipped login to prove the
# multi-login aggregation, without shipping that login in CODEX_REVIEW_LOGINS.
t_latest_review_by_resolves_extra_login() {
  MOCK_REVIEWS_BODY='[{"user":{"login":"policy-app[bot]"},"state":"APPROVED","submitted_at":"2026-07-21T05:15:00Z"}]'
  local out state
  out=$(latest_review_by "owner" "repo" "1" "${CODEX_REVIEW_LOGINS[@]}" "policy-app[bot]")
  state=$(echo "$out" | jq -r '.state')
  assert_eq "extra-login review resolves to policy state" "APPROVED" "$state"
}

# Resolving across two logins must return the newest verdict when both appear.
t_latest_review_by_policy_reviewer_picks_newest_across_logins() {
  MOCK_REVIEWS_BODY='[{"user":{"login":"github-actions[bot]"},"state":"COMMENTED","submitted_at":"2026-07-21T05:00:00Z"},{"user":{"login":"policy-app[bot]"},"state":"CHANGES_REQUESTED","submitted_at":"2026-07-21T05:10:00Z"}]'
  local out state
  out=$(latest_review_by "owner" "repo" "1" "${CODEX_REVIEW_LOGINS[@]}" "policy-app[bot]")
  state=$(echo "$out" | jq -r '.state')
  assert_eq "newest policy verdict across both logins" "CHANGES_REQUESTED" "$state"
}

# Fail-safe: an active CHANGES_REQUESTED from one policy-reviewer identity must
# NOT be masked by a LATER clean review from the other identity — a gating
# watcher that surfaced the newest-overall would think the reviewer was clean
# while GitHub still blocked the merge.
t_latest_review_by_changes_requested_not_masked_by_later_other_login() {
  MOCK_REVIEWS_BODY='[{"user":{"login":"github-actions[bot]"},"state":"CHANGES_REQUESTED","submitted_at":"2026-07-21T05:00:00Z"},{"user":{"login":"policy-app[bot]"},"state":"COMMENTED","submitted_at":"2026-07-21T05:10:00Z"}]'
  local out state
  out=$(latest_review_by "owner" "repo" "1" "${CODEX_REVIEW_LOGINS[@]}" "policy-app[bot]")
  state=$(echo "$out" | jq -r '.state')
  assert_eq "active block not masked by later clean review" "CHANGES_REQUESTED" "$state"
}

# An unknown login is NOT the policy reviewer: its review must not surface as
# .reviews.codex, and its comments must not count toward the reply gate.
t_main_ignores_unknown_login_review() {
  MOCK_MERGE_STATE=clean
  MOCK_REVIEWS_BODY='[{"user":{"login":"some-other-app[bot]"},"state":"APPROVED","submitted_at":"2026-07-21T05:15:00Z","commit_id":"'"$HEAD_SHA"'"}]'
  local out state
  out=$(main "owner" "repo" "1")
  state=$(echo "$out" | jq -r '.reviews.codex.state')
  assert_eq "unknown-login review is not the policy reviewer" "none" "$state"
}

t_toplevel_comments_by_ignores_unknown_login() {
  MOCK_COMMENTS_BODY='[{"user":{"login":"some-other-app[bot]"},"in_reply_to_id":null}]'
  local count
  count=$(toplevel_comments_by "owner" "repo" "1" "${CODEX_COMMENT_LOGINS[@]}")
  assert_eq "unknown-login inline comment not counted for policy reviewer" "0" "$count"
}

# A login's own latest review must be selected by submitted_at, NOT by the
# API's array position: jq group_by preserves input order within a group, so
# if the reviews API returns a login's reviews out of chronological order, a
# `last`-based pick would gate on a superseded verdict. Here the newest review
# (CHANGES_REQUESTED @18:00) is NOT last in the input — a correct max_by pick
# surfaces it; a `last` pick would wrongly return the APPROVED @16:00.
t_latest_review_by_picks_max_by_time_not_array_position() {
  MOCK_REVIEWS_BODY='[{"user":{"login":"github-actions[bot]"},"state":"CHANGES_REQUESTED","submitted_at":"2026-07-25T18:00:00Z"},{"user":{"login":"github-actions[bot]"},"state":"COMMENTED","submitted_at":"2026-07-25T17:00:00Z"},{"user":{"login":"github-actions[bot]"},"state":"APPROVED","submitted_at":"2026-07-25T16:00:00Z"}]'
  local out state submitted_at
  out=$(latest_review_by "owner" "repo" "1" "github-actions[bot]")
  state=$(echo "$out" | jq -r '.state')
  submitted_at=$(echo "$out" | jq -r '.submitted_at')
  assert_eq "latest by time, not array position" "CHANGES_REQUESTED"     "$state"        || return 1
  assert_eq "latest submitted_at"                "2026-07-25T18:00:00Z"  "$submitted_at"
}

# GitHub review states outside the snapshot schema (DISMISSED, PENDING) are
# not live verdicts, but watch-pr-reviews.sh treats any non-"none" state as
# "a bot posted" and would let one satisfy the ready gate. They must normalize
# to "none".
t_latest_review_by_normalizes_dismissed_to_none() {
  MOCK_REVIEWS_BODY='[{"user":{"login":"github-actions[bot]"},"state":"DISMISSED","submitted_at":"2026-07-25T16:00:00Z","body":"was dismissed"}]'
  local out state
  out=$(latest_review_by "owner" "repo" "1" "github-actions[bot]")
  state=$(echo "$out" | jq -r '.state')
  assert_eq "DISMISSED normalizes to none" "none" "$state"
}

t_latest_review_by_normalizes_pending_to_none() {
  MOCK_REVIEWS_BODY='[{"user":{"login":"github-actions[bot]"},"state":"PENDING","submitted_at":"2026-07-25T16:00:00Z"}]'
  local out state
  out=$(latest_review_by "owner" "repo" "1" "github-actions[bot]")
  state=$(echo "$out" | jq -r '.state')
  assert_eq "PENDING normalizes to none" "none" "$state"
}

# --- #186: review verdicts bound to the PR head SHA ---

# A verdict on the current head passes through unchanged, flagged not-stale.
t_resolve_against_head_fresh_passes_through() {
  local review out state stale
  review='{"state":"APPROVED","submitted_at":"2026-07-25T10:00:00Z","body":"ok","commit_id":"'"$HEAD_SHA"'"}'
  out=$(resolve_review_against_head "$review" "$HEAD_SHA")
  state=$(echo "$out" | jq -r '.state')
  stale=$(echo "$out" | jq -r '.stale')
  assert_eq "fresh review keeps its state" "APPROVED" "$state" || return 1
  assert_eq "fresh review not stale"       "false"    "$stale"
}

# A verdict on a superseded SHA collapses to "none" (absent, not clean) with
# stale=true, but keeps its body/commit_id visible for diagnosis. This is the
# #186 fix: a stale APPROVED/COMMENTED must not read as a live clean verdict.
t_resolve_against_head_stale_collapses_to_none() {
  local review out state stale body commit
  review='{"state":"APPROVED","submitted_at":"2026-07-25T09:00:00Z","body":"reviewed old code","commit_id":"'"$OLD_SHA"'"}'
  out=$(resolve_review_against_head "$review" "$HEAD_SHA")
  state=$(echo "$out" | jq -r '.state')
  stale=$(echo "$out" | jq -r '.stale')
  body=$(echo "$out" | jq -r '.body')
  commit=$(echo "$out" | jq -r '.commit_id')
  assert_eq "stale review resolves to none" "none"                "$state"  || return 1
  assert_eq "stale review flagged stale"    "true"                "$stale"  || return 1
  assert_eq "stale verdict body kept"       "reviewed old code"   "$body"   || return 1
  assert_eq "stale verdict commit kept"     "$OLD_SHA"            "$commit"
}

# A genuinely-absent review (never posted) stays none, stale=false — an agent
# can tell "never reviewed" (stale false) from "reviewed older commit" (true).
t_resolve_against_head_none_stays_none_not_stale() {
  local review out state stale
  review='{"state":"none","submitted_at":null,"body":null,"commit_id":null}'
  out=$(resolve_review_against_head "$review" "$HEAD_SHA")
  state=$(echo "$out" | jq -r '.state')
  stale=$(echo "$out" | jq -r '.stale')
  assert_eq "absent review stays none"        "none"  "$state" || return 1
  assert_eq "absent review is not stale" "false" "$stale"
}

# End-to-end #186 symptom 2 (false ready): a stale COMMENTED from an earlier
# SHA must NOT surface as a live verdict — it collapses to none so the watcher
# keeps waiting instead of merging unreviewed code.
t_main_stale_review_collapses_to_none() {
  MOCK_MERGE_STATE=clean
  MOCK_REVIEWS_BODY='[{"user":{"login":"github-actions[bot]"},"state":"COMMENTED","submitted_at":"2026-07-25T09:00:00Z","body":"looks fine","commit_id":"'"$OLD_SHA"'"}]'
  local out state stale
  out=$(main "owner" "repo" "1")
  state=$(echo "$out" | jq -r '.reviews.codex.state')
  stale=$(echo "$out" | jq -r '.reviews.codex.stale')
  assert_eq "stale codex verdict reads none in snapshot" "none" "$state" || return 1
  assert_eq "stale codex verdict flagged stale"          "true" "$stale"
}

# A fresh verdict (commit_id == head) surfaces normally.
t_main_fresh_review_surfaces_state() {
  MOCK_MERGE_STATE=clean
  MOCK_REVIEWS_BODY='[{"user":{"login":"github-actions[bot]"},"state":"APPROVED","submitted_at":"2026-07-25T10:00:00Z","body":"ok","commit_id":"'"$HEAD_SHA"'"}]'
  local out state stale
  out=$(main "owner" "repo" "1")
  state=$(echo "$out" | jq -r '.reviews.codex.state')
  stale=$(echo "$out" | jq -r '.reviews.codex.stale')
  assert_eq "fresh codex verdict surfaces" "APPROVED" "$state" || return 1
  assert_eq "fresh codex verdict not stale" "false"   "$stale"
}

t_main_surfaces_head_sha_top_level() {
  MOCK_MERGE_STATE=clean
  MOCK_REVIEWS_BODY='[]'
  local out head
  out=$(main "owner" "repo" "1")
  head=$(echo "$out" | jq -r '.head_sha')
  assert_eq "head_sha surfaced top-level" "$HEAD_SHA" "$head"
}

# Guard: an empty headRefOid must fail loudly, never silently void the review
# gate by marking every verdict stale.
t_main_no_head_sha_fails() {
  MOCK_MERGE_STATE=no-head
  MOCK_REVIEWS_BODY='[]'
  # Subshell: main() calls `exit` on the guard, which would otherwise
  # terminate this sourced test script rather than just the call.
  local rc=0 err
  err=$( ( main "owner" "repo" "1" >/dev/null ) 2>&1 ) || rc=$?
  [[ "$rc" -ne 0 ]] || { echo "    FAIL: main should exit non-zero on empty headRefOid, got rc=0" >&2; return 1; }
  gh_hints_pinned "$err"
}

# --- #182: cancelled superseded runs are no-signal, not failure ---

# A cancelled bucket alongside a success is still success (the cancelled twin
# was superseded; its replacement concluded). Folding cancel into pending would
# have wedged this.
t_ci_status_cancel_with_success_is_success() {
  MOCK_MERGE_STATE=clean
  MOCK_CHECKS_BODY='[{"name":"CI","bucket":"pass"},{"name":"CI","bucket":"cancel"}]'
  local out ci
  out=$(main "owner" "repo" "1")
  ci=$(echo "$out" | jq -r '.ci.status')
  assert_eq "success alongside a cancelled twin is success" "success" "$ci"
}

# Only cancels present (replacement not yet registered) → pending, so the
# watcher waits rather than concluding.
t_ci_status_only_cancels_is_pending() {
  MOCK_MERGE_STATE=clean
  MOCK_CHECKS_BODY='[{"name":"CI","bucket":"cancel"},{"name":"review","bucket":"cancel"}]'
  local out ci
  out=$(main "owner" "repo" "1")
  ci=$(echo "$out" | jq -r '.ci.status')
  assert_eq "all-cancelled reads pending" "pending" "$ci"
}

# Fixtures below use the exact `gh pr checks --json name,bucket` shape this
# repo's own PRs return. Supersession is per check name: a cancel is settled
# only by a live run of the SAME name on head, never by other checks passing.

# A cancelled required check with no replacement, next to other passing
# checks, stays pending — never success, so the watcher never reports ready.
t_ci_status_unreplaced_cancel_with_other_passes_is_pending() {
  MOCK_MERGE_STATE=clean
  MOCK_CHECKS_BODY='[{"bucket":"cancel","name":"unit-tests"},{"bucket":"pass","name":"review"},{"bucket":"pass","name":"lint"},{"bucket":"pass","name":"changelog-fragment"},{"bucket":"pass","name":"diagnostics"}]'
  local out ci
  out=$(main "owner" "repo" "1")
  ci=$(echo "$out" | jq -r '.ci.status')
  assert_eq "other passing checks do not settle an unreplaced cancel" "pending" "$ci"
}

# The cancelled check re-ran on head and passed: its replacement's result
# drives the state.
t_ci_status_cancel_replaced_by_pass_is_success() {
  MOCK_MERGE_STATE=clean
  MOCK_CHECKS_BODY='[{"bucket":"cancel","name":"unit-tests"},{"bucket":"pass","name":"unit-tests"},{"bucket":"pass","name":"review"},{"bucket":"pass","name":"lint"}]'
  local out ci
  out=$(main "owner" "repo" "1")
  ci=$(echo "$out" | jq -r '.ci.status')
  assert_eq "a passing same-name replacement settles the cancel" "success" "$ci"
}

# The cancelled check re-ran on head and failed: the replacement's failure
# is the result.
t_ci_status_cancel_replaced_by_fail_is_failure() {
  MOCK_MERGE_STATE=clean
  MOCK_CHECKS_BODY='[{"bucket":"cancel","name":"unit-tests"},{"bucket":"fail","name":"unit-tests"},{"bucket":"pass","name":"review"},{"bucket":"pass","name":"lint"}]'
  local out ci
  out=$(main "owner" "repo" "1")
  ci=$(echo "$out" | jq -r '.ci.status')
  assert_eq "a failing same-name replacement is failure" "failure" "$ci"
}

# One cancelled check's replacement cannot settle a different cancelled check.
t_ci_status_replacement_does_not_cover_another_cancel() {
  MOCK_MERGE_STATE=clean
  MOCK_CHECKS_BODY='[{"bucket":"cancel","name":"unit-tests"},{"bucket":"pass","name":"unit-tests"},{"bucket":"cancel","name":"diagnostics"},{"bucket":"pass","name":"lint"}]'
  local out ci
  out=$(main "owner" "repo" "1")
  ci=$(echo "$out" | jq -r '.ci.status')
  assert_eq "each cancelled check needs its own replacement" "pending" "$ci"
}

# A real failure on head still fails, even next to a cancelled run.
t_ci_status_fail_with_cancel_is_failure() {
  MOCK_MERGE_STATE=clean
  MOCK_CHECKS_BODY='[{"name":"CI","bucket":"fail"},{"name":"old","bucket":"cancel"}]'
  local out ci
  out=$(main "owner" "repo" "1")
  ci=$(echo "$out" | jq -r '.ci.status')
  assert_eq "a real fail still fails next to a cancel" "failure" "$ci"
}

# --- review lanes (docs/adr/0010-review-lanes.md) ---
# Codex and Claude both post as github-actions[bot]; reviews.codex must read
# the codex-policy lane only, and the lanes view must report every lane.
t_main_reads_codex_verdict_per_lane() {
  MOCK_MERGE_STATE=clean
  MOCK_REVIEWS_BODY='[
    {"id":1,"user":{"login":"github-actions[bot]","type":"Bot"},"state":"CHANGES_REQUESTED","submitted_at":"2026-10-20T10:00:00Z","commit_id":"'"$HEAD_SHA"'","body":"<!-- coding-policy:codex-policy-review:v1 -->\nPolicy loaded: 30 rule files. One violation."},
    {"id":2,"user":{"login":"github-actions[bot]","type":"Bot"},"state":"COMMENTED","submitted_at":"2026-10-20T10:05:00Z","commit_id":"'"$HEAD_SHA"'","body":"<!-- coding-policy:claude-quality-review:v1 -->\nNo correctness issues found."},
    {"id":4,"user":{"login":"copilot-pull-request-reviewer[bot]","type":"Bot"},"state":"COMMENTED","submitted_at":"2026-10-20T10:10:00Z","commit_id":"'"$HEAD_SHA"'","body":"Copilot reviewed 3 files."}]'
  local out
  out=$(main owner repo 1)
  unset MOCK_REVIEWS_BODY
  assert_eq "reviews.codex state" "CHANGES_REQUESTED" "$(jq -r '.reviews.codex.state' <<<"$out")" || return 1
  assert_eq "codex lane gating"   "true"              "$(jq -r '.lanes["codex-policy"].gating' <<<"$out")" || return 1
  assert_eq "claude lane state"   "COMMENTED"         "$(jq -r '.lanes["claude-quality"].state' <<<"$out")" || return 1
  assert_eq "claude lane gating"  "false"             "$(jq -r '.lanes["claude-quality"].gating' <<<"$out")" || return 1
  assert_eq "lane set"            '["claude-quality","codex-policy"]' "$(jq -c '.lanes | keys' <<<"$out")" || return 1
  assert_eq "review keys"         '["codex"]'         "$(jq -c '.reviews | keys' <<<"$out")" || return 1
  assert_eq "copilot is an other review" '[{"login":"copilot-pull-request-reviewer[bot]","is_bot":true,"state":"COMMENTED","body":"Copilot reviewed 3 files."}]' \
    "$(jq -c '.other_reviews | map({login, is_bot, state, body})' <<<"$out")"
}

# A human CHANGES_REQUESTED on an older commit stays CHANGES_REQUESTED: GitHub
# keeps it active across pushes, so it is not collapsed against head.
t_main_reports_human_review_unresolved_by_head() {
  MOCK_MERGE_STATE=clean
  MOCK_REVIEWS_BODY='[
    {"id":5,"user":{"login":"alice","type":"User"},"state":"COMMENTED","submitted_at":"2026-10-20T09:00:00Z","commit_id":"'"$OLD_SHA"'","body":"first look"},
    {"id":6,"user":{"login":"alice","type":"User"},"state":"CHANGES_REQUESTED","submitted_at":"2026-10-20T09:30:00Z","commit_id":"'"$OLD_SHA"'","body":"please rename"},
    {"id":7,"user":{"login":"github-actions[bot]","type":"Bot"},"state":"COMMENTED","submitted_at":"2026-10-20T10:00:00Z","commit_id":"'"$HEAD_SHA"'","body":"<!-- coding-policy:codex-policy-review:v1 -->\nclean"}]'
  local out
  out=$(main owner repo 1)
  unset MOCK_REVIEWS_BODY
  assert_eq "human review kept" '[{"login":"alice","is_bot":false,"state":"CHANGES_REQUESTED","body":"please rename","stale":true}]' \
    "$(jq -c '.other_reviews | map({login, is_bot, state, body, stale})' <<<"$out")"
}

t_main_unmarked_review_is_codex_lane() {
  MOCK_MERGE_STATE=clean
  MOCK_REVIEWS_BODY='[{"id":3,"user":{"login":"github-actions[bot]","type":"Bot"},"state":"COMMENTED","submitted_at":"2026-10-20T10:00:00Z","commit_id":"'"$HEAD_SHA"'","body":"Policy loaded: 30 rule files. All rules pass."}]'
  local out
  out=$(main owner repo 1)
  unset MOCK_REVIEWS_BODY
  assert_eq "legacy review is codex" "COMMENTED" "$(jq -r '.reviews.codex.state' <<<"$out")" || return 1
  assert_eq "claude lane empty"      "none"      "$(jq -r '.lanes["claude-quality"].state' <<<"$out")"
}

# GitHub resolves a reviewer's state from decisive reviews only: a later
# COMMENTED (a thread reply is one) never clears CHANGES_REQUESTED.
human_state_after() { # <state1> <state2> -> other_reviews[0] {state, body}
  MOCK_MERGE_STATE=clean
  MOCK_REVIEWS_BODY='[
    {"id":5,"user":{"login":"alice","type":"User"},"state":"'"$1"'","submitted_at":"2026-10-20T09:00:00Z","commit_id":"'"$OLD_SHA"'","body":"first"},
    {"id":6,"user":{"login":"alice","type":"User"},"state":"'"$2"'","submitted_at":"2026-10-20T09:30:00Z","commit_id":"'"$HEAD_SHA"'","body":"second"}]'
  local out
  out=$(main owner repo 1)
  unset MOCK_REVIEWS_BODY
  jq -c '.other_reviews | map({state, body})' <<<"$out"
}

t_human_comment_does_not_clear_changes_requested() {
  assert_eq "active verdict" '[{"state":"CHANGES_REQUESTED","body":"first"}]' "$(human_state_after CHANGES_REQUESTED COMMENTED)"
}

# Every body stays readable, not only the one that decided the verdict.
t_human_reviews_keep_every_body() {
  MOCK_MERGE_STATE=clean
  MOCK_REVIEWS_BODY='[
    {"id":6,"user":{"login":"alice","type":"User"},"state":"COMMENTED","submitted_at":"2026-10-20T09:30:00Z","commit_id":"'"$HEAD_SHA"'","body":"second"},
    {"id":5,"user":{"login":"alice","type":"User"},"state":"CHANGES_REQUESTED","submitted_at":"2026-10-20T09:00:00Z","commit_id":"'"$OLD_SHA"'","body":"first"}]'
  local out
  out=$(main owner repo 1)
  unset MOCK_REVIEWS_BODY
  assert_eq "every body" '[[{"state":"CHANGES_REQUESTED","body":"first"},{"state":"COMMENTED","body":"second"}]]' \
    "$(jq -c '.other_reviews | map(.reviews | map({state, body}))' <<<"$out")"
}

t_human_approval_clears_changes_requested() {
  assert_eq "active verdict" '[{"state":"APPROVED","body":"second"}]' "$(human_state_after CHANGES_REQUESTED APPROVED)"
}

t_human_dismissal_then_comment_reads_commented() {
  assert_eq "active verdict" '[{"state":"COMMENTED","body":"second"}]' "$(human_state_after DISMISSED COMMENTED)"
}

claude_expected_with_checks() { # <checks-json>
  MOCK_MERGE_STATE=clean
  MOCK_CHECKS_BODY="$1"
  local out
  out=$(main owner repo 1)
  unset MOCK_CHECKS_BODY
  jq -c '[.lanes["codex-policy"].expected, .lanes["claude-quality"].expected]' <<<"$out"
}

t_claude_lane_expected_when_workflow_ran() {
  assert_eq "expected" '[true,true]' \
    "$(claude_expected_with_checks '[{"name":"review","bucket":"pending","workflow":"PR Code Review (Claude, subscription)"}]')"
}

t_claude_lane_not_expected_without_workflow() {
  assert_eq "expected" '[true,false]' \
    "$(claude_expected_with_checks '[{"name":"review","bucket":"pass","workflow":"PR Policy Review (Codex, subscription)"}]')"
}

t_claude_lane_not_expected_when_skipped() {
  assert_eq "expected" '[true,false]' \
    "$(claude_expected_with_checks '[{"name":"review","bucket":"skipping","workflow":"PR Code Review (Claude, subscription)"}]')"
}

# --- driver ---

echo "== poll-pr-reviews.sh tests =="
run "fetch_merge_state returns {CLEAN, MERGEABLE} for a clean PR"     t_fetch_merge_state_clean_returns_mergeable_envelope
run "fetch_merge_state returns {DIRTY, CONFLICTING} on conflict"      t_fetch_merge_state_dirty_returns_conflicting_envelope
run "fetch_merge_state propagates UNKNOWN/UNKNOWN while computing"    t_fetch_merge_state_unknown_returns_unknown_envelope
run "main surfaces merge_state as a top-level field"                  t_main_surfaces_merge_state_as_top_level_field
run "main propagates DIRTY merge_state end-to-end"                    t_main_propagates_dirty_state
run "latest_review_by picks newest review on page 2 (issue #83)"      t_latest_review_by_picks_from_last_page
run "latest_review_by returns 'none' for empty reviews"               t_latest_review_by_returns_none_when_no_reviews
run "latest_review_by surfaces the review body text"                  t_latest_review_by_surfaces_body_text
run "latest_review_by ignores other logins across pages"              t_latest_review_by_filters_other_logins_across_pages
run "toplevel_comments_by sums counts across pages (issue #83)"       t_toplevel_comments_by_sums_across_pages
run "toplevel_comments_by returns 0 for empty comments"               t_toplevel_comments_by_returns_zero_for_no_comments
run "toplevel_comments_by excludes replies and foreign logins"        t_toplevel_comments_by_excludes_replies_and_other_logins
run "main counts Copilot comments as other"                           t_main_counts_copilot_comments_as_other
run "main reports a human review without head collapse"               t_main_reports_human_review_unresolved_by_head
run "a later human COMMENTED never clears CHANGES_REQUESTED"          t_human_comment_does_not_clear_changes_requested
run "every unlaned review body stays in the snapshot"                  t_human_reviews_keep_every_body
run "a later human APPROVED clears CHANGES_REQUESTED"                 t_human_approval_clears_changes_requested
run "a dismissed human block reads as the later comment"              t_human_dismissal_then_comment_reads_commented
run "the Claude lane is expected when its workflow ran on head"       t_claude_lane_expected_when_workflow_ran
run "the Claude lane is not expected without its workflow"            t_claude_lane_not_expected_without_workflow
run "a skipped Claude workflow does not make its lane expected"       t_claude_lane_not_expected_when_skipped
run "toplevel_comments_by survives a non-object element (#300)"       t_toplevel_comments_by_survives_non_object_element
run "toplevel_comments_by warns about the dropped element (#300)"     t_toplevel_comments_by_warns_on_non_object_element
run "latest_review_by survives a non-object element (#300)"           t_latest_review_by_survives_non_object_element
run "main returns a snapshot despite a non-object element (#300)"     t_main_survives_non_object_comment_element
run "latest_review_by resolves an explicitly passed extra login"      t_latest_review_by_resolves_extra_login
run "latest_review_by picks newest policy verdict across logins"      t_latest_review_by_policy_reviewer_picks_newest_across_logins
run "latest_review_by never masks an active block with a later clean" t_latest_review_by_changes_requested_not_masked_by_later_other_login
run "latest_review_by picks latest by time, not array position"       t_latest_review_by_picks_max_by_time_not_array_position
run "latest_review_by normalizes DISMISSED to none"                   t_latest_review_by_normalizes_dismissed_to_none
run "latest_review_by normalizes PENDING to none"                     t_latest_review_by_normalizes_pending_to_none
run "main does not surface an unknown login as .reviews.codex"        t_main_ignores_unknown_login_review
run "toplevel_comments_by ignores an unknown comment login"           t_toplevel_comments_by_ignores_unknown_login
run "resolve_review_against_head: fresh verdict passes through"       t_resolve_against_head_fresh_passes_through
run "resolve_review_against_head: stale verdict collapses to none"    t_resolve_against_head_stale_collapses_to_none
run "resolve_review_against_head: absent stays none, not stale"       t_resolve_against_head_none_stays_none_not_stale
run "main collapses a stale review to none (#186 false ready)"        t_main_stale_review_collapses_to_none
run "main surfaces a fresh (head-bound) review's state"               t_main_fresh_review_surfaces_state
run "main surfaces head_sha as a top-level field"                     t_main_surfaces_head_sha_top_level
run "main fails loudly on an empty headRefOid"                        t_main_no_head_sha_fails
run "ci.status: success next to a cancelled twin is success (#182)"   t_ci_status_cancel_with_success_is_success
run "ci.status: only-cancels reads pending (#182)"                    t_ci_status_only_cancels_is_pending
run "ci.status: an unreplaced cancel next to passes is pending"      t_ci_status_unreplaced_cancel_with_other_passes_is_pending
run "ci.status: a cancel replaced by a pass is success"               t_ci_status_cancel_replaced_by_pass_is_success
run "ci.status: a cancel replaced by a fail is failure"               t_ci_status_cancel_replaced_by_fail_is_failure
run "ci.status: one replacement cannot settle another cancel"         t_ci_status_replacement_does_not_cover_another_cancel
run "ci.status: a real fail next to a cancel still fails (#182)"      t_ci_status_fail_with_cancel_is_failure

run "requested_among: a pending bot request matches (#369)"           t_requested_among_matches_a_pending_bot
run "requested_among: no pending request reads false (#369)"          t_requested_among_is_false_when_nobody_asked
run "requested_among: another reviewer's request is not this one"     t_requested_among_ignores_another_reviewer
run "fetch_requested_logins filters the raw GraphQL response"         t_fetch_requested_logins_reads_the_raw_graphql_shape
run "a non-integer PR number is refused before any call"              t_non_integer_pr_number_is_refused_before_any_call
run "main marks a lane nobody requested (#369)"                       t_main_marks_an_unrequested_lane
run "main marks a requested-and-pending lane (#369)"                  t_main_marks_a_pending_lane_requested
run "main reads the Codex verdict from its own lane"                  t_main_reads_codex_verdict_per_lane
run "main classifies an unmarked legacy review as Codex"              t_main_unmarked_review_is_codex_lane

echo "== summary: ${PASS_COUNT} passed, ${FAIL_COUNT} failed =="
echo "test-summary: passed=${PASS_COUNT} failed=${FAIL_COUNT} token=${SUMMARY_TOKEN}"
[[ "$FAIL_COUNT" -eq 0 ]]

skills

README.md

tile.json