"""Ü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() == []