M7.3: nisse-review's first slice
A new package over nisse-core: it reviews every ready PR in the repositories its account can read, and posts one COMMENT review. - scheduler: each round, open PRs per readable repository (or the configured repositories); an item per PR, ready unless draft or WIP; closed with the PR. One review at a time, oldest first. - reader: the repository at head and base from Gitea archives, unpacked and read in Python (read_file, list_files, search_code), and its issues; bound to the item's repository; no git, no shell. - review: starts from the PR, commits, diff, comments, closed issues, CI; comment drafts a point on any existing line; post_review posts one COMMENT review at the head, inline where Gitea takes it, else as links in the summary. ask_expert; vault and memory (memory/nisse-review). - L8: posting is an intent; recovery asks Gitea for the marked review and never posts twice; with Gitea down, the item waits. - Failures retried, then said on the PR until the next push. - Startup refuses an account that can push to or administer any repo. - nisse-core gitea: readable_repositories, open_pull_requests, diff, commits, archive, issue, search_issues, reviews, post_review; PR fields. - Bring-up: scripts/review-bringup.sh; nisse-review.example.yaml. - Tests: unit over a fake Gitea and scripted model; a live test (NISSE_REVIEW_GITEA_TOKEN) reviews a PR in a fresh test repository. - Docs: Development Plan M7.3, Tuning Guide (tuning.review.*). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
This commit is contained in:
1 parent
fc71d3da75
commit
f59d974a95
24 files changed
+2330
-7
No files matched your search
@@ -0,0 +1,95 @@
|
||||
"""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; 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 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
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.e2e]
|
||||
|
||||
|
||||
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],
|
||||
"tuning": {"review": {"max_turns": 25}},
|
||||
}
|
||||
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
|
||||
finally:
|
||||
built.store.close()
|
||||
head = 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 == head
|
||||
assert marker(head) in mine[0].body
|
||||
Reference in new issue
Block a user