fix(robomp): route issues.reopened to admission-gated re-triage

The finalized-issue boilerplate promises "reopen and I'll triage again from
scratch," but github_events.route() dropped issues.reopened to the ignored
skip branch (only opened/closed handled), while pull_request.reopened was
already routed. A user following the instruction got silence.

Route issues.reopened like issues.opened (submitter-attributable triage_issue,
same per-user rate budget) and dispatch it in queue._dispatch(). triage_issue
now tears down a stale finalized workspace (merged/closed/abandoned) before
re-provisioning, mirroring the maintainer directive-reopen teardown so the
re-triage branches afresh from default instead of a merged/deleted branch.

Fixes #5891
This commit is contained in:
roboomp
2026-07-23 19:18:48 +00:00
parent c64e7146e0
commit 3967ad0d5d
5 changed files with 103 additions and 4 deletions
+21
View File
@@ -53,6 +53,27 @@ def test_route_issue_opened_queues_triage() -> None:
assert decision.issue_key == "octo/widget#4"
def test_route_issue_reopened_queues_triage() -> None:
# `finalized_issue_comment.md` promises re-triage on reopen; the router must
# queue it as a submitter-attributable triage (not drop it to the skip branch).
decision = route(
"issues",
{
"action": "reopened",
"issue": {"number": 4, "user": {"login": "alice"}, "author_association": "CONTRIBUTOR"},
"repository": {"full_name": "octo/widget"},
},
allowlist=ALLOWLIST,
bot_login=BOT,
)
assert decision.should_queue
assert decision.task == "triage_issue"
assert decision.issue_key == "octo/widget#4"
assert decision.reason == "issues.reopened"
assert decision.submitter == "alice"
assert decision.association == "CONTRIBUTOR"
def test_route_skips_disallowed_repo() -> None:
decision = route(
"issues",
+68
View File
@@ -167,3 +167,71 @@ async def test_run_workspace_op_logs_worker_exception_on_concurrent_cancel(caplo
assert any(r.exc_info and r.exc_info[1] is boom for r in warnings), (
"the worker's exception was not attached to the warning"
)
async def test_triage_issue_reopen_tears_down_finalized_workspace(db, settings, monkeypatch, tmp_path):
"""Re-triage of a finalized (reopened) issue must clear the stale workspace first.
The prior branch was merged/deleted when the issue finalized, so a reopen has
to branch afresh — mirroring the maintainer directive-reopen teardown.
"""
async def _resolve_repo_and_issue(_github, _payload):
repo = RepoInfo(
full_name="octo/widget",
default_branch="main",
clone_url="https://x/octo/widget.git",
private=False,
)
issue = IssueInfo(
repo="octo/widget",
number=1,
title="bug",
body="b",
state="open",
author="alice",
labels=(),
is_pull_request=False,
)
return repo, issue
monkeypatch.setattr(tasks, "_resolve_repo_and_issue", _resolve_repo_and_issue)
# The bot previously finalized this issue: a stale row + workspace exist.
db.upsert_issue(key="octo/widget#1", repo="octo/widget", number=1, state="closed")
calls: list[str] = []
def _remove(**_kwargs):
calls.append("remove")
def _ensure(**_kwargs):
calls.append("ensure")
return SimpleNamespace(branch="farm/x/y", session_dir=str(tmp_path / "sess"))
async def _fail_closing(*_a, **_k):
raise AssertionError("closing-PR guard must not run when a DB row already exists")
github = SimpleNamespace(list_closing_pull_requests=_fail_closing)
sandbox = SimpleNamespace(natives_cache=None, ensure_workspace=_ensure, remove_workspace=_remove)
async def _noop_run_task(**_kwargs):
return None
monkeypatch.setattr(tasks, "run_task", _noop_run_task)
await tasks.triage_issue(
settings=settings,
db=db,
github=github,
sandbox=sandbox,
git_transport=SimpleNamespace(),
payload={},
delivery_id="d1",
)
# Teardown must precede re-provisioning, and the row resets to a live state.
assert calls == ["remove", "ensure"]
row = db.get_issue("octo/widget#1")
assert row is not None
assert row.state == "reproducing"