diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 7010b262..0687c941 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -1915,6 +1915,7 @@ jobs: local changed_file="$1" case "${GH_REPOSITORY}:${changed_file}" in ContextualWisdomLab/.github:.github/workflows/opencode-review.yml | \ + ContextualWisdomLab/.github:.github/workflows/noema-review.yml | \ ContextualWisdomLab/.github:.github/workflows/pr-review-merge-scheduler.yml | \ ContextualWisdomLab/.github:.github/workflows/strix.yml | \ ContextualWisdomLab/.github:.jules/bolt.md | \ @@ -1924,8 +1925,13 @@ jobs: ContextualWisdomLab/.github:opencode.jsonc | \ ContextualWisdomLab/.github:scripts/ci/changed_file_syntax_gate.py | \ ContextualWisdomLab/.github:scripts/ci/javascript_coverage_gate.py | \ + ContextualWisdomLab/.github:scripts/ci/materialize_pr_review_source.py | \ + ContextualWisdomLab/.github:scripts/ci/noema_review_gate.py | \ ContextualWisdomLab/.github:scripts/ci/opencode_review_approve_gate.sh | \ + ContextualWisdomLab/.github:scripts/ci/opencode_dispatch_status.py | \ + ContextualWisdomLab/.github:scripts/ci/opencode_existing_approval_gate.py | \ ContextualWisdomLab/.github:scripts/ci/pr_head_replay_guard.py | \ + ContextualWisdomLab/.github:scripts/ci/pr_review_fix_scheduler.py | \ ContextualWisdomLab/.github:scripts/ci/pr_review_merge_scheduler.py | \ ContextualWisdomLab/.github:scripts/ci/run_opencode_review_model_pool.sh | \ ContextualWisdomLab/.github:scripts/ci/opencode_review_normalize_output.py | \ @@ -1933,9 +1939,14 @@ jobs: ContextualWisdomLab/.github:scripts/ci/validate_opencode_failed_check_review.sh | \ ContextualWisdomLab/.github:tests/test_changed_file_syntax_gate.py | \ ContextualWisdomLab/.github:tests/test_javascript_coverage_gate.py | \ + ContextualWisdomLab/.github:tests/test_materialize_pr_review_source.py | \ + ContextualWisdomLab/.github:tests/test_noema_review_gate.py | \ ContextualWisdomLab/.github:tests/test_opencode_agent_contract.py | \ + ContextualWisdomLab/.github:tests/test_opencode_existing_approval_gate.py | \ ContextualWisdomLab/.github:tests/test_opencode_model_pool_runner.py | \ + ContextualWisdomLab/.github:tests/test_opencode_security_boundaries.py | \ ContextualWisdomLab/.github:tests/test_pr_head_replay_guard.py | \ + ContextualWisdomLab/.github:tests/test_pr_review_fix_scheduler.py | \ ContextualWisdomLab/.github:tests/test_pr_review_fix_scheduler_coverage.py | \ ContextualWisdomLab/.github:tests/test_pr_review_merge_scheduler.py | \ ContextualWisdomLab/.github:tests/test_required_workflow_queue_contract.py | \ @@ -3506,25 +3517,25 @@ jobs: continue-on-error: true env: STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }} - # Native OpenAI backend for the lead review model. GitHub Models + # Native OpenAI backend for the direct review model. GitHub Models # rate-limits every request and caps bodies at ~4000 tokens, so the # rate-starved shared pool never returned a verdict; hitting - # api.openai.com directly with the org OPENAI_API_KEY gives the lead + # api.openai.com directly with the org OPENAI_API_KEY gives that # model a working, un-throttled backend. Resolves {env:OPENAI_API_KEY} # in the opencode.jsonc "openai" provider block. OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" - # High-sensitivity review candidates only. DeepSeek V3 has been the - # most reliable first-pass reviewer in the org queue, then the pool - # falls through to the direct GPT-5.6 Luna slot, then the full-size - # GPT-4.1 long-context endpoint and provider-specific GPT/o3 fallbacks. + # High-sensitivity review candidates only. GPT-4.1 is first because it + # was the only provider to return a parseable control artifact during + # the latest current-head exhaustion incident; DeepSeek remains the + # first fallback but cannot monopolize a full 90-minute cadence slot. # The direct-OpenAI slot runs GPT-5.6 Luna: the newest family's # cost-efficient tier, cheaper than the legacy gpt-5 it replaced # ($1/$6 vs $1.25/$10 per 1M tokens) so the org OpenAI budget # stretches further between top-ups. - OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-4.1 github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" + OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-4.1 github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" # One attempt per model, then fall through to the next model. Retrying # the SAME model 5x let a rate-limited/hung leader consume the whole # step, so the pool never reached a healthy fallback model. @@ -3561,6 +3572,10 @@ jobs: # larger. Keep the exact runtime failure visible without spending a # full medium/large cadence slot after the long-context candidate. OPENCODE_GITHUB_GPT5_RUN_TIMEOUT_SECONDS: "45" + # DeepSeek returned no provider detail while consuming two complete + # 90-minute slots. Five minutes preserves a useful fallback window + # while allowing the remaining independent providers to run. + OPENCODE_GITHUB_DEEPSEEK_RUN_TIMEOUT_SECONDS: "300" OPENCODE_DYNAMIC_MAX_CYCLES: "0" CENTRAL_REVIEW_PROCESS_FALLBACK_ELIGIBLE: ${{ steps.central_review_process_fallback_scope.outputs.eligible || 'false' }} CENTRAL_REVIEW_PROCESS_FALLBACK_SCOPE_LABEL: ${{ steps.central_review_process_fallback_scope.outputs.scope_label || 'unsupported' }} diff --git a/scripts/ci/opencode_review_normalize_output.py b/scripts/ci/opencode_review_normalize_output.py index 4045d457..25d3c393 100755 --- a/scripts/ci/opencode_review_normalize_output.py +++ b/scripts/ci/opencode_review_normalize_output.py @@ -653,6 +653,80 @@ def adversarial_probe_source_receipt_error( return "" +def repair_adversarial_probe_evidence_bindings(value: Any) -> dict[str, Any] | Any: + """Bind trusted source receipts and locations into otherwise valid evidence. + + Models sometimes place the exact changed-file path and positive line in the + structured ``path``/``line`` fields but omit the duplicate ``path:line`` text + from ``evidence``. Restore only that redundant location after the existing + single receipt already matches the sealed current-head line. Missing, + duplicate, or mismatched receipts, unsafe paths, missing changed-file + evidence, and circular or unobserved claims remain unmodified and fail closed. + """ + if not isinstance(value, dict): + return value + validation = value.get("adversarial_validation") + if not isinstance(validation, dict): + return value + probes = validation.get("probes") + if not isinstance(probes, list): + return value + + changed_files = current_changed_files() + repaired_probes: list[Any] = [] + changed = False + for probe in probes: + if not isinstance(probe, dict): + repaired_probes.append(probe) + continue + path = probe.get("path") + line = probe.get("line") + evidence = probe.get("evidence") + if ( + not isinstance(path, str) + or path not in changed_files + or isinstance(line, bool) + or not isinstance(line, int) + or line <= 0 + or not isinstance(evidence, str) + or not evidence.strip() + or adversarial_probe_location_error(path, line) + ): + repaired_probes.append(probe) + continue + + receipts = SOURCE_LINE_RECEIPT_RE.findall(evidence) + if len(receipts) != 1 or adversarial_probe_source_receipt_error( + evidence, path, line + ): + repaired_probes.append(probe) + continue + repaired_evidence = evidence + rejection = adversarial_evidence_rejection_reason(repaired_evidence, path, line) + if rejection == "must cite the exact probe path and positive line": + repaired_evidence = f"{path}:{line} {repaired_evidence}" + elif rejection: + repaired_probes.append(probe) + continue + if adversarial_probe_source_receipt_error( + repaired_evidence, path, line + ) or adversarial_evidence_rejection_reason(repaired_evidence, path, line): + repaired_probes.append(probe) + continue + if repaired_evidence == evidence: + repaired_probes.append(probe) + else: + repaired_probes.append({**probe, "evidence": repaired_evidence}) + changed = True + + if not changed: + return value + return { + **value, + "adversarial_validation": {**validation, "probes": repaired_probes}, + } + + def adversarial_validation_error( value: Any, *, @@ -1267,6 +1341,7 @@ def reject(reason: str) -> None: return reject("APPROVE cannot contain findings") if result == "REQUEST_CHANGES" and not findings: return reject("REQUEST_CHANGES requires at least one finding") + value = repair_adversarial_probe_evidence_bindings(value) adversarial_error = adversarial_validation_error( value.get("adversarial_validation"), result=result, diff --git a/scripts/ci/run_opencode_review_model_pool.sh b/scripts/ci/run_opencode_review_model_pool.sh index 85e122ab..5a784f2c 100644 --- a/scripts/ci/run_opencode_review_model_pool.sh +++ b/scripts/ci/run_opencode_review_model_pool.sh @@ -346,6 +346,9 @@ cap_model_run_timeout() { github-models/openai/gpt-5 | github-models/openai/gpt-5-chat) cap_seconds="$(env_integer_or_default OPENCODE_GITHUB_GPT5_RUN_TIMEOUT_SECONDS 45)" ;; + github-models/deepseek/*) + cap_seconds="$(env_integer_or_default OPENCODE_GITHUB_DEEPSEEK_RUN_TIMEOUT_SECONDS 300)" + ;; *) printf '%s\n' "$run_timeout_seconds" return 0 @@ -548,7 +551,7 @@ main() { uncapped_run_timeout="$OPENCODE_RUN_TIMEOUT_SECONDS" OPENCODE_RUN_TIMEOUT_SECONDS="$(cap_model_run_timeout "$model_candidate" "$OPENCODE_RUN_TIMEOUT_SECONDS")" if [ "$OPENCODE_RUN_TIMEOUT_SECONDS" -lt "$uncapped_run_timeout" ]; then - printf 'OpenCode %s runtime cap selected %ss instead of %ss because this installation has returned a constrained request-body limit for that endpoint.\n' \ + printf 'OpenCode %s runtime cap selected %ss instead of %ss because the configured provider-specific cap is lower than the cadence timeout.\n' \ "$model_candidate" "$OPENCODE_RUN_TIMEOUT_SECONDS" "$uncapped_run_timeout" fi export OPENCODE_RUN_TIMEOUT_SECONDS diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index c8eb1d34..e7729c56 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -103,9 +103,9 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): assert candidate_pairs assert candidate_pairs == [ + ["github-models", "openai/gpt-4.1"], ["github-models", "deepseek/deepseek-v3-0324"], ["openai", "gpt-5.6-luna"], - ["github-models", "openai/gpt-4.1"], ["github-models", "openai/gpt-5"], ["github-models", "openai/gpt-5-chat"], ["github-models", "openai/o3"], @@ -115,8 +115,8 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): assert direct_openai_models == ["gpt-5.6-luna"] assert set(github_candidate_models).issubset(set(github_models)) assert github_candidate_models == [ - "deepseek/deepseek-v3-0324", "openai/gpt-4.1", + "deepseek/deepseek-v3-0324", "openai/gpt-5", "openai/gpt-5-chat", "openai/o3", @@ -939,6 +939,20 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): in workflow ) assert "tests/test_opencode_agent_contract.py | \\" in workflow + for central_review_path in ( + ".github/workflows/noema-review.yml", + "scripts/ci/materialize_pr_review_source.py", + "scripts/ci/noema_review_gate.py", + "scripts/ci/opencode_dispatch_status.py", + "scripts/ci/opencode_existing_approval_gate.py", + "scripts/ci/pr_review_fix_scheduler.py", + "tests/test_materialize_pr_review_source.py", + "tests/test_noema_review_gate.py", + "tests/test_opencode_existing_approval_gate.py", + "tests/test_opencode_security_boundaries.py", + "tests/test_pr_review_fix_scheduler.py", + ): + assert f"ContextualWisdomLab/.github:{central_review_path}" in workflow assert ( "ContextualWisdomLab/appguardrail:scripts/ci/collect_org_security_failures.py" in workflow @@ -1090,9 +1104,9 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): in workflow ) assert ( - 'OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 ' + 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-4.1 ' + "github-models/deepseek/deepseek-v3-0324 " "openai/gpt-5.6-luna " - "github-models/openai/gpt-4.1 " "github-models/openai/gpt-5 " "github-models/openai/gpt-5-chat " "github-models/openai/o3 " @@ -1105,6 +1119,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "11700"' in workflow assert 'OPENCODE_POOL_STEP_TIMEOUT_SECONDS: "12000"' in workflow assert 'OPENCODE_POOL_MAX_CYCLES: "0"' in workflow + assert 'OPENCODE_GITHUB_DEEPSEEK_RUN_TIMEOUT_SECONDS: "300"' in workflow assert 'OPENCODE_DYNAMIC_REVIEW_CADENCE: "true"' in workflow assert ( "OPENCODE_CHANGED_FILES_FILE: ${{ runner.temp }}/opencode-changed-files.txt" @@ -1179,7 +1194,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "while :" in model_pool_runner assert "should_skip_model_candidate" in model_pool_runner assert "cap_model_run_timeout" in model_pool_runner - assert "constrained request-body limit" in model_pool_runner + assert "configured provider-specific cap" in model_pool_runner assert "run_central_adversarial_harness" not in model_pool_runner assert "finish_pool_without_model" in model_pool_runner assert "central-current-head-adversarial-harness" not in model_pool_runner @@ -1213,9 +1228,9 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-5-nano"' not in workflow ) assert ( - 'OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 ' + 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-4.1 ' + "github-models/deepseek/deepseek-v3-0324 " "openai/gpt-5.6-luna " - "github-models/openai/gpt-4.1 " "github-models/openai/gpt-5 " "github-models/openai/gpt-5-chat " "github-models/openai/o3 " diff --git a/tests/test_opencode_model_pool_runner.py b/tests/test_opencode_model_pool_runner.py index 71e96add..e567d3f7 100644 --- a/tests/test_opencode_model_pool_runner.py +++ b/tests/test_opencode_model_pool_runner.py @@ -14,6 +14,8 @@ import pytest +from scripts.ci import opencode_review_normalize_output as normalizer + ROOT = Path(__file__).resolve().parents[1] RUNNER = ROOT / "scripts" / "ci" / "run_opencode_review_model_pool.sh" @@ -29,6 +31,16 @@ } +@pytest.fixture(autouse=True) +def clear_normalizer_artifact_caches(): + """Keep trusted artifact caches isolated from later review-gate tests.""" + normalizer.current_changed_files.cache_clear() + normalizer.trusted_execution_receipts.cache_clear() + yield + normalizer.current_changed_files.cache_clear() + normalizer.trusted_execution_receipts.cache_clear() + + def bash_command() -> str: """Return a Bash executable that can run repository shell scripts locally.""" if os.name == "nt": @@ -87,6 +99,227 @@ def seal_artifacts( return hashlib.sha256(manifest.read_bytes()).hexdigest() +def prepare_probe_binding_artifacts( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> tuple[str, int, str]: + """Create one sealed changed source line for normalizer binding tests.""" + runner_temp = tmp_path / "binding-runner-temp" + source_root = tmp_path / "binding-source" + source_path = source_root / "scripts" / "ci" / "example.py" + runner_temp.mkdir() + source_path.parent.mkdir(parents=True) + source_path.write_text("return False\n", encoding="utf-8") + changed_files = runner_temp / "opencode-changed-files.txt" + changed_files.write_text("scripts/ci/example.py\n", encoding="utf-8") + manifest_digest = seal_artifacts( + runner_temp, + head_sha="binding-head", + run_id="binding-run", + run_attempt="1", + paths=(changed_files,), + ) + monkeypatch.setenv("RUNNER_TEMP", str(runner_temp)) + monkeypatch.setenv("OPENCODE_SOURCE_WORKDIR", str(source_root)) + monkeypatch.setenv("OPENCODE_CHANGED_FILES_FILE", str(changed_files)) + monkeypatch.setenv("OPENCODE_ARTIFACT_MANIFEST_SHA256", manifest_digest) + normalizer.current_changed_files.cache_clear() + normalizer.trusted_execution_receipts.cache_clear() + digest = hashlib.sha256(b"return False").hexdigest() + return "scripts/ci/example.py", 1, f"source-line-sha256={digest}" + + +def test_normalizer_binds_only_a_verified_structured_probe_location( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A matching receipt may restore only redundant path:line evidence text.""" + path, line, receipt = prepare_probe_binding_artifacts(tmp_path, monkeypatch) + evidence = ( + f"Regression command rejected malformed input with exit code 1; {receipt}" + ) + value = { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": evidence, + } + ] + } + } + + repaired = normalizer.repair_adversarial_probe_evidence_bindings(value) + + assert repaired is not value + assert repaired["adversarial_validation"]["probes"][0]["evidence"] == ( + f"{path}:{line} {evidence}" + ) + + +def test_valid_control_repairs_probe_bindings_before_adversarial_validation( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The production validator must apply the bounded repair before validation.""" + captured: dict[str, object] = {} + + monkeypatch.setattr(normalizer, "artifact_identity_error", lambda *args: "") + monkeypatch.setattr( + normalizer, + "repair_adversarial_probe_evidence_bindings", + lambda value: { + **value, + "adversarial_validation": {"status": "repaired-before-validation"}, + }, + ) + + def capture_validation(value, *, result, findings): + captured["value"] = value + captured["result"] = result + captured["findings"] = findings + return "expected integration stop" + + monkeypatch.setattr( + normalizer, + "adversarial_validation_error", + capture_validation, + ) + rejection_reasons: list[str] = [] + control = { + "head_sha": "head", + "run_id": "run", + "run_attempt": "1", + "result": "APPROVE", + "reason": "reviewed current-head evidence", + "summary": "verified current-head evidence", + "findings": [], + "adversarial_validation": {"status": "raw"}, + } + + assert ( + normalizer.valid_control( + control, + expected_head_sha="head", + expected_run_id="run", + expected_run_attempt="1", + rejection_reasons=rejection_reasons, + ) + is None + ) + assert captured == { + "value": {"status": "repaired-before-validation"}, + "result": "APPROVE", + "findings": [], + } + assert rejection_reasons[-1] == "expected integration stop" + + +def test_normalizer_probe_binding_repair_remains_fail_closed( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Malformed, untrusted, circular, and already-bound probes are not rewritten.""" + path, line, receipt = prepare_probe_binding_artifacts(tmp_path, monkeypatch) + assert normalizer.repair_adversarial_probe_evidence_bindings(None) is None + + missing_validation: dict[str, object] = {} + assert ( + normalizer.repair_adversarial_probe_evidence_bindings(missing_validation) + is missing_validation + ) + malformed = {"adversarial_validation": {"probes": "not-a-list"}} + assert normalizer.repair_adversarial_probe_evidence_bindings(malformed) is malformed + + invalid_values = [ + { + "adversarial_validation": { + "probes": [ + "not-an-object", + {"path": path, "line": 0, "evidence": receipt}, + ] + } + }, + { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": ( + "Regression command rejected malformed input with exit code 1; " + "source-line-sha256=" + "0" * 64 + ), + } + ] + } + }, + { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": ( + "Regression command rejected malformed input with exit code 1; " + f"{receipt} {receipt}" + ), + } + ] + } + }, + { + "adversarial_validation": { + "probes": [ + { + "path": "scripts/ci/not-changed.py", + "line": line, + "evidence": ( + "Regression command rejected malformed input with exit code 1; " + f"{receipt}" + ), + } + ] + } + }, + { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": receipt, + } + ] + } + }, + { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": f"Source inspection properly handles all cases; {receipt}", + } + ] + } + }, + { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": ( + f"Regression command at {path}:{line} rejected malformed input " + f"with exit code 1; {receipt}" + ), + } + ] + } + }, + ] + for invalid in invalid_values: + assert normalizer.repair_adversarial_probe_evidence_bindings(invalid) is invalid + + def skip_if_windows_bash_is_unresponsive(command: str) -> None: """Skip with a visible reason when local Git Bash cannot start on Windows.""" if os.name != "nt": @@ -654,7 +887,7 @@ def test_github_gpt5_runtime_cap_preserves_queue_budget(tmp_path: Path) -> None: assert result.returncode == 1 assert ( "OpenCode github-models/openai/gpt-5 runtime cap selected 3s instead of 9s " - "because this installation has returned a constrained request-body limit for that endpoint." + "because the configured provider-specific cap is lower than the cadence timeout." ) in result.stdout attempt_budget = re.search( r"OpenCode github-models/openai/gpt-5 attempt 1/1 using (\d+)s run timeout " @@ -667,6 +900,34 @@ def test_github_gpt5_runtime_cap_preserves_queue_budget(tmp_path: Path) -> None: assert run_timeout <= remaining_budget <= 30 +def test_github_deepseek_runtime_cap_preserves_queue_budget(tmp_path: Path) -> None: + """No-output DeepSeek endpoints cannot consume a full cadence slot.""" + result = run_failed_model( + tmp_path, + model_candidates="github-models/deepseek/deepseek-v3-0324", + extra_env={ + "OPENCODE_GITHUB_DEEPSEEK_RUN_TIMEOUT_SECONDS": "2", + "OPENCODE_RUN_TIMEOUT_SECONDS": "9", + }, + ) + + assert result.returncode == 1 + assert ( + "OpenCode github-models/deepseek/deepseek-v3-0324 runtime cap selected 2s " + "instead of 9s because the configured provider-specific cap is lower than " + "the cadence timeout." + ) in result.stdout + attempt_budget = re.search( + r"OpenCode github-models/deepseek/deepseek-v3-0324 attempt 1/1 using " + r"(\d+)s run timeout with (\d+)s retry budget remaining\.", + result.stdout, + ) + assert attempt_budget is not None + run_timeout, remaining_budget = map(int, attempt_budget.groups()) + assert run_timeout == 2 + assert run_timeout <= remaining_budget <= 30 + + def test_github_models_openai_prompt_references_evidence_without_inlining( tmp_path: Path, ) -> None: