The model now sees the complete new version of every changed file, because a constraint in the header of tools/deploy.sh was invisible in the diff and three models missed its violation. werkrevisor reads the files at the head commit through the Gitea API and appends them, line-numbered, to the diff that the PR-Agent reviewer puts into the user prompt. Files beyond max_full_file_chars or max_full_files_chars are left out with a YELLOW reason, and the cost estimate includes the full files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
159 lines
6.2 KiB
Python
159 lines
6.2 KiB
Python
"""Runs the real PR-Agent 0.47.0 review code with a fake Gitea provider and a fake model, fully offline."""
|
|
|
|
import unittest
|
|
from types import SimpleNamespace
|
|
|
|
from tests.fakes import HEAD, config, pr_ref
|
|
|
|
try:
|
|
import pr_agent.git_providers as providers
|
|
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
|
from pr_agent.algo.run_details import record_ai_call
|
|
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
|
|
from pr_agent.git_providers.git_provider import GitProvider, IncrementalPR
|
|
from pr_agent.git_providers.gitea_provider import GiteaProvider, _GiteaCommitAdapter
|
|
HAVE_PR_AGENT = True
|
|
except ImportError:
|
|
HAVE_PR_AGENT = False
|
|
|
|
ANSWER = """```yaml
|
|
review:
|
|
estimated_effort_to_review_[1-5]: |
|
|
1
|
|
risk_level: |
|
|
low
|
|
merge_recommendation: |
|
|
changes_required
|
|
relevant_tests: |
|
|
No
|
|
key_issues_to_review:
|
|
- relevant_file: |
|
|
src/a.py
|
|
issue_header: |
|
|
Possible Bug
|
|
issue_content: |
|
|
Division by zero when n is 0.
|
|
start_line: 2
|
|
end_line: 2
|
|
security_concerns: |
|
|
No
|
|
```"""
|
|
|
|
PATCH = "@@ -1,1 +1,2 @@\n def f(n):\n+ return 1 / n\n"
|
|
|
|
|
|
def _patch_info(name, patch):
|
|
return FilePatchInfo(base_file="def f(n):\n", head_file="def f(n):\n return 1 / n\n", patch=patch,
|
|
filename=name, edit_type=EDIT_TYPE.MODIFIED, num_plus_lines=1, num_minus_lines=0)
|
|
|
|
|
|
def fake_provider_class(files):
|
|
class FakeGiteaProvider(GiteaProvider):
|
|
def __init__(self, url=None):
|
|
GitProvider.__init__(self)
|
|
self.pr_url = url
|
|
self.base_url = "https://git.example.org"
|
|
self.owner, self.repo, self.pr_number = "team", "app", 7
|
|
self.pr = SimpleNamespace(title="Add f", body="Adds f.", labels=[], html_url=url,
|
|
head=SimpleNamespace(sha=HEAD, ref="feature"),
|
|
base=SimpleNamespace(sha="b" * 40, ref="main"))
|
|
self.sha = HEAD
|
|
self.git_files = [{"filename": f.filename, "status": "modified"} for f in files]
|
|
self.diff_files = list(files)
|
|
self.incremental = IncrementalPR(False)
|
|
self.last_commit = self.last_commit_id = _GiteaCommitAdapter({"sha": HEAD})
|
|
self.enabled_pr, self.enabled_issue = True, False
|
|
self.max_comment_chars = 65000
|
|
|
|
def get_languages(self):
|
|
return {"Python": 100}
|
|
|
|
def get_commit_messages(self):
|
|
return "Add f"
|
|
|
|
def get_diff_files(self):
|
|
return self.diff_files
|
|
|
|
def publish_comment(self, *args, **kwargs):
|
|
raise AssertionError("publish_output is false, nothing may be published")
|
|
|
|
return FakeGiteaProvider
|
|
|
|
|
|
def fake_handler_class(answer, prompts):
|
|
class FakeHandler(BaseAiHandler):
|
|
def __init__(self):
|
|
pass
|
|
|
|
@property
|
|
def deployment_id(self):
|
|
return None
|
|
|
|
async def chat_completion(self, model, system, user, temperature=0.2, img_path=None):
|
|
prompts.append((model, system, user))
|
|
record_ai_call({"prompt_tokens": 1234, "completion_tokens": 210, "total_tokens": 1444}, model=model)
|
|
return answer, "stop"
|
|
|
|
return FakeHandler
|
|
|
|
|
|
@unittest.skipUnless(HAVE_PR_AGENT, "runs inside the PR-Agent image")
|
|
class PrAgentRunTest(unittest.TestCase):
|
|
def setUp(self):
|
|
from werkrevisor.redact import Redactor
|
|
from werkrevisor.review import configure_logging
|
|
configure_logging(Redactor(), "CRITICAL")
|
|
|
|
def run_review(self, files, answer=ANSWER, section="", **config_overrides):
|
|
from werkrevisor.review import run_model_review
|
|
prompts = []
|
|
original = providers._GIT_PROVIDERS["gitea"]
|
|
providers._GIT_PROVIDERS["gitea"] = fake_provider_class(files)
|
|
try:
|
|
run = run_model_review(config(**config_overrides), pr_ref(), "t" * 20, "k" * 20, "Check X.", section,
|
|
ai_handler=fake_handler_class(answer, prompts))
|
|
finally:
|
|
providers._GIT_PROVIDERS["gitea"] = original
|
|
return run, prompts
|
|
|
|
def test_review_result_usage_and_prompt(self):
|
|
run, prompts = self.run_review([_patch_info("src/a.py", PATCH)])
|
|
self.assertEqual(len(prompts), 1)
|
|
model, system, user = prompts[0]
|
|
self.assertEqual(model, config().model.id)
|
|
self.assertIn("Check X.", system)
|
|
self.assertIn("merge_recommendation", system)
|
|
self.assertIn("## File: 'src/a.py'", user)
|
|
self.assertEqual(run.prompt_files, {"src/a.py"})
|
|
self.assertFalse(run.pruned)
|
|
self.assertEqual(run.remaining_files, [])
|
|
self.assertEqual(run.reviewed_head_sha, HEAD)
|
|
self.assertEqual(run.data["review"]["merge_recommendation"].strip(), "changes_required")
|
|
self.assertEqual(run.details["num_ai_calls"], 1)
|
|
self.assertEqual((run.details["prompt_tokens"], run.details["completion_tokens"]), (1234, 210))
|
|
self.assertEqual(run.details["cost_status"], "unavailable")
|
|
|
|
def test_full_files_follow_the_diff_in_the_user_prompt(self):
|
|
from werkrevisor import fullfiles
|
|
section = fullfiles.render_section([fullfiles.render_file("src/a.py", "def f(n):\n return 1 / n\n")], HEAD)
|
|
run, prompts = self.run_review([_patch_info("src/a.py", PATCH)], section=section)
|
|
_, system, user = prompts[0]
|
|
self.assertNotIn("Full content of the changed files", system)
|
|
diff_at = user.index("## File: 'src/a.py'")
|
|
files_at = user.index("## Full content of the changed files")
|
|
self.assertLess(diff_at, files_at)
|
|
self.assertLess(files_at, user.index(fullfiles.SECTION_END))
|
|
self.assertEqual(run.prompt_files, {"src/a.py"})
|
|
|
|
def test_pruning_is_detected(self):
|
|
big = "@@ -1,1 +1,4001 @@\n def f(n):\n" + "".join(f"+ x{i} = {i} * n\n" for i in range(4000))
|
|
files = [_patch_info("src/a.py", PATCH), _patch_info("src/big.py", big)]
|
|
run, _ = self.run_review(files, model={"max_input_tokens": 3000})
|
|
self.assertTrue(run.pruned)
|
|
self.assertEqual(run.remaining_files, ["src/big.py"])
|
|
self.assertEqual(run.prompt_files, {"src/a.py"})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|