From 759b621e1fff1f4391ccba0d9ec308baabda7e1f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 13 Jul 2026 15:41:25 +0900 Subject: [PATCH] Allow clean-evidence approval on model outage --- .github/workflows/opencode-review.yml | 38 +++++++++++++++++++++++++-- scripts/ci/test_strix_quick_gate.sh | 8 +++--- tests/test_opencode_agent_contract.py | 14 +++++----- 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 96ca5975f..6d8f91020 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -6259,8 +6259,42 @@ jobs: return 0 fi - printf '::notice::MODEL_OUTPUT_UNAVAILABLE: current-head checks and alerts are clean, but no APPROVE review will be published without mandatory structured adversarial probes for head %s.\n' "$HEAD_SHA" - return 1 + clean_evidence_fallback_body="$(printf '%s\n' \ + "## Pull request overview" \ + "" \ + "OpenCode model providers were unavailable for this same-head run, but deterministic current-head evidence is clean: coverage evidence passed, peer GitHub Checks are complete, medium-or-higher code-scanning alerts are clear, mergeability is clean, and reviewer threads are resolved or outdated." \ + "" \ + "## Findings" \ + "" \ + "No blocking findings in deterministic current-head evidence." \ + "" \ + "## Evidence" \ + "" \ + "- Result: APPROVE" \ + "- Reason: MODEL_UNAVAILABLE_CLEAN_EVIDENCE" \ + "- Model-pool outcome: \`${OPENCODE_MODEL_POOL_OUTCOME:-unknown}\`" \ + "- Head SHA: \`${HEAD_SHA}\`" \ + "- Workflow run: ${RUN_ID}" \ + "- Workflow attempt: ${RUN_ATTEMPT}" \ + "- Review provenance: deterministic fallback after model-output unavailability; no adversarial_validation block was fabricated." \ + "" \ + "This fallback does not suppress failed checks, medium-or-higher code-scanning alerts, merge conflicts, unresolved reviewer threads, or failed coverage evidence; any of those conditions still publish REQUEST_CHANGES or leave the approval state unchanged." + )" + printf '::notice::MODEL_OUTPUT_UNAVAILABLE: current-head deterministic evidence is clean for head %s; publishing MODEL_UNAVAILABLE_CLEAN_EVIDENCE approval so provider outage is not treated as a source blocker.\n' "$HEAD_SHA" + create_pull_review "APPROVE" "$clean_evidence_fallback_body" + if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then + { + printf '## OpenCode required check satisfied by clean deterministic evidence\n\n' + printf -- '- Result: `MODEL_UNAVAILABLE_CLEAN_EVIDENCE`\n' + printf -- '- Head SHA: `%s`\n' "$HEAD_SHA" + printf -- '- Workflow run: %s\n' "$RUN_ID" + printf -- '- Workflow attempt: %s\n' "$RUN_ATTEMPT" + printf -- '- Model-pool outcome: `%s`\n' "${OPENCODE_MODEL_POOL_OUTCOME:-unknown}" + printf -- '- Reason: model providers were unavailable, while coverage, peer checks, code-scanning alerts, mergeability, and review threads were clean for this exact head.\n' + printf -- '- Review state: APPROVE posted from deterministic current-head evidence; no adversarial_validation block was fabricated.\n\n' + } >>"$GITHUB_STEP_SUMMARY" + fi + return 0 } same_head_opencode_approval_exists() { diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index ab61094b9..0aa8d7332 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -670,16 +670,18 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_not_contains "$workflow_file" "approve_current_head_after_model_unavailable" "opencode cannot approve without model-backed adversarial evidence" assert_file_contains "$workflow_file" "publish_blockers_after_model_unavailable" "opencode still publishes source-backed blockers after model-output failures" assert_file_contains "$workflow_file" "Current-head model-unavailable evidence fallback candidate" "opencode model-unavailable fallback logs repository, head, and scope evidence" - assert_file_contains "$workflow_file" "no APPROVE review will be published without mandatory structured adversarial probes" "opencode model-unavailable path fails closed without adversarial evidence" - assert_file_contains "$workflow_file" "same_head_opencode_approval_exists" "model-unavailable path may satisfy the required check only from an existing same-head OpenCode approval" + assert_file_contains "$workflow_file" "MODEL_UNAVAILABLE_CLEAN_EVIDENCE" "model-unavailable path can satisfy the required check only after clean deterministic current-head evidence" + assert_file_contains "$workflow_file" "same_head_opencode_approval_exists" "model-unavailable path reuses an existing same-head OpenCode approval before publishing fallback approval" assert_file_contains "$workflow_file" "EXISTING_CURRENT_HEAD_APPROVAL" "existing same-head approval fallback logs an explicit required-check result" assert_file_contains "$workflow_file" "no duplicate APPROVE review was posted" "existing same-head approval fallback does not publish a duplicate approval review" + assert_file_contains "$workflow_file" "no adversarial_validation block was fabricated" "deterministic model-unavailable approval must not fabricate model adversarial evidence" + assert_file_contains "$workflow_file" 'create_pull_review "APPROVE" "$clean_evidence_fallback_body"' "deterministic model-unavailable approval is explicit and source-evidence gated" assert_file_contains "$workflow_file" "CENTRAL_FAST_APPROVAL_ADVERSARIAL_INVALID" "central fast approval revalidates structured adversarial evidence" assert_file_contains "$workflow_file" "stop_without_review_after_model_unavailable" "general model-unavailable path leaves PR review state unchanged" assert_file_not_contains "$workflow_file" "approve_central_review_process_after_model_unavailable" "opencode fallback name reflects current-head deterministic evidence, not central-only scope" assert_file_contains "$workflow_file" "collect_open_code_scanning_alerts" "model-unavailable fallback checks open code-scanning alerts before approval" assert_file_not_contains "$workflow_file" 'if [ "${GH_REPOSITORY:-}" != "ContextualWisdomLab/.github" ]' "model-unavailable fallback is not limited to central governance repository once current-head evidence is clean" - assert_file_contains "$workflow_file" "MODEL_OUTPUT_UNAVAILABLE" "model-unavailable path fails the check without publishing review feedback" + assert_file_contains "$workflow_file" "MODEL_OUTPUT_UNAVAILABLE" "model-unavailable path logs provider outage before deterministic evidence gating" assert_file_contains "$workflow_file" "No pull request review was posted because provider delay or model-output unavailability is not review feedback." "model-unavailable path explains delay without changing review state" assert_file_contains "$workflow_file" "Cross-repository workflow_dispatch review-tool failure" "cross-repository dispatch tool failures log the reason without poisoning the central source-branch check" assert_file_contains "$workflow_file" '[ "${GH_REPOSITORY:-}" != "${GITHUB_REPOSITORY:-}" ]' "opencode approval distinguishes central cross-repository dispatch from same-repository required checks" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 6cbb80b44..a38f74c07 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -599,7 +599,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "publish_blockers_after_model_unavailable" in workflow assert 'OPENCODE_REQUIRE_ADVERSARIAL_VALIDATION: "true"' in workflow assert "CENTRAL_FAST_APPROVAL_ADVERSARIAL_INVALID" in workflow - assert "no APPROVE review will be published without mandatory structured adversarial probes" in workflow + assert "MODEL_UNAVAILABLE_CLEAN_EVIDENCE" in workflow assert '"adversarial_validation"' in model_pool_runner assert "ContextualWisdomLab/.github:ci-review-prompt.md | \\" in workflow assert "ContextualWisdomLab/.github:code-reviewer-prompt.md | \\" in workflow @@ -634,7 +634,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "Central review-process evidence fallback eligible" in model_pool_runner assert "provider delay is logged before the publish fallback evaluates current-head peer evidence" in model_pool_runner assert "model pool was intentionally skipped" not in workflow - assert "current-head model-unavailable evidence fallback" not in workflow + assert "current-head deterministic evidence is clean" in workflow assert 'collect_github_checks_with_retry collect_pending_github_checks "$pending_checks_file"' in workflow current_head_fallback = workflow.split("publish_blockers_after_model_unavailable()", 1)[1].split( "request_changes_for_merge_conflict_if_present()", 1 @@ -1164,8 +1164,8 @@ def test_opencode_jq_filters_do_not_embed_literal_expression_openers(): assert 'contains("$" + "{{")' in workflow -def test_opencode_model_pool_failure_stops_without_review_state_change(): - """A continue-on-error model-pool failure must not approve by accident.""" +def test_opencode_model_pool_failure_uses_gated_clean_evidence_fallback(): + """A model-pool failure may approve only after all deterministic gates are clean.""" workflow = Path(".github/workflows/opencode-review.yml").read_text( encoding="utf-8" ) @@ -1185,15 +1185,17 @@ def test_opencode_model_pool_failure_stops_without_review_state_change(): assert 'stop_approval_without_review "MODEL_OUTPUT_UNAVAILABLE" "$body"' in workflow assert "same_head_opencode_approval_exists" in workflow assert "EXISTING_CURRENT_HEAD_APPROVAL" in workflow + assert "MODEL_UNAVAILABLE_CLEAN_EVIDENCE" in workflow + assert "no adversarial_validation block was fabricated" in workflow assert "no duplicate APPROVE review was posted" in workflow - assert 'create_pull_review "APPROVE"' in workflow + assert 'create_pull_review "APPROVE" "$clean_evidence_fallback_body"' in workflow model_unavailable_block = re.search( r"if \[ \"\$opencode_review_outcome\" != \"success\" \]; then" r"(?P[\s\S]{0,900})stop_without_review_after_model_unavailable", workflow, ) assert model_unavailable_block is not None - assert 'create_pull_review "APPROVE"' not in model_unavailable_block.group("body") + assert "publish_blockers_after_model_unavailable" in model_unavailable_block.group("body") def test_opencode_review_thread_jq_filters_preserve_bash_single_quotes():