Files
nisse/tests/integration/test_review_live.py
T
dongarandClaude Opus 5.5 37bbac7e72 nisse-review: later reviews, replies, rounds, stop and review (M7.5)
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
2026-10-17 07:06:06 +00:00

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