251 lines
12 KiB
Python
251 lines
12 KiB
Python
"""Ü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() == []
|