diff --git a/.github/workflows/agent-review-runtime-quality-ci.yml b/.github/workflows/agent-review-runtime-quality-ci.yml index 3976424bfb..23e3b80789 100644 --- a/.github/workflows/agent-review-runtime-quality-ci.yml +++ b/.github/workflows/agent-review-runtime-quality-ci.yml @@ -60,6 +60,7 @@ on: - "tests/test_github_hourly_conflict_repair.py" - "tests/test_hourly_scheduler_runtime_budget.py" - "tests/test_hourly_autofix_context_quality_gate.py" + - "tests/test_review_repair_full_suite_dependencies.py" - "tests/test_pr_review_conflict_scope.py" - "tests/test_pr_review_conflict_scope_control_files.py" - "tests/test_pr_review_conflict_scope_git_executable.py" @@ -255,6 +256,7 @@ jobs: tests/test_github_hourly_conflict_repair.py|\ tests/test_hourly_scheduler_runtime_budget.py|\ tests/test_hourly_autofix_context_quality_gate.py|\ + tests/test_review_repair_full_suite_dependencies.py|\ tests/test_pr_review_conflict_scope.py|\ tests/test_pr_review_conflict_scope_control_files.py|\ tests/test_pr_review_conflict_scope_git_executable.py|\ @@ -346,7 +348,7 @@ jobs: -r requirements-opencode-review-ci-hashes.txt - name: Install exact Noema document dependencies - if: steps.affected_suites.outputs.noema == 'true' + if: steps.affected_suites.outputs.noema == 'true' || steps.affected_suites.outputs.review_repair == 'true' run: >- python -m pip install --disable-pip-version-check --require-hashes --no-deps -r requirements-noema-document-ci-hashes.txt @@ -446,6 +448,7 @@ jobs: tests/test_hourly_scheduler_runtime_budget.py \ tests/test_pr_review_conflict_scope_control_files.py \ tests/test_hourly_autofix_context_quality_gate.py \ + tests/test_review_repair_full_suite_dependencies.py \ tests/test_pr_review_conflict_scope_git_executable.py \ tests/test_pr_review_conflict_scope_ignored_paths.py \ tests/test_pr_review_conflict_scope_symlink_targets.py \ diff --git a/scripts/ci/pr_review_fix_scheduler.py b/scripts/ci/pr_review_fix_scheduler.py index bc2868c5a4..c1e71c4ae3 100755 --- a/scripts/ci/pr_review_fix_scheduler.py +++ b/scripts/ci/pr_review_fix_scheduler.py @@ -99,6 +99,9 @@ REST_UNKNOWN_GITHUB_ACTIONS_WORKFLOW, } ) +RCA_SOURCE_BACKED_PRE_REVIEW_CHECKS = frozenset( + {("Required OpenCode Review", "coverage-evidence")} +) FAILED_CHECK_CONCLUSIONS = frozenset( {"FAILURE", "STARTUP_FAILURE", "TIMED_OUT"} ) @@ -285,7 +288,10 @@ def current_head_failed_checks(pr: dict[str, Any]) -> tuple[str, ...]: conclusion = str(node.get("conclusion") or "").upper() if ( name not in RCA_IGNORED_CHECK_NAMES - and workflow_name not in RCA_IGNORED_WORKFLOW_NAMES + and ( + workflow_name not in RCA_IGNORED_WORKFLOW_NAMES + or (workflow_name, name) in RCA_SOURCE_BACKED_PRE_REVIEW_CHECKS + ) and conclusion in FAILED_CHECK_CONCLUSIONS ): failed.append(name or "unnamed check") diff --git a/tests/test_pr_review_fix_scheduler_required_opencode_coverage_rca.py b/tests/test_pr_review_fix_scheduler_required_opencode_coverage_rca.py new file mode 100644 index 0000000000..df931f72b5 --- /dev/null +++ b/tests/test_pr_review_fix_scheduler_required_opencode_coverage_rca.py @@ -0,0 +1,117 @@ +"""Regression contracts for Required OpenCode pre-review coverage RCA routing.""" + +from scripts.ci import pr_review_fix_scheduler as fix + + +def _required_opencode_check( + *, + name: str, + conclusion: str | None, + status: str = "COMPLETED", + created_at: str = "2026-09-13T00:00:00Z", + workflow_name: str = "Required OpenCode Review", +) -> dict: + """Build one OpenCode-family check-run fixture with explicit workflow provenance.""" + return { + "__typename": "CheckRun", + "name": name, + "status": status, + "conclusion": conclusion, + "checkSuite": { + "createdAt": created_at, + "workflowRun": {"workflow": {"name": workflow_name}}, + }, + } + + +def _pr_with_checks(*checks: dict) -> dict: + """Build the minimal clean same-head PR context required by RCA classification.""" + return { + "number": 2079, + "isDraft": False, + "baseRefName": "main", + "baseRefOid": "b" * 40, + "headRefName": "feature", + "headRefOid": "a" * 40, + "headRepository": {"nameWithOwner": "owner/repo"}, + "mergeStateStatus": "CLEAN", + "reviews": {"nodes": []}, + "reviewThreads": {"nodes": []}, + "statusCheckRollup": {"contexts": {"nodes": list(checks)}}, + } + + +def test_required_opencode_coverage_failure_routes_to_rca_without_review() -> None: + """A failed source-backed coverage gate must reach RCA before model review exists.""" + pr = _pr_with_checks( + _required_opencode_check(name="coverage-evidence", conclusion="FAILURE") + ) + + assert fix.current_head_failed_checks(pr) == ("coverage-evidence",) + assert fix.needs_rca_repair(pr) == ( + True, + ("current-head failed check(s) require RCA: coverage-evidence",), + ) + + +def test_other_opencode_workflow_coverage_name_stays_excluded() -> None: + """Coverage RCA admission must bind the exact authoritative workflow and check name.""" + pr = _pr_with_checks( + _required_opencode_check( + name="coverage-evidence", + conclusion="FAILURE", + workflow_name="OpenCode Review", + ) + ) + + assert fix.current_head_failed_checks(pr) == () + assert fix.needs_rca_repair(pr) == (False, ()) + + +def test_required_opencode_orchestrator_failure_stays_nonrecursive() -> None: + """The OpenCode orchestration status must not recursively dispatch its own repair.""" + pr = _pr_with_checks( + _required_opencode_check(name="opencode-review", conclusion="FAILURE") + ) + + assert fix.current_head_failed_checks(pr) == () + assert fix.needs_rca_repair(pr) == (False, ()) + + +def test_pending_required_opencode_coverage_attempt_supersedes_stale_failure() -> None: + """A pending coverage rerun must retire stale failure evidence until it is terminal.""" + pr = _pr_with_checks( + _required_opencode_check( + name="coverage-evidence", + conclusion="FAILURE", + created_at="2026-09-13T00:00:00Z", + ), + _required_opencode_check( + name="coverage-evidence", + conclusion=None, + status="IN_PROGRESS", + created_at="2026-09-13T00:05:00Z", + ), + ) + + assert fix.current_head_failed_checks(pr) == () + assert fix.needs_rca_repair(pr) == (False, ()) + + +def test_newer_required_opencode_coverage_success_supersedes_stale_failure() -> None: + """A newer successful coverage attempt must retire the older failure evidence.""" + pr = _pr_with_checks( + _required_opencode_check( + name="coverage-evidence", + conclusion="FAILURE", + created_at="2026-09-13T00:00:00Z", + ), + _required_opencode_check( + name="coverage-evidence", + conclusion="SUCCESS", + created_at="2026-09-13T00:05:00Z", + ), + ) + + assert fix.current_head_failed_checks(pr) == () + assert fix.needs_rca_repair(pr) == (False, ()) diff --git a/tests/test_review_repair_full_suite_dependencies.py b/tests/test_review_repair_full_suite_dependencies.py new file mode 100644 index 0000000000..cf2f5146cc --- /dev/null +++ b/tests/test_review_repair_full_suite_dependencies.py @@ -0,0 +1,19 @@ +"""Regression contracts for review-repair full-suite dependency closure.""" + +from pathlib import Path + + +WORKFLOW = Path(".github/workflows/agent-review-runtime-quality-ci.yml") + + +def test_review_repair_full_suite_installs_noema_document_dependency() -> None: + """Full-suite review-repair collection must provision Noema's XML reader.""" + workflow = WORKFLOW.read_text(encoding="utf-8") + install_step = workflow.split( + "- name: Install exact Noema document dependencies", + maxsplit=1, + )[1].split("- name: Verify Noema token-lifetime contracts", maxsplit=1)[0] + + assert "steps.affected_suites.outputs.noema == 'true'" in install_step + assert "steps.affected_suites.outputs.review_repair == 'true'" in install_step + assert "requirements-noema-document-ci-hashes.txt" in install_step