From 27c5b55d5d1b28367430412b2c889d6cb87f6396 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 10 Jul 2026 16:24:26 +0900 Subject: [PATCH 1/3] fix: tolerate manifestless OSV scans --- .github/workflows/opencode-review.yml | 16 +++++ .github/workflows/security-scan.yml | 2 +- scripts/ci/collect_failed_check_evidence.sh | 63 +++++++++++++------ .../test_required_workflow_queue_contract.py | 38 +++++++++++ 4 files changed, 98 insertions(+), 21 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 0c13c56d1..a6e6b8d98 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -4334,6 +4334,7 @@ jobs: if ! gh api -X GET "repos/${GH_REPOSITORY}/actions/workflows/strix.yml" \ --jq '.id' >/dev/null 2>"$workflow_lookup_err"; then if grep -Fq "HTTP 404" "$workflow_lookup_err"; then + printf 'Strix workflow is not installed on %s; skipping optional current-head Strix workflow-run lookup.\n' "$GH_REPOSITORY" >&2 : >"$output_file" rm -f "$runs_json" "$workflow_lookup_err" return 0 @@ -4540,7 +4541,22 @@ jobs: latest_current_head_manual_strix_run() { local runs_json + local workflow_lookup_err runs_json="$(mktemp)" + workflow_lookup_err="$(mktemp)" + + if ! gh api -X GET "repos/${GH_REPOSITORY}/actions/workflows/strix.yml" \ + --jq '.id' >/dev/null 2>"$workflow_lookup_err"; then + if grep -Fq "HTTP 404" "$workflow_lookup_err"; then + printf 'Strix workflow is not installed on %s; skipping optional manual Strix run lookup.\n' "$GH_REPOSITORY" >&2 + rm -f "$runs_json" "$workflow_lookup_err" + return 0 + fi + cat "$workflow_lookup_err" >&2 + rm -f "$runs_json" "$workflow_lookup_err" + return 1 + fi + rm -f "$workflow_lookup_err" if ! gh run list \ --repo "$GH_REPOSITORY" \ diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 5605124b5..5bdbd0bf7 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -142,7 +142,7 @@ jobs: def iter_findings(path): data = json.loads(Path(path).read_text(encoding="utf-8")) - for result in data.get("results", []): + for result in data.get("results") or []: source = result.get("source", {}) source_name = source.get("path") or source.get("name") or "unknown" for package in result.get("packages", []): diff --git a/scripts/ci/collect_failed_check_evidence.sh b/scripts/ci/collect_failed_check_evidence.sh index c0926d4ee..6a69d240c 100755 --- a/scripts/ci/collect_failed_check_evidence.sh +++ b/scripts/ci/collect_failed_check_evidence.sh @@ -396,6 +396,27 @@ cleanup() { } trap cleanup EXIT +target_workflow_available() { + local workflow_file="$1" + local workflow_lookup_err + + workflow_lookup_err="$(mktemp)" + tmp_files+=("$workflow_lookup_err") + + if gh api -X GET "repos/${GH_REPOSITORY}/actions/workflows/${workflow_file}" \ + --jq '.id' >/dev/null 2>"$workflow_lookup_err"; then + return 0 + fi + + if grep -Fq "HTTP 404" "$workflow_lookup_err"; then + printf 'Optional workflow %s is not installed on %s; skipping current-head workflow-run lookup.\n' "$workflow_file" "$GH_REPOSITORY" >&2 + return 1 + fi + + cat "$workflow_lookup_err" >&2 + return 1 +} + manual_success_for_label() { local label="$1" local failed_run_id="${2:-}" @@ -580,26 +601,28 @@ gh api graphql \ | @tsv ' >"$manual_success_check_runs" -env HEAD_SHA="$HEAD_SHA" gh run list \ - --repo "$GH_REPOSITORY" \ - --workflow strix.yml \ - --commit "$HEAD_SHA" \ - --limit 200 \ - --json databaseId,workflowName,status,conclusion,url,event,headSha \ - --jq ' - .[] - | select((.event // "") == "workflow_dispatch") - | select((.headSha // "") == env.HEAD_SHA) - | select((.workflowName // "") == "Strix Security Scan" or (.workflowName // "") == "Strix") - | select((.status // "") == "completed") - | select((.conclusion // "" | ascii_downcase) == "success") - | [ - "strix", - (.url // ""), - "Manual workflow_dispatch Strix evidence passed" - ] - | @tsv - ' >>"$manual_success_check_runs" || true +if target_workflow_available "strix.yml"; then + env HEAD_SHA="$HEAD_SHA" gh run list \ + --repo "$GH_REPOSITORY" \ + --workflow strix.yml \ + --commit "$HEAD_SHA" \ + --limit 200 \ + --json databaseId,workflowName,status,conclusion,url,event,headSha \ + --jq ' + .[] + | select((.event // "") == "workflow_dispatch") + | select((.headSha // "") == env.HEAD_SHA) + | select((.workflowName // "") == "Strix Security Scan" or (.workflowName // "") == "Strix") + | select((.status // "") == "completed") + | select((.conclusion // "" | ascii_downcase) == "success") + | [ + "strix", + (.url // ""), + "Manual workflow_dispatch Strix evidence passed" + ] + | @tsv + ' >>"$manual_success_check_runs" || true +fi env HEAD_SHA="$HEAD_SHA" gh run list \ --repo "$GH_REPOSITORY" \ diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index b39052c5a..71ad28ca7 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -1,6 +1,7 @@ import json import subprocess import sys +import textwrap from pathlib import Path @@ -128,6 +129,43 @@ def test_osv_scan_logs_and_retries_without_transitive_resolution_on_resolver_fai assert "OSV {label} scan produced {len(findings)} finding(s)" in workflow +def test_osv_findings_log_accepts_null_results_for_manifestless_repos(tmp_path: Path) -> None: + workflow = workflow_text("security-scan.yml") + step = " - name: Print OSV findings being compared\n" + start = workflow.index(step) + run_start = workflow.index(" run: |\n", start) + len(" run: |\n") + run_end = workflow.index("\n - name:", run_start) + script = textwrap.dedent( + "\n".join(line[10:] for line in workflow[run_start:run_end].splitlines()) + ) + + for filename in ("old-results.json", "new-results.json"): + (tmp_path / filename).write_text('{"results": null}\n', encoding="utf-8") + + result = subprocess.run( + [sys.executable, "-c", script], + cwd=tmp_path, + check=True, + capture_output=True, + text=True, + ) + + assert "OSV base scan produced 0 finding(s) in old-results.json." in result.stdout + assert "OSV head scan produced 0 finding(s) in new-results.json." in result.stdout + + +def test_optional_strix_workflow_absence_is_logged_without_failing_lookup() -> None: + workflow = workflow_text("opencode-review.yml") + failed_check_evidence = (REPO_ROOT / "scripts/ci/collect_failed_check_evidence.sh").read_text( + encoding="utf-8" + ) + + assert "skipping optional current-head Strix workflow-run lookup" in workflow + assert "skipping optional manual Strix run lookup" in workflow + assert "Optional workflow %s is not installed" in failed_check_evidence + assert 'if target_workflow_available "strix.yml"; then' in failed_check_evidence + + def test_pr_scorecard_sarif_delegates_sast_and_vulnerability_posture_to_hard_gates() -> None: """PR Scorecard SARIF should not duplicate CodeQL/OSV/Trivy hard gates.""" for filename in ("scorecard-pr.yml", "security-scan.yml"): From 9e4524b719c0675a04a864a17bf26c34ca673b30 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 10 Jul 2026 16:35:48 +0900 Subject: [PATCH 2/3] fix: keep deep opencode review budget --- .github/workflows/opencode-review.yml | 10 +++++----- scripts/ci/run_opencode_review_model_pool.sh | 4 ++-- scripts/ci/test_strix_quick_gate.sh | 10 +++++----- tests/test_opencode_agent_contract.py | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index a6e6b8d98..f7fe1454d 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -2592,7 +2592,7 @@ jobs: - name: Run OpenCode PR Review model pool id: opencode_review_model_pool if: needs.coverage-evidence.result == 'success' - timeout-minutes: 45 + timeout-minutes: 350 continue-on-error: true env: STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }} @@ -2621,11 +2621,11 @@ jobs: # the SAME model 5x let a rate-limited/hung leader consume the whole # step, so the pool never reached a healthy fallback model. OPENCODE_MODEL_ATTEMPTS: "1" - # Bound provider stalls so the org queue gets an actionable log line - # and reaches the next candidate instead of pinning the PR for hours. - OPENCODE_RUN_TIMEOUT_SECONDS: "900" + # 90 min per model is long enough for deep tool-using review while + # still yielding to the next candidate before the job budget is gone. + OPENCODE_RUN_TIMEOUT_SECONDS: "5400" OPENCODE_EXPORT_TIMEOUT_SECONDS: "120" - OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "2400" + OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "18000" # Stop after one catalog pass; the long per-model timeout and total # budget provide room for deep reviews without looping to job timeout. OPENCODE_POOL_MAX_CYCLES: "1" diff --git a/scripts/ci/run_opencode_review_model_pool.sh b/scripts/ci/run_opencode_review_model_pool.sh index c537e8ca3..065885659 100644 --- a/scripts/ci/run_opencode_review_model_pool.sh +++ b/scripts/ci/run_opencode_review_model_pool.sh @@ -193,8 +193,8 @@ main() { local -a model_candidates attempts="${OPENCODE_MODEL_ATTEMPTS:-3}" - original_run_timeout="${OPENCODE_RUN_TIMEOUT_SECONDS:-900}" - budget_seconds="${OPENCODE_TOTAL_RETRY_BUDGET_SECONDS:-2400}" + original_run_timeout="${OPENCODE_RUN_TIMEOUT_SECONDS:-5400}" + budget_seconds="${OPENCODE_TOTAL_RETRY_BUDGET_SECONDS:-18000}" max_cycles="${OPENCODE_POOL_MAX_CYCLES:-0}" deadline=0 if [ "$budget_seconds" -gt 0 ]; then diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 9a659e6f4..6dcfa8c33 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -533,10 +533,10 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { 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" 'timeout-minutes: 360' "opencode review target uses the maximum GitHub-hosted runner timeout" - assert_file_contains "$workflow_file" 'timeout-minutes: 45' "opencode model pool has a queue-friendly runner budget while the script deadline leaves approval headroom" + assert_file_contains "$workflow_file" 'timeout-minutes: 350' "opencode model pool has a deep-review runner budget while the script deadline leaves approval headroom" assert_file_contains "$workflow_file" 'continue-on-error: true' "opencode approval gate still runs after model-pool failure to publish a reason" - assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "900"' "opencode primary review has a bounded per-model timeout before trying fallback models" - assert_file_contains "$workflow_file" 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "2400"' "opencode model pool exits before the job timeout so the approval gate can publish a reason" + assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "5400"' "opencode primary review has a deep per-model timeout before trying fallback models" + assert_file_contains "$workflow_file" 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "18000"' "opencode model pool exits before the job timeout so the approval gate can publish a reason" assert_file_contains "$workflow_file" 'OPENCODE_POOL_MAX_CYCLES: "1"' "opencode model pool stops after one full candidate pass instead of looping to the job timeout" 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" @@ -644,11 +644,11 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "OpenCode model pool has no configured model candidates." "opencode model pool fails fast when no candidates are configured" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "OPENAI_API_KEY is not configured" "opencode model pool skips native OpenAI candidates when the org secret is absent" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "configured max cycle count" "opencode model pool exits before the job timeout after configured cycles" - assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS:-2400' "opencode model pool keeps a safe default retry budget unless the workflow explicitly disables it" + assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS:-18000' "opencode model pool keeps a safe default retry budget unless the workflow explicitly disables it" assert_file_not_contains "$workflow_file" "no model produced a valid review control block" "opencode model-failure path no longer documents a final exhausted state" 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: "900"' "opencode catalog fallback has a bounded per-model review timeout before step timeout" + assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "5400"' "opencode catalog fallback has a deep per-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/deepseek/deepseek-v3-0324 openai/gpt-5-mini openai/gpt-5 github-models/openai/o4-mini" "opencode review tries the observed high-success DeepSeek V3 path before native OpenAI and compact OpenAI reasoning fallbacks" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 82a84b784..e0e4141af 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -364,7 +364,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "publish REQUEST_CHANGES when coverage-evidence blocker states" in workflow assert re.search(r"opencode-review-target:[\s\S]*?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: 45", workflow) + assert re.search(r"Run OpenCode PR Review model pool[\s\S]{0,240}timeout-minutes: 350", workflow) assert re.search(r"Run OpenCode PR Review model pool[\s\S]{0,280}continue-on-error: true", workflow) assert 'APPROVAL_CHECK_WAIT_ATTEMPTS: "81"' in workflow assert 'APPROVAL_CHECK_WAIT_SLEEP_SECONDS: "30"' in workflow @@ -386,9 +386,9 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): 'github-models/openai/gpt-5"' ) in workflow assert 'OPENCODE_MODEL_ATTEMPTS: "1"' in workflow - assert 'OPENCODE_RUN_TIMEOUT_SECONDS: "900"' in workflow + assert 'OPENCODE_RUN_TIMEOUT_SECONDS: "5400"' in workflow assert 'OPENCODE_EXPORT_TIMEOUT_SECONDS: "120"' in workflow - assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "2400"' in workflow + assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "18000"' in workflow assert 'OPENCODE_POOL_MAX_CYCLES: "1"' in workflow assert 'OPENCODE_BACKOFF_MAX_SECONDS: "30"' in workflow assert "steps.opencode_review_model_pool.outcome == 'success'" not in workflow @@ -398,7 +398,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "OPENAI_API_KEY is not configured" in model_pool_runner assert "configured max cycle count" in model_pool_runner assert "OpenCode model pool has no configured model candidates." in model_pool_runner - assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS:-2400' in model_pool_runner + assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS:-18000' in model_pool_runner assert "completed a full model-candidate cycle without a valid control conclusion" in model_pool_runner assert "retry budget/GitHub Actions job timeout" in model_pool_runner assert 'record_review_status "exhausted"' not in model_pool_runner From 4dfece2235fa4eb2773978cfca13486f7a9e325b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 10 Jul 2026 16:40:23 +0900 Subject: [PATCH 3/3] fix: bound opencode model pool stalls --- .github/workflows/opencode-review.yml | 10 +++++----- scripts/ci/run_opencode_review_model_pool.sh | 4 ++-- scripts/ci/test_strix_quick_gate.sh | 10 +++++----- tests/test_opencode_agent_contract.py | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index f7fe1454d..a6e6b8d98 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -2592,7 +2592,7 @@ jobs: - name: Run OpenCode PR Review model pool id: opencode_review_model_pool if: needs.coverage-evidence.result == 'success' - timeout-minutes: 350 + timeout-minutes: 45 continue-on-error: true env: STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }} @@ -2621,11 +2621,11 @@ jobs: # the SAME model 5x let a rate-limited/hung leader consume the whole # step, so the pool never reached a healthy fallback model. OPENCODE_MODEL_ATTEMPTS: "1" - # 90 min per model is long enough for deep tool-using review while - # still yielding to the next candidate before the job budget is gone. - OPENCODE_RUN_TIMEOUT_SECONDS: "5400" + # Bound provider stalls so the org queue gets an actionable log line + # and reaches the next candidate instead of pinning the PR for hours. + OPENCODE_RUN_TIMEOUT_SECONDS: "900" OPENCODE_EXPORT_TIMEOUT_SECONDS: "120" - OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "18000" + OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "2400" # Stop after one catalog pass; the long per-model timeout and total # budget provide room for deep reviews without looping to job timeout. OPENCODE_POOL_MAX_CYCLES: "1" diff --git a/scripts/ci/run_opencode_review_model_pool.sh b/scripts/ci/run_opencode_review_model_pool.sh index 065885659..c537e8ca3 100644 --- a/scripts/ci/run_opencode_review_model_pool.sh +++ b/scripts/ci/run_opencode_review_model_pool.sh @@ -193,8 +193,8 @@ main() { local -a model_candidates attempts="${OPENCODE_MODEL_ATTEMPTS:-3}" - original_run_timeout="${OPENCODE_RUN_TIMEOUT_SECONDS:-5400}" - budget_seconds="${OPENCODE_TOTAL_RETRY_BUDGET_SECONDS:-18000}" + original_run_timeout="${OPENCODE_RUN_TIMEOUT_SECONDS:-900}" + budget_seconds="${OPENCODE_TOTAL_RETRY_BUDGET_SECONDS:-2400}" max_cycles="${OPENCODE_POOL_MAX_CYCLES:-0}" deadline=0 if [ "$budget_seconds" -gt 0 ]; then diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 6dcfa8c33..9a659e6f4 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -533,10 +533,10 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { 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" 'timeout-minutes: 360' "opencode review target uses the maximum GitHub-hosted runner timeout" - assert_file_contains "$workflow_file" 'timeout-minutes: 350' "opencode model pool has a deep-review runner budget while the script deadline leaves approval headroom" + assert_file_contains "$workflow_file" 'timeout-minutes: 45' "opencode model pool has a queue-friendly runner budget while the script deadline leaves approval headroom" assert_file_contains "$workflow_file" 'continue-on-error: true' "opencode approval gate still runs after model-pool failure to publish a reason" - assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "5400"' "opencode primary review has a deep per-model timeout before trying fallback models" - assert_file_contains "$workflow_file" 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "18000"' "opencode model pool exits before the job timeout so the approval gate can publish a reason" + assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "900"' "opencode primary review has a bounded per-model timeout before trying fallback models" + assert_file_contains "$workflow_file" 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "2400"' "opencode model pool exits before the job timeout so the approval gate can publish a reason" assert_file_contains "$workflow_file" 'OPENCODE_POOL_MAX_CYCLES: "1"' "opencode model pool stops after one full candidate pass instead of looping to the job timeout" 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" @@ -644,11 +644,11 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "OpenCode model pool has no configured model candidates." "opencode model pool fails fast when no candidates are configured" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "OPENAI_API_KEY is not configured" "opencode model pool skips native OpenAI candidates when the org secret is absent" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "configured max cycle count" "opencode model pool exits before the job timeout after configured cycles" - assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS:-18000' "opencode model pool keeps a safe default retry budget unless the workflow explicitly disables it" + assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS:-2400' "opencode model pool keeps a safe default retry budget unless the workflow explicitly disables it" assert_file_not_contains "$workflow_file" "no model produced a valid review control block" "opencode model-failure path no longer documents a final exhausted state" 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: "5400"' "opencode catalog fallback has a deep per-model review timeout before step timeout" + assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "900"' "opencode catalog fallback has a bounded per-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/deepseek/deepseek-v3-0324 openai/gpt-5-mini openai/gpt-5 github-models/openai/o4-mini" "opencode review tries the observed high-success DeepSeek V3 path before native OpenAI and compact OpenAI reasoning fallbacks" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index e0e4141af..82a84b784 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -364,7 +364,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "publish REQUEST_CHANGES when coverage-evidence blocker states" in workflow assert re.search(r"opencode-review-target:[\s\S]*?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 re.search(r"Run OpenCode PR Review model pool[\s\S]{0,240}timeout-minutes: 45", workflow) assert re.search(r"Run OpenCode PR Review model pool[\s\S]{0,280}continue-on-error: true", workflow) assert 'APPROVAL_CHECK_WAIT_ATTEMPTS: "81"' in workflow assert 'APPROVAL_CHECK_WAIT_SLEEP_SECONDS: "30"' in workflow @@ -386,9 +386,9 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): 'github-models/openai/gpt-5"' ) in workflow assert 'OPENCODE_MODEL_ATTEMPTS: "1"' in workflow - assert 'OPENCODE_RUN_TIMEOUT_SECONDS: "5400"' in workflow + assert 'OPENCODE_RUN_TIMEOUT_SECONDS: "900"' in workflow assert 'OPENCODE_EXPORT_TIMEOUT_SECONDS: "120"' in workflow - assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "18000"' in workflow + assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "2400"' in workflow assert 'OPENCODE_POOL_MAX_CYCLES: "1"' in workflow assert 'OPENCODE_BACKOFF_MAX_SECONDS: "30"' in workflow assert "steps.opencode_review_model_pool.outcome == 'success'" not in workflow @@ -398,7 +398,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "OPENAI_API_KEY is not configured" in model_pool_runner assert "configured max cycle count" in model_pool_runner assert "OpenCode model pool has no configured model candidates." in model_pool_runner - assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS:-18000' in model_pool_runner + assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS:-2400' in model_pool_runner assert "completed a full model-candidate cycle without a valid control conclusion" in model_pool_runner assert "retry budget/GitHub Actions job timeout" in model_pool_runner assert 'record_review_status "exhausted"' not in model_pool_runner