improvements: Big Reviewer Update #4

Merged
space merged 7 commits from improv/big-reviewer-update into main 2026-05-23 14:22:33 +02:00
7 changed files with 193 additions and 8 deletions
Showing only changes of commit 9392591429 - Show all commits

View File

@@ -495,7 +495,7 @@ async def gitea_webhook(
gitea.post_issue_comment(repo, pr_number, format_queue_ack(head_sha))
return {"accepted": True, "job_id": job.id, "status": "queued"}
if parsed_command.name in {"fix", "explain", "ignore"}:
if parsed_command.name in {"fix", "explain", "ignore", "help"}:
job = enqueue_job(
session,
repo=repo,

View File

@@ -5,12 +5,14 @@ from collections.abc import Iterable
from gitea_codex_bot.types import ParsedCommand
COMMAND_RE = re.compile(r"^@([^\s]+)\s+(review|explain|fix|ignore|rerun)\b(.*)$", re.IGNORECASE | re.DOTALL)
PREFIX_RE = re.compile(r"^@([^\s]+)\s+(.+)$", re.IGNORECASE | re.DOTALL)
HELP_ALIASES = {"-h", "--help", "help"}
SUPPORTED_COMMANDS = {"review", "explain", "fix", "ignore", "rerun"}
def parse_command(body: str, aliases: Iterable[str] | None = None) -> ParsedCommand | None:
stripped = body.strip()
match = COMMAND_RE.match(stripped)
match = PREFIX_RE.match(stripped)
if not match:
return None
command_alias = match.group(1).lstrip("@").lower()
@@ -18,8 +20,17 @@ def parse_command(body: str, aliases: Iterable[str] | None = None) -> ParsedComm
if command_alias not in allowed_aliases:
return None
name = match.group(2).lower()
rest = match.group(3).strip()
remainder = match.group(2).strip()
if not remainder:
return None
parts = remainder.split(maxsplit=1)
raw_name = parts[0].lower()
rest = parts[1].strip() if len(parts) > 1 else ""
if raw_name in HELP_ALIASES:
return ParsedCommand(name="help", raw=stripped, arguments=[token for token in rest.split() if token])
if raw_name not in SUPPORTED_COMMANDS:
return None
name = raw_name
tokens = [token for token in rest.split() if token]
parsed = ParsedCommand(name=name, raw=stripped, arguments=tokens)

View File

@@ -4,7 +4,7 @@ from dataclasses import dataclass, field
from typing import Literal
CommandName = Literal["review", "explain", "fix", "ignore", "rerun"]
CommandName = Literal["review", "explain", "fix", "ignore", "rerun", "help"]
@dataclass(slots=True)

View File

@@ -4,12 +4,12 @@ import asyncio
import logging
from typing import Any
from sqlalchemy import select
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from gitea_codex_bot.config import Settings
from gitea_codex_bot.db import get_session_factory
from gitea_codex_bot.models import ReviewJob
from gitea_codex_bot.models import JobStatus, ReviewJob
from gitea_codex_bot.services.comments import upsert_persistent_review_comment_id
from gitea_codex_bot.services.gitea import GiteaClient
from gitea_codex_bot.services.jobs import claim_next_job, finish_job
@@ -34,6 +34,13 @@ def _handle_non_review_command(
job: ReviewJob,
command: ParsedCommand,
) -> tuple[bool, bool, dict[str, Any] | None, str | None]:
if command.name == "help":
try:
message = _build_help_comment(settings, session, gitea, job)
gitea.post_issue_comment(job.repo, job.pr_number, message)
return True, True, {"summary": "Help/status summary posted."}, None
except Exception as exc:
return True, False, None, f"Failed to post help summary: {exc}"
if command.name == "ignore":
return True, True, {"summary": "Ignore command acknowledged. No review run executed."}, None
if command.name == "explain":
@@ -74,6 +81,91 @@ def _handle_non_review_command(
return False, False, None, None
def _build_help_comment(settings: Settings, session: Session, gitea: GiteaClient, job: ReviewJob) -> str:
comments = gitea.list_issue_comments(job.repo, job.pr_number)
comment_summaries = _summarize_comments(comments, settings.gitea_bot_username)
latest_review = session.execute(
select(ReviewJob)
.where(
ReviewJob.repo == job.repo,
ReviewJob.pr_number == job.pr_number,
ReviewJob.command.in_(["review", "rerun"]),
)
.order_by(ReviewJob.id.desc())
.limit(1)
).scalar_one_or_none()
pending_count = session.execute(
select(func.count(ReviewJob.id)).where(
ReviewJob.repo == job.repo,
ReviewJob.pr_number == job.pr_number,
ReviewJob.status.in_([JobStatus.queued, JobStatus.running]),
)
).scalar_one()
latest_status_line = "No previous review run."
if latest_review is not None:
latest_status = latest_review.status.value if hasattr(latest_review.status, "value") else str(latest_review.status)
latest_summary = ""
if isinstance(latest_review.result_json, dict):
summary_raw = latest_review.result_json.get("summary")
if isinstance(summary_raw, str):
latest_summary = " ".join(summary_raw.split())
latest_status_line = f"Latest review command: `{latest_review.command}` status `{latest_status}`."
if latest_summary:
latest_status_line = f"{latest_status_line} Summary: {latest_summary[:180]}"
lines = [
"## Codex Help",
"",
"Supported commands:",
"- `@codex review [security|performance|tests] [--full]`",
"- `@codex rerun`",
"- `@codex explain`",
"- `@codex fix [--branch ...]`",
"- `@codex ignore`",
"- `@codex -h` / `@codex --help` / `@codex help`",
"",
"Status note:",
f"- Pending jobs on this PR: `{pending_count}`",
f"- Fix command enabled: `{str(settings.enable_fix_commands).lower()}`",
f"- {latest_status_line}",
"",
f"Discussion summary ({comment_summaries['total']} comments, human `{comment_summaries['human']}`, bot `{comment_summaries['bot']}`):",
]
if comment_summaries["items"]:
lines.extend(comment_summaries["items"])
else:
lines.append("- No comments available to summarize.")
return "\n".join(lines).strip()
def _summarize_comments(comments: list[dict[str, Any]], bot_username: str) -> dict[str, Any]:
normalized_bot = (bot_username or "").strip().lower()
bot_count = 0
summarized: list[str] = []
recent = comments[-8:] if comments else []
for row in comments:
user = row.get("user")
username = ""
if isinstance(user, dict):
username = str(user.get("username") or user.get("login") or "").strip().lower()
if username and username == normalized_bot:
bot_count += 1
for row in recent:
body_raw = str(row.get("body") or "").strip()
if not body_raw:
continue
one_line = " ".join(body_raw.split())
preview = one_line if len(one_line) <= 180 else f"{one_line[:180]}..."
user = row.get("user")
username = "unknown"
if isinstance(user, dict):
username = str(user.get("username") or user.get("login") or "unknown").strip() or "unknown"
summarized.append(f"- @{username}: {preview}")
total = len(comments)
human_count = max(total - bot_count, 0)
return {"total": total, "human": human_count, "bot": bot_count, "items": summarized}
def _post_review_failure_comment(gitea: GiteaClient, job: ReviewJob, error_message: str) -> None:
message = (
"⚠️ Codex review run failed after queueing.\n\n"

View File

@@ -39,3 +39,16 @@ def test_parse_review_command_for_custom_alias() -> None:
assert cmd is not None
assert cmd.name == "review"
assert cmd.mode == "tests"
def test_parse_help_short_flag() -> None:
cmd = parse_command("@codex -h")
assert cmd is not None
assert cmd.name == "help"
def test_parse_help_long_flag_and_arguments() -> None:
cmd = parse_command("@codex --help status quick", aliases={"codex"})
assert cmd is not None
assert cmd.name == "help"
assert cmd.arguments == ["status", "quick"]

View File

@@ -146,3 +146,43 @@ def test_process_one_job_skips_review_when_repo_config_disabled(monkeypatch) ->
with session_factory() as session:
stored_job = session.execute(select(ReviewJob).where(ReviewJob.id == job.id)).scalar_one()
assert stored_job.status.value == "skipped"
def test_process_one_job_help_command_posts_summary(monkeypatch) -> None:
posted_comments: list[str] = []
session_factory = get_session_factory()
with session_factory() as session:
enqueue_job(
session,
repo="acme/repo",
pr_number=12,
head_sha="abc12345",
trigger_comment_id=114,
trigger_comment_body="@codex -h",
requested_by="alice",
command=ParsedCommand(name="help", raw="@codex -h"),
)
class _FakeGiteaClient:
def __init__(self, _settings) -> None:
pass
def list_issue_comments(self, _repo: str, _pr_number: int):
return [
{"body": "Please check auth edge cases", "user": {"username": "alice"}},
{"body": "On it, running review now.", "user": {"username": "codex-bot"}},
]
def post_issue_comment(self, _repo: str, _pr_number: int, body: str) -> int:
posted_comments.append(body)
return 903
monkeypatch.setattr("gitea_codex_bot.workers.dispatcher.GiteaClient", _FakeGiteaClient)
assert process_one_job(get_settings()) is True
assert posted_comments
body = posted_comments[0]
assert "## Codex Help" in body
assert "@codex -h" in body
assert "Discussion summary" in body
assert "@alice: Please check auth edge cases" in body

View File

@@ -225,6 +225,35 @@ def test_webhook_logs_when_codex_command_is_not_review(monkeypatch) -> None:
assert any("Webhook without @codex review command" in item for item in messages)
def test_webhook_accepts_help_short_flag_and_queues(monkeypatch) -> None:
monkeypatch.setattr(
"gitea_codex_bot.services.gitea.GiteaClient.get_pull_request",
lambda *_args, **_kwargs: type("PR", (), {"head_sha": "abcdef123"})(),
)
client = TestClient(app)
payload_obj = _payload("@codex -h", username="alice", comment_id=333)
raw = json.dumps(payload_obj).encode()
response = client.post(
"/webhook/gitea",
content=raw,
headers={
"X-Gitea-Event": "issue_comment",
"X-Gitea-Delivery": "d-help-1",
"X-Gitea-Signature": _sign(raw),
"Content-Type": "application/json",
},
)
assert response.status_code == 200
assert response.json()["status"] == "queued"
session_factory = get_session_factory()
with session_factory() as session:
queued = session.execute(select(ReviewJob).where(ReviewJob.trigger_comment_id == 333)).scalar_one()
assert queued.command == "help"
def test_webhook_logs_when_repo_not_allowed(monkeypatch) -> None:
messages: list[str] = []