From 6e697b55a89f8d7ad2df5c5eb2785226118fb7af Mon Sep 17 00:00:00 2001 From: Don Garrett Date: Fri, 16 Oct 2026 22:56:43 +0000 Subject: [PATCH] nisse-review: one unlistable repository holds up no other Found at bring-up: an empty private repository's /pulls answered 404, and intake stopped there, so the whole round failed and nothing was reviewed. A repository whose PRs can't be listed is now logged (intake.repo_failed) and skipped for the round; its items stay as they are. Also, from nisse-review's review of this PR: a two-page test pins the search listing the patterns rest on. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk --- nisse-core/tests/test_gitea.py | 17 +++++++++++++++++ nisse-review/src/nisse_review/scheduler.py | 10 +++++++++- nisse-review/tests/review_fakes.py | 3 +++ nisse-review/tests/test_review.py | 14 ++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/nisse-core/tests/test_gitea.py b/nisse-core/tests/test_gitea.py index eb1abd2..edb51a5 100644 --- a/nisse-core/tests/test_gitea.py +++ b/nisse-core/tests/test_gitea.py @@ -375,6 +375,23 @@ def test_visible_repositories_read_the_search_and_a_404_is_not_found() -> None: gitea.repository("d/gone") +def test_visible_repositories_page_through_the_search() -> None: + # The patterns rest on the search listing everything (#154's review). + asked: list[dict[str, str]] = [] + + def handler(request: httpx.Request) -> httpx.Response: + asked.append(dict(request.url.params)) + page = int(request.url.params["page"]) + count = PR_PAGE_SIZE if page == 1 else 3 + data = [{"full_name": f"o/r{page}-{n}"} for n in range(count)] + return httpx.Response(200, json={"ok": True, "data": data}) + + found = client(handler).visible_repositories() + assert len(found) == PR_PAGE_SIZE + 3 and found[-1] == "o/r2-2" + assert [a["page"] for a in asked] == ["1", "2"] + assert all(a["limit"] == str(PR_PAGE_SIZE) for a in asked) + + def test_a_pull_request_carries_what_a_review_needs() -> None: def handler(request: httpx.Request) -> httpx.Response: if request.url.path.endswith(".diff"): diff --git a/nisse-review/src/nisse_review/scheduler.py b/nisse-review/src/nisse_review/scheduler.py index b611b74..d51aee0 100644 --- a/nisse-review/src/nisse_review/scheduler.py +++ b/nisse-review/src/nisse_review/scheduler.py @@ -106,8 +106,16 @@ class Scheduler: repos = reviewed(self._gitea, self._patterns, self._oplog) open_items = {i.id: i for i in self._store.work_items("open")} for repo in repos: + try: + prs = self._gitea.open_pull_requests(repo) + except GiteaError as exc: + # One repository Gitea won't list (an empty one, one without + # pull requests) holds up none of the others; its items stay + # as they are. + self._oplog.emit("intake.repo_failed", cause=str(exc)[:300], repo=repo) + continue seen: set[str] = set() - for pr in self._gitea.open_pull_requests(repo): + for pr in prs: key = item_id(repo, pr.number) seen.add(key) self._see(open_items.get(key), repo, pr) diff --git a/nisse-review/tests/review_fakes.py b/nisse-review/tests/review_fakes.py index 8dca73b..5014320 100644 --- a/nisse-review/tests/review_fakes.py +++ b/nisse-review/tests/review_fakes.py @@ -72,6 +72,7 @@ class FakeGitea: down: bool = False # Readable but not given (public, no grant): missing from /user/repos. public: set[str] = field(default_factory=set) + unlisted: set[str] = field(default_factory=set) # its PRs can't be listed def _up(self) -> None: if self.down: @@ -99,6 +100,8 @@ class FakeGitea: def open_pull_requests(self, repo: str) -> list[PullRequest]: self._up() + if repo in self.unlisted: + raise GiteaNotFound(f"gitea GET /repos/{repo}/pulls returned 404") return list(self.repos[repo]) def pull_request(self, repo: str, index: int) -> PullRequest: diff --git a/nisse-review/tests/test_review.py b/nisse-review/tests/test_review.py index d9a2e89..b5fa65e 100644 --- a/nisse-review/tests/test_review.py +++ b/nisse-review/tests/test_review.py @@ -328,6 +328,20 @@ def test_patterns_reach_public_repositories_the_account_was_not_given( assert w["store"].work_item("infra/tofu#3") is None +def test_a_repository_whose_prs_cannot_be_listed_holds_up_no_other( + tmp_path: Path, +) -> None: + # Found at bring-up: an empty private repository's /pulls was a 404, + # and the round stopped there, so no repository was reviewed. + gitea = FakeGitea(repos={"a/empty": [], REPO: [pr()]}, unlisted={"a/empty"}) + out = io.StringIO() + w = world(tmp_path, Scripted(*REVIEW), gitea) + w["scheduler"]._oplog = OpLog("t", out) + assert w["scheduler"].round() is True # REPO's PR was reviewed + assert item(w).position == REVIEWED + assert '"intake.repo_failed"' in out.getvalue() and "a/empty" in out.getvalue() + + def test_a_configured_repository_it_cannot_read_is_left_out() -> None: gitea = FakeGitea(repos={REPO: []}) out = io.StringIO()