Skip to content
Merged
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
22 changes: 21 additions & 1 deletion libs/hackbot-runtime/hackbot_runtime/actions/phabricator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@

from __future__ import annotations

import re
from typing import Annotated

from agent_tools.registry import tool, tools_in
from agent_tools.registry import ToolError, tool, tools_in
from pydantic import Field

from hackbot_runtime.actions.recorder import ActionsRecorder
Expand All @@ -25,11 +26,29 @@
# in ``context.publish_changes`` — has to cover both types.
PATCH_ACTION_TYPES = frozenset({"phabricator.submit_patch", "phabricator.update_patch"})

_PHABRICATOR_TEST_PLAN_HEADER_RE = re.compile(
r"^(?:Test Plan|Testplan|Tested|Tests):",
re.IGNORECASE | re.MULTILINE,
)


def _confirm(recorder: ActionsRecorder, action_type: str) -> str:
return f"Recorded {action_type} (#{len(recorder.actions) - 1})."


def _validate_summary(summary: str | None) -> None:
if not summary:
return

match = _PHABRICATOR_TEST_PLAN_HEADER_RE.search(summary)
if match:
raise ToolError(
f'Invalid Phabricator summary: "{match.group()}" at the beginning of '
"a line is interpreted as a Test Plan field. Call submit_patch again "
"with that fixed."
)


@tool
async def submit_patch(
recorder: ActionsRecorder,
Expand Down Expand Up @@ -80,6 +99,7 @@ async def submit_patch(
in the same run, written as `{{actions.<ref>.url}}` (for example, inside a
bug comment).
"""
_validate_summary(summary)
recorder.record(
"phabricator.submit_patch",
{"bug_id": bug_id, "title": title, "summary": summary},
Expand Down
42 changes: 42 additions & 0 deletions libs/hackbot-runtime/tests/test_phabricator_actions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Tests for the phabricator recording tools (submit/update patch, comment)."""

import pytest
from agent_tools.registry import ToolError
from hackbot_runtime.actions import ActionsRecorder, phabricator


Expand All @@ -19,6 +20,47 @@ async def test_submit_records_create_params_only():
assert "ref" not in action


@pytest.mark.parametrize(
"header",
["Tests", "tests", "Test Plan", "Testplan", "Tested"],
)
async def test_submit_rejects_test_plan_headers(header):
rec = ActionsRecorder()

with pytest.raises(ToolError) as exc:
await phabricator.submit_patch(
rec,
bug_id=1,
title="Fix",
reasoning="r",
summary=f"Explanation\n\n{header}: details",
)

assert header in str(exc.value)
assert "Call submit_patch again with that fixed" in str(exc.value)
assert rec.actions == []


@pytest.mark.parametrize(
"summary",
[
None,
"",
"Testing: details",
"Some Tests: details",
" Tests: already indented",
],
)
async def test_submit_accepts_safe_summary(summary):
rec = ActionsRecorder()

await phabricator.submit_patch(
rec, bug_id=1, title="Fix", reasoning="r", summary=summary
)

assert rec.actions[0]["params"]["summary"] == summary


async def test_submit_requires_title():
rec = ActionsRecorder()
with pytest.raises(TypeError):
Expand Down
1 change: 1 addition & 0 deletions libs/hackbot-runtime/tests/test_phabricator_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ async def test_submit_patch_creates_planned_changes_revision(monkeypatch):
assert transactions["plan-changes"] is True
assert "reviewers.add" not in transactions
assert transactions["bugzilla.bug-id"] == "1"
assert transactions["summary"] == "s"


async def test_submit_patch_sets_local_commits_property(monkeypatch):
Expand Down