From b5004fa1f12a80e9aacedccf6221effeef8ad12a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 1 Jul 2026 15:59:24 +0900 Subject: [PATCH 01/12] [codex] Fail OpenCode approve when review publish fails --- .github/workflows/opencode-review.yml | 22 ---------------------- scripts/ci/test_strix_quick_gate.sh | 7 +++---- 2 files changed, 3 insertions(+), 26 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index c84d59430..27268a8ab 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -2378,12 +2378,6 @@ jobs: fi } - gh_error_is_rate_limited() { - local error_file="$1" - [ -s "$error_file" ] || return 1 - grep -Eiq '(API rate limit exceeded|rate limit exceeded|secondary rate limit)' "$error_file" - } - emit_change_flow_mermaid_graph() { local merge_state="${1:-UNKNOWN}" local changed_files_file surfaces_file idx next_node @@ -2902,22 +2896,6 @@ jobs: fi warn_gh_publication_failure "pull review with fallback review token" "$gh_error_file" fi - if [ "$event" = "APPROVE" ] && gh_error_is_rate_limited "$gh_error_file"; then - rm -f "$gh_error_file" "$review_payload_file" - update_review_overview "$event" "$body" || true - if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then - { - printf '## OpenCode approve review publication skipped\n\n' - printf -- '- Head SHA: `%s`\n' "$HEAD_SHA" - printf -- '- Workflow run: %s\n' "$RUN_ID" - printf -- '- Workflow attempt: %s\n\n' "$RUN_ATTEMPT" - printf 'OpenCode completed the approval gate, but GitHub rejected the pull-review write due to API rate limiting. The required workflow remains successful because failed checks, mergeability, and unresolved review threads were already gated before approval.\n\n' - printf '%s\n' "$body" - } >>"$GITHUB_STEP_SUMMARY" - fi - printf '::warning::OpenCode could not publish the APPROVE pull review for head %s because the GitHub API rate limit was exceeded; keeping the successful approval gate result because pre-approval source, check, mergeability, and review-thread gates passed.\n' "$HEAD_SHA" - return 0 - fi rm -f "$gh_error_file" "$review_payload_file" update_review_overview "$event" "$body" || true printf '::error::OpenCode could not publish the pull review for head %s, so the review state was not changed.\n' "$HEAD_SHA" diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 5ed9b1a70..329bb8f0d 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -785,10 +785,9 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'warn_gh_publication_failure "initial review overview comment"' "opencode initial overview comment soft-fails permission-denied publication errors" assert_file_contains "$workflow_file" 'warn_gh_publication_failure "pull review with primary review token"' "opencode approval explains primary review publication failures" assert_file_contains "$workflow_file" 'warn_gh_publication_failure "pull review with fallback review token"' "opencode approval explains fallback review publication failures" - assert_file_contains "$workflow_file" 'gh_error_is_rate_limited()' "opencode approval detects rate-limited publication failures" - assert_file_contains "$workflow_file" '[ "$event" = "APPROVE" ] && gh_error_is_rate_limited "$gh_error_file"' "opencode approval only soft-fails rate-limited approve publication failures" - assert_file_contains "$workflow_file" 'OpenCode could not publish the APPROVE pull review for head %s because the GitHub API rate limit was exceeded' "opencode approval keeps successful gate results for rate-limited approval review publication" - assert_file_contains "$workflow_file" 'OpenCode could not publish the pull review for head %s, so the review state was not changed.' "opencode approval fails when review publication fails" + assert_file_contains "$workflow_file" 'OpenCode could not publish the pull review for head %s, so the review state was not changed.' "opencode approval fails closed when review publication fails" + assert_file_not_contains "$workflow_file" 'OpenCode approve review publication skipped' "opencode approval does not report skipped approval publication as success" + assert_file_not_contains "$workflow_file" 'keeping the successful approval gate result' "opencode approval does not soft-pass without publishing an approving review" assert_file_contains "$workflow_file" 'warn_gh_publication_failure "review overview comment"' "opencode approval soft-fails permission-denied overview publication" assert_file_not_contains "$workflow_file" 'gh api -X DELETE "repos/${GH_REPOSITORY}/issues/comments/${comment_id}"' "opencode review must not delete Review Overview gate evidence" assert_file_not_contains "$workflow_file" '--file "$OPENCODE_EVIDENCE_FILE"' "opencode review must not attach evidence content to GitHub Models requests" From fee55ca0a11032c0fba173d5f0928c74a6f893df Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 1 Jul 2026 17:06:40 +0900 Subject: [PATCH 02/12] [codex] Bound OpenCode model pool candidates --- .github/workflows/opencode-review.yml | 8 ++++---- scripts/ci/test_strix_quick_gate.sh | 15 +++++++-------- tests/test_opencode_agent_contract.py | 13 +++++++++---- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 27268a8ab..b6b8fc373 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -2239,7 +2239,7 @@ jobs: id: opencode_review_model_pool if: needs.coverage-evidence.result == 'success' continue-on-error: true - timeout-minutes: 75 + timeout-minutes: 25 env: STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }} GITHUB_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }} @@ -2247,11 +2247,11 @@ jobs: SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" - OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1 github-models/deepseek/deepseek-v3-0324 github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/gpt-5-mini github-models/openai/gpt-5-nano github-models/openai/o3 github-models/openai/o3-mini github-models/openai/o4-mini github-models/mistral-ai/mistral-medium-2505 github-models/meta/llama-4-maverick-17b-128e-instruct-fp8 github-models/meta/llama-4-scout-17b-16e-instruct" + OPENCODE_MODEL_CANDIDATES: "github-models/openai/o4-mini github-models/openai/o3-mini github-models/openai/o3 github-models/mistral-ai/mistral-medium-2505 github-models/meta/llama-4-maverick-17b-128e-instruct-fp8 github-models/meta/llama-4-scout-17b-16e-instruct" OPENCODE_MODEL_ATTEMPTS: "1" - OPENCODE_RUN_TIMEOUT_SECONDS: "600" + OPENCODE_RUN_TIMEOUT_SECONDS: "180" OPENCODE_EXPORT_TIMEOUT_SECONDS: "120" - OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "3600" + OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "1200" OPENCODE_BACKOFF_INITIAL_SECONDS: "30" OPENCODE_BACKOFF_MAX_SECONDS: "600" OPENCODE_FIRST_ATTEMPT_AGENT: ci-review diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 329bb8f0d..3770d4463 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -508,15 +508,15 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "Read and follow the complete review contract" "opencode review uses a compact launcher while keeping the full review contract on disk" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "tokens_limit_reached" "opencode review detects provider context-window overflow" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "skipping remaining attempts for this model" "opencode review skips same-model retries after context-window overflow" - assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "600"' "opencode primary review has a bounded per-model timeout before trying fallback models" - assert_file_contains "$workflow_file" 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "3600"' "opencode model pool has a one-hour total retry budget" + assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "180"' "opencode primary review has a bounded per-model timeout before trying fallback models" + assert_file_contains "$workflow_file" 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "1200"' "opencode model pool has a twenty-minute total retry budget" assert_file_contains "$workflow_file" "needs.coverage-evidence.result == 'success'" "opencode model pool only runs after coverage evidence passed" assert_file_contains "$workflow_file" "id: opencode_review_model_pool" "opencode DeepSeek V3 fallback still runs after a primary model timeout or step failure when coverage evidence passed" assert_file_contains "$workflow_file" "always()" "opencode fallback chain uses always() so failed model steps cannot skip every fallback" assert_file_contains "$workflow_file" 'OPENCODE_MODEL_ATTEMPTS: "1"' "opencode fallback tries the catalog promptly instead of spending the entire review on one model" assert_file_contains "$workflow_file" "Run OpenCode PR Review model pool" "opencode review includes a broad catalog fallback pool" assert_file_contains "$workflow_file" "continue-on-error: true" "opencode model step timeouts do not prevent fallback review publication" - assert_file_contains "$workflow_file" "github-models/openai/gpt-5-chat github-models/openai/gpt-5-mini github-models/openai/gpt-5-nano github-models/openai/o3 github-models/openai/o3-mini github-models/openai/o4-mini github-models/mistral-ai/mistral-medium-2505 github-models/meta/llama-4-maverick-17b-128e-instruct-fp8 github-models/meta/llama-4-scout-17b-16e-instruct" "opencode review tries catalog-available tool-calling fallbacks after DeepSeek and GPT-5 paths" + assert_file_contains "$workflow_file" "github-models/openai/o4-mini github-models/openai/o3-mini github-models/openai/o3 github-models/mistral-ai/mistral-medium-2505 github-models/meta/llama-4-maverick-17b-128e-instruct-fp8 github-models/meta/llama-4-scout-17b-16e-instruct" "opencode review tries catalog-available tool-calling fallbacks before spending time on known failing candidates" assert_file_contains "$workflow_file" "The publish gate re-runs source-backed validation against PR-head data" "opencode review publish gate validates model output against the PR-head worktree" assert_file_contains "$workflow_file" '"openai/o3"' "opencode config declares OpenAI o3 fallback" assert_file_contains "$workflow_file" '"openai/o4-mini"' "opencode config declares OpenAI o4-mini fallback" @@ -618,10 +618,10 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" "no model produced a valid review control block" "opencode model-failure path documents why approval is withheld" assert_file_contains "$workflow_file" 'OPENCODE_MODEL_ATTEMPTS: "1"' "opencode primary and fallback paths avoid multi-attempt stalls on one model" assert_file_contains "$workflow_file" 'OPENCODE_MODEL_ATTEMPTS: "1"' "opencode catalog fallback tries each model once before moving on" - assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "600"' "opencode catalog fallback has a bounded model review timeout before step timeout" + assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "180"' "opencode catalog fallback has a bounded model review timeout before step timeout" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "OpenCode %s attempt %s/%s failed" "opencode catalog fallback records per-model retry failures" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "exponential backoff" "opencode model retry paths use exponential backoff instead of fixed sleeps" - assert_file_contains "$workflow_file" "github-models/openai/o3 github-models/openai/o3-mini github-models/openai/o4-mini" "opencode review includes additional OpenAI reasoning model fallbacks" + assert_file_contains "$workflow_file" "github-models/openai/o4-mini github-models/openai/o3-mini github-models/openai/o3" "opencode review includes additional OpenAI reasoning model fallbacks" assert_file_contains "$workflow_file" "coverage-evidence:" "opencode workflow measures coverage before review" assert_file_contains "$workflow_file" "github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target'" "manual and required OpenCode reviews measure coverage instead of approving skipped coverage evidence" assert_file_contains "$workflow_file" "Exchange OpenCode app token for target repository coverage reads" "coverage evidence can read private target repositories through the OpenCode app token" @@ -798,9 +798,8 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'GH_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || steps.review_read_app_token.outputs.token || github.token }}' "opencode manual dispatch uses the cross-repo approval token for target PR evidence lookups with app-token fallback" assert_file_contains "$workflow_file" 'repos/${GH_REPOSITORY}' "opencode review workflow uses env-backed repository context in shell commands" assert_file_contains "$workflow_file" "Run OpenCode PR Review model pool" "opencode review starts the central model pool" - assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-r1-0528" "opencode review starts with a reachable DeepSeek R1 reasoning model" - assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-v3-0324" "opencode review has a reachable DeepSeek V3 fallback model" - assert_file_contains "$workflow_file" "github-models/openai/gpt-5" "opencode review still has a bounded GPT-5 fallback model" + assert_file_contains "$workflow_file" "OPENCODE_MODEL_CANDIDATES: \"github-models/openai/o4-mini github-models/openai/o3-mini github-models/openai/o3" "opencode review starts with bounded tool-calling OpenAI reasoning models" + assert_file_not_contains "$workflow_file" "OPENCODE_MODEL_CANDIDATES: \"github-models/deepseek" "opencode review does not start with known unsupported DeepSeek tool-use candidates" assert_file_contains "$workflow_file" "Publish bounded OpenCode review comment" "opencode review workflow publishes the agent control comment for the approval gate" assert_file_contains "$workflow_file" "statusCheckRollup" "opencode review workflow reads current-head GitHub Checks before approval" assert_file_contains "$workflow_file" "OPENCODE_FAILED_CHECK_EVIDENCE_FILE" "opencode review workflow persists failed-check evidence across review and approval steps" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 082a139e7..bd01ba2be 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -76,7 +76,10 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): candidates = candidates_match.group(1).split() candidate_models = [candidate.removeprefix("github-models/") for candidate in candidates] - assert set(candidate_models) == set(models) + assert set(candidate_models).issubset(set(models)) + assert "deepseek/deepseek-r1-0528" not in candidate_models + assert "deepseek/deepseek-r1" not in candidate_models + assert "deepseek/deepseek-v3-0324" not in candidate_models def is_reasoning_capable(model_name: str) -> bool: return ( @@ -207,13 +210,15 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert '"## Review outcome"' in workflow assert '"## Check outcome"' not in workflow assert "publish REQUEST_CHANGES when coverage-evidence blocker states" in workflow - assert 'timeout-minutes: 75' in workflow + assert 'timeout-minutes: 25' in workflow assert 'APPROVAL_CHECK_WAIT_ATTEMPTS: "81"' in workflow assert 'APPROVAL_CHECK_WAIT_SLEEP_SECONDS: "30"' in workflow assert 'OPENCODE_MODEL_ATTEMPTS: "1"' in workflow - assert 'OPENCODE_RUN_TIMEOUT_SECONDS: "600"' in workflow + assert 'OPENCODE_RUN_TIMEOUT_SECONDS: "180"' in workflow assert 'OPENCODE_EXPORT_TIMEOUT_SECONDS: "120"' in workflow - assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "3600"' in workflow + assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "1200"' in workflow + assert "github-models/openai/o4-mini github-models/openai/o3-mini github-models/openai/o3" in workflow + assert "OPENCODE_MODEL_CANDIDATES: \"github-models/deepseek" not in workflow assert "${{ runner.temp }}/opencode-review-model-pool.md" in workflow strix_workflow = Path(".github/workflows/strix.yml").read_text(encoding="utf-8") From e6d38d18adf8803311c0e790275ed6c11c38b8a3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 1 Jul 2026 17:08:46 +0900 Subject: [PATCH 03/12] [codex] Guard OpenCode publish failure log groups --- .github/workflows/opencode-review.yml | 4 +++- scripts/ci/test_strix_quick_gate.sh | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index b6b8fc373..4c397b162 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -2899,7 +2899,9 @@ jobs: rm -f "$gh_error_file" "$review_payload_file" update_review_overview "$event" "$body" || true printf '::error::OpenCode could not publish the pull review for head %s, so the review state was not changed.\n' "$HEAD_SHA" - echo "::endgroup::" + case "$event" in + REQUEST_CHANGES | INLINE_COMMENT_PUBLISH_FAILED) echo "::endgroup::" ;; + esac exit 1 fi rm -f "$gh_error_file" "$review_payload_file" diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 3770d4463..fbbe34225 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -786,6 +786,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'warn_gh_publication_failure "pull review with primary review token"' "opencode approval explains primary review publication failures" assert_file_contains "$workflow_file" 'warn_gh_publication_failure "pull review with fallback review token"' "opencode approval explains fallback review publication failures" assert_file_contains "$workflow_file" 'OpenCode could not publish the pull review for head %s, so the review state was not changed.' "opencode approval fails closed when review publication fails" + assert_file_contains "$workflow_file" 'REQUEST_CHANGES | INLINE_COMMENT_PUBLISH_FAILED) echo "::endgroup::" ;;' "opencode only closes a review-body log group for events that opened one" assert_file_not_contains "$workflow_file" 'OpenCode approve review publication skipped' "opencode approval does not report skipped approval publication as success" assert_file_not_contains "$workflow_file" 'keeping the successful approval gate result' "opencode approval does not soft-pass without publishing an approving review" assert_file_contains "$workflow_file" 'warn_gh_publication_failure "review overview comment"' "opencode approval soft-fails permission-denied overview publication" From a392b9f769f8f77b9ba5eb75d591fea6e0ba9e02 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 1 Jul 2026 17:14:21 +0900 Subject: [PATCH 04/12] [codex] Cancel stale OpenCode PR review runs --- .github/workflows/opencode-review.yml | 4 ++-- tests/test_opencode_agent_contract.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 4c397b162..fb3a992b4 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -36,8 +36,8 @@ concurrency: group: >- opencode-review-${{ github.event_name }}-${{ github.event.pull_request.base.repo.full_name || github.event.inputs.target_repository || github.repository }}-${{ - github.event_name == 'pull_request_target' && format('pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || - github.event.inputs.pr_number != '' && github.event.inputs.pr_head_sha != '' && format('pr-{0}-{1}', github.event.inputs.pr_number, github.event.inputs.pr_head_sha) || + github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number) || + github.event.inputs.pr_number != '' && format('pr-{0}', github.event.inputs.pr_number) || github.event.inputs.pr_number || github.run_id }} cancel-in-progress: true diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index bd01ba2be..1cfac8769 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -186,6 +186,8 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "Run OpenCode PR Review model pool" in workflow assert "opencode_review_model_pool" in workflow assert "run_opencode_review_model_pool.sh" in workflow + assert "format('pr-{0}', github.event.pull_request.number)" in workflow + assert "format('pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha)" not in workflow assert "OPENCODE_MODEL_CANDIDATES" in workflow model_pool_runner = Path("scripts/ci/run_opencode_review_model_pool.sh").read_text(encoding="utf-8") assert "assert_reasoning_effort_for_candidate" in model_pool_runner From 1197abeee9373e564d30f7aeef7158e85bab8030 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:11:52 +0000 Subject: [PATCH 05/12] Re-kick model pool once on OpenCode exhaustion --- .github/workflows/opencode-review.yml | 40 +++++++++++++++++++++++++++ tests/test_opencode_agent_contract.py | 2 ++ 2 files changed, 42 insertions(+) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 7294aa6e1..fa95f486a 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -2627,6 +2627,16 @@ jobs: APPROVAL_CHECK_WAIT_SLEEP_SECONDS: "30" CHECK_LOOKUP_RETRY_ATTEMPTS: "5" CHECK_LOOKUP_RETRY_SLEEP_SECONDS: "5" + OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-5-nano" + OPENCODE_MODEL_ATTEMPTS: "1" + OPENCODE_RUN_TIMEOUT_SECONDS: "240" + OPENCODE_EXPORT_TIMEOUT_SECONDS: "120" + OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "360" + OPENCODE_BACKOFF_INITIAL_SECONDS: "30" + OPENCODE_BACKOFF_MAX_SECONDS: "30" + OPENCODE_FIRST_ATTEMPT_AGENT: ci-review + OPENCODE_AGENT: ci-review-fallback + OPENCODE_EXHAUSTED_REKICK_ATTEMPTS: "1" run: | set -euo pipefail echo "::group::OpenCode Review Approval Gate" @@ -4765,6 +4775,35 @@ jobs: scripts/ci/collect_failed_check_evidence.sh "$evidence_file" } + rekick_model_pool_on_exhaustion() { + local max_rekicks="${OPENCODE_EXHAUSTED_REKICK_ATTEMPTS:-0}" + local rekick_attempt=1 + local rekick_output outcome model + + while [ "${OPENCODE_MODEL_POOL_OUTCOME:-}" = "exhausted" ] && [ "$rekick_attempt" -le "$max_rekicks" ]; do + printf 'OpenCode model pool exhausted; re-kicking model pool (%s/%s).\n' "$rekick_attempt" "$max_rekicks" + rekick_output="$(mktemp)" + if GITHUB_OUTPUT="$rekick_output" OPENCODE_OUTPUT_FILE="$OPENCODE_MODEL_POOL_OUTPUT_FILE" \ + bash "$GITHUB_WORKSPACE/scripts/ci/run_opencode_review_model_pool.sh"; then + : + fi + outcome="$(awk -F= '/^review_status=/{v=$2} END{print v}' "$rekick_output")" + model="$(awk -F= '/^review_model=/{v=$2} END{print v}' "$rekick_output")" + rm -f "$rekick_output" + + if [ -z "$outcome" ]; then + outcome="exhausted" + fi + OPENCODE_MODEL_POOL_OUTCOME="$outcome" + OPENCODE_MODEL_POOL_MODEL="$model" + if [ "$outcome" = "success" ]; then + printf 'OpenCode model pool re-kick recovered with model: %s\n' "${model:-unknown}" + break + fi + rekick_attempt=$((rekick_attempt + 1)) + done + } + live_head_sha="$(gh api -X GET "repos/${GH_REPOSITORY}/pulls/${PR_NUMBER}" --jq '.head.sha')" if [ "$live_head_sha" != "$HEAD_SHA" ]; then echo "stale OpenCode run: event head=${HEAD_SHA}, live head=${live_head_sha}; skipping review side effects." @@ -4776,6 +4815,7 @@ jobs: request_changes_for_coverage_evidence_failure fi + rekick_model_pool_on_exhaustion opencode_review_outcome="${OPENCODE_MODEL_POOL_OUTCOME:-unknown}" if [ "$opencode_review_outcome" != "success" ]; then diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 4ce626884..933e1c918 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -186,6 +186,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "Run OpenCode PR Review model pool" in workflow assert "opencode_review_model_pool" in workflow assert "run_opencode_review_model_pool.sh" in workflow + assert "rekick_model_pool_on_exhaustion" in workflow assert "format('pr-{0}', github.event.pull_request.number)" in workflow assert "format('pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha)" not in workflow assert "OPENCODE_MODEL_CANDIDATES" in workflow @@ -222,6 +223,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert 'OPENCODE_EXPORT_TIMEOUT_SECONDS: "120"' in workflow assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "360"' in workflow assert 'OPENCODE_BACKOFF_MAX_SECONDS: "30"' in workflow + assert 'OPENCODE_EXHAUSTED_REKICK_ATTEMPTS: "1"' in workflow assert "${{ runner.temp }}/opencode-review-model-pool.md" in workflow assert re.search(r'check-runs" \\\n\s+-f per_page=100 \\\n\s+--paginate \\\n\s+--slurp \|\n\s+jq -r "\$jq_filter"', workflow) assert not re.search(r"--slurp\s*\\\n\s*--jq", workflow) From 5a48da0c1dd94c5a5d5be8b4237fa40cca4993e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:13:27 +0000 Subject: [PATCH 06/12] Log empty rekick outcome before fail-closed fallback --- .github/workflows/opencode-review.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index fa95f486a..d34ccaf7f 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -4792,6 +4792,7 @@ jobs: rm -f "$rekick_output" if [ -z "$outcome" ]; then + printf 'OpenCode model pool re-kick did not publish review_status; treating outcome as exhausted.\n' >&2 outcome="exhausted" fi OPENCODE_MODEL_POOL_OUTCOME="$outcome" From 1402f1dbcf85a2453d635d4cafd034f9fc0d5bca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:14:28 +0000 Subject: [PATCH 07/12] Simplify re-kick run invocation in approval gate --- .github/workflows/opencode-review.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index d34ccaf7f..138b27a75 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -4783,10 +4783,8 @@ jobs: while [ "${OPENCODE_MODEL_POOL_OUTCOME:-}" = "exhausted" ] && [ "$rekick_attempt" -le "$max_rekicks" ]; do printf 'OpenCode model pool exhausted; re-kicking model pool (%s/%s).\n' "$rekick_attempt" "$max_rekicks" rekick_output="$(mktemp)" - if GITHUB_OUTPUT="$rekick_output" OPENCODE_OUTPUT_FILE="$OPENCODE_MODEL_POOL_OUTPUT_FILE" \ - bash "$GITHUB_WORKSPACE/scripts/ci/run_opencode_review_model_pool.sh"; then - : - fi + GITHUB_OUTPUT="$rekick_output" OPENCODE_OUTPUT_FILE="$OPENCODE_MODEL_POOL_OUTPUT_FILE" \ + bash "$GITHUB_WORKSPACE/scripts/ci/run_opencode_review_model_pool.sh" || true outcome="$(awk -F= '/^review_status=/{v=$2} END{print v}' "$rekick_output")" model="$(awk -F= '/^review_model=/{v=$2} END{print v}' "$rekick_output")" rm -f "$rekick_output" From 1224db9dd75437f42e2b98f38107622a23d7cd31 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:15:34 +0000 Subject: [PATCH 08/12] Harden re-kick logging for exhausted model pool --- .github/workflows/opencode-review.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 138b27a75..75b0f3a11 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -4778,24 +4778,31 @@ jobs: rekick_model_pool_on_exhaustion() { local max_rekicks="${OPENCODE_EXHAUSTED_REKICK_ATTEMPTS:-0}" local rekick_attempt=1 - local rekick_output outcome model + local rekick_output outcome model rekick_status while [ "${OPENCODE_MODEL_POOL_OUTCOME:-}" = "exhausted" ] && [ "$rekick_attempt" -le "$max_rekicks" ]; do printf 'OpenCode model pool exhausted; re-kicking model pool (%s/%s).\n' "$rekick_attempt" "$max_rekicks" rekick_output="$(mktemp)" + rekick_status=0 GITHUB_OUTPUT="$rekick_output" OPENCODE_OUTPUT_FILE="$OPENCODE_MODEL_POOL_OUTPUT_FILE" \ - bash "$GITHUB_WORKSPACE/scripts/ci/run_opencode_review_model_pool.sh" || true + bash "$GITHUB_WORKSPACE/scripts/ci/run_opencode_review_model_pool.sh" || rekick_status=$? + if [ "$rekick_status" -ne 0 ]; then + printf 'OpenCode model pool re-kick command exited with status %s; continuing with fail-closed handling.\n' "$rekick_status" >&2 + fi outcome="$(awk -F= '/^review_status=/{v=$2} END{print v}' "$rekick_output")" model="$(awk -F= '/^review_model=/{v=$2} END{print v}' "$rekick_output")" rm -f "$rekick_output" if [ -z "$outcome" ]; then - printf 'OpenCode model pool re-kick did not publish review_status; treating outcome as exhausted.\n' >&2 + printf 'OpenCode model pool re-kick produced no review_status output; treating outcome as exhausted.\n' >&2 outcome="exhausted" fi OPENCODE_MODEL_POOL_OUTCOME="$outcome" OPENCODE_MODEL_POOL_MODEL="$model" if [ "$outcome" = "success" ]; then + if [ -z "$model" ]; then + printf 'OpenCode model pool re-kick succeeded but published an empty review_model.\n' >&2 + fi printf 'OpenCode model pool re-kick recovered with model: %s\n' "${model:-unknown}" break fi From 22abddfad938c00405100636407e12eb84dfe8e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:51:08 +0000 Subject: [PATCH 09/12] Retry exhausted OpenCode model pool with exponential backoff --- .github/workflows/opencode-review.yml | 20 ++++++++++++++++---- tests/test_opencode_agent_contract.py | 3 ++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 75b0f3a11..864f0b79e 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -2636,7 +2636,8 @@ jobs: OPENCODE_BACKOFF_MAX_SECONDS: "30" OPENCODE_FIRST_ATTEMPT_AGENT: ci-review OPENCODE_AGENT: ci-review-fallback - OPENCODE_EXHAUSTED_REKICK_ATTEMPTS: "1" + OPENCODE_EXHAUSTED_REKICK_INITIAL_SLEEP_SECONDS: "15" + OPENCODE_EXHAUSTED_REKICK_MAX_SLEEP_SECONDS: "300" run: | set -euo pipefail echo "::group::OpenCode Review Approval Gate" @@ -4776,12 +4777,13 @@ jobs: } rekick_model_pool_on_exhaustion() { - local max_rekicks="${OPENCODE_EXHAUSTED_REKICK_ATTEMPTS:-0}" local rekick_attempt=1 local rekick_output outcome model rekick_status + local sleep_seconds="${OPENCODE_EXHAUSTED_REKICK_INITIAL_SLEEP_SECONDS:-15}" + local max_sleep_seconds="${OPENCODE_EXHAUSTED_REKICK_MAX_SLEEP_SECONDS:-300}" - while [ "${OPENCODE_MODEL_POOL_OUTCOME:-}" = "exhausted" ] && [ "$rekick_attempt" -le "$max_rekicks" ]; do - printf 'OpenCode model pool exhausted; re-kicking model pool (%s/%s).\n' "$rekick_attempt" "$max_rekicks" + while [ "${OPENCODE_MODEL_POOL_OUTCOME:-}" = "exhausted" ]; do + printf 'OpenCode model pool exhausted; re-kicking model pool (attempt %s).\n' "$rekick_attempt" rekick_output="$(mktemp)" rekick_status=0 GITHUB_OUTPUT="$rekick_output" OPENCODE_OUTPUT_FILE="$OPENCODE_MODEL_POOL_OUTPUT_FILE" \ @@ -4806,6 +4808,16 @@ jobs: printf 'OpenCode model pool re-kick recovered with model: %s\n' "${model:-unknown}" break fi + if [ "$sleep_seconds" -gt 0 ]; then + printf 'OpenCode model pool still exhausted after re-kick attempt %s; retrying in %ss.\n' "$rekick_attempt" "$sleep_seconds" + sleep "$sleep_seconds" + fi + if [ "$sleep_seconds" -lt "$max_sleep_seconds" ]; then + sleep_seconds=$((sleep_seconds * 2)) + if [ "$sleep_seconds" -gt "$max_sleep_seconds" ]; then + sleep_seconds="$max_sleep_seconds" + fi + fi rekick_attempt=$((rekick_attempt + 1)) done } diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 933e1c918..43ecebbb8 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -223,7 +223,8 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert 'OPENCODE_EXPORT_TIMEOUT_SECONDS: "120"' in workflow assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "360"' in workflow assert 'OPENCODE_BACKOFF_MAX_SECONDS: "30"' in workflow - assert 'OPENCODE_EXHAUSTED_REKICK_ATTEMPTS: "1"' in workflow + assert 'OPENCODE_EXHAUSTED_REKICK_INITIAL_SLEEP_SECONDS: "15"' in workflow + assert 'OPENCODE_EXHAUSTED_REKICK_MAX_SLEEP_SECONDS: "300"' in workflow assert "${{ runner.temp }}/opencode-review-model-pool.md" in workflow assert re.search(r'check-runs" \\\n\s+-f per_page=100 \\\n\s+--paginate \\\n\s+--slurp \|\n\s+jq -r "\$jq_filter"', workflow) assert not re.search(r"--slurp\s*\\\n\s*--jq", workflow) From 3c3bf6dc619682f82b284ca760009ac154d55d13 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:54:09 +0000 Subject: [PATCH 10/12] Bound exhausted model re-kicks with exponential backoff window --- .github/workflows/opencode-review.yml | 15 ++++++++++++++- tests/test_opencode_agent_contract.py | 1 + 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 864f0b79e..f2a317be3 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -2638,6 +2638,7 @@ jobs: OPENCODE_AGENT: ci-review-fallback OPENCODE_EXHAUSTED_REKICK_INITIAL_SLEEP_SECONDS: "15" OPENCODE_EXHAUSTED_REKICK_MAX_SLEEP_SECONDS: "300" + OPENCODE_EXHAUSTED_REKICK_MAX_TOTAL_SECONDS: "4200" run: | set -euo pipefail echo "::group::OpenCode Review Approval Gate" @@ -4781,8 +4782,20 @@ jobs: local rekick_output outcome model rekick_status local sleep_seconds="${OPENCODE_EXHAUSTED_REKICK_INITIAL_SLEEP_SECONDS:-15}" local max_sleep_seconds="${OPENCODE_EXHAUSTED_REKICK_MAX_SLEEP_SECONDS:-300}" + local max_total_seconds="${OPENCODE_EXHAUSTED_REKICK_MAX_TOTAL_SECONDS:-4200}" + local started_at now elapsed + + started_at="$(date +%s)" while [ "${OPENCODE_MODEL_POOL_OUTCOME:-}" = "exhausted" ]; do + if [ "$max_total_seconds" -gt 0 ]; then + now="$(date +%s)" + elapsed="$((now - started_at))" + if [ "$elapsed" -ge "$max_total_seconds" ]; then + printf 'OpenCode model pool remained exhausted for %s seconds; stopping re-kicks and continuing with fail-closed handling.\n' "$elapsed" >&2 + break + fi + fi printf 'OpenCode model pool exhausted; re-kicking model pool (attempt %s).\n' "$rekick_attempt" rekick_output="$(mktemp)" rekick_status=0 @@ -4809,7 +4822,7 @@ jobs: break fi if [ "$sleep_seconds" -gt 0 ]; then - printf 'OpenCode model pool still exhausted after re-kick attempt %s; retrying in %ss.\n' "$rekick_attempt" "$sleep_seconds" + printf 'OpenCode model pool still exhausted after re-kick attempt %s; retrying in %s seconds.\n' "$rekick_attempt" "$sleep_seconds" sleep "$sleep_seconds" fi if [ "$sleep_seconds" -lt "$max_sleep_seconds" ]; then diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 43ecebbb8..04eda5db6 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -225,6 +225,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert 'OPENCODE_BACKOFF_MAX_SECONDS: "30"' in workflow assert 'OPENCODE_EXHAUSTED_REKICK_INITIAL_SLEEP_SECONDS: "15"' in workflow assert 'OPENCODE_EXHAUSTED_REKICK_MAX_SLEEP_SECONDS: "300"' in workflow + assert 'OPENCODE_EXHAUSTED_REKICK_MAX_TOTAL_SECONDS: "4200"' in workflow assert "${{ runner.temp }}/opencode-review-model-pool.md" in workflow assert re.search(r'check-runs" \\\n\s+-f per_page=100 \\\n\s+--paginate \\\n\s+--slurp \|\n\s+jq -r "\$jq_filter"', workflow) assert not re.search(r"--slurp\s*\\\n\s*--jq", workflow) From f3a40b183063cd69cbb676ddf5693b7ab4571acf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 8 Jul 2026 10:39:33 +0900 Subject: [PATCH 11/12] fix(ci): unblock coverage-evidence by fixing three stale contract assertions Three tests failed on this branch (inherited red state from main), blocking the coverage-evidence job: 1. test_noema_review_gate: #297 re-added a case-sensitive `api_url.startswith("http://"/"https://")` guard AFTER #294's case-insensitive urlparse scheme check. It was redundant for rejecting file:// (the scheme check already raises "URL scheme must be http or https" first) and, worse, wrongly rejected valid uppercase-scheme URLs like HTTPS:// that #294 intentionally supports and tests. Removed the redundant/buggy check; SSRF protection is fully retained by the urlparse scheme + hostname + private-IP validation. Updated the stale assertion to expect the scheme-check message. 2. test_opencode_agent_contract (opencode-review-target timeout): the job grew a legitimate `if:` guard, pushing timeout-minutes:360 to 327 chars from the job key, past the 240-char proximity window. Widened the window to 360; still asserts the timeout belongs to that job. 3. test_opencode_agent_contract (scheduler default): #338 "Throttle central PR workflow queue" deliberately changed the review_dispatch_limit input default from "-1" to "1" (and shipped its own contract test), but this assertion was left stale. Updated it to "1". Full suite: 174 passed, coverage 100% (fail_under=100). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01RTAMs4bpSZS77Xe3RQjv9P --- scripts/ci/noema_review_gate.py | 3 --- tests/test_noema_review_gate.py | 2 +- tests/test_opencode_agent_contract.py | 4 ++-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/scripts/ci/noema_review_gate.py b/scripts/ci/noema_review_gate.py index 621e4506f..cb039dba1 100644 --- a/scripts/ci/noema_review_gate.py +++ b/scripts/ci/noema_review_gate.py @@ -299,9 +299,6 @@ def call_llm(repo: str, number: int, pr: dict[str, Any], diff: str, truncated: b if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_multicast or ip.is_unspecified: raise ValueError("URL cannot target internal IP addresses") - if not (api_url.startswith("http://") or api_url.startswith("https://")): - raise ValueError(f"NOEMA_LLM_API_URL must start with http:// or https:// to prevent SSRF vulnerabilities, got: {api_url}") - prompt = { "role": "user", "content": "\n".join( diff --git a/tests/test_noema_review_gate.py b/tests/test_noema_review_gate.py index cc68ff289..8285fceb5 100644 --- a/tests/test_noema_review_gate.py +++ b/tests/test_noema_review_gate.py @@ -206,7 +206,7 @@ def test_call_llm_handles_configuration_and_verdicts(monkeypatch): monkeypatch.setenv("NOEMA_LLM_API_URL", "file:///etc/passwd") monkeypatch.setenv("NOEMA_LLM_API_KEY", "secret") - with pytest.raises(ValueError, match="must start with http:// or https://"): + with pytest.raises(ValueError, match="URL scheme must be http or https"): noema.call_llm("owner/repo", 1, pr, "diff", False) monkeypatch.setenv("NOEMA_LLM_API_URL", "https://llm.example.test/chat") diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 5fce57259..797f41def 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -283,7 +283,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert '"## Review outcome"' in workflow assert '"## Check outcome"' not in workflow assert "publish REQUEST_CHANGES when coverage-evidence blocker states" in workflow - assert re.search(r"opencode-review-target:[\s\S]{0,240}timeout-minutes: 360", workflow) + assert re.search(r"opencode-review-target:[\s\S]{0,360}timeout-minutes: 360", workflow) assert 'timeout-minutes: 75' in workflow assert re.search(r"Run OpenCode PR Review model pool[\s\S]{0,240}timeout-minutes: 350", workflow) assert 'APPROVAL_CHECK_WAIT_ATTEMPTS: "81"' in workflow @@ -418,7 +418,7 @@ def test_merge_scheduler_uses_escalating_mutation_credentials(): assert "steps.scheduler_app_token.outputs.token" in workflow assert "SCHEDULER_READ_TOKEN: ${{ github.token }}" in workflow assert "SCHEDULER_MUTATION_TOKEN_SOURCE" in workflow - assert 'default: "-1"' in workflow + assert 'default: "1"' in workflow assert 'review_dispatch_limit="-1"' in workflow From 4fe796270e0a9d0fa10074a0d3a62139e2723a87 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 10 Jul 2026 17:16:55 +0900 Subject: [PATCH 12/12] test: cover noema parsed scheme guard --- tests/test_assert_opencode_reasoning_effort.py | 7 ++++++- tests/test_noema_review_gate.py | 12 ++++++++++++ tests/test_render_opencode_prompt_template.py | 7 ++++++- tests/test_review_execution_contracts.py | 7 ++++++- tests/test_sandboxed_verify.py | 7 ++++++- tests/test_sandboxed_web_e2e.py | 7 ++++++- 6 files changed, 42 insertions(+), 5 deletions(-) diff --git a/tests/test_assert_opencode_reasoning_effort.py b/tests/test_assert_opencode_reasoning_effort.py index d84ca6cc8..c864beb6a 100644 --- a/tests/test_assert_opencode_reasoning_effort.py +++ b/tests/test_assert_opencode_reasoning_effort.py @@ -159,7 +159,12 @@ def test_module_entrypoint_success(monkeypatch, tmp_path): ], ) + module = sys.modules.pop("scripts.ci.assert_opencode_reasoning_effort", None) with pytest.raises(SystemExit) as exc_info: - runpy.run_module("scripts.ci.assert_opencode_reasoning_effort", run_name="__main__") + try: + runpy.run_module("scripts.ci.assert_opencode_reasoning_effort", run_name="__main__") + finally: + if module is not None: + sys.modules["scripts.ci.assert_opencode_reasoning_effort"] = module assert exc_info.value.code == 0 diff --git a/tests/test_noema_review_gate.py b/tests/test_noema_review_gate.py index a4fb4a05a..db78775e4 100644 --- a/tests/test_noema_review_gate.py +++ b/tests/test_noema_review_gate.py @@ -305,6 +305,18 @@ def raise_gaierror(host, port, *args, **kwargs): noema.call_llm("owner/repo", 1, pr, "diff", False) +def test_call_llm_rejects_non_http_parsed_scheme(monkeypatch): + """Keep the parsed-scheme SSRF guard covered as defense in depth.""" + pr = make_pr() + monkeypatch.setenv("NOEMA_LLM_API_KEY", "secret") + monkeypatch.setenv("NOEMA_LLM_API_URL", "https://llm.example.test/chat") + parsed = noema.urllib.parse.ParseResult("file", "llm.example.test", "/chat", "", "", "") + monkeypatch.setattr(noema.urllib.parse, "urlparse", lambda _: parsed) + + with pytest.raises(ValueError, match="URL scheme must be http or https"): + noema.call_llm("owner/repo", 1, pr, "diff", False) + + def test_format_findings_and_submit_review(monkeypatch): findings = noema.format_findings( [ diff --git a/tests/test_render_opencode_prompt_template.py b/tests/test_render_opencode_prompt_template.py index ecc2eb37a..7c543338c 100644 --- a/tests/test_render_opencode_prompt_template.py +++ b/tests/test_render_opencode_prompt_template.py @@ -62,8 +62,13 @@ def test_module_entrypoint(monkeypatch, tmp_path): ) monkeypatch.setenv("HEAD_SHA", "abc123") + module = sys.modules.pop("scripts.ci.render_opencode_prompt_template", None) with pytest.raises(SystemExit) as exc_info: - runpy.run_module("scripts.ci.render_opencode_prompt_template", run_name="__main__") + try: + runpy.run_module("scripts.ci.render_opencode_prompt_template", run_name="__main__") + finally: + if module is not None: + sys.modules["scripts.ci.render_opencode_prompt_template"] = module assert exc_info.value.code == 0 assert prompt_file.read_text(encoding="utf-8") == "abc123\n" diff --git a/tests/test_review_execution_contracts.py b/tests/test_review_execution_contracts.py index 9e38b12ac..da17c02a0 100644 --- a/tests/test_review_execution_contracts.py +++ b/tests/test_review_execution_contracts.py @@ -119,7 +119,12 @@ def test_discovers_package_managers_java_r_json_and_main(tmp_path, capsys, monke assert '"java"' in capsys.readouterr().out monkeypatch.setattr(sys, "argv", ["review_execution_contracts.py", "--repo-root", str(repo), "--format", "json"]) + module = sys.modules.pop("scripts.ci.review_execution_contracts", None) try: - runpy.run_module("scripts.ci.review_execution_contracts", run_name="__main__") + try: + runpy.run_module("scripts.ci.review_execution_contracts", run_name="__main__") + finally: + if module is not None: + sys.modules["scripts.ci.review_execution_contracts"] = module except SystemExit as exc: assert exc.code == 0 diff --git a/tests/test_sandboxed_verify.py b/tests/test_sandboxed_verify.py index 8635349e8..c711f3489 100644 --- a/tests/test_sandboxed_verify.py +++ b/tests/test_sandboxed_verify.py @@ -190,6 +190,11 @@ def test_module_main_entrypoint(monkeypatch, tmp_path): repo = tmp_path / "repo" repo.mkdir() monkeypatch.setattr(sys, "argv", ["sandboxed_verify.py", "--repo-root", str(repo), "--", sys.executable, "-c", "raise SystemExit(0)"]) + module = sys.modules.pop("scripts.ci.sandboxed_verify", None) with pytest.raises(SystemExit) as exc_info: - runpy.run_module("scripts.ci.sandboxed_verify", run_name="__main__") + try: + runpy.run_module("scripts.ci.sandboxed_verify", run_name="__main__") + finally: + if module is not None: + sys.modules["scripts.ci.sandboxed_verify"] = module assert exc_info.value.code == 0 diff --git a/tests/test_sandboxed_web_e2e.py b/tests/test_sandboxed_web_e2e.py index 501f25366..035afeb35 100644 --- a/tests/test_sandboxed_web_e2e.py +++ b/tests/test_sandboxed_web_e2e.py @@ -282,6 +282,11 @@ def test_module_import_and_main_entrypoint(monkeypatch, tmp_path): f"{sys.executable} -c \"raise SystemExit(0)\"", ], ) + module = sys.modules.pop("scripts.ci.sandboxed_web_e2e", None) with pytest.raises(SystemExit) as exc_info: - runpy.run_module("scripts.ci.sandboxed_web_e2e", run_name="__main__") + try: + runpy.run_module("scripts.ci.sandboxed_web_e2e", run_name="__main__") + finally: + if module is not None: + sys.modules["scripts.ci.sandboxed_web_e2e"] = module assert exc_info.value.code == 0