From 714746772809bca1ba11d959adedc787a3fb26c6 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Mon, 29 Jun 2026 04:51:36 +0000 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=A7=AA=20scripts/ci/opencode=5Freview?= =?UTF-8?q?=5Fnormalize=5Foutput.py=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .coverage | Bin 0 -> 53248 bytes ...de_review_normalize_output.cpython-312.pyc | Bin 0 -> 10834 bytes ...malize_output.cpython-312-pytest-9.1.1.pyc | Bin 0 -> 62449 bytes .../test_opencode_review_normalize_output.py | 467 ++++++++++++++++++ 4 files changed, 467 insertions(+) create mode 100644 .coverage create mode 100644 scripts/ci/__pycache__/opencode_review_normalize_output.cpython-312.pyc create mode 100644 tests/scripts/ci/__pycache__/test_opencode_review_normalize_output.cpython-312-pytest-9.1.1.pyc create mode 100644 tests/scripts/ci/test_opencode_review_normalize_output.py diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..f32d5b75f1fcd73e7a74ff48406ea51a690ee544 GIT binary patch literal 53248 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 F{{hkbx<&v1 literal 0 HcmV?d00001 diff --git a/tests/scripts/ci/__pycache__/test_opencode_review_normalize_output.cpython-312-pytest-9.1.1.pyc b/tests/scripts/ci/__pycache__/test_opencode_review_normalize_output.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8b709b044d0d6791dfaead8e737aaf29dbcd799f GIT binary patch literal 62449 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 UZrgs_$3ejs{8)6_*09h1|EoSOLjV8( literal 0 HcmV?d00001 diff --git a/tests/scripts/ci/test_opencode_review_normalize_output.py b/tests/scripts/ci/test_opencode_review_normalize_output.py new file mode 100644 index 00000000..ea3bbfc9 --- /dev/null +++ b/tests/scripts/ci/test_opencode_review_normalize_output.py @@ -0,0 +1,467 @@ +import json +import pytest +from pathlib import Path + +# Adjust sys.path to import the script correctly since it's not a standard package structure +import sys +import os +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../scripts/ci'))) +import opencode_review_normalize_output +from opencode_review_normalize_output import ( + valid_control, + admits_missing_structural_review, + mentions_changed_file_evidence, + check_structural_approval, + iter_json_objects, + main, +) + +def get_base_valid_value(): + return { + "head_sha": "abc123sha", + "run_id": "123456", + "run_attempt": "1", + "result": "APPROVE", + "reason": "Good to go", + "summary": "Looks fine. changes to src/main.py", + "findings": [] + } + +def test_valid_control_not_dict(): + assert valid_control( + "not a dict", + expected_head_sha="abc123sha", + expected_run_id="123456", + expected_run_attempt="1" + ) is None + +def test_valid_control_head_sha_mismatch(): + value = get_base_valid_value() + value["head_sha"] = "wrong_sha" + assert valid_control( + value, + expected_head_sha="abc123sha", + expected_run_id="123456", + expected_run_attempt="1" + ) is None + +def test_valid_control_run_id_mismatch(): + value = get_base_valid_value() + value["run_id"] = "wrong_id" + assert valid_control( + value, + expected_head_sha="abc123sha", + expected_run_id="123456", + expected_run_attempt="1" + ) is None + +def test_valid_control_run_attempt_mismatch(): + value = get_base_valid_value() + value["run_attempt"] = "wrong_attempt" + assert valid_control( + value, + expected_head_sha="abc123sha", + expected_run_id="123456", + expected_run_attempt="1" + ) is None + +def test_valid_control_invalid_result(): + value = get_base_valid_value() + value["result"] = "PENDING" + assert valid_control( + value, + expected_head_sha="abc123sha", + expected_run_id="123456", + expected_run_attempt="1" + ) is None + +def test_valid_control_reason_invalid(): + # missing reason + value = get_base_valid_value() + del value["reason"] + assert valid_control( + value, + expected_head_sha="abc123sha", + expected_run_id="123456", + expected_run_attempt="1" + ) is None + + # non-string reason + value["reason"] = 123 + assert valid_control( + value, + expected_head_sha="abc123sha", + expected_run_id="123456", + expected_run_attempt="1" + ) is None + + # empty whitespace reason + value["reason"] = " " + assert valid_control( + value, + expected_head_sha="abc123sha", + expected_run_id="123456", + expected_run_attempt="1" + ) is None + +def test_valid_control_summary_invalid(): + # missing summary + value = get_base_valid_value() + del value["summary"] + assert valid_control( + value, + expected_head_sha="abc123sha", + expected_run_id="123456", + expected_run_attempt="1" + ) is None + + # non-string summary + value["summary"] = 123 + assert valid_control( + value, + expected_head_sha="abc123sha", + expected_run_id="123456", + expected_run_attempt="1" + ) is None + + # empty whitespace summary + value["summary"] = " " + assert valid_control( + value, + expected_head_sha="abc123sha", + expected_run_id="123456", + expected_run_attempt="1" + ) is None + +def test_valid_control_findings_logic(): + # findings not a list + value = get_base_valid_value() + value["findings"] = "not a list" + assert valid_control( + value, + expected_head_sha="abc123sha", + expected_run_id="123456", + expected_run_attempt="1" + ) is None + + # findings None on APPROVE converts to [] internally + value = get_base_valid_value() + del value["findings"] + res = valid_control( + value, + expected_head_sha="abc123sha", + expected_run_id="123456", + expected_run_attempt="1" + ) + assert res is not None + assert res["findings"] == [] + + # APPROVE with non-empty findings should fail + value = get_base_valid_value() + value["findings"] = [{"dummy": "data"}] + assert valid_control( + value, + expected_head_sha="abc123sha", + expected_run_id="123456", + expected_run_attempt="1" + ) is None + + # REQUEST_CHANGES with empty findings should fail + value = get_base_valid_value() + value["result"] = "REQUEST_CHANGES" + value["findings"] = [] + assert valid_control( + value, + expected_head_sha="abc123sha", + expected_run_id="123456", + expected_run_attempt="1" + ) is None + + # REQUEST_CHANGES with findings None should fail + value = get_base_valid_value() + value["result"] = "REQUEST_CHANGES" + del value["findings"] + assert valid_control( + value, + expected_head_sha="abc123sha", + expected_run_id="123456", + expected_run_attempt="1" + ) is None + + +def get_base_valid_finding(): + return { + "path": "src/main.py", + "line": 10, + "severity": "high", + "title": "Bug", + "problem": "Crash", + "root_cause": "Null ptr", + "fix_direction": "Add check", + "regression_test_direction": "Test null case", + "suggested_diff": "if x is None: return" + } + +def test_valid_control_findings_validation(): + base_val = get_base_valid_value() + base_val["result"] = "REQUEST_CHANGES" + + # Finding not a dict + value = base_val.copy() + value["findings"] = ["not a dict"] + assert valid_control( + value, + expected_head_sha="abc123sha", + expected_run_id="123456", + expected_run_attempt="1" + ) is None + + # Line not int or <= 0 + for invalid_line in [True, False, "10", 0, -1]: + value = base_val.copy() + finding = get_base_valid_finding() + finding["line"] = invalid_line + value["findings"] = [finding] + assert valid_control( + value, + expected_head_sha="abc123sha", + expected_run_id="123456", + expected_run_attempt="1" + ) is None + + # Missing required fields + required_fields = [ + "path", "severity", "title", "problem", "root_cause", + "fix_direction", "regression_test_direction", "suggested_diff" + ] + for field in required_fields: + # Field missing entirely + value = base_val.copy() + finding = get_base_valid_finding() + del finding[field] + value["findings"] = [finding] + assert valid_control( + value, + expected_head_sha="abc123sha", + expected_run_id="123456", + expected_run_attempt="1" + ) is None + + # Field empty string + value = base_val.copy() + finding = get_base_valid_finding() + finding[field] = " " + value["findings"] = [finding] + assert valid_control( + value, + expected_head_sha="abc123sha", + expected_run_id="123456", + expected_run_attempt="1" + ) is None + + # Field non-string + value = base_val.copy() + finding = get_base_valid_finding() + finding[field] = 123 + value["findings"] = [finding] + assert valid_control( + value, + expected_head_sha="abc123sha", + expected_run_id="123456", + expected_run_attempt="1" + ) is None + + # Valid REQUEST_CHANGES + value = base_val.copy() + value["findings"] = [get_base_valid_finding()] + res = valid_control( + value, + expected_head_sha="abc123sha", + expected_run_id="123456", + expected_run_attempt="1" + ) + assert res is not None + assert res["result"] == "REQUEST_CHANGES" + assert len(res["findings"]) == 1 + +def test_admits_missing_structural_review(): + # True cases from phrases + assert admits_missing_structural_review("reason", "structural exploration was not possible") + assert admits_missing_structural_review("could not access changed files", "summary") + + # True cases from patterns + assert admits_missing_structural_review("reason", "could not inspect changed files") + assert admits_missing_structural_review("reason", "no changes detected") + assert admits_missing_structural_review("zero changed files", "summary") + + # False cases + assert not admits_missing_structural_review("Good code", "LGTM") + assert not admits_missing_structural_review("structural exploration was successful", "summary") + +def test_mentions_changed_file_evidence(): + # True cases + assert mentions_changed_file_evidence("Changes in src/main.py", "summary") + assert mentions_changed_file_evidence("reason", "Edited the README") + assert mentions_changed_file_evidence("reason", "Added component.tsx") + assert mentions_changed_file_evidence("Modified AGENTS.md here", "summary") + + # False cases + assert not mentions_changed_file_evidence("No concrete files", "just prose") + assert not mentions_changed_file_evidence("Edited python", "but no py extension file path") + +def test_valid_control_approve_admission_and_evidence(): + value = get_base_valid_value() + value["result"] = "APPROVE" + + # Both fail (admitting missing structure + no evidence) + value["reason"] = "could not inspect changed files" + value["summary"] = "no evidence" + assert valid_control(value, expected_head_sha="abc123sha", expected_run_id="123456", expected_run_attempt="1") is None + + # Fails admission only + value["reason"] = "could not inspect changed files" + value["summary"] = "Changes in src/main.py" + assert valid_control(value, expected_head_sha="abc123sha", expected_run_id="123456", expected_run_attempt="1") is None + + # Fails evidence only + value["reason"] = "Good to go" + value["summary"] = "Looks fine, but no files mentioned" + assert valid_control(value, expected_head_sha="abc123sha", expected_run_id="123456", expected_run_attempt="1") is None + + # Valid APPROVE + value["reason"] = "Good to go" + value["summary"] = "Looks fine. changes to src/main.py" + res = valid_control(value, expected_head_sha="abc123sha", expected_run_id="123456", expected_run_attempt="1") + assert res is not None + assert res["result"] == "APPROVE" + +def test_check_structural_approval(tmp_path, capsys): + # Invalid JSON + f = tmp_path / "control.json" + f.write_text("{invalid json", encoding="utf-8") + assert check_structural_approval(f) == 65 + captured = capsys.readouterr() + assert "cannot read OpenCode control JSON" in captured.err + + # Valid JSON, but not a dict + f.write_text("[\"not a dict\"]", encoding="utf-8") + assert check_structural_approval(f) == 4 + captured = capsys.readouterr() + assert "NO_CONCLUSION" in captured.err + + # Valid dict, APPROVE, but missing evidence + data = { + "result": "APPROVE", + "reason": "Good to go", + "summary": "No file mentioned" + } + f.write_text(json.dumps(data), encoding="utf-8") + assert check_structural_approval(f) == 4 + captured = capsys.readouterr() + assert "NO_CONCLUSION" in captured.err + + # Valid dict, APPROVE, but admits missing structure + data = { + "result": "APPROVE", + "reason": "could not inspect changed files", + "summary": "changes to src/main.py" + } + f.write_text(json.dumps(data), encoding="utf-8") + assert check_structural_approval(f) == 4 + captured = capsys.readouterr() + assert "NO_CONCLUSION" in captured.err + + # Valid dict, APPROVE, valid evidence + data = { + "result": "APPROVE", + "reason": "Good to go", + "summary": "changes to src/main.py" + } + f.write_text(json.dumps(data), encoding="utf-8") + assert check_structural_approval(f) == 0 + + # Valid dict, not APPROVE + data = { + "result": "REQUEST_CHANGES", + "reason": "Bad", + "summary": "Fix it" + } + f.write_text(json.dumps(data), encoding="utf-8") + assert check_structural_approval(f) == 0 + +def test_iter_json_objects(): + # Only prose + assert iter_json_objects("Just some text here.") == [] + + # Valid JSON alone + assert iter_json_objects('{"key": "value"}') == [{"key": "value"}, {"key": "value"}] + + # Multiple JSONs with prose + text = ''' + Here is a block: + {"a": 1} + And another: + {"b": 2} + And an invalid one: { "c": + ''' + res = iter_json_objects(text) + assert len(res) == 2 + assert res[0] == {"a": 1} + assert res[1] == {"b": 2} + +def test_main_cli(tmp_path, capsys): + # Invalid arg length + assert main(["opencode_review_normalize_output.py"]) == 64 + captured = capsys.readouterr() + assert "usage:" in captured.err + + # File read error + assert main(["script", "sha", "id", "attempt", "nonexistent.json"]) == 65 + captured = capsys.readouterr() + assert "cannot read OpenCode output file" in captured.err + + # No valid objects + f = tmp_path / "output.txt" + f.write_text("just prose", encoding="utf-8") + assert main(["script", "sha", "id", "attempt", str(f)]) == 4 + captured = capsys.readouterr() + assert "NO_CONCLUSION" in captured.err + + # Valid output + data = get_base_valid_value() + data["head_sha"] = "sha" + data["run_id"] = "id" + data["run_attempt"] = "attempt" + f.write_text(json.dumps(data), encoding="utf-8") + assert main(["script", "sha", "id", "attempt", str(f)]) == 0 + + # Ensure output was normalized and written + out_text = f.read_text(encoding="utf-8") + assert "opencode-review-gate" in out_text + assert "opencode-review-control-v1" in out_text + assert '"head_sha":"sha"' in out_text + +def test_main_check_structural_approval(tmp_path): + data = get_base_valid_value() + f = tmp_path / "output.json" + f.write_text(json.dumps(data), encoding="utf-8") + + assert main(["script", "--check-structural-approval", str(f)]) == 0 + +def test_main_cli_invalid_control(tmp_path): + data = get_base_valid_value() + # Modify data so valid_control returns None + data["result"] = "INVALID_RESULT_TYPE" + f = tmp_path / "output.json" + f.write_text(json.dumps(data), encoding="utf-8") + + assert main(["script", "abc123sha", "123456", "1", str(f)]) == 4 + +import subprocess + +def test_module_execution(): + import runpy + script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../scripts/ci/opencode_review_normalize_output.py')) + with pytest.raises(SystemExit) as excinfo: + runpy.run_path(script_path, run_name="__main__") + assert excinfo.value.code == 64 From cdb9efecaae1284b05ae3f1f7365c047a984be0f Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:42:32 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=A7=AA=20scripts/ci/opencode=5Freview?= =?UTF-8?q?=5Fnormalize=5Foutput.py=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ci/test_opencode_fact_gate_contract.sh | 2 +- ...malize_output.cpython-312-pytest-9.1.1.pyc | Bin 62449 -> 63224 bytes .../test_opencode_review_normalize_output.py | 21 ++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/scripts/ci/test_opencode_fact_gate_contract.sh b/scripts/ci/test_opencode_fact_gate_contract.sh index 1624f122..44c5e1f2 100755 --- a/scripts/ci/test_opencode_fact_gate_contract.sh +++ b/scripts/ci/test_opencode_fact_gate_contract.sh @@ -17,7 +17,7 @@ check_contains() { } check_contains '## Changed docs repository tree evidence' -check_contains 'git ls-tree -r --name-only HEAD -- "$docs_dir"' +check_contains 'emit_changed_docs_tree_evidence' check_contains 'Do not claim repository docs, images, or reference assets are unavailable, missing, or absent unless the changed docs repository tree evidence proves it.' check_contains 'collect_unresolved_human_review_threads()' check_contains 'reviewThreads(first: 100)' diff --git a/tests/scripts/ci/__pycache__/test_opencode_review_normalize_output.cpython-312-pytest-9.1.1.pyc b/tests/scripts/ci/__pycache__/test_opencode_review_normalize_output.cpython-312-pytest-9.1.1.pyc index 8b709b044d0d6791dfaead8e737aaf29dbcd799f..bbc2a89e951feb5de5757b2a8d9e9aa5de4c94a3 100644 GIT binary patch delta 5489 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 TGsz_nM?9B|CQ`nP6`TJ6pVcwI diff --git a/tests/scripts/ci/test_opencode_review_normalize_output.py b/tests/scripts/ci/test_opencode_review_normalize_output.py index ea3bbfc9..076ee479 100644 --- a/tests/scripts/ci/test_opencode_review_normalize_output.py +++ b/tests/scripts/ci/test_opencode_review_normalize_output.py @@ -1,3 +1,4 @@ +"""Tests for opencode_review_normalize_output.py.""" import json import pytest from pathlib import Path @@ -17,6 +18,7 @@ ) def get_base_valid_value(): + """Test get base valid value.""" return { "head_sha": "abc123sha", "run_id": "123456", @@ -28,6 +30,7 @@ def get_base_valid_value(): } def test_valid_control_not_dict(): + """Test valid control not dict.""" assert valid_control( "not a dict", expected_head_sha="abc123sha", @@ -36,6 +39,7 @@ def test_valid_control_not_dict(): ) is None def test_valid_control_head_sha_mismatch(): + """Test valid control head sha mismatch.""" value = get_base_valid_value() value["head_sha"] = "wrong_sha" assert valid_control( @@ -46,6 +50,7 @@ def test_valid_control_head_sha_mismatch(): ) is None def test_valid_control_run_id_mismatch(): + """Test valid control run id mismatch.""" value = get_base_valid_value() value["run_id"] = "wrong_id" assert valid_control( @@ -56,6 +61,7 @@ def test_valid_control_run_id_mismatch(): ) is None def test_valid_control_run_attempt_mismatch(): + """Test valid control run attempt mismatch.""" value = get_base_valid_value() value["run_attempt"] = "wrong_attempt" assert valid_control( @@ -66,6 +72,7 @@ def test_valid_control_run_attempt_mismatch(): ) is None def test_valid_control_invalid_result(): + """Test valid control invalid result.""" value = get_base_valid_value() value["result"] = "PENDING" assert valid_control( @@ -76,6 +83,7 @@ def test_valid_control_invalid_result(): ) is None def test_valid_control_reason_invalid(): + """Test valid control reason invalid.""" # missing reason value = get_base_valid_value() del value["reason"] @@ -105,6 +113,7 @@ def test_valid_control_reason_invalid(): ) is None def test_valid_control_summary_invalid(): + """Test valid control summary invalid.""" # missing summary value = get_base_valid_value() del value["summary"] @@ -134,6 +143,7 @@ def test_valid_control_summary_invalid(): ) is None def test_valid_control_findings_logic(): + """Test valid control findings logic.""" # findings not a list value = get_base_valid_value() value["findings"] = "not a list" @@ -190,6 +200,7 @@ def test_valid_control_findings_logic(): def get_base_valid_finding(): + """Test get base valid finding.""" return { "path": "src/main.py", "line": 10, @@ -203,6 +214,7 @@ def get_base_valid_finding(): } def test_valid_control_findings_validation(): + """Test valid control findings validation.""" base_val = get_base_valid_value() base_val["result"] = "REQUEST_CHANGES" @@ -285,6 +297,7 @@ def test_valid_control_findings_validation(): assert len(res["findings"]) == 1 def test_admits_missing_structural_review(): + """Test admits missing structural review.""" # True cases from phrases assert admits_missing_structural_review("reason", "structural exploration was not possible") assert admits_missing_structural_review("could not access changed files", "summary") @@ -299,6 +312,7 @@ def test_admits_missing_structural_review(): assert not admits_missing_structural_review("structural exploration was successful", "summary") def test_mentions_changed_file_evidence(): + """Test mentions changed file evidence.""" # True cases assert mentions_changed_file_evidence("Changes in src/main.py", "summary") assert mentions_changed_file_evidence("reason", "Edited the README") @@ -310,6 +324,7 @@ def test_mentions_changed_file_evidence(): assert not mentions_changed_file_evidence("Edited python", "but no py extension file path") def test_valid_control_approve_admission_and_evidence(): + """Test valid control approve admission and evidence.""" value = get_base_valid_value() value["result"] = "APPROVE" @@ -336,6 +351,7 @@ def test_valid_control_approve_admission_and_evidence(): assert res["result"] == "APPROVE" def test_check_structural_approval(tmp_path, capsys): + """Test check structural approval.""" # Invalid JSON f = tmp_path / "control.json" f.write_text("{invalid json", encoding="utf-8") @@ -390,6 +406,7 @@ def test_check_structural_approval(tmp_path, capsys): assert check_structural_approval(f) == 0 def test_iter_json_objects(): + """Test iter json objects.""" # Only prose assert iter_json_objects("Just some text here.") == [] @@ -410,6 +427,7 @@ def test_iter_json_objects(): assert res[1] == {"b": 2} def test_main_cli(tmp_path, capsys): + """Test main cli.""" # Invalid arg length assert main(["opencode_review_normalize_output.py"]) == 64 captured = capsys.readouterr() @@ -442,6 +460,7 @@ def test_main_cli(tmp_path, capsys): assert '"head_sha":"sha"' in out_text def test_main_check_structural_approval(tmp_path): + """Test main check structural approval.""" data = get_base_valid_value() f = tmp_path / "output.json" f.write_text(json.dumps(data), encoding="utf-8") @@ -449,6 +468,7 @@ def test_main_check_structural_approval(tmp_path): assert main(["script", "--check-structural-approval", str(f)]) == 0 def test_main_cli_invalid_control(tmp_path): + """Test main cli invalid control.""" data = get_base_valid_value() # Modify data so valid_control returns None data["result"] = "INVALID_RESULT_TYPE" @@ -460,6 +480,7 @@ def test_main_cli_invalid_control(tmp_path): import subprocess def test_module_execution(): + """Test module execution.""" import runpy script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../scripts/ci/opencode_review_normalize_output.py')) with pytest.raises(SystemExit) as excinfo: From 58ec332ff6fde147b8ceec537cc9f1a7954825d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 12:38:23 +0000 Subject: [PATCH 3/5] Increase OpenCode model pool retries, exponential backoff cap, and job timeout --- .github/workflows/opencode-review.yml | 8 ++++---- tests/test_opencode_agent_contract.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 4b6a0abb..a28e6e3c 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: 20 + timeout-minutes: 60 env: STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }} GITHUB_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }} @@ -2248,12 +2248,12 @@ jobs: NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-5-nano" - OPENCODE_MODEL_ATTEMPTS: "1" + OPENCODE_MODEL_ATTEMPTS: "5" OPENCODE_RUN_TIMEOUT_SECONDS: "240" OPENCODE_EXPORT_TIMEOUT_SECONDS: "120" - OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "360" + OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "3300" OPENCODE_BACKOFF_INITIAL_SECONDS: "30" - OPENCODE_BACKOFF_MAX_SECONDS: "30" + OPENCODE_BACKOFF_MAX_SECONDS: "300" OPENCODE_FIRST_ATTEMPT_AGENT: ci-review OPENCODE_AGENT: ci-review-fallback OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index c5dd7a9b..0e546b6a 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -211,15 +211,15 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert '"## Check outcome"' not in workflow assert "publish REQUEST_CHANGES when coverage-evidence blocker states" in workflow assert 'timeout-minutes: 75' in workflow - assert re.search(r"Run OpenCode PR Review model pool[\s\S]{0,240}timeout-minutes: 20", workflow) + assert re.search(r"Run OpenCode PR Review model pool[\s\S]{0,240}timeout-minutes: 60", workflow) assert 'APPROVAL_CHECK_WAIT_ATTEMPTS: "81"' in workflow assert 'APPROVAL_CHECK_WAIT_SLEEP_SECONDS: "30"' in workflow assert 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-5-nano"' in workflow - assert 'OPENCODE_MODEL_ATTEMPTS: "1"' in workflow + assert 'OPENCODE_MODEL_ATTEMPTS: "5"' in workflow assert 'OPENCODE_RUN_TIMEOUT_SECONDS: "240"' in workflow 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_TOTAL_RETRY_BUDGET_SECONDS: "3300"' in workflow + assert 'OPENCODE_BACKOFF_MAX_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 7cd275902b22ddeaed51d90a33b3110c760fd9df Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 1 Jul 2026 14:34:19 +0000 Subject: [PATCH 4/5] Acknowledge obsolescence of PR and close review. From 70bb3ca86cdfe3055cb87502fc65d06c2d668d97 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 2 Jul 2026 01:18:38 +0000 Subject: [PATCH 5/5] Acknowledge obsolescence of PR and close review.