diff --git a/src/agentci/domain/commands.py b/src/agentci/domain/commands.py index 6f76cb4..877d1eb 100644 --- a/src/agentci/domain/commands.py +++ b/src/agentci/domain/commands.py @@ -4,7 +4,7 @@ import re from agentci.domain.models import CommandName, JobKind, ParsedCommand -COMMAND_RE = re.compile(r"^/agent[ \t]+([a-z]+)(?:[ \t]+([\s\S]*))?$") +COMMAND_RE = re.compile(r"^/agent[ \t]+([a-z]+)(?:[ \t\r\n]+([\s\S]*))?$") class CommandError(ValueError): @@ -46,4 +46,3 @@ def resolve_job_kind(command: ParsedCommand, *, is_pull_request: bool) -> JobKin if command.name not in mapping: raise CommandError(f"`/agent {command.name}` can only be used on a pull request.") return mapping[command.name] - diff --git a/tests/test_commands.py b/tests/test_commands.py index 029490d..1dc320c 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -8,12 +8,31 @@ def test_ignores_non_commands() -> None: assert parse_command("please run /agent plan") is None -def test_parses_multiline_message() -> None: - command = parse_command("/agent implement focus on API\nand add tests") +@pytest.mark.parametrize("name", list(CommandName)) +@pytest.mark.parametrize("line_breaks", [1, 2, 5]) +def test_all_commands_accept_messages_after_any_number_of_lines( + name: CommandName, line_breaks: int +) -> None: + command = parse_command( + f"/agent {name.value}{'\n' * line_breaks}" + "focus on the API\nand add tests" + ) assert command is not None - assert command.name is CommandName.IMPLEMENT - assert command.message == "focus on API\nand add tests" - assert resolve_job_kind(command, is_pull_request=False) is JobKind.IMPLEMENT + assert command.name is name + assert command.message == "focus on the API\nand add tests" + + +@pytest.mark.parametrize("name", list(CommandName)) +def test_all_commands_accept_crlf_separated_multiline_messages( + name: CommandName, +) -> None: + command = parse_command( + f"/agent {name.value}\r\n\r\n\r\n" + "focus on the API\r\nand add tests" + ) + assert command is not None + assert command.name is name + assert command.message == "focus on the API\r\nand add tests" def test_discuss_requires_message() -> None: @@ -28,9 +47,21 @@ def test_rejects_wrong_location() -> None: resolve_job_kind(command, is_pull_request=False) -def test_resolves_iterate_by_location() -> None: - command = parse_command("/agent iterate refine tests") +@pytest.mark.parametrize( + ("body", "message"), + [ + ("/agent iterate", ""), + ("/agent iterate refine tests", "refine tests"), + ( + "/agent iterate\n\nkeep the API stable\nlimit changes to the parser", + "keep the API stable\nlimit changes to the parser", + ), + ], +) +def test_iterate_accepts_optional_message(body: str, message: str) -> None: + command = parse_command(body) assert command is not None + assert command.name is CommandName.ITERATE + assert command.message == message assert resolve_job_kind(command, is_pull_request=False) is JobKind.ITERATE_PLAN assert resolve_job_kind(command, is_pull_request=True) is JobKind.ITERATE_IMPLEMENT - diff --git a/tests/test_webhook.py b/tests/test_webhook.py index 00b7bf3..70448ef 100644 --- a/tests/test_webhook.py +++ b/tests/test_webhook.py @@ -72,6 +72,27 @@ async def test_authorized_command_is_queued() -> None: assert "queued" in gitea.comments[0] +async def test_iterate_message_is_preserved_on_queued_job() -> None: + storage = FakeStorage() + container = SimpleNamespace(storage=storage, gitea=FakeGitea()) + event = _event_from_payload( + "delivery", + payload( + "/agent iterate\n\nkeep the API stable\nlimit changes to the parser", + is_pull=True, + ), + ) + assert event is not None + + response = await _handle_command(container, event) + + assert response.status_code == 202 + assert len(storage.jobs) == 1 + assert storage.jobs[0].message == ( + "keep the API stable\nlimit changes to the parser" + ) + + async def test_non_command_is_ignored() -> None: container = SimpleNamespace(storage=FakeStorage(), gitea=FakeGitea()) event = _event_from_payload("delivery", payload("ordinary discussion"))