From 23307c6442bf60fa602be39d6016c0ee3c20e1de Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 26 Oct 2026 08:31:54 +0900 Subject: [PATCH] fix(noema): preserve bounded complete personal-auto review input --- .../agent-review-runtime-quality-ci.yml | 9 +++ scripts/ci/noema_review_gate.py | 30 +++++++- ...nt_review_runtime_quality_consolidation.py | 16 +++++ tests/test_noema_complete_auto_input.py | 70 +++++++++++++++++++ 4 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 tests/test_noema_complete_auto_input.py diff --git a/.github/workflows/agent-review-runtime-quality-ci.yml b/.github/workflows/agent-review-runtime-quality-ci.yml index 38f617e7d6..1e3db8bde3 100644 --- a/.github/workflows/agent-review-runtime-quality-ci.yml +++ b/.github/workflows/agent-review-runtime-quality-ci.yml @@ -67,6 +67,7 @@ on: - "scripts/ci/zdr_policy.py" - "scripts/ci/contextual_orchestrator_review_policy.py" - "scripts/ci/contextual_orchestrator_review_launcher.py" + - "scripts/ci/noema_review_gate.py" - "scripts/ci/personal_review_route.py" - "scripts/ci/contextual_orchestrator_review_sidecar.sh" - "tests/test_zdr_policy.py" @@ -74,6 +75,7 @@ on: - "tests/test_contextual_orchestrator_review_launcher.py" - "tests/test_review_auto_route.py" - "tests/test_personal_review_route.py" + - "tests/test_noema_complete_auto_input.py" - "tests/test_tokyo_review_consumer.py" - "tests/test_review_auto_cost_admission.py" - "tests/test_review_ai_app_independence.py" @@ -221,6 +223,10 @@ jobs: noema_suite=true review_repair_suite=true ;; + scripts/ci/noema_review_gate.py) + noema_suite=true + review_repair_suite=true + ;; .github/workflows/noema-review.yml|\ .github/actions/noema-review/two_phase.py|\ tests/test_noema_reviewer_token_lifetime.py|\ @@ -304,6 +310,7 @@ jobs: tests/test_contextual_orchestrator_review_launcher.py|\ tests/test_review_auto_route.py|\ tests/test_personal_review_route.py|\ + tests/test_noema_complete_auto_input.py|\ tests/test_tokyo_review_consumer.py|\ tests/test_review_auto_cost_admission.py|\ tests/test_review_ai_app_independence.py|\ @@ -504,6 +511,7 @@ jobs: tests/test_contextual_orchestrator_review_launcher.py \ tests/test_review_auto_route.py \ tests/test_personal_review_route.py \ + tests/test_noema_complete_auto_input.py \ tests/test_tokyo_review_consumer.py \ tests/test_review_auto_cost_admission.py \ tests/test_review_ai_app_independence.py \ @@ -550,6 +558,7 @@ jobs: tests/test_contextual_orchestrator_review_launcher.py \ tests/test_review_auto_route.py \ tests/test_personal_review_route.py \ + tests/test_noema_complete_auto_input.py \ tests/test_tokyo_review_consumer.py \ tests/test_review_auto_cost_admission.py \ tests/test_review_ai_app_independence.py \ diff --git a/scripts/ci/noema_review_gate.py b/scripts/ci/noema_review_gate.py index 156f5aafa3..8e9c12c95e 100644 --- a/scripts/ci/noema_review_gate.py +++ b/scripts/ci/noema_review_gate.py @@ -66,6 +66,20 @@ # noema_review_handoff.py. NOEMA_REVIEW_BODY_HEAD_RE = re.compile(r"^- Head SHA:\s*`([0-9a-fA-F]{40})`$", re.MULTILINE) MAX_DIFF_CHARS = 60000 +# Personal auto advertises 500,000 input tokens. Bound the complete JSON +# request conservatively by UTF-8 bytes (including schema/envelope), reserving +# 8,192 for chat framing. Do not apply this route's capacity to other models. +MAX_AUTO_REVIEW_REQUEST_BYTES = 500000 - 8192 + + +def complete_auto_input() -> bool: + """Select only the existing personal-auto endpoint, not arbitrary aliases.""" + return ( + os.environ.get("NOEMA_LLM_MODEL", "").strip() == "auto" + and os.environ.get("NOEMA_LLM_API_URL", "").strip() + == "https://litellm.poinnetworks.net/v1/chat/completions" + ) + MAX_CONTEXT_FILES = 12 MAX_FILE_CONTEXT_CHARS = 4000 MAX_REVIEW_CONTEXT_CHARS = 24000 @@ -542,6 +556,10 @@ def require_distinct_pr_author(pr: dict[str, Any], actor: str) -> str: def fetch_diff(repo: str, number: int) -> tuple[str, bool]: """Fetch the PR diff and truncate it to the bounded LLM prompt size.""" diff = run(["gh", "api", f"repos/{repo}/pulls/{number}", "-H", "Accept: application/vnd.github.v3.diff"]) + if complete_auto_input(): + if len(diff.encode("utf-8")) > MAX_AUTO_REVIEW_REQUEST_BYTES: + raise RuntimeError("Noema complete review input exceeds personal auto budget") + return diff, False truncated = len(diff) > MAX_DIFF_CHARS if truncated: marker = "[overlong changed line content omitted]" @@ -1625,6 +1643,8 @@ def call_llm( "Noema LLM review unavailable: NOEMA_LLM_API_URL or NOEMA_LLM_API_KEY is not configured." ) reject_private_llm_url(api_url) + if complete_auto_input() and truncated: + raise RuntimeError("Noema complete review input requires an untruncated diff") allowed_locations = [ {"path": path, "line": line, "side": side} @@ -1633,7 +1653,10 @@ def call_llm( location_example = allowed_locations[0] if allowed_locations else { "path": "path", "line": 0, "side": "RIGHT" } - allowed_locations_json = _bounded_allowed_locations_json(allowed_locations) + allowed_locations_json = ( + json.dumps({"locations": allowed_locations, "truncated": False}, separators=(",", ":")) + if complete_auto_input() else _bounded_allowed_locations_json(allowed_locations) + ) prompt = { "role": "user", "content": "\n".join( @@ -1668,9 +1691,12 @@ def call_llm( prompt, ], } + payload_bytes = json.dumps(payload).encode("utf-8") + if complete_auto_input() and len(payload_bytes) > MAX_AUTO_REVIEW_REQUEST_BYTES: + raise RuntimeError("Noema complete review input exceeds personal auto budget") request = urllib.request.Request( api_url, - data=json.dumps(payload).encode("utf-8"), + data=payload_bytes, headers={ "authorization": f"Bearer {api_key}", "content-type": "application/json", diff --git a/tests/test_agent_review_runtime_quality_consolidation.py b/tests/test_agent_review_runtime_quality_consolidation.py index 4fecc93656..3bfa04ad5b 100644 --- a/tests/test_agent_review_runtime_quality_consolidation.py +++ b/tests/test_agent_review_runtime_quality_consolidation.py @@ -148,6 +148,8 @@ def test_ghas_runtime_changes_start_the_quality_runner(changed_path: str) -> Non @pytest.mark.parametrize( ("changed_path", "expected_suites"), ( + ("tests/test_noema_complete_auto_input.py", {"review_repair"}), + ("scripts/ci/noema_review_gate.py", {"noema", "review_repair"}), ("scripts/ci/codeql_ghas_configuration_identity.py", {"ghas"}), ("tests/test_codeql_ghas_configuration_identity.py", {"ghas"}), ("tests/test_codeql_ghas_analyses_pagination.py", {"ghas"}), @@ -385,3 +387,17 @@ def test_exact_artifact_suite_preserves_version_and_quality_contracts() -> None: assert "outputs.exact_artifact == 'true'" in workflow assert "--include=scripts/ci/verify_exact_artifact_sbom_handoff.py" in workflow assert "interrogate --fail-under=100" in workflow + + +def test_complete_serializer_enrollment(): + workflow = _workflow_text() + path = "tests/test_noema_complete_auto_input.py" + trigger = workflow.split("on:\n", 1)[1].split("\nconcurrency:\n", 1)[0] + assert f' - "{path}"' in trigger + assert workflow.count(path) == 4 + step = next(part for part in workflow.split(' - name: ') + if 'python -m pytest -q' in part and path in part) + pytest_args = step.split('python -m pytest -q', 1)[1].split('python -m interrogate', 1)[0] + compile_args = step.split('python -m compileall -q', 1)[1] + assert pytest_args.count(path) == 1 + assert compile_args.count(path) == 1 diff --git a/tests/test_noema_complete_auto_input.py b/tests/test_noema_complete_auto_input.py new file mode 100644 index 0000000000..543ce8ba68 --- /dev/null +++ b/tests/test_noema_complete_auto_input.py @@ -0,0 +1,70 @@ +"""Complete source admission through the real Noema serializer (no network).""" +import json +from unittest.mock import patch +import pytest +from scripts.ci import noema_review_gate as gate + + +def test_real_serializer_preserves_all_locations(monkeypatch): + monkeypatch.setenv('NOEMA_LLM_MODEL', 'auto') + monkeypatch.setenv('NOEMA_LLM_API_URL', 'https://litellm.poinnetworks.net/v1/chat/completions') + monkeypatch.setenv('NOEMA_LLM_API_KEY', 'inert-test-value') + monkeypatch.setattr(gate, 'reject_private_llm_url', lambda _: None) + diff = 'diff --git a/a.py b/a.py\n--- a/a.py\n+++ b/a.py\n@@ -0,0 +1,1000 @@\n' + '+line\n'*1000 + class Captured(BaseException): + pass + requests = [] + def capture(request): + requests.append(request.data) + raise Captured() + with patch.object(gate.urllib.request, 'build_opener') as opener: + opener.return_value.open.side_effect = capture + with pytest.raises(Captured): + gate.call_llm('o/r', 1, {}, diff, False, 'a'*40) + payload = json.loads(requests[0]) + prompt = payload['messages'][1]['content'] + locations = json.loads(next(line.split(': ', 1)[1] for line in prompt.splitlines() + if line.startswith('Allowed changed-side locations: '))) + assert not locations['truncated'] + assert len(locations['locations']) == 1000 + assert prompt.endswith(diff) + assert len(requests[0]) <= gate.MAX_AUTO_REVIEW_REQUEST_BYTES + + +def test_auto_fetch_preserves_complete_diff(monkeypatch): + monkeypatch.setenv('NOEMA_LLM_MODEL', 'auto') + monkeypatch.setenv('NOEMA_LLM_API_URL', 'https://litellm.poinnetworks.net/v1/chat/completions') + diff = 'diff --git a/a.py b/a.py\n--- a/a.py\n+++ b/a.py\n@@ -0,0 +1,2 @@\n+' + 'a'*61000 + '\n+final_source_line\n' + monkeypatch.setattr(gate, 'run', lambda _: diff) + assert gate.fetch_diff('o/r', 1) == (diff, False) + + +def test_real_serializer_rejects_oversized_auto_before_network(monkeypatch): + monkeypatch.setenv('NOEMA_LLM_MODEL', 'auto') + monkeypatch.setenv('NOEMA_LLM_API_URL', 'https://litellm.poinnetworks.net/v1/chat/completions') + monkeypatch.setenv('NOEMA_LLM_API_KEY', 'inert-test-value') + monkeypatch.setattr(gate, 'reject_private_llm_url', lambda _: None) + with patch.object(gate.urllib.request, 'build_opener') as opener: + with pytest.raises(RuntimeError, match='complete review input exceeds'): + gate.call_llm('o/r', 1, {'headRefOid':'a'*40}, '+'+'x'*500000, False, 'a'*40) + opener.assert_not_called() + + +@pytest.mark.parametrize('model,url', [('auto','https://example.com/v1/chat/completions'), ('orchestrator/auto','https://litellm.poinnetworks.net/v1/chat/completions')]) +def test_other_routes_keep_existing_bound(monkeypatch, model, url): + monkeypatch.setenv('NOEMA_LLM_MODEL', model) + monkeypatch.setenv('NOEMA_LLM_API_URL', url) + monkeypatch.setattr(gate, 'run', lambda _: 'x'*61000) + text, truncated = gate.fetch_diff('o/r', 1) + assert truncated and len(text) <= gate.MAX_DIFF_CHARS + + +def test_auto_rejects_partial_input(monkeypatch): + monkeypatch.setenv('NOEMA_LLM_MODEL', 'auto') + monkeypatch.setenv('NOEMA_LLM_API_URL', 'https://litellm.poinnetworks.net/v1/chat/completions') + monkeypatch.setenv('NOEMA_LLM_API_KEY', 'inert-test-value') + monkeypatch.setattr(gate, 'reject_private_llm_url', lambda _: None) + with patch.object(gate.urllib.request, 'build_opener') as opener: + with pytest.raises(RuntimeError, match='untruncated'): + gate.call_llm('o/r', 1, {}, 'partial', True, 'a'*40) + opener.assert_not_called()