Back on a PR it reviewed: a new head gets a later review of the commits since (each one's diff, through the API) and of its earlier points; what was said since gets replies, answered where it was said or skipped with a reason. It signs off once and stays quiet while still happy. Past tuning.review.max_rounds it sums up for the operator and goes quiet, until a push or someone other than the PR's author joins in. An operator's `stop` quiets it; `review` asks for a fresh review. Reviews carry numbered markers and replies their point's key, so recovery posts nothing twice. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
125 lines
4.8 KiB
Python
125 lines
4.8 KiB
Python
"""nisse-review against the real services (Part R, M7.3): a PR in a fresh
|
|
test repository gets one COMMENT review from the review account, by the
|
|
local model, and after a push and a question, a later review at the new
|
|
head (M7.5); an account that could write to a repository is refused.
|
|
|
|
Needs NISSE_REVIEW_GITEA_TOKEN, the review account's token (CI secret; .env
|
|
locally), besides what every live test needs.
|
|
"""
|
|
|
|
import base64
|
|
import io
|
|
import os
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from e2e_support import e2e_roles, fresh_repo, operator_gitea
|
|
|
|
from nisse_core.gitea import GiteaClient
|
|
from nisse_core.oplog import OpLog
|
|
from nisse_dev.config import AgentConfig
|
|
from nisse_review.build import StartupError, build
|
|
from nisse_review.config import ReviewConfig
|
|
from nisse_review.review import marker
|
|
|
|
# Smoke: every PR runs it (two reviews on the local model), the only
|
|
# check that the review account, its token and its read-only refusal work.
|
|
pytestmark = [pytest.mark.integration, pytest.mark.e2e, pytest.mark.smoke]
|
|
|
|
TEST_SHOUT = (
|
|
"from shout import shout\n\n\ndef test_shout():\n assert shout('hi') == 'HI!'\n"
|
|
)
|
|
|
|
|
|
def review_gitea(url: str) -> GiteaClient:
|
|
token = os.environ.get("NISSE_REVIEW_GITEA_TOKEN", "")
|
|
assert token.strip(), "NISSE_REVIEW_GITEA_TOKEN is not set: the review account's"
|
|
return GiteaClient(url, token)
|
|
|
|
|
|
def test_a_ready_pr_gets_one_comment_review(
|
|
live_config: AgentConfig, tmp_path: Path
|
|
) -> None:
|
|
run = uuid.uuid4().hex[:8]
|
|
operator = operator_gitea(live_config.gitea_url)
|
|
reviewer = review_gitea(live_config.gitea_url)
|
|
account = reviewer.whoami()
|
|
roles = e2e_roles()
|
|
with fresh_repo(
|
|
operator, f"review-{run}", tmp_path / "keys", agent=account
|
|
) as repo:
|
|
tree = {
|
|
"state_db": str(tmp_path / "review" / "state.db"),
|
|
"llm": {
|
|
"base_url": live_config.llm_base_url,
|
|
"token": live_config.llm_token,
|
|
},
|
|
"roles": {"reviewer": roles.driver, "expert": roles.expert},
|
|
"gitea": {
|
|
"url": live_config.gitea_url,
|
|
"token": os.environ["NISSE_REVIEW_GITEA_TOKEN"],
|
|
"operators": ["nisse-testing-operator"],
|
|
},
|
|
"repositories": [repo.name],
|
|
# The fresh repository has no CI: review at once, no grace.
|
|
"tuning": {"review": {"max_turns": 25, "ci_grace_s": 0}},
|
|
}
|
|
config = ReviewConfig.from_tree(tree, {})
|
|
# make_repo gives the agent write access: the reviewer refuses that.
|
|
with pytest.raises(StartupError, match="read access only"):
|
|
build(config, gitea=reviewer, oplog=OpLog("t", io.StringIO()))
|
|
operator._request(
|
|
"PUT",
|
|
f"/repos/{repo.name}/collaborators/{account}",
|
|
json={"permission": "read"},
|
|
)
|
|
operator.branch_with_file(
|
|
repo.name,
|
|
base="main",
|
|
branch="shout",
|
|
path="sandbox/shout.py",
|
|
text='def shout(text):\n return text.upper() + "!"\n',
|
|
message="Add shout",
|
|
)
|
|
pr = operator.open_pull_request(
|
|
repo.name,
|
|
head="shout",
|
|
base="main",
|
|
title="Add shout",
|
|
body="Adds shout(text): the text in capitals, with an exclamation mark.",
|
|
)
|
|
built = build(config, gitea=reviewer, oplog=OpLog("t", io.StringIO()))
|
|
try:
|
|
assert built.scheduler.round() is True
|
|
assert built.scheduler.round() is False # reviewed once, not again
|
|
first = operator.pull_request(repo.name, pr.number).head_sha
|
|
mine = [
|
|
r for r in operator.reviews(repo.name, pr.number) if r.author == account
|
|
]
|
|
assert len(mine) == 1
|
|
assert mine[0].state == "COMMENT" and mine[0].commit == first
|
|
assert marker(first) in mine[0].body
|
|
# M7.5: a push and a question get a later review of what changed.
|
|
operator._request(
|
|
"POST",
|
|
f"/repos/{repo.name}/contents/sandbox/test_shout.py",
|
|
json={
|
|
"content": base64.b64encode(TEST_SHOUT.encode()).decode(),
|
|
"message": "Test shout",
|
|
"branch": "shout",
|
|
},
|
|
)
|
|
operator.post_comment(repo.name, pr.number, "I added a test. Is it enough?")
|
|
assert built.scheduler.round() is True
|
|
finally:
|
|
built.store.close()
|
|
second = operator.pull_request(repo.name, pr.number).head_sha
|
|
assert second != first
|
|
later = [
|
|
r
|
|
for r in operator.reviews(repo.name, pr.number)
|
|
if r.author == account and marker(second, 2) in r.body
|
|
]
|
|
assert len(later) == 1 and later[0].commit == second
|