bifroest/tests/test_repo.py
2026-10-27 08:34:17 +01:00

251 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Ü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</span>") == 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("<code>feature/x</code>", 1)[1].split("</tr>", 1)[0]
assert "↑1" in row and "↓1" in row and "head=feature/x" in row
ai = page.split("<code>ai/c", 1)[1].split("</tr>", 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 "<li>def main():</li>" 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() == []