diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 3677f408bd..0814541a9d 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -24,12 +24,6 @@ permissions: contents: read jobs: - required-workflow-bootstrap: - name: required-workflow-bootstrap - runs-on: ubuntu-latest - steps: - - run: echo "OpenCode repository-dispatch review run materialized." - validate-pr-metadata: name: validate-pr-metadata if: github.event_name == 'repository_dispatch' diff --git a/CHANGELOG.md b/CHANGELOG.md index d7c6d40ae7..92a03a4901 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- Close a 99% `scripts/ci` coverage regression on protected main: merged #1546 added an + uncovered `live_head_matches` helper, an uncovered no-active/no-stale-runs fall-through in + `prepare_autofix_slot`, and an uncovered "current-head autofix run is already queued or + running" wait path in `pr_review_fix_scheduler.py::inspect_pr`, while the pre-existing + conflicted-draft and conflicted-unauthorized `inspect_pr` returns and the REST + `fetch_workflow_names_by_check_suite_rest` pagination/name-filtering/permission-denied paths + in `pr_review_merge_scheduler.py` remained untested. Every PR rebasing onto main inherited + this failure via the `coverage-evidence` required check regardless of its own diff; landed on + protected main via #1572, plus a `test_scheduler_wake_reuses_trusted_receipt_predicate` + SIGPIPE-flake fix (the fake dispatch `gh` shim now drains stdin before exiting, so it cannot + race the production `jq | gh api --input -` pipeline under `pipefail`). - **Fix `opencode-review.yml` admission gaps around stale/out-of-order events (`#1568`).** Building on the draft-poll exemption's live PR/head validation, Devin Review found two further defects. (1) The concurrency group was keyed only by repository and PR number, so @@ -232,6 +243,13 @@ Semantic Versioning where the repository publishes a release. then rejected via 429/404/timeout). New regression tests pin the default to the policy module's canonical value and forbid the total-routes constant from reappearing as the account-cap fallback. +- Remove the orphaned `required-workflow-bootstrap` job from + `opencode-review-dispatch.yml`: it only echoed a materialization message and + was never referenced by any `needs:` clause in that repository_dispatch-only + workflow, which fires only in an already-trusted, non-PR context (the + matching trust-boundary bootstrap that the pattern is for stays in the + `pull_request_target`-triggered `opencode-review.yml`, whose branch-protection + contract still requires it). - Fix a dangling reference #1468 left in `docs/product-goal-directive.md` (flagged by Devin Review on that PR): the standing operating directive still named the removed `free_family_diversity` evidence field instead of diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 9367d54f67..c7772cad3e 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2344,6 +2344,54 @@ contract assertion, and `docs/adr/0003-contextual-orchestrator-vendored-free-zdr "today" reference. Landed in the same PR (`#1463`) as the streaming revert, not split out, since the revert is unsafe without it. +## 2026-09-01 post-#1546 `scripts/ci` coverage regression on protected main: root-caused and closed + +**Context**: `#1546` (merged, exact head `5686de41660d51a7a7f22b8840dfa6ccfe5ff3f1`) reconciled +unbounded exact-head review agents and, as part of a 90-line expansion of +`scripts/ci/pr_review_fix_scheduler.py`, added a `live_head_matches` helper, a no-active/no-stale +fall-through branch in `prepare_autofix_slot`, and an "already queued or running" wait branch in +`inspect_pr` — none of which any test exercised directly. This compounded a narrower, older gap in +the same file (`inspect_pr`'s conflicted-draft and conflicted-unauthorized returns) and in +`scripts/ci/pr_review_merge_scheduler.py::fetch_workflow_names_by_check_suite_rest` (pagination, +missing-suite-id/blank-name filtering, non-access-error propagation), first found and attempted in +now-closed, unmerged `#1547`/`#1551`/`#1554` — none of whose evidence or diffs transferred here; +this pass re-derived the current gap from a clean `origin/main` clone rather than assuming those +predecessors were still accurate against `#1546`'s shifted line numbers and new branches. Verified +directly: `coverage report --show-missing` on unmodified `main` showed +`scripts/ci/pr_review_fix_scheduler.py` at 97% (missing 116-121, 459->466, 495, 503, 546) and +`scripts/ci/pr_review_merge_scheduler.py` at 99% (missing 1003, 1008->1005, 1012) — total repo-wide +99%, below the `pyproject.toml` `fail_under = 100` gate. Because `opencode-review-dispatch.yml`'s +`coverage-evidence` job measures the **merged** PR tree (base + head) and hard-fails below 100%, +every PR rebasing onto main inherited this failure regardless of its own diff — org-wide impact, +not scoped to one PR. + +**Fix**: `#1567` (test-only, no production code) adds direct unit coverage for `live_head_matches` +(case-insensitive match, mismatch, malformed-payload paths), `prepare_autofix_slot`'s empty-run +fall-through, the `inspect_pr` conflicted-draft/conflicted-unauthorized/already-queued cases, and +the `fetch_workflow_names_by_check_suite_rest` pagination/filtering/error-propagation paths. +Verified on the fix commit (`db106d50f2134ece147bc5318e389aeb124d198c`): `coverage run -m pytest +tests -q` (2251 passed, 1 skipped, 21 subtests), `coverage report` (repo-wide 100%, both files +individually 100% statement and 100% branch), `interrogate` (100.0%). + +**Devin Review raised a false positive on the fix itself**, claiming +`test_live_head_matches_compares_case_insensitively_and_fails_closed` left non-object-payload, +non-string-SHA, and wrong-length-SHA branches uncovered. Re-verified against the actual gate rather +than accepted at face value: `live_head_matches` has exactly one `if` statement (two arcs, both +exercised by the committed test), and its final `return (isinstance(...) and len(...) == 40 and +...)` is a single boolean expression with no `if`/`else` of its own — `coverage.py`'s branch mode +(what `fail_under = 100` actually measures here) tracks control-flow arcs between statements, not +sub-clause condition coverage within one expression. The cited cases are additional test +thoroughness, not something the gate is currently failing on; confirmed by a full-suite run on the +exact same head showing both files at 100% branch coverage with zero missing branches. Replied with +this evidence on the review thread and did not widen the PR's diff for a claim that does not hold +against this repo's own tooling. + +**One test in the full suite remains a known, pre-existing flake**, unrelated to this change: +`tests/test_opencode_required_verdict_regression.py::test_scheduler_wake_reuses_trusted_receipt_predicate` +intermittently exits 141 (SIGPIPE) under full-suite parallel load; reproduces identically on +unmodified `origin/main` and passes cleanly in file isolation. Not remediated here — out of scope +for a coverage-gap-only PR, and not itself a coverage regression. + ## 2026-09-01 naruon#1486 transport-crash: root cause, owner, status **Live incident**: the required `noema-review` check on `ContextualWisdomLab/naruon#1486` crashed with an diff --git a/tests/test_noema_removed_file_context.py b/tests/test_noema_removed_file_context.py index 8c5d8ca539..500d406f73 100644 --- a/tests/test_noema_removed_file_context.py +++ b/tests/test_noema_removed_file_context.py @@ -3,6 +3,7 @@ from __future__ import annotations import base64 +import json from scripts.ci import noema_review_gate as noema @@ -12,7 +13,14 @@ def test_fetch_changed_files_preserves_path_and_status(monkeypatch): monkeypatch.setattr( noema, "run", - lambda args, stdin=None: "a.py\tmodified\n\nb.py\tremoved\nfuzz/x.py\tadded\n", + lambda args, stdin=None: ( + json.dumps(["a.py", "modified"]) + + "\n\n" + + json.dumps(["b.py", "removed"]) + + "\n" + + json.dumps(["fuzz/x.py", "added"]) + + "\n" + ), ) assert noema.fetch_changed_files("owner/repo", 7) == [ @@ -22,8 +30,11 @@ def test_fetch_changed_files_preserves_path_and_status(monkeypatch): ] -def test_removed_file_context_uses_base_content(monkeypatch): - """A deleted file must be reviewed from immutable pre-deletion evidence.""" +def test_removed_file_context_uses_merge_base_content(monkeypatch): + """A deleted file must be reviewed from immutable merge-base evidence.""" + head_sha = "a" * 40 + base_sha = "b" * 40 + merge_base_sha = "c" * 40 encoded = base64.b64encode(b"def doomed():\n pass\n").decode("ascii") calls: list[str] = [] @@ -31,24 +42,88 @@ def fake_run(args, stdin=None): target = args[2] calls.append(target) if target.endswith("/files"): - return "fuzz/fuzz_opencode_normalize_output.py\tremoved\n" - if "contents/fuzz/fuzz_opencode_normalize_output.py?ref=base-sha" in target: + return json.dumps(["fuzz/fuzz_opencode_normalize_output.py", "removed"]) + "\n" + if target == f"repos/owner/repo/compare/{base_sha}...{head_sha}": + return merge_base_sha + if f"contents/fuzz/fuzz_opencode_normalize_output.py?ref={merge_base_sha}" in target: return encoded raise AssertionError(args) monkeypatch.setattr(noema, "run", fake_run) - context = noema.changed_file_context( - "owner/repo", 1486, "head-sha", "base-sha" - ) + context = noema.changed_file_context("owner/repo", 1486, head_sha, base_sha) - assert "File removed in this PR. Pre-deletion content at base ref" in context + assert f"Pre-deletion content at merge base `{merge_base_sha}`" in context assert "def doomed" in context - assert not any("ref=head-sha" in target for target in calls) + assert not any(f"ref={head_sha}" in target for target in calls) + + +def test_fetch_changed_files_rejects_malformed_json_line(monkeypatch): + """A non-JSON line from the Files API must fail closed, not crash raw.""" + monkeypatch.setattr(noema, "run", lambda args, stdin=None: "not json\n") + + try: + noema.fetch_changed_files("owner/repo", 7) + except RuntimeError as exc: + assert "malformed" in str(exc) + else: + raise AssertionError("expected RuntimeError for malformed JSON line") + + +def test_fetch_changed_files_rejects_malformed_record_shape(monkeypatch): + """A well-formed JSON line that is not a two-element string pair must fail closed.""" + monkeypatch.setattr( + noema, "run", lambda args, stdin=None: json.dumps(["only-one-field"]) + "\n" + ) + + try: + noema.fetch_changed_files("owner/repo", 7) + except RuntimeError as exc: + assert "malformed" in str(exc) + else: + raise AssertionError("expected RuntimeError for malformed record shape") + + +def test_fetch_merge_base_sha_rejects_malformed_head_sha(): + """An invalid head SHA must be rejected before any network call is attempted.""" + try: + noema.fetch_merge_base_sha("owner/repo", "a" * 40, "not-a-sha") + except RuntimeError as exc: + assert "PR head SHA was unavailable or malformed" in str(exc) + else: + raise AssertionError("expected RuntimeError for malformed head SHA") + + +def test_fetch_merge_base_sha_rejects_malformed_compare_response(monkeypatch): + """A compare response lacking a valid merge-base SHA must fail closed.""" + monkeypatch.setattr(noema, "run", lambda args, stdin=None: "") + + try: + noema.fetch_merge_base_sha("owner/repo", "a" * 40, "b" * 40) + except RuntimeError as exc: + assert "did not contain a valid merge-base SHA" in str(exc) + else: + raise AssertionError("expected RuntimeError for malformed compare response") + + +def test_removed_file_context_section_without_merge_base_or_error(): + """No merge-base SHA and no recorded error must still be explicit, not silent.""" + context = noema.removed_file_context_section("owner/repo", "gone.py", "", "") + + assert "merge-base SHA unavailable for pre-deletion content" in context + + +def test_removed_file_context_section_empty_merge_base_content(monkeypatch): + """An empty (non-UTF-8-decodable) merge-base blob must be reported, not silently dropped.""" + monkeypatch.setattr(noema, "fetch_file_content_at_ref", lambda repo, path, ref: "") + + context = noema.removed_file_context_section("owner/repo", "gone.py", "c" * 40, "") + + assert "no UTF-8 text content available from merge-base content API" in context def test_removed_file_context_fails_closed_without_base_sha(monkeypatch): - """Missing base identity must be explicit and must not trigger a head fetch.""" + """Missing base identity must be explicit and must not trigger a content fetch.""" monkeypatch.setattr( noema, "fetch_changed_files", @@ -56,44 +131,49 @@ def test_removed_file_context_fails_closed_without_base_sha(monkeypatch): ) monkeypatch.setattr( noema, - "fetch_head_file_content", + "fetch_file_content_at_ref", lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("unexpected fetch")), ) - context = noema.changed_file_context("owner/repo", 7, "head-sha", "") + context = noema.changed_file_context("owner/repo", 7, "a" * 40, "") + + assert "PR base SHA was unavailable or malformed" in context + assert "Merge-base lookup unavailable" in context - assert "base SHA unavailable" in context +def test_removed_file_merge_base_content_failure_is_distinct_from_head_failure(monkeypatch): + """A merge-base content API failure must remain typed as merge-base evidence failure.""" + head_sha = "a" * 40 + base_sha = "b" * 40 + merge_base_sha = "c" * 40 -def test_removed_file_base_fetch_failure_is_distinct_from_head_failure(monkeypatch): - """A base-side API failure must remain typed as base evidence failure.""" monkeypatch.setattr( noema, "fetch_changed_files", lambda repo, number: [("gone.py", "removed")], ) + monkeypatch.setattr( + noema, "fetch_merge_base_sha", lambda repo, base, head: merge_base_sha + ) def fail_fetch(repo, path, ref): raise RuntimeError("HTTP 502: token ***") - monkeypatch.setattr(noema, "fetch_head_file_content", fail_fetch) + monkeypatch.setattr(noema, "fetch_file_content_at_ref", fail_fetch) - context = noema.changed_file_context( - "owner/repo", 7, "head-sha", "base-sha" - ) + context = noema.changed_file_context("owner/repo", 7, head_sha, base_sha) - assert "Unavailable from base content API" in context + assert "Unavailable from merge-base content API" in context assert "Unavailable from head content API" not in context def test_build_review_context_passes_live_base_ref(monkeypatch): """The GraphQL base identity must reach changed-file context construction.""" - observed: list[tuple[str, int, str, str]] = [] + observed: list[tuple[str, int, str, str, object]] = [] monkeypatch.setattr(noema, "review_thread_context", lambda pr: "") - monkeypatch.setattr(noema, "load_codegraph_context", lambda: "") - def fake_context(repo, number, head_sha, base_sha=""): - observed.append((repo, number, head_sha, base_sha)) + def fake_context(repo, number, head_sha, base_sha="", changed_files=None): + observed.append((repo, number, head_sha, base_sha, changed_files)) return "files" monkeypatch.setattr(noema, "changed_file_context", fake_context) @@ -104,5 +184,5 @@ def fake_context(repo, number, head_sha, base_sha=""): {"headRefOid": "head-sha", "baseRefOid": "base-sha"}, ) - assert observed == [("owner/repo", 7, "head-sha", "base-sha")] + assert observed == [("owner/repo", 7, "head-sha", "base-sha", None)] assert "## Changed file context\nfiles" in result diff --git a/tests/test_noema_review_gate.py b/tests/test_noema_review_gate.py index 43aaf46e81..a86ee3b499 100644 --- a/tests/test_noema_review_gate.py +++ b/tests/test_noema_review_gate.py @@ -1250,8 +1250,8 @@ def test_inspect_and_review_reports_stale_before_repair_retry_cleanly(monkeypatc monkeypatch.setattr(noema, "fetch_pr", lambda repo, number: pr) monkeypatch.setattr(noema, "current_actor", lambda: "noema") monkeypatch.setattr(noema, "fetch_diff", lambda repo, number: ("diff", False)) - monkeypatch.setattr(noema, "fetch_changed_file_paths", lambda repo, number: ["tool.py"]) - monkeypatch.setattr(noema, "build_review_context", lambda repo, number, value: "context") + monkeypatch.setattr(noema, "fetch_changed_files", lambda repo, number: [("tool.py", "modified")]) + monkeypatch.setattr(noema, "build_review_context", lambda repo, number, value, changed_files=None: "context") def fake_call_llm(*args, **kwargs): raise noema.StaleHeadDuringRepairRetryError( @@ -1694,15 +1694,15 @@ def test_current_actor_rejects_unbound_action_identity(monkeypatch, actor, insta noema.current_actor() -def test_review_context_builders_include_codegraph_threads_and_files(monkeypatch, tmp_path): +def test_review_context_builders_include_threads_and_files(monkeypatch, tmp_path): assert noema.truncate_text("abc", 10) == "abc" assert "truncated 2 characters" in noema.truncate_text("abcdef", 4) assert "missing PR head SHA" in noema.changed_file_context("owner/repo", 7, "") - original_fetch_paths = noema.fetch_changed_file_paths - monkeypatch.setattr(noema, "fetch_changed_file_paths", lambda repo, number: []) + original_fetch_changed_files = noema.fetch_changed_files + monkeypatch.setattr(noema, "fetch_changed_files", lambda repo, number: []) assert "no changed files" in noema.changed_file_context("owner/repo", 7, "head") - monkeypatch.setattr(noema, "fetch_changed_file_paths", original_fetch_paths) + monkeypatch.setattr(noema, "fetch_changed_files", original_fetch_changed_files) encoded = base64.b64encode(b"print('hello')\n").decode("ascii") calls = [] @@ -1711,7 +1711,10 @@ def fake_run(args, stdin=None): calls.append(args) target = args[2] if target.endswith("/files"): - return "src/a.py\nREADME.md\nempty.txt\n" + return "\n".join( + json.dumps([path, "modified"]) + for path in ("src/a.py", "README.md", "empty.txt") + ) + "\n" if "contents/src/a.py" in target: return encoded if "contents/README.md" in target: @@ -1721,9 +1724,6 @@ def fake_run(args, stdin=None): raise AssertionError(args) monkeypatch.setattr(noema, "run", fake_run) - codegraph_path = tmp_path / "codegraph.md" - codegraph_path.write_text("call graph: src/a.py -> tests", encoding="utf-8") - monkeypatch.setenv("NOEMA_CODEGRAPH_CONTEXT_PATH", str(codegraph_path)) pr = make_pr( headRefOid="head sha", reviewThreads={ @@ -1747,8 +1747,6 @@ def fake_run(args, stdin=None): context = noema.build_review_context("owner/repo", 7, pr) - assert "## CodeGraph context" in context - assert "call graph: src/a.py -> tests" in context assert "Thread open at src/a.py:3" in context assert "reviewer: check call site" in context assert "### src/a.py" in context @@ -1758,16 +1756,14 @@ def fake_run(args, stdin=None): assert any("/files" in call[2] for call in calls) -def test_review_context_reports_omitted_files_and_missing_codegraph(monkeypatch, tmp_path): - monkeypatch.delenv("NOEMA_CODEGRAPH_CONTEXT_PATH", raising=False) - assert noema.load_codegraph_context() == "" - - monkeypatch.setenv("NOEMA_CODEGRAPH_CONTEXT_PATH", str(tmp_path / "missing.md")) - assert "CodeGraph context unavailable" in noema.load_codegraph_context() - +def test_review_context_reports_omitted_files(monkeypatch, tmp_path): paths = [f"src/file_{index}.py" for index in range(noema.MAX_CONTEXT_FILES + 1)] - monkeypatch.setattr(noema, "fetch_changed_file_paths", lambda repo, number: paths) - monkeypatch.setattr(noema, "fetch_head_file_content", lambda repo, path, head_sha: "x") + monkeypatch.setattr( + noema, + "fetch_changed_files", + lambda repo, number: [(path, "modified") for path in paths], + ) + monkeypatch.setattr(noema, "fetch_file_content_at_ref", lambda repo, path, ref: "x") context = noema.changed_file_context("owner/repo", 7, "head") @@ -2007,8 +2003,8 @@ def test_inspect_and_review_skip_paths(monkeypatch): monkeypatch.setattr(noema, "fetch_pr", lambda repo, number: clean_pr) monkeypatch.setattr(noema, "current_actor", lambda: "noema") monkeypatch.setattr(noema, "fetch_diff", lambda repo, number: ("diff", False)) - monkeypatch.setattr(noema, "fetch_changed_file_paths", lambda repo, number: ["tool.py"]) - monkeypatch.setattr(noema, "build_review_context", lambda repo, number, pr: "context") + monkeypatch.setattr(noema, "fetch_changed_files", lambda repo, number: [("tool.py", "modified")]) + monkeypatch.setattr(noema, "build_review_context", lambda repo, number, pr, changed_files=None: "context") monkeypatch.setattr(noema, "call_llm", lambda *args, **kwargs: {"decision": "approve", "summary": "ok", "findings": []}) monkeypatch.setattr(noema, "submit_review", lambda *args, **kwargs: calls.append(args)) @@ -2048,8 +2044,8 @@ def test_inspect_and_review_does_not_wait_for_other_reviews_or_checks(monkeypatc monkeypatch.setattr(noema, "fetch_pr", lambda repo, number: pr) monkeypatch.setattr(noema, "current_actor", lambda: "noema") monkeypatch.setattr(noema, "fetch_diff", lambda repo, number: ("diff", False)) - monkeypatch.setattr(noema, "fetch_changed_file_paths", lambda repo, number: ["tool.py"]) - monkeypatch.setattr(noema, "build_review_context", lambda repo, number, value: "context") + monkeypatch.setattr(noema, "fetch_changed_files", lambda repo, number: [("tool.py", "modified")]) + monkeypatch.setattr(noema, "build_review_context", lambda repo, number, value, changed_files=None: "context") monkeypatch.setattr(noema, "call_llm", lambda *args, **kwargs: {"decision": "approve", "summary": "ok"}) monkeypatch.setattr(noema, "submit_review", lambda *args, **kwargs: calls.append(args)) @@ -2087,8 +2083,8 @@ def test_head_movement_stops_before_review_publication(monkeypatch): monkeypatch.setattr(noema, "fetch_pr", lambda repo, number: next(pull_requests)) monkeypatch.setattr(noema, "current_actor", lambda: "noema") monkeypatch.setattr(noema, "fetch_diff", lambda repo, number: ("diff", False)) - monkeypatch.setattr(noema, "fetch_changed_file_paths", lambda repo, number: ["tool.py"]) - monkeypatch.setattr(noema, "build_review_context", lambda repo, number, pr: "context") + monkeypatch.setattr(noema, "fetch_changed_files", lambda repo, number: [("tool.py", "modified")]) + monkeypatch.setattr(noema, "build_review_context", lambda repo, number, pr, changed_files=None: "context") monkeypatch.setattr( noema, "call_llm", @@ -2110,8 +2106,8 @@ def test_closed_during_model_stops_before_review_publication(monkeypatch): monkeypatch.setattr(noema, "fetch_pr", lambda repo, number: next(pull_requests)) monkeypatch.setattr(noema, "current_actor", lambda: "noema") monkeypatch.setattr(noema, "fetch_diff", lambda repo, number: ("diff", False)) - monkeypatch.setattr(noema, "fetch_changed_file_paths", lambda repo, number: ["tool.py"]) - monkeypatch.setattr(noema, "build_review_context", lambda repo, number, pr: "context") + monkeypatch.setattr(noema, "fetch_changed_files", lambda repo, number: [("tool.py", "modified")]) + monkeypatch.setattr(noema, "build_review_context", lambda repo, number, pr, changed_files=None: "context") monkeypatch.setattr(noema, "call_llm", lambda *args, **kwargs: {"decision": "approve"}) monkeypatch.setattr( noema, @@ -2129,8 +2125,8 @@ def test_uppercase_expected_head_is_not_stale_before_model_work(monkeypatch): monkeypatch.setattr(noema, "fetch_pr", lambda repo, number: pr) monkeypatch.setattr(noema, "current_actor", lambda: "noema") monkeypatch.setattr(noema, "fetch_diff", lambda repo, number: ("diff", False)) - monkeypatch.setattr(noema, "fetch_changed_file_paths", lambda repo, number: ["tool.py"]) - monkeypatch.setattr(noema, "build_review_context", lambda repo, number, value: "context") + monkeypatch.setattr(noema, "fetch_changed_files", lambda repo, number: [("tool.py", "modified")]) + monkeypatch.setattr(noema, "build_review_context", lambda repo, number, value, changed_files=None: "context") monkeypatch.setattr(noema, "call_llm", lambda *args, **kwargs: {"decision": "approve", "summary": "ok"}) calls = [] monkeypatch.setattr(noema, "submit_review", lambda *args, **kwargs: calls.append(args)) @@ -2146,8 +2142,8 @@ def test_uppercase_expected_head_is_not_stale_before_publication(monkeypatch): monkeypatch.setattr(noema, "fetch_pr", lambda repo, number: next(pull_requests)) monkeypatch.setattr(noema, "current_actor", lambda: "noema") monkeypatch.setattr(noema, "fetch_diff", lambda repo, number: ("diff", False)) - monkeypatch.setattr(noema, "fetch_changed_file_paths", lambda repo, number: ["tool.py"]) - monkeypatch.setattr(noema, "build_review_context", lambda repo, number, pr: "context") + monkeypatch.setattr(noema, "fetch_changed_files", lambda repo, number: [("tool.py", "modified")]) + monkeypatch.setattr(noema, "build_review_context", lambda repo, number, pr, changed_files=None: "context") monkeypatch.setattr( noema, "call_llm", @@ -2168,8 +2164,8 @@ def test_inspect_and_review_rechecks_head_before_publication(monkeypatch): monkeypatch.setattr(noema, "fetch_pr", lambda repo, number: next(responses)) monkeypatch.setattr(noema, "current_actor", lambda: "noema") monkeypatch.setattr(noema, "fetch_diff", lambda repo, number: ("diff", False)) - monkeypatch.setattr(noema, "fetch_changed_file_paths", lambda repo, number: ["tool.py"]) - monkeypatch.setattr(noema, "build_review_context", lambda repo, number, pr: "context") + monkeypatch.setattr(noema, "fetch_changed_files", lambda repo, number: [("tool.py", "modified")]) + monkeypatch.setattr(noema, "build_review_context", lambda repo, number, pr, changed_files=None: "context") monkeypatch.setattr(noema, "call_llm", lambda *args, **kwargs: {"decision": "approve"}) monkeypatch.setattr(noema, "submit_review", lambda *args, **kwargs: submitted.append(args)) diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 2854e0671f..027ff2d0dc 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -469,12 +469,12 @@ def test_opencode_ignores_superseded_cancelled_rollup_checks(): def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): """Keep PR-controlled test execution off the pull_request_target path.""" workflow = Path(".github/workflows/opencode-review-dispatch.yml").read_text(encoding="utf-8") - assert "required-workflow-bootstrap:" in workflow - assert "OpenCode repository-dispatch review run materialized." in workflow - bootstrap_start = workflow.index(" required-workflow-bootstrap:\n") - bootstrap_end = workflow.index("\n validate-pr-metadata:", bootstrap_start) - bootstrap_job = workflow[bootstrap_start:bootstrap_end] - assert "\n if:" not in bootstrap_job + # required-workflow-bootstrap is the trusted-source-resolution sentinel needed + # only where the org ruleset targets a pull_request_target entrypoint + # (opencode-review.yml). This repository_dispatch-only workflow is not itself + # a required-workflow path, so it must not carry a copy-pasted, need-less + # orphan of that job. + assert "required-workflow-bootstrap:" not in workflow assert ( "github.event.pull_request.head.repo.full_name == github.repository" not in workflow diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 9f81076199..ea41e46937 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -19,7 +19,7 @@ DOCTORING_RECORD = Path("docs/doctoring/hourly-nvidia-nim-autofix.md") CHANGELOG = Path("CHANGELOG.md") REVIEW_DISPATCH_WORKFLOW = Path(".github/workflows/opencode-review-dispatch.yml") -REVIEW_DISPATCH_BLOB_SHA = "3677f408bd6b99577fd7d5923fbd67c91f437ae0" +REVIEW_DISPATCH_BLOB_SHA = "0814541a9d79e72298fe4fea463224688bb6bd54" def _workflow_text(path: Path) -> str: