"""Übersicht „Repository“: Neu seit dem letzten Besuch, Branches, Commit-Details und Chats mit Bezug darauf.""" from pathlib import Path import pytest from sqlalchemy import select from test_branches import add_to_main, commit, git, setup, work # noqa: F401 – Fixture setup from app.changes.models import ChatBranch from app.chat.models import ChatSession, Message from app.repo import service as repo_service from helpers import create_workspace README = {"path": "README.md", "replace": [{"old": "# Test\n", "new": "# Test\n\nAuf dem Feature-Branch.\n"}]} @pytest.fixture(autouse=True) def fresh(): """Drosselung je Prozess zurücksetzen: jeder Test beginnt mit einem neuen Klon.""" repo_service._refreshed.clear() yield repo_service._refreshed.clear() def url(setup, path: str = "") -> str: return f"/workspaces/{setup['workspace_id']}/repo{path}" def refresh(setup, who: str = "ben", tab: str = "new"): return setup[who].post(url(setup, "/refresh"), {"tab": tab}) def new_chat(db) -> ChatSession: with db.session() as session: chat = session.scalar(select(ChatSession).order_by(ChatSession.id.desc())) session.expunge(chat) return chat def feature_branch(repo: str, name: str = "feature/x") -> None: git(repo, "checkout", "-q", "-b", name) add_to_main(repo, "feature.txt", "neu\n") git(repo, "checkout", "-q", "main") def test_eintrag_nur_mit_git_quelle(setup, people): page = setup["ben"].get(f"/workspaces/{setup['workspace_id']}").text assert f'href="{url(setup)}"' in page other = create_workspace(setup["anna"], people["team_demo"], name="Ohne Repo") assert f"/workspaces/{other}/repo" not in setup["anna"].get(f"/workspaces/{other}").text assert setup["anna"].get(f"/workspaces/{other}/repo").status_code == 404 def test_neu_seit_dem_letzten_besuch(setup): first = setup["ben"].get(url(setup)).text assert "Dein erster Besuch" in first and "Erster Commit" in first add_to_main(setup["repo"], "a.txt", "a\n") add_to_main(setup["repo"], "b.txt", "b\n") page = refresh(setup).text assert "Seit deinem letzten Besuch" in page and "2 Commits, 2 Dateien geändert" in page assert page.count(">neu") == 2 assert "Zusammenfassen lassen" in page # Danach gilt der Stand als gesehen; für andere Personen zählt ihr eigener Besuch assert "Nichts Neues" in setup["ben"].get(url(setup)).text assert "Dein erster Besuch" in setup["anna"].get(url(setup)).text def test_filter_nach_autor_und_pfad(setup): (Path(setup["repo"].removeprefix("file://")) / "src").mkdir() add_to_main(setup["repo"], "src/app.py", "x = 1\n") refresh(setup) by_path = setup["ben"].get(url(setup), params={"path": "src", "period": ""}).text assert "src/app.py anlegen" in by_path and "Erster Commit" not in by_path by_author = setup["ben"].get(url(setup), params={"author": "test@example", "period": ""}).text assert "Erster Commit" in by_author and "src/app.py anlegen" not in by_author assert 'name="path" value="src"' in by_path assert "Ungültiger Pfad" in setup["ben"].get(url(setup), params={"path": "../x"}).text def test_commit_mit_diff(setup): add_to_main(setup["repo"], "a.txt", "Hallo\n") refresh(setup) sha = git(setup["repo"], "rev-parse", "HEAD") page = setup["ben"].get(url(setup, f"/commits/{sha}")) assert page.status_code == 200 assert "a.txt anlegen" in page.text and 'class="aiw-diff-add">+Hallo' in page.text assert "Im Chat erklären" in page.text and "Datei erklären" in page.text assert setup["ben"].get(url(setup, "/commits/" + "0" * 40)).status_code == 404 assert setup["ben"].get(url(setup, "/commits/kein-sha")).status_code == 400 # Nicht-Mitglieder sehen nichts assert setup["dora"].get(url(setup)).status_code == 404 assert setup["dora"].get(url(setup, f"/commits/{sha}")).status_code == 404 def test_branches_mit_vorsprung_und_chat(setup, db, settings, fake_provider): feature_branch(setup["repo"]) add_to_main(setup["repo"], "main.txt", "m\n") commit(setup, db, settings, fake_provider) setup["ben"].post(f"/branches/{_branch_id(db)}/push") page = refresh(setup, tab="branches").text assert "Ziel-Branch" in page row = page.split("feature/x", 1)[1].split("", 1)[0] assert "↑1" in row and "↓1" in row and "head=feature/x" in row ai = page.split("ai/c", 1)[1].split("", 1)[0] assert f'href="/chats/{setup["chat_id"]}"' in ai and "README ergänzen" in ai def _branch_id(db) -> int: with db.session() as session: return session.scalar(select(ChatBranch.id).order_by(ChatBranch.id.desc())) def test_zusammenfassen_und_erklaeren_starten_chats(setup, db): start = git(setup["repo"], "rev-parse", "HEAD") add_to_main(setup["repo"], "a.txt", "a\n") end = git(setup["repo"], "rev-parse", "HEAD") refresh(setup) response = setup["ben"].post(url(setup, "/chat"), {"action": "summarize", "start": start, "end": end}) chat = new_chat(db) assert response.headers["location"] == f"/chats/{chat.id}" with db.session() as session: text = session.scalar(select(Message.content).where(Message.chat_session_id == chat.id)) assert chat.title.startswith("Neu in") and f"git_diff von {start} bis {end}" in text setup["ben"].post(url(setup, "/chat"), {"action": "commit", "sha": end}) chat = new_chat(db) assert chat.title == f"Commit {end[:10]} erklären" setup["ben"].post(url(setup, "/chat"), {"action": "file", "path": "a.txt"}) assert new_chat(db).title == "a.txt erklären" assert setup["ben"].post(url(setup, "/chat"), {"action": "file", "path": "../etc"}).status_code == 400 assert setup["dora"].post(url(setup, "/chat"), {"action": "commit", "sha": end}).status_code == 404 def test_chat_auf_bestehendem_branch(setup, db, settings, fake_provider): feature_branch(setup["repo"]) feature = git(setup["repo"], "rev-parse", "feature/x") refresh(setup, tab="branches") response = setup["ben"].post(url(setup, "/chat"), {"action": "branch", "branch": "feature/x"}) chat = new_chat(db) assert response.headers["location"] == f"/chats/{chat.id}" and chat.title == "Arbeit an feature/x" assert list(chat.git_base.values()) == ["feature/x"] assert setup["ben"].post(url(setup, "/chat"), {"action": "branch", "branch": "gibt-es-nicht"}).status_code == 400 # Die KI liest feature/x und zweigt davon ab; Abgleich und PR beziehen sich auf feature/x fake_provider.script = [[("git_read_file", {"path": "feature.txt"}), ("commit_change", {"message": "README ergänzen", "changes": [README]})], "Erledigt."] setup["ben"].post(f"/chats/{chat.id}/messages", {"content": "Bitte README ergänzen"}, headers={"HX-Request": "true"}) work(db, settings) call = fake_provider.calls[-1] assert "arbeitet auf Branch feature/x" in call["instructions"] outputs = [i["output"] for i in call["input"] if i.get("type") == "function_call_output"] assert "neu" in outputs[0] and "Commit" in outputs[1] with db.session() as session: branch = session.scalar(select(ChatBranch).where(ChatBranch.chat_session_id == chat.id)) assert branch.target == "feature/x" and branch.base == feature class OpenPRs: """Bitbucket-Ersatz für die Liste offener Pull Requests.""" def __init__(self, prs): self.prs = prs def open(self): return self.prs def test_pull_requests_mit_diff_und_review(setup, db, monkeypatch): from app.changes import service as changes from app.data_sources.connectors.pull_requests import PullRequest from app.data_sources.models import DataSource page = setup["ben"].get(url(setup), params={"tab": "prs"}).text assert "kein Pull-Request-Anbieter eingerichtet" in page with db.session() as session: source = session.scalar(select(DataSource)) source.config = {**source.config, "pr_provider": "bitbucket_dc"} feature_branch(setup["repo"]) refresh(setup, tab="prs") pr = PullRequest("7", "https://bitbucket.intern/pr/7", title="Feature X", author="Clara", source="feature/x", target="main", comments=2, reviewers=[{"name": "Dirk", "status": "NEEDS_WORK"}]) monkeypatch.setattr(changes, "prs", lambda *a: OpenPRs([pr])) page = setup["ben"].get(url(setup), params={"tab": "prs"}).text assert "#7 Feature X" in page and "Clara" in page and "2 Kommentare" in page and "Dirk: ✗ Nacharbeit" in page diff = setup["ben"].get(url(setup, "/compare"), params={"base": "main", "head": "feature/x", "pr": "7"}) assert diff.status_code == 200 and "1 Commit, 1 Datei" in diff.text and "+neu" in diff.text assert setup["ben"].get(url(setup, "/compare"), params={"base": "main", "head": "weg"}).status_code == 400 setup["ben"].post(url(setup, "/chat"), {"action": "review", "branch": "feature/x", "base": "main", "pr": "7"}) chat = new_chat(db) assert chat.title == "Review Pull Request #7" and list(chat.git_base.values()) == ["feature/x"] with db.session() as session: text = session.scalar(select(Message.content).where(Message.chat_session_id == chat.id)) merge_base = git(setup["repo"], "merge-base", "main", "feature/x") assert f"git_diff von {merge_base} bis {git(setup['repo'], 'rev-parse', 'feature/x')}" in text assert "Ändere nichts" in text # Bitbucket nicht erreichbar → Meldung statt Fehlerseite from app.data_sources.connectors.git_repo import GitError class Down: def open(self): raise GitError("Bitbucket nicht erreichbar (ConnectError).") monkeypatch.setattr(changes, "prs", lambda *a: Down()) assert "Bitbucket nicht erreichbar" in setup["ben"].get(url(setup), params={"tab": "prs"}).text def test_dateien_baum_datei_und_branch(setup, db): (Path(setup["repo"].removeprefix("file://")) / "src").mkdir() add_to_main(setup["repo"], "src/app.py", "def main():\n return 1\n") feature_branch(setup["repo"]) refresh(setup, tab="files") root = setup["ben"].get(url(setup), params={"tab": "files"}).text assert "📁 src/" in root and "README.md" in root and "feature.txt" not in root folder = setup["ben"].get(url(setup), params={"tab": "files", "at": "src"}).text assert "app.py" in folder and 'aria-current="page">src<' in folder file = setup["ben"].get(url(setup), params={"tab": "files", "at": "src/app.py"}).text assert "
  • def main():
  • " in file and "src/app.py anlegen" in file and "Datei erklären" in file on_branch = setup["ben"].get(url(setup), params={"tab": "files", "ref": "feature/x"}).text assert "feature.txt" in on_branch assert setup["ben"].get(url(setup), params={"tab": "files", "at": "../x"}).status_code == 400 assert setup["ben"].get(url(setup), params={"tab": "files", "ref": "gibt-es-nicht"}).status_code == 400 # Datei auf einem anderen Branch erklären → Chat liest diesen Branch setup["ben"].post(url(setup, "/chat"), {"action": "file", "path": "feature.txt", "branch": "feature/x"}) chat = new_chat(db) assert list(chat.git_base.values()) == ["feature/x"] def test_bitbucket_offene_pull_requests(): import httpx from app.data_sources.connectors import pull_requests seen = [] def handle(request: httpx.Request) -> httpx.Response: seen.append(request) return httpx.Response(200, json={"values": [{ "id": 9, "title": "Feature X", "state": "OPEN", "draft": False, "version": 2, "updatedDate": 1700000000000, "author": {"user": {"displayName": "Clara"}}, "fromRef": {"displayId": "feature/x"}, "toRef": {"displayId": "main"}, "properties": {"commentCount": 3}, "reviewers": [{"user": {"displayName": "Dirk"}, "status": "APPROVED"}], "links": {"self": [{"href": "https://bb.intern/pr/9"}]}}]}) client = pull_requests.BitbucketDataCenter("https://bb.intern/scm/PROJ/repo.git", "tok", 5, transport=httpx.MockTransport(handle)) [pr] = client.open() assert seen[0].url.params["state"] == "OPEN" and seen[0].url.params["order"] == "NEWEST" assert seen[0].url.path == "/rest/api/1.0/projects/PROJ/repos/repo/pull-requests" assert (pr.id, pr.title, pr.author, pr.source, pr.target, pr.comments) == ("9", "Feature X", "Clara", "feature/x", "main", 3) assert pr.updated.year == 2023 and pr.reviewers == [{"name": "Dirk", "status": "APPROVED"}] assert pull_requests.NoPullRequests().open() == []