Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions bert_e/reactor.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ def __init__(self, keyword: str):
self.keyword = keyword


class IncorrectSyntax(Error):
"""An option was called with arguments using the command syntax."""
def __init__(self, keyword: str):
super().__init__()
self.keyword = keyword


LOG = logging.getLogger(__name__)

Command = namedtuple('Command', ['handler', 'help', 'privileged', 'authored'])
Expand Down Expand Up @@ -412,6 +419,8 @@ def handle_commands(self, job, text, prefix, privileged=False):
registered command to be considered a typo).
NotPrivileged: when a privileged command call is found
and the method is called with privileged=False.
IncorrectSyntax: if an option is used like a command, with
positional arguments.

"""
raw = text.strip()
Expand Down Expand Up @@ -445,6 +454,10 @@ def handle_commands(self, job, text, prefix, privileged=False):
return
raise NotFound(key)
if not isinstance(command, Command):
if args:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Critical — Correct key=value and multi-option comments now block every PR that uses them.

The if args: guard also fires on option comments that are valid. The command regex (?P<command>[A-Za-z_]+[^= ,])(?P<args>.*) stops the key before =, a space or a comma, so args is non-empty for every option that has a value or is followed by another option. I ran it: @bert-e after_pull_request=1509, /after_pull_request=12, @bert-e approve wait, @bert-e: approve, wait and /approve /wait all raise IncorrectSyntax now. Before this change they returned silently. The docstring documents exactly these forms as valid ({prefix}option1=val1 option2). handle_comments turns the exception into IncorrectCommandSyntax, which blocks the PR. Any user who posts the documented key=value syntax, or several options in one comment, gets an error reply and the PR does not progress. The check should only fire when the key is followed by whitespace and then a bare token without =, e.g. by checking that the remainder after the key does not start with = and that no token is itself a registered option.

# An option followed by arguments (e.g. ``/after_pull_request
# 1509``) is not valid: options take ``key=value``.
raise IncorrectSyntax(key)
return

if command.privileged and not privileged:
Expand Down
2 changes: 1 addition & 1 deletion bert_e/templates/incorrect_command_syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Incorrect command syntax
{% endblock %}

{% block message %}
It seems that your command syntax is incorrect. The correct usage is:
It seems that your command syntax is incorrect.{% if extra_message %} {{ extra_message }}{% endif %} The correct usage is:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Minor — Users see confusing internal Python error text in bot replies.

The template now renders extra_message. The existing except TypeError path in handle_comments (around line 338) passes extra_message=str(err), and until now that text was never shown. After this change, raw Python TypeError text such as a() takes from 1 to 2 positional arguments but 3 were given is posted verbatim into PR comments. That text exposes internal handler names and can come from bugs unrelated to user syntax.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The template now checks extra_message with a plain if-test, but Jinja2 StrictUndefined raises UndefinedError when the variable is not passed at all. The existing path in commands.py:272 raises IncorrectCommandSyntax without extra_message, so this template change will crash there. Use 'if extra_message is defined and extra_message' instead. — Claude Code


```
@{{ robot }} option[=argument]
Expand Down
20 changes: 19 additions & 1 deletion bert_e/tests/unit/test_reactor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@

import pytest

from bert_e.reactor import Command, NotFound, NotPrivileged, Option, Reactor
from bert_e.reactor import (Command, IncorrectSyntax, NotFound, NotPrivileged,
Option, Reactor)


# All tests are run on a Reactor subclass to avoid sharing state.
Expand Down Expand Up @@ -391,3 +392,20 @@ def help(job, *args):
assert reactor._has_close_match('gemini') is False
assert reactor._has_close_match('copilot') is False
assert reactor._has_close_match('other-bot-name') is False


def test_handle_commands_option_with_positional_args_raises(reactor_cls, job):
"""``/after_pull_request 1509`` must not be silently ignored."""

@reactor_cls.option(default=set())
def after_pull_request(job, pr_id=None):
job.settings['after_pull_request'].add(pr_id)

reactor = reactor_cls()

with pytest.raises(IncorrectSyntax) as err:
reactor.handle_commands(job, '/after_pull_request 1509', '@bert-e')
assert err.value.keyword == 'after_pull_request'

# an option alone is still ignored by handle_commands
reactor.handle_commands(job, '/after_pull_request', '@bert-e')
10 changes: 9 additions & 1 deletion bert_e/workflow/gitwaterflow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
from bert_e.job import handler, CommitJob, PullRequestJob, QueuesJob
from bert_e.lib.cli import confirm
from bert_e.lib.simplecmd import CommandError
from bert_e.reactor import Reactor, NotFound, NotPrivileged, NotAuthored
from bert_e.reactor import (Reactor, NotFound, NotPrivileged, NotAuthored,
IncorrectSyntax)
from ..git_utils import push, clone_git_repo
from ..pr_utils import find_comment, notify_user
from .branches import (
Expand Down Expand Up @@ -360,6 +361,13 @@ def handle_comments(job):
active_options=job.active_options, command=err.keyword,
author=author, self_pr=(author == pr_author), comment=text
) from err
except IncorrectSyntax as err:
raise messages.IncorrectCommandSyntax(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning — The usage hint in the new error reply leaves out the bot name.

The new messages.IncorrectCommandSyntax(...) is built without robot=. The template renders @{{ robot }} option[=argument], and neither TemplateException nor message.md fills robot in. The reply therefore shows the correct usage as @ option[=argument], with no bot name. The existing after_pull_request handler in commands.py passes robot=job.bert_e.client.login for this reason.

extra_message="'%s' is an option, not a command; use "
"'%s=<value>' instead." % (
err.keyword, err.keyword),
active_options=job.active_options
) from err

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The template uses robot variable but this handler does not pass robot. With StrictUndefined in template_loader.py, this will crash at render time when /after_pull_request 1509 is used. Add robot=job.settings.robot to the kwargs. — Claude Code



def check_commit_diff(job):
Expand Down
Loading