Skip to content

Commit 02f19ef

Browse files
Add test plans to Phabricator patch submissions (#6737)
1 parent 9574957 commit 02f19ef

5 files changed

Lines changed: 118 additions & 26 deletions

File tree

docs/hackbot/actions.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,17 @@ record in its `config.py` and passes them to `actions_server_for`, which builds
3434
carrying only those. `bug-fix`, for instance, allows `phabricator.submit_patch` on a fresh
3535
triage run but swaps it for `phabricator.update_patch` on a follow-up.
3636

37-
| Action type | Records the intent to… | Params |
38-
| --------------------------- | ------------------------------------------ | ---------------------------------- |
39-
| `bugzilla.update_bug` | Change a bug's fields | `bug_id`, `changes` |
40-
| `bugzilla.add_comment` | Comment on a bug | `bug_id`, `text`, `is_private` |
41-
| `bugzilla.add_attachment` | Attach a file to a bug | `bug_id`, + a `file` attachment |
42-
| `bugzilla.create_bug` | File a new bug | the new bug's fields |
43-
| `phabricator.submit_patch` | Deliver a fix as a **new** revision | `bug_id`, `title`, `summary` |
44-
| `phabricator.update_patch` | Add a new diff to an **existing** revision | `revision_id` |
45-
| `phabricator.add_comment` | Reply on a revision without changing code | `revision_id`, `text` |
46-
| `testrail.submit_test_plan` | Submit a generated test plan to TestRail | the validated feature + test cases |
47-
| `slack.post_message` | Post a message to Slack | `channel`, `text` |
37+
| Action type | Records the intent to… | Params |
38+
| --------------------------- | ------------------------------------------ | ----------------------------------------- |
39+
| `bugzilla.update_bug` | Change a bug's fields | `bug_id`, `changes` |
40+
| `bugzilla.add_comment` | Comment on a bug | `bug_id`, `text`, `is_private` |
41+
| `bugzilla.add_attachment` | Attach a file to a bug | `bug_id`, + a `file` attachment |
42+
| `bugzilla.create_bug` | File a new bug | the new bug's fields |
43+
| `phabricator.submit_patch` | Deliver a fix as a **new** revision | `bug_id`, `title`, `summary`, `test_plan` |
44+
| `phabricator.update_patch` | Add a new diff to an **existing** revision | `revision_id` |
45+
| `phabricator.add_comment` | Reply on a revision without changing code | `revision_id`, `text` |
46+
| `testrail.submit_test_plan` | Submit a generated test plan to TestRail | the validated feature + test cases |
47+
| `slack.post_message` | Post a message to Slack | `channel`, `text` |
4848

4949
All but `testrail.submit_test_plan` take a **`reasoning`** argument — a free-text audit trail
5050
stored on the action and shown in the UI beside the proposed change. `phabricator.submit_patch`

libs/hackbot-runtime/hackbot_runtime/actions/handlers/phabricator_handler.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,13 @@ def _local_commit_author_fields(previous_commits: list[dict]) -> dict[str, str]:
145145
}
146146

147147

148-
def _arc_commit_message(title: str, summary: str | None, bug_id: Any, url: str) -> str:
148+
def _arc_commit_message(
149+
title: str,
150+
summary: str | None,
151+
test_plan: str | None,
152+
bug_id: Any,
153+
url: str,
154+
) -> str:
149155
"""Build moz-phab's arc commit message, with the Differential Revision URL.
150156
151157
Mirrors ``Commit.build_arc_commit_message`` + ``amend_revision_url`` so the
@@ -159,7 +165,7 @@ def _arc_commit_message(title: str, summary: str | None, bug_id: Any, url: str)
159165
return _ARC_COMMIT_MESSAGE_TEMPLATE.format(
160166
title=title,
161167
body=body,
162-
test_plan="",
168+
test_plan=test_plan or "",
163169
reviewers="",
164170
bug_id=bug_id if bug_id is not None else "",
165171
)
@@ -170,6 +176,7 @@ async def _set_local_commits(
170176
local_commits: dict,
171177
title: str,
172178
summary: str | None,
179+
test_plan: str | None,
173180
bug_id: Any,
174181
revision_id: int,
175182
) -> None:
@@ -180,7 +187,9 @@ async def _set_local_commits(
180187
arc-formatted ``message`` are filled in here, since they need the revision
181188
URL.
182189
"""
183-
message = _arc_commit_message(title, summary, bug_id, _revision_url(revision_id))
190+
message = _arc_commit_message(
191+
title, summary, test_plan, bug_id, _revision_url(revision_id)
192+
)
184193
for commit_info in local_commits.values():
185194
commit_info["summary"] = title
186195
commit_info["message"] = message
@@ -220,6 +229,7 @@ class SubmitPatchHandler:
220229
async def apply(self, params: dict[str, Any], ctx: ApplyContext) -> ActionResult:
221230
bug_id = params["bug_id"]
222231
summary = params.get("summary")
232+
test_plan = params.get("test_plan")
223233

224234
try:
225235
raw = await ctx.download_artifact(_DIFF_ARTIFACT_KEY)
@@ -251,6 +261,8 @@ async def apply(self, params: dict[str, Any], ctx: ApplyContext) -> ActionResult
251261
]
252262
if summary:
253263
transactions.append({"type": "summary", "value": summary})
264+
if test_plan:
265+
transactions.append({"type": "testPlan", "value": test_plan})
254266

255267
revision_result = await _conduit_request(
256268
"differential.revision.edit", transactions=transactions
@@ -266,6 +278,7 @@ async def apply(self, params: dict[str, Any], ctx: ApplyContext) -> ActionResult
266278
submission["local_commits"],
267279
title,
268280
summary,
281+
test_plan,
269282
bug_id,
270283
revision_id,
271284
)
@@ -329,6 +342,7 @@ async def apply(self, params: dict[str, Any], ctx: ApplyContext) -> ActionResult
329342
submission["local_commits"],
330343
fields.get("title") or f"D{revision_id}",
331344
fields.get("summary"),
345+
fields.get("testPlan"),
332346
fields.get("bugzilla.bug-id"),
333347
revision_id,
334348
)

libs/hackbot-runtime/hackbot_runtime/actions/phabricator.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ def _validate_summary(summary: str | None) -> None:
4444
if match:
4545
raise ToolError(
4646
f'Invalid Phabricator summary: "{match.group()}" at the beginning of '
47-
"a line is interpreted as a Test Plan field. Call submit_patch again "
48-
"with that fixed."
47+
"a line belongs in the Test Plan field. Move the verification details "
48+
"to the test_plan argument and call submit_patch again."
4949
)
5050

5151

@@ -65,9 +65,19 @@ async def submit_patch(
6565
reasoning: Annotated[
6666
str, Field(description="Why you are submitting this patch (for audit log).")
6767
],
68+
test_plan: Annotated[
69+
str | None,
70+
Field(default=None, description="Revision test plan."),
71+
] = None,
6872
summary: Annotated[
6973
str | None,
70-
Field(default=None, description="Revision summary/description."),
74+
Field(
75+
default=None,
76+
description=(
77+
"Revision summary/description. Keep test and verification details "
78+
"in test_plan instead."
79+
),
80+
),
7181
] = None,
7282
ref: Annotated[
7383
str | None,
@@ -102,7 +112,12 @@ async def submit_patch(
102112
_validate_summary(summary)
103113
recorder.record(
104114
"phabricator.submit_patch",
105-
{"bug_id": bug_id, "title": title, "summary": summary},
115+
{
116+
"bug_id": bug_id,
117+
"title": title,
118+
"summary": summary,
119+
"test_plan": test_plan,
120+
},
106121
reasoning=reasoning,
107122
ref=ref,
108123
)

libs/hackbot-runtime/tests/test_phabricator_actions.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,23 @@
55
from hackbot_runtime.actions import ActionsRecorder, phabricator
66

77

8-
async def test_submit_records_create_params_only():
8+
async def test_submit_records_test_plan():
99
rec = ActionsRecorder()
1010
await phabricator.submit_patch(
11-
rec, bug_id=1, title="Fix the thing", reasoning="r", summary="Details"
11+
rec,
12+
bug_id=1,
13+
title="Fix the thing",
14+
test_plan="uv run pytest tests/test_thing.py (passed)",
15+
reasoning="r",
16+
summary="Details",
1217
)
1318
action = rec.actions[0]
1419
assert action["type"] == "phabricator.submit_patch"
1520
assert action["params"] == {
1621
"bug_id": 1,
1722
"title": "Fix the thing",
1823
"summary": "Details",
24+
"test_plan": "uv run pytest tests/test_thing.py (passed)",
1925
}
2026
assert "ref" not in action
2127

@@ -37,7 +43,7 @@ async def test_submit_rejects_test_plan_headers(header):
3743
)
3844

3945
assert header in str(exc.value)
40-
assert "Call submit_patch again with that fixed" in str(exc.value)
46+
assert "test_plan argument" in str(exc.value)
4147
assert rec.actions == []
4248

4349

@@ -55,7 +61,11 @@ async def test_submit_accepts_safe_summary(summary):
5561
rec = ActionsRecorder()
5662

5763
await phabricator.submit_patch(
58-
rec, bug_id=1, title="Fix", reasoning="r", summary=summary
64+
rec,
65+
bug_id=1,
66+
title="Fix",
67+
reasoning="r",
68+
summary=summary,
5969
)
6070

6171
assert rec.actions[0]["params"]["summary"] == summary
@@ -67,10 +77,20 @@ async def test_submit_requires_title():
6777
await phabricator.submit_patch(rec, bug_id=1, reasoning="r")
6878

6979

80+
async def test_submit_accepts_missing_test_plan():
81+
rec = ActionsRecorder()
82+
await phabricator.submit_patch(rec, bug_id=1, title="Fix", reasoning="r")
83+
assert rec.actions[0]["params"]["test_plan"] is None
84+
85+
7086
async def test_submit_ref_is_recorded():
7187
rec = ActionsRecorder()
7288
await phabricator.submit_patch(
73-
rec, bug_id=1, title="Fix", reasoning="r", ref="patch"
89+
rec,
90+
bug_id=1,
91+
title="Fix",
92+
reasoning="r",
93+
ref="patch",
7494
)
7595
assert rec.actions[0]["ref"] == "patch"
7696

@@ -96,7 +116,12 @@ def test_agent_facing_schemas_are_case_specific():
96116
update = next(t for t in phabricator.TOOLS if t.name == "update_patch")
97117

98118
assert "revision_id" not in submit.input_schema["properties"]
99-
assert set(submit.input_schema["required"]) == {"bug_id", "title", "reasoning"}
119+
assert set(submit.input_schema["required"]) == {
120+
"bug_id",
121+
"title",
122+
"reasoning",
123+
}
124+
assert "test_plan" in submit.input_schema["properties"]
100125

101126
# An update carries the revision id and nothing else: the revision's title,
102127
# summary and bug id stay as they are, and there is no new URL to reference.

libs/hackbot-runtime/tests/test_phabricator_handler.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,12 @@ async def test_submit_patch_creates_planned_changes_revision(monkeypatch):
9999
)
100100

101101
result = await phabricator_handler.SubmitPatchHandler().apply(
102-
{"bug_id": 1, "title": "Fix", "summary": "s"},
102+
{
103+
"bug_id": 1,
104+
"title": "Fix",
105+
"summary": "s",
106+
"test_plan": "uv run pytest (passed)",
107+
},
103108
_ctx(),
104109
)
105110

@@ -124,6 +129,31 @@ async def test_submit_patch_creates_planned_changes_revision(monkeypatch):
124129
assert "reviewers.add" not in transactions
125130
assert transactions["bugzilla.bug-id"] == "1"
126131
assert transactions["summary"] == "s"
132+
assert transactions["testPlan"] == "uv run pytest (passed)"
133+
134+
135+
async def test_submit_patch_accepts_legacy_action_without_test_plan(monkeypatch):
136+
fake, calls = _fake_conduit(
137+
{
138+
"differential.creatediff": {"phid": "PHID-DIFF-1", "diffid": 1},
139+
"differential.revision.edit": {"object": {"id": 555}},
140+
"differential.setdiffproperty": {},
141+
}
142+
)
143+
monkeypatch.setattr(phabricator_handler, "_conduit_request", fake)
144+
monkeypatch.setattr(
145+
phabricator_handler, "_repository_phid", AsyncMock(return_value="PHID-REPO-1")
146+
)
147+
148+
result = await phabricator_handler.SubmitPatchHandler().apply(
149+
{"bug_id": 1, "title": "Fix", "summary": "s"},
150+
_ctx(),
151+
)
152+
153+
assert result.status == "applied"
154+
edit_call = next(c for c in calls if c[0] == "differential.revision.edit")
155+
transaction_types = {t["type"] for t in edit_call[1]["transactions"]}
156+
assert "testPlan" not in transaction_types
127157

128158

129159
async def test_submit_patch_sets_local_commits_property(monkeypatch):
@@ -150,7 +180,12 @@ async def test_submit_patch_sets_local_commits_property(monkeypatch):
150180
"tree": "tree1",
151181
}
152182
result = await phabricator_handler.SubmitPatchHandler().apply(
153-
{"bug_id": 5, "title": "Fix the thing", "summary": "does it"},
183+
{
184+
"bug_id": 5,
185+
"title": "Fix the thing",
186+
"summary": "does it",
187+
"test_plan": "uv run pytest tests/test_thing.py (passed)",
188+
},
154189
_ctx(local_commits={"node1": dict(git_fields)}),
155190
)
156191
assert result.status == "applied"
@@ -173,6 +208,7 @@ async def test_submit_patch_sets_local_commits_property(monkeypatch):
173208
# The stored title matches the visible revision title and reviewers are empty.
174209
assert stored["summary"] == "Fix the thing"
175210
assert stored["message"].startswith("Fix the thing\n\nSummary:\ndoes it")
211+
assert "Test Plan:\nuv run pytest tests/test_thing.py (passed)" in stored["message"]
176212
assert (
177213
"Differential Revision: https://phabricator.services.mozilla.com/D77"
178214
in stored["message"]
@@ -228,6 +264,7 @@ async def test_update_patch_local_commits_use_the_revisions_own_fields(monkeypat
228264
"fields": {
229265
"title": "WIP: Existing title",
230266
"summary": "old sum",
267+
"testPlan": "existing test plan",
231268
"bugzilla.bug-id": "9",
232269
}
233270
}
@@ -254,6 +291,7 @@ async def test_update_patch_local_commits_use_the_revisions_own_fields(monkeypat
254291
)["n"]
255292
assert stored["summary"] == "WIP: Existing title"
256293
assert stored["message"].startswith("WIP: Existing title\n\nSummary:\nold sum")
294+
assert "Test Plan:\nexisting test plan" in stored["message"]
257295
assert "Bug #: 9" in stored["message"]
258296
assert (
259297
"Differential Revision: https://phabricator.services.mozilla.com/D42"

0 commit comments

Comments
 (0)