M7.3: nisse-review's first slice
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m49s
CI / e2e (pull_request) Successful in 3m18s

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:
dongarandClaude Opus 5.5 committed 2026-10-13 22:34:02 +00:00
1 parent fc71d3da75
commit f59d974a95
24 files changed
+2330 -7

No files matched your search

+95
View File
@@ -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