[feat]. Add @codex -h help summary command
All checks were successful
ci / test (pull_request) Successful in 38s
ci / publish (pull_request) Has been skipped

This commit is contained in:
Space-Banane
2026-05-23 14:07:48 +02:00
parent 30aa737516
commit 9392591429
7 changed files with 193 additions and 8 deletions

View File

@@ -495,7 +495,7 @@ async def gitea_webhook(
gitea.post_issue_comment(repo, pr_number, format_queue_ack(head_sha)) gitea.post_issue_comment(repo, pr_number, format_queue_ack(head_sha))
return {"accepted": True, "job_id": job.id, "status": "queued"} 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( job = enqueue_job(
session, session,
repo=repo, repo=repo,

View File

@@ -5,12 +5,14 @@ from collections.abc import Iterable
from gitea_codex_bot.types import ParsedCommand 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: def parse_command(body: str, aliases: Iterable[str] | None = None) -> ParsedCommand | None:
stripped = body.strip() stripped = body.strip()
match = COMMAND_RE.match(stripped) match = PREFIX_RE.match(stripped)
if not match: if not match:
return None return None
command_alias = match.group(1).lstrip("@").lower() 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: if command_alias not in allowed_aliases:
return None return None
name = match.group(2).lower() remainder = match.group(2).strip()
rest = match.group(3).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] tokens = [token for token in rest.split() if token]
parsed = ParsedCommand(name=name, raw=stripped, arguments=tokens) parsed = ParsedCommand(name=name, raw=stripped, arguments=tokens)

View File

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

View File

@@ -4,12 +4,12 @@ import asyncio
import logging import logging
from typing import Any from typing import Any
from sqlalchemy import select from sqlalchemy import func, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from gitea_codex_bot.config import Settings from gitea_codex_bot.config import Settings
from gitea_codex_bot.db import get_session_factory 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.comments import upsert_persistent_review_comment_id
from gitea_codex_bot.services.gitea import GiteaClient from gitea_codex_bot.services.gitea import GiteaClient
from gitea_codex_bot.services.jobs import claim_next_job, finish_job from gitea_codex_bot.services.jobs import claim_next_job, finish_job
@@ -34,6 +34,13 @@ def _handle_non_review_command(
job: ReviewJob, job: ReviewJob,
command: ParsedCommand, command: ParsedCommand,
) -> tuple[bool, bool, dict[str, Any] | None, str | None]: ) -> 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": if command.name == "ignore":
return True, True, {"summary": "Ignore command acknowledged. No review run executed."}, None return True, True, {"summary": "Ignore command acknowledged. No review run executed."}, None
if command.name == "explain": if command.name == "explain":
@@ -74,6 +81,91 @@ def _handle_non_review_command(
return False, False, None, None 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: def _post_review_failure_comment(gitea: GiteaClient, job: ReviewJob, error_message: str) -> None:
message = ( message = (
"⚠️ Codex review run failed after queueing.\n\n" "⚠️ 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 is not None
assert cmd.name == "review" assert cmd.name == "review"
assert cmd.mode == "tests" 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: with session_factory() as session:
stored_job = session.execute(select(ReviewJob).where(ReviewJob.id == job.id)).scalar_one() stored_job = session.execute(select(ReviewJob).where(ReviewJob.id == job.id)).scalar_one()
assert stored_job.status.value == "skipped" 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) 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: def test_webhook_logs_when_repo_not_allowed(monkeypatch) -> None:
messages: list[str] = [] messages: list[str] = []