diff --git a/backend/app/gateway/github/triggers.py b/backend/app/gateway/github/triggers.py index 2c1e3ab0c1..efa5b63827 100644 --- a/backend/app/gateway/github/triggers.py +++ b/backend/app/gateway/github/triggers.py @@ -81,6 +81,11 @@ def _comment_body(event: str, payload: dict[str, Any]) -> str: def _author_login(event: str, payload: dict[str, Any]) -> str | None: """Login of the human who triggered the event, for ``allow_authors``.""" + # Object authors need not be the actor on edits, labels, or dismissals. + # Keep the object fallback for payloads without a sender identity. + sender_login = (payload.get("sender") or {}).get("login") + if isinstance(sender_login, str): + return sender_login if event in ("issue_comment", "pull_request_review_comment"): login = (payload.get("comment") or {}).get("user", {}).get("login") elif event == "pull_request": diff --git a/backend/tests/test_github_dispatcher.py b/backend/tests/test_github_dispatcher.py index ab69c40613..3e24c147ca 100644 --- a/backend/tests/test_github_dispatcher.py +++ b/backend/tests/test_github_dispatcher.py @@ -47,6 +47,40 @@ async def _drain(bus: MessageBus) -> list[InboundMessage]: return out +@pytest.mark.asyncio +@pytest.mark.parametrize("event, object_key", [("issues", "issue"), ("pull_request", "pull_request")]) +@pytest.mark.parametrize("author, sender, should_fire", [("owner", "other", False), ("other", "owner", True)]) +async def test_allow_authors_uses_update_sender(base_dir: Path, event: str, object_key: str, author: str, sender: str, should_fire: bool) -> None: + """Editing another user's issue/PR must use the editor's mention exemption.""" + _write_agent( + base_dir, + "default", + "reviewer", + { + "name": "reviewer", + "github": {"bindings": [{"repo": "a/b", "triggers": {event: {"actions": ["edited"], "require_mention": True, "allow_authors": ["owner"]}}}]}, + }, + ) + bus = MessageBus() + result = await fanout_event( + bus, + event, + "delivery-edited", + { + "action": "edited", + object_key: {"number": 7, "user": {"login": author}, "title": "Updated title", "body": "No mention"}, + "repository": {"full_name": "a/b"}, + "sender": {"login": sender}, + }, + ) + assert result["matched_agents"] == ["reviewer"] + assert result["fired_agents"] == (["reviewer"] if should_fire else []) + messages = await _drain(bus) + assert len(messages) == int(should_fire) + if messages: + assert messages[0].user_id == sender + + @pytest.mark.asyncio async def test_fanout_redelivery_progresses_beyond_queue_sized_prefix(base_dir: Path) -> None: """A redelivery must not keep failing on the same queue-sized prefix."""