From 62b81cabd7137e5e54abc305b94fc780366632c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 05:57:51 +0000 Subject: [PATCH 1/9] chore(ci): remove orphaned required-workflow-bootstrap job from dispatch workflow The required-workflow-bootstrap job in opencode-review-dispatch.yml only echoed a materialization message and was never referenced by any needs: clause in the file. It looks copy-pasted from the real trust-boundary bootstrap job in the sibling pull_request_target-triggered opencode-review.yml, but this workflow fires only on repository_dispatch (an already-trusted, non-PR context) and is not itself a path the org required-workflow ruleset targets, so that pattern's reason for existing does not apply here. Update the two byte-for-byte blob-hash pins on this workflow file (tests/test_pr_review_autofix_nvidia_nim_contract.py, tests/test_opencode_rust_coverage_toolchain_contract.py reads the same constant) and the contract test that had pinned the orphaned job's presence, replacing it with a regression guard against its reintroduction. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015Gs7KmNvH75nxz1sL8mKjw --- .github/workflows/opencode-review-dispatch.yml | 6 ------ CHANGELOG.md | 7 +++++++ tests/test_opencode_agent_contract.py | 12 ++++++------ tests/test_pr_review_autofix_nvidia_nim_contract.py | 2 +- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 2aa245e7f2..2a8ab6f896 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 256b0cdf94..c6e94cfd9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- 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/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 79fdba39aa..ebdb5eb6fd 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 3dcfe2cdd8..61659716cd 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 = "2aa245e7f2a053a4c0b7a9cc8bac0d5d44d38092" +REVIEW_DISPATCH_BLOB_SHA = "2a8ab6f896be9935a7b4575515d9ac16372753dd" def _workflow_text(path: Path) -> str: From b00e374a983ef4899fbbfc0bab5d315a898b023a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 09:43:04 +0000 Subject: [PATCH 2/9] fix(ci): bound required-workflow-bootstrap awk extraction to its own job Ports the identical fix from #1506 into this branch. This PR's exact-head-path-policy check runs its own head-branch copy of scripts/ci/test_strix_quick_gate.sh (plain `pull_request` trigger in strix-changed-path-quality-ci.yml, not pull_request_target), so the pre-existing main-branch bug is not fixed here just by #1506 merging into main -- it needs porting into this branch directly. Root cause: assert_opencode_review_uses_codegraph_and_contextual_orchestrator extracted the required-workflow-bootstrap job block from opencode-review.yml with awk '/^ required-workflow-bootstrap:$/,/^[^ ]/'. Every job key in that workflow is indented 2 spaces (never column 0), so the end pattern never matched until EOF, sweeping an unrelated `if:` line from a later job (added by already-merged PR #1497) into the "block" and failing the assertion on unrelated content. Fixed by using an explicit state flag so the end pattern (`^ [A-Za-z0-9_-]+:`) is only tested starting on the line after the start match, correctly bounding the block to just its own lines. See ContextualWisdomLab/.github#1506 for the full root-cause writeup and validation against origin/main. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015Gs7KmNvH75nxz1sL8mKjw --- scripts/ci/test_strix_quick_gate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 4053f4fd53..1fc45a34b9 100644 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -522,7 +522,7 @@ assert_opencode_review_uses_codegraph_and_contextual_orchestrator() { assert_file_not_contains "$workflow_file" "Wait for trusted OpenCode approval review" "opencode pull_request bridge was removed to avoid duplicate required-check resource use" assert_file_not_contains "$workflow_file" "Trusted OpenCode requested changes for head" "opencode pull_request bridge no longer reconsumes stale trusted review state" assert_file_not_contains "$workflow_file" "github.event.pull_request.number == 240" "opencode review workflow must not hard-code repository-specific PR bypasses" - if awk '/^ required-workflow-bootstrap:$/,/^[^ ]/' "$bootstrap_file" | grep -q '^[[:space:]]*if:'; then + if awk '/^ required-workflow-bootstrap:$/{p=1; print; next} p && /^ [A-Za-z0-9_-]+:/{exit} p' "$bootstrap_file" | grep -q '^[[:space:]]*if:'; then record_failure "opencode required workflow bootstrap must not depend on required-workflow event payload fields" fi assert_file_contains "$workflow_file" 'github.event.client_payload.target_repository || github.repository' "opencode review scopes concurrency by target repository" From d8412543bc4b7014cf3b79d4fc3f64fe4c16e433 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 11:38:04 +0000 Subject: [PATCH 3/9] fix(ci): remove grep -q from test_strix_quick_gate.sh pipeline checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit grep -q exits on first match and closes its end of the pipe; if the upstream awk is still writing a large block, it gets SIGPIPE (141). Under `set -o pipefail` that non-zero awk status wins over grep's real 0, so `if pipeline; then` sees the pipeline as failed even though grep found a genuine match — silently missing e.g. a forbidden `if:` key or a fenced-diff marker that should have failed the check. Ports the same-file fix from PR #1506 to this branch's two call sites (required-workflow-bootstrap job-block check; opencode review REQUEST_CHANGES fenced-diff check). This branch already carried #1506's awk job-block-boundary correction, so only the grep -q removal was needed here. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015Gs7KmNvH75nxz1sL8mKjw --- scripts/ci/test_strix_quick_gate.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 1fc45a34b9..a92871b7ce 100644 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -522,7 +522,7 @@ assert_opencode_review_uses_codegraph_and_contextual_orchestrator() { assert_file_not_contains "$workflow_file" "Wait for trusted OpenCode approval review" "opencode pull_request bridge was removed to avoid duplicate required-check resource use" assert_file_not_contains "$workflow_file" "Trusted OpenCode requested changes for head" "opencode pull_request bridge no longer reconsumes stale trusted review state" assert_file_not_contains "$workflow_file" "github.event.pull_request.number == 240" "opencode review workflow must not hard-code repository-specific PR bypasses" - if awk '/^ required-workflow-bootstrap:$/{p=1; print; next} p && /^ [A-Za-z0-9_-]+:/{exit} p' "$bootstrap_file" | grep -q '^[[:space:]]*if:'; then + if awk '/^ required-workflow-bootstrap:$/{p=1; print; next} p && /^ [A-Za-z0-9_-]+:/{exit} p' "$bootstrap_file" | grep '^[[:space:]]*if:' >/dev/null; then record_failure "opencode required workflow bootstrap must not depend on required-workflow event payload fields" fi assert_file_contains "$workflow_file" 'github.event.client_payload.target_repository || github.repository' "opencode review scopes concurrency by target repository" @@ -1501,7 +1501,7 @@ assert_opencode_review_posts_suggested_diffs_inline() { assert_file_contains "$workflow_file" "publish_request_changes_from_control" "opencode review REQUEST_CHANGES path publishes findings from the control JSON" if awk '/format_request_changes_body\(\)/,/build_request_changes_review_payload\(\)/ { print }' "$workflow_file" | - grep -Fq '```diff'; then + grep -F '```diff' >/dev/null; then record_failure "opencode review PR-level REQUEST_CHANGES body must not contain fenced suggested diffs" fi } From db106d50f2134ece147bc5318e389aeb124d198c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 07:21:06 +0000 Subject: [PATCH 4/9] test(ci): close main's post-#1546 scheduler coverage regression Protected main regressed to 99% scripts/ci coverage after #1546 added live_head_matches, a no-active/no-stale fall-through in prepare_autofix_slot, and an "already queued or running" wait branch to pr_review_fix_scheduler.py without covering them, while the pre-existing inspect_pr conflicted-draft/conflicted-unauthorized returns and pr_review_merge_scheduler.py's fetch_workflow_names_by_check_suite_rest pagination/filtering/ permission-denied paths stayed untested. Every PR rebasing onto main inherits this via the coverage-evidence required check regardless of its own diff. Test-only change; no production code touched. --- CHANGELOG.md | 9 +++ tests/test_pr_review_fix_scheduler.py | 49 ++++++++++++++ ...ew_fix_scheduler_rest_workflow_identity.py | 67 +++++++++++++++++++ 3 files changed, 125 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5810d5308..1c46c64657 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ 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; this adds + test-only coverage for all of the above with no production code change. - Avoid redundant merge-scheduler wakes when the trusted receipt predicate already finds a substantive exact-head OpenCode verdict. Missing, stale, or fallback-only evidence still dispatches review work, while receipt lookup or diff --git a/tests/test_pr_review_fix_scheduler.py b/tests/test_pr_review_fix_scheduler.py index 9860eeaec7..f6abd64b0f 100644 --- a/tests/test_pr_review_fix_scheduler.py +++ b/tests/test_pr_review_fix_scheduler.py @@ -177,6 +177,40 @@ def test_prepare_autofix_slot_preserves_new_head_workers_after_head_advance(monk workflow_repository=fix.DEFAULT_AUTOFIX_REPOSITORY, dry_run=False, ) is None + + +def test_prepare_autofix_slot_returns_directly_with_no_active_or_stale_runs(monkeypatch): + """An empty Actions run list needs no reconciliation and skips cancellation.""" + monkeypatch.setattr(fix, "run_json", lambda _args: {"workflow_runs": []}) + monkeypatch.setattr( + fix, + "force_cancel_workflow_runs", + lambda *_args: pytest.fail("no stale runs must not attempt cancellation"), + ) + + assert fix.prepare_autofix_slot( + "owner/repo", + make_pr(), + workflow=fix.DEFAULT_AUTOFIX_WORKFLOW, + workflow_repository=fix.DEFAULT_AUTOFIX_REPOSITORY, + dry_run=False, + ) is False + + +def test_live_head_matches_compares_case_insensitively_and_fails_closed(monkeypatch): + """Live head lookup normalizes case and rejects malformed or mismatched payloads.""" + head = "a" * 40 + + monkeypatch.setattr(fix, "run_json", lambda _args: {"head": {"sha": head.upper()}}) + assert fix.live_head_matches("owner/repo", make_pr(headRefOid=head)) + + monkeypatch.setattr(fix, "run_json", lambda _args: {"head": {"sha": "b" * 40}}) + assert not fix.live_head_matches("owner/repo", make_pr(headRefOid=head)) + + monkeypatch.setattr(fix, "run_json", lambda _args: {"nothead": {}}) + assert not fix.live_head_matches("owner/repo", make_pr(headRefOid=head)) + + def test_terminal_failed_check_triggers_rca_without_prior_opencode_review(): """Exact-head check evidence can start RCA without a circular review prerequisite.""" pr = make_pr( @@ -1329,6 +1363,21 @@ def test_fix_inspect_skip_wait_and_error_paths(monkeypatch): monkeypatch.setattr(fix, "issue_comments", lambda repo, number: [{"body": f"{fix.FIX_MARKER} head_sha={'a' * 40} epoch={int(time.time())} -->"}]) assert fix.inspect_pr("owner/repo", make_pr(), args) == ("wait", ("recent autofix marker exists for this head",)) + assert fix.inspect_pr( + "owner/repo", make_pr(mergeStateStatus="DIRTY", isDraft=True), args + ) == ("skip", ("draft PR",)) + assert fix.inspect_pr("owner/repo", make_pr(mergeStateStatus="DIRTY"), args) == ( + "skip", + ("merge conflict is not authorized for repair",), + ) + + monkeypatch.setattr(fix, "issue_comments", lambda repo, number: []) + monkeypatch.setattr(fix, "prepare_autofix_slot", lambda *_args, **_kwargs: True) + assert fix.inspect_pr("owner/repo", make_pr(), args) == ( + "wait", + ("current-head autofix run is already queued or running",), + ) + pr1 = make_pr(number=1) pr2 = make_pr(number=2) monkeypatch.setattr(fix, "fetch_open_prs", lambda repo, max_prs: [pr1, pr2]) diff --git a/tests/test_pr_review_fix_scheduler_rest_workflow_identity.py b/tests/test_pr_review_fix_scheduler_rest_workflow_identity.py index c24cfb05f9..f261ce5beb 100644 --- a/tests/test_pr_review_fix_scheduler_rest_workflow_identity.py +++ b/tests/test_pr_review_fix_scheduler_rest_workflow_identity.py @@ -154,3 +154,70 @@ def fake_api(path: str) -> Any: assert merge.is_strix_context(context) assert merge.strix_evidence_state(pr) == expected_state assert fix.current_head_failed_checks(pr) == () + + +def test_fetch_workflow_names_by_check_suite_rest_paginates_past_100( + monkeypatch: Any, +) -> None: + """A first page of exactly 100 runs must fetch a second page and merge both.""" + head_sha = "e" * 40 + page1 = [ + {"check_suite_id": i, "name": f"workflow-{i}"} for i in range(100) + ] + page2 = [{"check_suite_id": 100, "name": "workflow-100"}] + calls: list[str] = [] + + def fake_api(path: str) -> Any: + calls.append(path) + if path.endswith("page=1"): + return {"workflow_runs": page1} + if path.endswith("page=2"): + return {"workflow_runs": page2} + raise AssertionError(f"unexpected path {path}") + + monkeypatch.setattr(merge, "gh_api_json", fake_api) + + names = merge.fetch_workflow_names_by_check_suite_rest("owner/repo", head_sha) + + assert names == {i: f"workflow-{i}" for i in range(101)} + assert calls == [ + f"repos/owner/repo/actions/runs?head_sha={head_sha}&per_page=100&page=1", + f"repos/owner/repo/actions/runs?head_sha={head_sha}&per_page=100&page=2", + ] + + +def test_fetch_workflow_names_by_check_suite_rest_skips_entries_missing_suite_id_or_name( + monkeypatch: Any, +) -> None: + """A run with no check-suite id or a blank name must not populate the map.""" + head_sha = "f" * 40 + + def fake_api(path: str) -> Any: + return { + "workflow_runs": [ + {"check_suite_id": None, "name": "orphaned run"}, + {"check_suite_id": 900, "name": ""}, + {"check_suite_id": 901, "name": "kept run"}, + ] + } + + monkeypatch.setattr(merge, "gh_api_json", fake_api) + + names = merge.fetch_workflow_names_by_check_suite_rest("owner/repo", head_sha) + + assert names == {901: "kept run"} + + +def test_fetch_workflow_names_by_check_suite_rest_propagates_non_access_errors( + monkeypatch: Any, +) -> None: + """A page-fetch failure unrelated to integration access must fail closed.""" + head_sha = "0" * 40 + + def fake_api(path: str) -> Any: + raise RuntimeError("gh: HTTP 502 (exhausted retries)") + + monkeypatch.setattr(merge, "gh_api_json", fake_api) + + with pytest.raises(RuntimeError, match="HTTP 502"): + merge.fetch_workflow_names_by_check_suite_rest("owner/repo", head_sha) From 85c2469e1e624f8d4dfbc71c79fe18c927d315ab Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 07:38:38 +0000 Subject: [PATCH 5/9] docs(gap-baseline): record post-#1546 scheduler coverage regression Adds a dated traceability entry for the coverage gap this PR closes: root cause (#1546's uncovered additions plus the older #1547/#1551/ #1554 gap, neither of which merged or transfers evidence here), the fix and its verification, the resolved Devin false-positive on sub-clause coverage, and the known pre-existing SIGPIPE test flake left unremediated as out of scope. --- docs/product-technical-gap-baseline.md | 48 ++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 76d85b949b..812f068e34 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. + ## 5. 실행 루프와 고객의 다음 행동 각 hourly pass는 아래 순서를 유지한다. From 6f40a0637da94da60f43ca72086d27e1034e8bbc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 16:46:11 +0900 Subject: [PATCH 6/9] test(ci): document nested REST fixture helpers Raise scoped docstring coverage for the newly added scheduler REST regression helpers to 100% without changing test behavior or production code. --- tests/test_pr_review_fix_scheduler_rest_workflow_identity.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_pr_review_fix_scheduler_rest_workflow_identity.py b/tests/test_pr_review_fix_scheduler_rest_workflow_identity.py index f261ce5beb..4e36544061 100644 --- a/tests/test_pr_review_fix_scheduler_rest_workflow_identity.py +++ b/tests/test_pr_review_fix_scheduler_rest_workflow_identity.py @@ -168,6 +168,7 @@ def test_fetch_workflow_names_by_check_suite_rest_paginates_past_100( calls: list[str] = [] def fake_api(path: str) -> Any: + """Return deterministic paginated workflow-run fixtures.""" calls.append(path) if path.endswith("page=1"): return {"workflow_runs": page1} @@ -193,6 +194,7 @@ def test_fetch_workflow_names_by_check_suite_rest_skips_entries_missing_suite_id head_sha = "f" * 40 def fake_api(path: str) -> Any: + """Return workflow runs that exercise incomplete-identity filtering.""" return { "workflow_runs": [ {"check_suite_id": None, "name": "orphaned run"}, @@ -215,6 +217,7 @@ def test_fetch_workflow_names_by_check_suite_rest_propagates_non_access_errors( head_sha = "0" * 40 def fake_api(path: str) -> Any: + """Simulate a non-access REST failure that must propagate.""" raise RuntimeError("gh: HTTP 502 (exhausted retries)") monkeypatch.setattr(merge, "gh_api_json", fake_api) From febf09717dd39bb647655309ad9032277274afa1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 07:58:51 +0000 Subject: [PATCH 7/9] fix(ci): repair review-dispatch blob pin after merging current main --- tests/test_pr_review_autofix_nvidia_nim_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: From 0be68ade93ebc631dc41ace75c95286477d008cb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 17:01:37 +0900 Subject: [PATCH 8/9] fix(tests): drain dispatch fixture stdin to break CI dependency cycle RCA: the #1567 exact-head Hourly NVIDIA NIM Review Repair run failed in test_scheduler_wake_reuses_trusted_receipt_predicate with exit 141. The production block pipes jq JSON into gh api --input -, while the test fake exited without reading stdin. Under pipefail that can SIGPIPE jq. Reuse the already RED/GREEN-verified #1569 fixture blob and drain stdin before recording the fake dispatch. This makes #1567 self-contained so the central 100% coverage repair no longer depends on a separate PR that itself inherits the coverage failure. --- tests/test_opencode_required_verdict_regression.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_opencode_required_verdict_regression.py b/tests/test_opencode_required_verdict_regression.py index 8f8047ff10..0e5d30805b 100644 --- a/tests/test_opencode_required_verdict_regression.py +++ b/tests/test_opencode_required_verdict_regression.py @@ -173,6 +173,7 @@ def test_scheduler_wake_reuses_trusted_receipt_predicate( elif [[ "$*" == *"/pulls/7/reviews"* ]]; then printf '[%s]' "$FAKE_REVIEWS" elif [[ "$*" == *"repos/ContextualWisdomLab/.github/dispatches"* ]]; then + cat >/dev/null printf 'dispatch\n' >>"$DISPATCH_CALLS" fi """, From 665946bc4db2985cabf557cf1b3089c269d6ab39 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 12:04:23 +0000 Subject: [PATCH 9/9] test(ci,noema): port stale-test fixes from #1598 (post-#1587/#1564) Merging current main pulled in #1587 (free-pool admission filtering) and #1564 (merge-base-anchored deleted-file review evidence), both of which left pre-existing tests referencing removed/renamed names or excluded fixtures broken. Port the same fix already opened as its own dedicated PR (#1598) rather than widening this PR's own scope: - tests/test_contextual_orchestrator_review_policy.py: swap the stale "openai" free-pool fixture to "bytez" in test_build_catalog_applies_account_cap and test_build_catalog_respects_limit. - tests/test_noema_review_gate.py: rename fetch_changed_file_paths call sites to fetch_changed_files with (path, status) tuples; accept the new changed_files parameter in build_review_context mocks; drop the two CodeGraph-only assertions/tests for the removed function. - tests/test_noema_removed_file_context.py: rewrite against the real run() JSON-per-line contract, fetch_merge_base_sha's SHA validation, and fetch_file_content_at_ref; add direct coverage for the malformed-input and empty-content branches #1564 introduced. Full suite: 2318 passed, 100% branch coverage, 100% docstrings. --- ...t_contextual_orchestrator_review_policy.py | 6 +- tests/test_noema_removed_file_context.py | 132 ++++++++++++++---- tests/test_noema_review_gate.py | 66 ++++----- 3 files changed, 140 insertions(+), 64 deletions(-) diff --git a/tests/test_contextual_orchestrator_review_policy.py b/tests/test_contextual_orchestrator_review_policy.py index b10fc4a0b9..e61ca70f4e 100644 --- a/tests/test_contextual_orchestrator_review_policy.py +++ b/tests/test_contextual_orchestrator_review_policy.py @@ -354,7 +354,7 @@ def test_build_catalog_applies_account_cap() -> None: for i in range(6) ] + [ - {"provider": "openai", "model": f"o{i}", "agent_id": f"oa_{i}", "is_free": True, **FREE_PRICE} + {"provider": "bytez", "model": f"b{i}", "agent_id": f"bytez_{i}", "is_free": True, **FREE_PRICE} for i in range(3) ] } @@ -367,14 +367,14 @@ def test_build_catalog_applies_account_cap() -> None: account_counts[account] = account_counts.get(account, 0) + 1 assert account_counts["nvidia_nim"] == 2 assert account_counts["nvidia_nim_sub"] == 2 - assert account_counts["openai"] == 2 + assert account_counts["bytez"] == 2 def test_build_catalog_respects_limit() -> None: """The catalog never exceeds the configured agent limit.""" report = { "models": [ - {"provider": "openai", "model": f"m{i}", "agent_id": f"oa_{i}", "is_free": True, **FREE_PRICE} + {"provider": "bytez", "model": f"m{i}", "agent_id": f"bytez_{i}", "is_free": True, **FREE_PRICE} for i in range(20) ] } 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))