nisse-review: repository patterns, and the wall checked every round
At bring-up, nisse-review read no repositories: Gitea's /user/repos leaves out a public repository's Read collaborators, so dev/nisse never showed up. `repositories` now holds patterns (dev/nisse, dev/*, */*) matched against every repository the account can read, public ones included (an organization's or user's list, or the instance's search); unset, it is /user/repos as before. A configured repository it can't read is left out with an oplog line. The read-only wall (R3) is checked every round, not only at startup: an account granted push or admin while running stops (exit 3) rather than review on. nisse-core's Gitea client gains GiteaNotFound for 404s, owner_repositories and visible_repositories. 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
52c6704b17
commit
2139ddd05c
12 files changed
+283
-44
No files matched your search
@@ -15,6 +15,7 @@ from nisse_core.knowledge.vault import Vault
|
||||
from nisse_core.log.canonical import AssistantText
|
||||
from nisse_core.loop.driver import Driver
|
||||
from nisse_core.oplog import OpLog
|
||||
from nisse_review.access import AccessRefused, check, reviewed
|
||||
from nisse_review.build import StartupError, check_access
|
||||
from nisse_review.review import Reviewer, _diff_lines, marker
|
||||
from nisse_review.scheduler import Scheduler, ready
|
||||
@@ -102,7 +103,7 @@ def test_intake_opens_an_item_per_pr_and_closes_it_with_the_pr(
|
||||
assert (item(w, 7).position, item(w, 8).position) == (READY, WAITING)
|
||||
assert item(w, 7).scope == REPO
|
||||
gitea.repos["o/elsewhere"] = [pr(9)]
|
||||
w["scheduler"]._only = frozenset({REPO})
|
||||
w["scheduler"]._patterns = frozenset({REPO})
|
||||
w["scheduler"].intake()
|
||||
assert w["store"].work_item("o/elsewhere#9") is None
|
||||
gitea.repos[REPO] = [pr(8, draft=False)] # 7 merged; 8 is ready now
|
||||
@@ -300,10 +301,69 @@ def test_with_gitea_down_a_crashed_review_waits(tmp_path: Path) -> None:
|
||||
|
||||
def test_an_account_that_can_write_is_refused(tmp_path: Path) -> None:
|
||||
gitea = FakeGitea(repos={REPO: [], "o/other": []})
|
||||
assert check_access(gitea) == ("nisse-review", [REPO, "o/other"]) # type: ignore[arg-type]
|
||||
log = OpLog("t", io.StringIO())
|
||||
assert check_access(gitea, None, log) == ("nisse-review", [REPO, "o/other"]) # type: ignore[arg-type]
|
||||
gitea.permissions["o/other"] = {"pull": True, "push": True}
|
||||
with pytest.raises(StartupError, match="read access only"):
|
||||
check_access(gitea) # type: ignore[arg-type]
|
||||
check_access(gitea, None, log) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_patterns_reach_public_repositories_the_account_was_not_given(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
# Gitea leaves a public repository's Read collaborators out of
|
||||
# /user/repos: a pattern finds it all the same (option a: the
|
||||
# configuration decides).
|
||||
gitea = FakeGitea(
|
||||
repos={"dev/nisse": [pr(7)], "dev/wordle": [], "infra/tofu": [pr(3)]},
|
||||
public={"dev/nisse", "dev/wordle", "infra/tofu"},
|
||||
)
|
||||
log = OpLog("t", io.StringIO())
|
||||
assert reviewed(gitea, None, log) == [] # type: ignore[arg-type]
|
||||
assert reviewed(gitea, {"dev/*"}, log) == ["dev/nisse", "dev/wordle"] # type: ignore[arg-type]
|
||||
assert reviewed(gitea, {"*/*"}, log) == ["dev/nisse", "dev/wordle", "infra/tofu"] # type: ignore[arg-type]
|
||||
assert reviewed(gitea, {"dev/n*", "infra/tofu"}, log) == ["dev/nisse", "infra/tofu"] # type: ignore[arg-type]
|
||||
w = world(tmp_path, Scripted(), gitea, patterns=frozenset({"dev/*"}))
|
||||
w["scheduler"].round()
|
||||
assert w["store"].work_item("dev/nisse#7") is not None
|
||||
assert w["store"].work_item("infra/tofu#3") is None
|
||||
|
||||
|
||||
def test_a_configured_repository_it_cannot_read_is_left_out() -> None:
|
||||
gitea = FakeGitea(repos={REPO: []})
|
||||
out = io.StringIO()
|
||||
log = OpLog("t", out)
|
||||
patterns = {REPO, "o/gone", "nobody/*"}
|
||||
assert check(gitea, reviewed(gitea, patterns, log), log) == [REPO] # type: ignore[arg-type]
|
||||
assert '"access.unreadable"' in out.getvalue() and "o/gone" in out.getvalue()
|
||||
assert '"access.no_owner"' in out.getvalue()
|
||||
|
||||
|
||||
def test_write_access_granted_while_running_stops_it(tmp_path: Path) -> None:
|
||||
gitea = FakeGitea(repos={REPO: [pr()]})
|
||||
w = world(tmp_path, Scripted(), gitea)
|
||||
w["scheduler"].intake()
|
||||
gitea.permissions[REPO] = {"pull": True, "push": True}
|
||||
with pytest.raises(AccessRefused, match="read access only"):
|
||||
w["scheduler"].round()
|
||||
assert item(w).position == READY # nothing reviewed
|
||||
|
||||
|
||||
def test_a_repository_pattern_must_be_owner_slash_name(tmp_path: Path) -> None:
|
||||
from nisse_core.config import ConfigError
|
||||
from nisse_review.config import ReviewConfig
|
||||
|
||||
tree = {
|
||||
"state_db": str(tmp_path / "s.db"),
|
||||
"llm": {"base_url": "http://llm", "token": "k"},
|
||||
"roles": {"reviewer": "local", "expert": "frontier"},
|
||||
"gitea": {"url": "https://g", "token": "t", "operators": ["don"]},
|
||||
}
|
||||
ok = ReviewConfig.from_tree({**tree, "repositories": ["dev/*", "*/*"]}, {})
|
||||
assert ok.repositories == {"dev/*", "*/*"}
|
||||
for bad in ("dev", "dev/", "/nisse", "a/b/c"):
|
||||
with pytest.raises(ConfigError, match="is not owner/name"):
|
||||
ReviewConfig.from_tree({**tree, "repositories": [bad]}, {})
|
||||
|
||||
|
||||
def test_diff_lines_are_the_heads_lines_a_diff_shows() -> None:
|
||||
|
||||
Reference in new issue
Block a user