feat: test-generation + issue/PR summary via slash commands (/gen-tests, /summarize)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
53
scripts/ai_summary.py
Normal file
53
scripts/ai_summary.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Feature E — KI-Zusammenfassung langer Issue-/PR-Diskussionen.
|
||||
|
||||
Per Slash-Command `/summarize` an einem Issue oder PR (issue_comment). Holt
|
||||
Titel/Body + alle menschlichen Kommentare und fasst Diskussion, Stand und offene
|
||||
Punkte zusammen — als idempotenten Kommentar.
|
||||
"""
|
||||
import sys
|
||||
|
||||
import context
|
||||
import gitea_api
|
||||
import llm
|
||||
|
||||
MARKER = "<!-- ci-ai:summary -->"
|
||||
THREAD_CAP = 24000
|
||||
|
||||
SYSTEM = (
|
||||
"Du fasst Issue-/PR-Diskussionen prägnant auf Deutsch zusammen: worum es geht, "
|
||||
"welche Entscheidungen/Argumente fielen und was offen ist. Keine Erfindungen — "
|
||||
"nur was im Thread steht."
|
||||
)
|
||||
|
||||
PROMPT = """\
|
||||
Fasse die folgende Diskussion zusammen. Genau diese drei Abschnitte (Markdown):
|
||||
|
||||
### Worum geht's
|
||||
### Stand & Entscheidungen
|
||||
### Offene Punkte / nächste Schritte
|
||||
|
||||
--- THREAD ---
|
||||
{thread}
|
||||
"""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
owner, repo = context.repo_slug()
|
||||
iss = context.issue()
|
||||
index = iss["index"] if iss else (context.pull_request() or {}).get("index")
|
||||
if not index:
|
||||
print("[ai_summary] Kein Issue/PR-Kontext. Übersprungen.")
|
||||
return 0
|
||||
ok, why = llm.configured()
|
||||
if not ok:
|
||||
print(f"[ai_summary] KI inaktiv ({why}) — übersprungen, kein Fehler.")
|
||||
return 0
|
||||
thread = gitea_api.get_issue_thread(owner, repo, index)[:THREAD_CAP]
|
||||
answer = llm.complete(SYSTEM, PROMPT.format(thread=thread), max_tokens=1500)
|
||||
gitea_api.upsert_comment(owner, repo, index, MARKER, f"## 📝 KI-Zusammenfassung\n\n{answer}")
|
||||
print("[ai_summary] Kommentar gepostet/aktualisiert.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
68
scripts/ai_tests.py
Normal file
68
scripts/ai_tests.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""Feature D — KI-Test-Generierung (Vorschlag als PR-Kommentar).
|
||||
|
||||
Per Slash-Command `/gen-tests` an einem PR (issue_comment) ODER aus einem
|
||||
pull_request-Event. Holt den Diff, lässt das LLM passende Unit-Tests vorschlagen
|
||||
und postet sie als idempotenten Kommentar. Bewusst NUR Vorschlag — kein
|
||||
Auto-Commit (der Mensch entscheidet, was übernommen wird).
|
||||
"""
|
||||
import sys
|
||||
|
||||
import context
|
||||
import gitea_api
|
||||
import llm
|
||||
|
||||
MARKER = "<!-- ci-ai:tests -->"
|
||||
DIFF_CAP = 30000
|
||||
|
||||
SYSTEM = (
|
||||
"Du bist ein erfahrener Test-Engineer. Schlage sinnvolle, lauffähige Unit-Tests "
|
||||
"für die geänderten Stellen vor. Erkenne Sprache und Test-Framework aus dem Diff. "
|
||||
"Antworte auf Deutsch, Test-Code in passenden Code-Blöcken. Keine Tests für reine "
|
||||
"Trivialitäten; erfinde keine nicht vorhandenen APIs."
|
||||
)
|
||||
|
||||
PROMPT = """\
|
||||
Generiere Unit-Tests für die Änderungen in diesem Pull-Request.
|
||||
|
||||
PR-Titel: {title}
|
||||
|
||||
Pro relevanter Datei/Funktion: ein kurzer Hinweis + der Test-Code (richtige Sprache,
|
||||
lauffähig, Framework benennen). Wenn nichts sinnvoll testbar ist, sag das ehrlich.
|
||||
|
||||
--- DIFF ---
|
||||
{diff}
|
||||
"""
|
||||
|
||||
|
||||
def _pr_index():
|
||||
pr = context.pull_request()
|
||||
if pr and pr.get("index"):
|
||||
return pr["index"], pr.get("title", "")
|
||||
iss = context.issue()
|
||||
if iss and iss.get("is_pr"):
|
||||
return iss["index"], iss.get("title", "")
|
||||
return None, ""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
owner, repo = context.repo_slug()
|
||||
index, title = _pr_index()
|
||||
if not index:
|
||||
print("[ai_tests] Kein PR-Kontext — Test-Generierung läuft nur auf PRs. Übersprungen.")
|
||||
return 0
|
||||
ok, why = llm.configured()
|
||||
if not ok:
|
||||
print(f"[ai_tests] KI inaktiv ({why}) — übersprungen, kein Fehler.")
|
||||
return 0
|
||||
diff = gitea_api.get_pr_diff(owner, repo, index)
|
||||
if not diff.strip():
|
||||
print("[ai_tests] Leerer Diff — nichts zu testen.")
|
||||
return 0
|
||||
answer = llm.complete(SYSTEM, PROMPT.format(title=title, diff=diff[:DIFF_CAP]), max_tokens=2000)
|
||||
gitea_api.upsert_comment(owner, repo, index, MARKER, f"## 🧪 KI-Test-Vorschläge\n\n{answer}")
|
||||
print("[ai_tests] Kommentar gepostet/aktualisiert.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -41,3 +41,30 @@ def pull_request():
|
||||
"base_ref": (pr.get("base") or {}).get("ref", ""),
|
||||
"head_sha": (pr.get("head") or {}).get("sha", ""),
|
||||
}
|
||||
|
||||
|
||||
def event_name():
|
||||
"""Name des auslösenden Events (push, pull_request, issue_comment, …)."""
|
||||
return os.environ.get("GITHUB_EVENT_NAME", "")
|
||||
|
||||
|
||||
def comment_body():
|
||||
"""Body des auslösenden Kommentars (issue_comment-Event) oder ''."""
|
||||
return (event().get("comment") or {}).get("body") or ""
|
||||
|
||||
|
||||
def issue():
|
||||
"""Issue/PR-Infos aus einem issue_comment-Event oder None.
|
||||
|
||||
Liefert dict: index, title, body, author, is_pr.
|
||||
"""
|
||||
iss = event().get("issue")
|
||||
if not iss:
|
||||
return None
|
||||
return {
|
||||
"index": iss.get("number"),
|
||||
"title": iss.get("title", ""),
|
||||
"body": iss.get("body", "") or "",
|
||||
"author": (iss.get("user") or {}).get("login", ""),
|
||||
"is_pr": bool(iss.get("pull_request")),
|
||||
}
|
||||
|
||||
@@ -47,6 +47,24 @@ def get_pr_diff(owner: str, repo: str, index: int) -> str:
|
||||
return _request("GET", f"/repos/{owner}/{repo}/pulls/{index}.diff", raw=True)
|
||||
|
||||
|
||||
def get_issue_thread(owner: str, repo: str, index: int) -> str:
|
||||
"""Titel + Body + alle MENSCHLICHEN Kommentare eines Issues/PRs als Text.
|
||||
|
||||
Eigene Bot-Kommentare (Marker `<!-- ci-ai:`) werden übersprungen, damit das
|
||||
Summary nicht die eigene Ausgabe mitliest.
|
||||
"""
|
||||
iss = _request("GET", f"/repos/{owner}/{repo}/issues/{index}")
|
||||
parts = [f"# {iss.get('title', '')}\n\n{iss.get('body') or '(kein Text)'}"]
|
||||
comments = _request("GET", f"/repos/{owner}/{repo}/issues/{index}/comments") or []
|
||||
for c in comments:
|
||||
body = c.get("body") or ""
|
||||
if "<!-- ci-ai:" in body:
|
||||
continue
|
||||
author = (c.get("user") or {}).get("login", "?")
|
||||
parts.append(f"--- @{author}:\n{body}")
|
||||
return "\n\n".join(parts)
|
||||
|
||||
|
||||
def upsert_comment(owner: str, repo: str, index: int, marker: str, body: str) -> int:
|
||||
"""Legt einen Kommentar an ODER editiert den vorhandenen mit gleichem Marker.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user