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
12 changes: 12 additions & 0 deletions bert_e/reactor.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,13 @@ def __init__(self, keyword: str):
self.keyword = keyword


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


class NotFound(Error):
"""The requested command or option doesn't exist."""
def __init__(self, keyword: str):
Expand Down Expand Up @@ -445,6 +452,11 @@ def handle_commands(self, job, text, prefix, privileged=False):
return
raise NotFound(key)
if not isinstance(command, Command):
if slash_shorthand and args and not args[0].startswith("="):

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 — Valid multi-option comments get rejected and block the merge run.

The new check raises IncorrectSyntax for any /-shorthand option that has trailing tokens not starting with =. That includes valid multi-option comments that handle_options accepts on purpose, through its regex ^/[\w=]+([\s,.\-:;|+]+/[\w=]+)*\s*$. For /approve /bypass_jira_check or /approve, /after_pull_request=3, handle_commands parses key approve with args ['/bypass_jira_check'], and '/bypass_jira_check' does not start with =, so it raises IncorrectSyntax('approve'). I ran both comments through a Reactor subclass: handle_options applied them without error, and handle_commands raised IncorrectSyntax each time. Posted after Bert-E's last message, these valid comments now stop the run with an "Incorrect command syntax" reply that blames approve. Before this change they were accepted. The check should reject only args that are not further /-prefixed option tokens, or should reuse the options regex.

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 — One malformed variant is still ignored silently.

/after_pull_request = 1509 (spaces around =) gives args ['=', '1509'], and args[0].startswith('=') passes, so no error is raised. handle_options also drops this form (verified), so the malformed declaration is still ignored silently, the same failure mode issue #284 describes.

# An option followed by free-form arguments (e.g.
# ``/after_pull_request 1509``) is not a valid option
# declaration: options take their argument as ``key=value``.
raise IncorrectSyntax(key)

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: the handle_commands docstring Raises section (line ~415) only documents NotFound and NotPrivileged. Consider adding IncorrectSyntax to that list now that this method can raise it.

— Claude Code

return

if command.privileged and not privileged:
Expand Down
5 changes: 4 additions & 1 deletion bert_e/templates/incorrect_command_syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ It seems that your command syntax is incorrect. The correct usage is:
```
@{{ robot }} option[=argument]
```

{% if keyword is defined and keyword %}
Options take their argument after an equal sign, without spaces, e.g.
`/{{ keyword }}=<value>` or `@{{ robot }} {{ keyword }}=<value>`.
{% endif %}
Please **edit** or **delete** the corresponding comment so I can move on.

{% endblock %}
16 changes: 16 additions & 0 deletions bert_e/tests/test_bert_e.py
Original file line number Diff line number Diff line change
Expand Up @@ -4358,6 +4358,22 @@ def test_after_pull_request_wrong_syntax(self):
with self.assertRaises(exns.IncorrectCommandSyntax):
self.handle(blocked_pr.id, options=self.bypass_all, backtrace=True)

def test_after_pull_request_slash_space_syntax(self):
pr_declined = self.create_pr('bugfix/TEST-00002', 'development/4.3')
pr_declined.decline()
blocked_pr = self.create_pr('bugfix/TEST-00003', 'development/4.3')

# Commands are only read after Bert-E's last message
with self.assertRaises(exns.ApprovalRequired):
self.handle(blocked_pr.id, options=['bypass_jira_check',
'bypass_build_status'],
backtrace=True)

blocked_pr.add_comment('/after_pull_request %s' % pr_declined.id)

with self.assertRaises(exns.IncorrectCommandSyntax):
self.handle(blocked_pr.id, options=self.bypass_all, backtrace=True)

def test_after_pull_request_wrong_pr_id(self):
blocked_pr = self.create_pr('bugfix/TEST-00003', 'development/4.3')

Expand Down
22 changes: 21 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,22 @@
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_slash_option_with_space_arg_raises(reactor_cls, job):
"""``/after_pull_request 1509`` (space instead of ``=``) must not be
silently ignored."""

@reactor_cls.option(default=set())
def after_pull_request(job, pr_id=None):
pass

Check warning on line 403 in bert_e/tests/unit/test_reactor.py

View check run for this annotation

Codecov / codecov/patch

bert_e/tests/unit/test_reactor.py#L403

Added line #L403 was not covered by tests

reactor = reactor_cls()

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

# Valid syntaxes are still fine.
reactor.handle_commands(job, '/after_pull_request', '@bert-e')
reactor.handle_commands(job, '/after_pull_request=1509', '@bert-e')
9 changes: 8 additions & 1 deletion bert_e/workflow/gitwaterflow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
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 +362,11 @@ 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:

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 dependency is silently dropped after one reply, so the PR can merge too early.

The new error comes only from the commands loop, and that loop reads only the comments posted after the robot's last message (if author == job.settings.robot: return). Once Bert-E posts the IncorrectCommandSyntax reply, the next run stops at that reply and never sees /after_pull_request 1509 again. The options loop doesn't catch it either: the comment fails the /-shorthand regex in handle_options because the second token has no /, so it is dropped silently. The next run then goes ahead as if the dependency had never been declared and can merge the PR before PR 1509. The message says "edit or delete the comment so I can move on", but the block lasts for one run only. Compare the @bert-e after_pull_request N form, which fails inside handle_options (a TypeError) on every run, so its block persists.

raise messages.IncorrectCommandSyntax(
robot=job.settings.robot, keyword=err.keyword,
active_options=job.active_options
) from err


def check_commit_diff(job):
Expand Down
Loading