From 0b4821446c870a4d79ac96e4e3dd7d2b70d02636 Mon Sep 17 00:00:00 2001 From: Hiroshi Nishio Date: Sun, 22 Feb 2026 21:11:37 -0800 Subject: [PATCH 1/2] Add display_name support to user upsert and propagate sender info through all handlers - Rename get_user_public_email to get_user_public_info, return UserPublicInfo dataclass with email and display_name - Add display_name (required) to upsert_user and create_user_request - Add sender_display_name to BaseArgs, populate from get_user_public_info in all handlers - Title-case display names that are all lowercase or all uppercase - Fix check_suite_handler and review_run_handler to call get_user_public_info instead of hardcoding fake emails - Type user_data as dict[str, str | None] to fix Any propagation --- conftest.py | 1 + schemas/supabase/types.py | 2 + services/github/refs/test_update_reference.py | 1 + services/github/types/github_types.py | 1 + .../github/users/get_user_public_email.py | 31 +- .../users/test_get_user_public_email.py | 278 +++++++++++++----- .../utils/deconstruct_github_payload.py | 7 +- .../utils/test_deconstruct_github_payload.py | 114 ++++--- services/supabase/create_user_request.py | 5 +- services/supabase/test_create_user_request.py | 3 + services/supabase/users/test_upsert_user.py | 19 +- services/supabase/users/upsert_user.py | 18 +- services/test_chat_with_agent.py | 10 +- services/webhook/check_suite_handler.py | 8 +- services/webhook/handle_installation.py | 11 +- services/webhook/new_pr_handler.py | 2 + services/webhook/review_run_handler.py | 8 +- services/webhook/test_handle_installation.py | 157 +++++++--- 18 files changed, 486 insertions(+), 190 deletions(-) diff --git a/conftest.py b/conftest.py index 55e2d603c..623ee29df 100644 --- a/conftest.py +++ b/conftest.py @@ -70,6 +70,7 @@ def _create(**overrides) -> BaseArgs: "sender_id": random.randint(1, 999999), "sender_name": "test-sender", "sender_email": "test@example.com", + "sender_display_name": "Test Sender", "is_automation": False, "reviewers": [], "github_urls": [], diff --git a/schemas/supabase/types.py b/schemas/supabase/types.py index b76a4639a..e6617fa5b 100644 --- a/schemas/supabase/types.py +++ b/schemas/supabase/types.py @@ -602,6 +602,7 @@ class Users(TypedDict): created_at: datetime.datetime created_by: str | None user_rules: str + display_name: str class UsersInsert(TypedDict): @@ -610,6 +611,7 @@ class UsersInsert(TypedDict): email: NotRequired[str | None] created_by: NotRequired[str | None] user_rules: str + display_name: str class WebhookDeliveries(TypedDict): diff --git a/services/github/refs/test_update_reference.py b/services/github/refs/test_update_reference.py index 2ec44ed3b..b57c40caa 100644 --- a/services/github/refs/test_update_reference.py +++ b/services/github/refs/test_update_reference.py @@ -33,6 +33,7 @@ def sample_base_args(): sender_id=111222333, sender_name="test-sender", sender_email="test@example.com", + sender_display_name="Test Sender", is_automation=False, reviewers=[], github_urls=[], diff --git a/services/github/types/github_types.py b/services/github/types/github_types.py index 8aaeb7fac..7058d0def 100644 --- a/services/github/types/github_types.py +++ b/services/github/types/github_types.py @@ -32,6 +32,7 @@ class BaseArgs(TypedDict): sender_id: int sender_name: str sender_email: str | None + sender_display_name: str is_automation: bool reviewers: list[str] github_urls: list[str] diff --git a/services/github/users/get_user_public_email.py b/services/github/users/get_user_public_email.py index 68e470ba7..1f6ab197b 100644 --- a/services/github/users/get_user_public_email.py +++ b/services/github/users/get_user_public_email.py @@ -1,23 +1,38 @@ +from dataclasses import dataclass + import requests + from config import GITHUB_API_URL, TIMEOUT from services.github.utils.create_headers import create_headers from utils.error.handle_exceptions import handle_exceptions -@handle_exceptions(default_return_value=None, raise_on_error=False) -def get_user_public_email(username: str, token: str) -> str | None: +@dataclass +class UserPublicInfo: + email: str | None + display_name: str + + +@handle_exceptions( + default_return_value=UserPublicInfo(email=None, display_name=""), + raise_on_error=False, +) +def get_user_public_info(username: str, token: str): """https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-a-user""" - # If the user is a bot, the email is not available. + # Bots (e.g. dependabot[bot]) have name=null and email=null on the API, so skip the call if "[bot]" in username: - return None + return UserPublicInfo(email=None, display_name="") - # If the user is not a bot, get the user's email response: requests.Response = requests.get( url=f"{GITHUB_API_URL}/users/{username}", headers=create_headers(token=token), timeout=TIMEOUT, ) response.raise_for_status() - user_data: dict = response.json() - email: str | None = user_data.get("email") - return email + user_data: dict[str, str | None] = response.json() + name: str = user_data.get("name") or "" + # Title-case if all lowercase or all uppercase (e.g., "wes nishio" -> "Wes Nishio", "HIROSHI" -> "Hiroshi") + # Cross-ref: website/app/api/auth/[...nextauth]/route.ts + if name == name.lower() or name == name.upper(): + name = name.title() + return UserPublicInfo(email=user_data.get("email"), display_name=name) diff --git a/services/github/users/test_get_user_public_email.py b/services/github/users/test_get_user_public_email.py index 68524234b..08ff47e01 100644 --- a/services/github/users/test_get_user_public_email.py +++ b/services/github/users/test_get_user_public_email.py @@ -1,22 +1,28 @@ -"""Unit tests for get_user_public_email function. +"""Unit tests for get_user_public_info function. Related Documentation: https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-a-user """ -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch + import pytest from requests.exceptions import HTTPError, RequestException, Timeout from config import GITHUB_API_URL, TIMEOUT -from services.github.users.get_user_public_email import get_user_public_email +from services.github.users.get_user_public_email import ( + UserPublicInfo, + get_user_public_info, +) + +DEFAULT_RETURN = UserPublicInfo(email=None, display_name="") @pytest.fixture def mock_response(): """Fixture to provide a mocked response object.""" mock_resp = MagicMock() - mock_resp.json.return_value = {"email": "test@example.com"} + mock_resp.json.return_value = {"email": "test@example.com", "name": "Test User"} mock_resp.raise_for_status.return_value = None return mock_resp @@ -33,37 +39,39 @@ def sample_token(): return "ghp_test_token_123456789" -def test_get_user_public_email_successful_request( +def test_get_user_public_info_successful_request( mock_response, sample_username, sample_token ): - """Test successful API request returns the email.""" + """Test successful API request returns email and display_name.""" with patch( "services.github.users.get_user_public_email.requests.get", return_value=mock_response, ): - result = get_user_public_email(username=sample_username, token=sample_token) - assert result == "test@example.com" + result = get_user_public_info(username=sample_username, token=sample_token) + assert result.email == "test@example.com" + assert result.display_name == "Test User" -def test_get_user_public_email_bot_user_returns_none(sample_token): - """Test that bot usernames return None without making API calls.""" +def test_get_user_public_info_bot_user_skips_api_call(sample_token): + """Test that bot usernames skip the API call (bots have name=null, email=null).""" with patch("services.github.users.get_user_public_email.requests.get") as mock_get: - result = get_user_public_email( + result = get_user_public_info( username="github-actions[bot]", token=sample_token ) - assert result is None + assert result == DEFAULT_RETURN mock_get.assert_not_called() -def test_get_user_public_email_calls_correct_api_endpoint( - sample_username, sample_token -): +def test_get_user_public_info_calls_correct_api_endpoint(sample_username, sample_token): """Test that the function calls the correct GitHub API endpoint.""" with patch("services.github.users.get_user_public_email.requests.get") as mock_get: - mock_get.return_value.json.return_value = {"email": "test@example.com"} + mock_get.return_value.json.return_value = { + "email": "test@example.com", + "name": "Test User", + } mock_get.return_value.raise_for_status.return_value = None - get_user_public_email(username=sample_username, token=sample_token) + get_user_public_info(username=sample_username, token=sample_token) expected_url = f"{GITHUB_API_URL}/users/{sample_username}" mock_get.assert_called_once() @@ -71,20 +79,22 @@ def test_get_user_public_email_calls_correct_api_endpoint( assert kwargs["url"] == expected_url -def test_get_user_public_email_uses_correct_headers(sample_username, sample_token): +def test_get_user_public_info_uses_correct_headers(sample_username, sample_token): """Test that the function uses correct headers including authorization.""" with patch( "services.github.users.get_user_public_email.requests.get" ) as mock_get, patch( "services.github.users.get_user_public_email.create_headers" ) as mock_create_headers: - mock_headers = {"Authorization": f"Bearer {sample_token}"} mock_create_headers.return_value = mock_headers - mock_get.return_value.json.return_value = {"email": "test@example.com"} + mock_get.return_value.json.return_value = { + "email": "test@example.com", + "name": "Test User", + } mock_get.return_value.raise_for_status.return_value = None - get_user_public_email(username=sample_username, token=sample_token) + get_user_public_info(username=sample_username, token=sample_token) mock_create_headers.assert_called_once_with(token=sample_token) mock_get.assert_called_once() @@ -92,20 +102,23 @@ def test_get_user_public_email_uses_correct_headers(sample_username, sample_toke assert kwargs["headers"] == mock_headers -def test_get_user_public_email_uses_correct_timeout(sample_username, sample_token): +def test_get_user_public_info_uses_correct_timeout(sample_username, sample_token): """Test that the function uses the configured timeout.""" with patch("services.github.users.get_user_public_email.requests.get") as mock_get: - mock_get.return_value.json.return_value = {"email": "test@example.com"} + mock_get.return_value.json.return_value = { + "email": "test@example.com", + "name": "Test User", + } mock_get.return_value.raise_for_status.return_value = None - get_user_public_email(username=sample_username, token=sample_token) + get_user_public_info(username=sample_username, token=sample_token) mock_get.assert_called_once() _, kwargs = mock_get.call_args assert kwargs["timeout"] == TIMEOUT -def test_get_user_public_email_calls_raise_for_status( +def test_get_user_public_info_calls_raise_for_status( mock_response, sample_username, sample_token ): """Test that the function calls raise_for_status on the response.""" @@ -113,19 +126,20 @@ def test_get_user_public_email_calls_raise_for_status( "services.github.users.get_user_public_email.requests.get", return_value=mock_response, ): - get_user_public_email(username=sample_username, token=sample_token) + get_user_public_info(username=sample_username, token=sample_token) mock_response.raise_for_status.assert_called_once() -def test_get_user_public_email_extracts_email_from_response( +def test_get_user_public_info_extracts_email_and_name_from_response( sample_username, sample_token ): - """Test that the function extracts the email field from the JSON response.""" + """Test that the function extracts email and name from the JSON response.""" expected_email = "user@example.com" mock_response = MagicMock() mock_response.json.return_value = { "email": expected_email, "login": sample_username, + "name": "Some User", } mock_response.raise_for_status.return_value = None @@ -133,33 +147,38 @@ def test_get_user_public_email_extracts_email_from_response( "services.github.users.get_user_public_email.requests.get", return_value=mock_response, ): - result = get_user_public_email(username=sample_username, token=sample_token) - assert result == expected_email + result = get_user_public_info(username=sample_username, token=sample_token) + assert result.email == expected_email + assert result.display_name == "Some User" -def test_get_user_public_email_with_different_usernames(): +def test_get_user_public_info_with_different_usernames(): """Test the function with different username values.""" test_cases = [ - ("user1", "user1@example.com"), - ("organization-name", "org@example.com"), - ("user-with-dashes", "dashes@example.com"), - ("user_with_underscores", "underscores@example.com"), + ("user1", "user1@example.com", "User One"), + ("organization-name", "org@example.com", "Org Name"), + ("user-with-dashes", "dashes@example.com", "Dash User"), + ("user_with_underscores", "underscores@example.com", "Underscore User"), ] - for username, expected_email in test_cases: + for username, expected_email, expected_name in test_cases: mock_response = MagicMock() - mock_response.json.return_value = {"email": expected_email} + mock_response.json.return_value = { + "email": expected_email, + "name": expected_name, + } mock_response.raise_for_status.return_value = None with patch( "services.github.users.get_user_public_email.requests.get", return_value=mock_response, ): - result = get_user_public_email(username=username, token="test-token") - assert result == expected_email + result = get_user_public_info(username=username, token="test-token") + assert result.email == expected_email + assert result.display_name == expected_name -def test_get_user_public_email_with_different_tokens(): +def test_get_user_public_info_with_different_tokens(): """Test the function with different token formats.""" test_tokens = [ "ghp_1234567890abcdef", @@ -169,7 +188,10 @@ def test_get_user_public_email_with_different_tokens(): for token in test_tokens: mock_response = MagicMock() - mock_response.json.return_value = {"email": "test@example.com"} + mock_response.json.return_value = { + "email": "test@example.com", + "name": "Test User", + } mock_response.raise_for_status.return_value = None with patch( @@ -178,18 +200,17 @@ def test_get_user_public_email_with_different_tokens(): ), patch( "services.github.users.get_user_public_email.create_headers" ) as mock_create_headers: - mock_create_headers.return_value = {"Authorization": f"Bearer {token}"} - result = get_user_public_email(username="testuser", token=token) + result = get_user_public_info(username="testuser", token=token) - assert result == "test@example.com" + assert result.email == "test@example.com" + assert result.display_name == "Test User" mock_create_headers.assert_called_with(token=token) -def test_get_user_public_email_http_error_returns_none(sample_username, sample_token): - """Test that HTTP errors are handled and return None due to decorator.""" +def test_get_user_public_info_http_error_returns_default(sample_username, sample_token): + """Test that HTTP errors are handled and return default UserPublicInfo due to decorator.""" mock_response = MagicMock() - # Create a proper HTTPError with a response object http_error = HTTPError("404 Not Found") error_response = MagicMock() error_response.status_code = 404 @@ -202,36 +223,36 @@ def test_get_user_public_email_http_error_returns_none(sample_username, sample_t "services.github.users.get_user_public_email.requests.get", return_value=mock_response, ): - result = get_user_public_email(username=sample_username, token=sample_token) - assert result is None + result = get_user_public_info(username=sample_username, token=sample_token) + assert result == DEFAULT_RETURN -def test_get_user_public_email_request_exception_returns_none( +def test_get_user_public_info_request_exception_returns_default( sample_username, sample_token ): - """Test that request exceptions are handled and return None due to decorator.""" + """Test that request exceptions are handled and return default UserPublicInfo due to decorator.""" with patch( "services.github.users.get_user_public_email.requests.get", side_effect=RequestException("Network error"), ): - result = get_user_public_email(username=sample_username, token=sample_token) - assert result is None + result = get_user_public_info(username=sample_username, token=sample_token) + assert result == DEFAULT_RETURN -def test_get_user_public_email_timeout_returns_none(sample_username, sample_token): - """Test that timeout exceptions are handled and return None due to decorator.""" +def test_get_user_public_info_timeout_returns_default(sample_username, sample_token): + """Test that timeout exceptions are handled and return default UserPublicInfo due to decorator.""" with patch( "services.github.users.get_user_public_email.requests.get", side_effect=Timeout("Request timed out"), ): - result = get_user_public_email(username=sample_username, token=sample_token) - assert result is None + result = get_user_public_info(username=sample_username, token=sample_token) + assert result == DEFAULT_RETURN -def test_get_user_public_email_json_decode_error_returns_none( +def test_get_user_public_info_json_decode_error_returns_default( sample_username, sample_token ): - """Test that JSON decode errors are handled and return None due to decorator.""" + """Test that JSON decode errors are handled and return default UserPublicInfo due to decorator.""" mock_response = MagicMock() mock_response.raise_for_status.return_value = None mock_response.json.side_effect = ValueError("Invalid JSON") @@ -240,40 +261,155 @@ def test_get_user_public_email_json_decode_error_returns_none( "services.github.users.get_user_public_email.requests.get", return_value=mock_response, ): - result = get_user_public_email(username=sample_username, token=sample_token) - assert result is None + result = get_user_public_info(username=sample_username, token=sample_token) + assert result == DEFAULT_RETURN + + +def test_get_user_public_info_missing_email_key_returns_none_email( + sample_username, sample_token +): + """Test that missing email key in response returns None for email.""" + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = { + "login": sample_username, + "name": "Test User", + } + + with patch( + "services.github.users.get_user_public_email.requests.get", + return_value=mock_response, + ): + result = get_user_public_info(username=sample_username, token=sample_token) + assert result.email is None + assert result.display_name == "Test User" -def test_get_user_public_email_missing_email_key_returns_none( +def test_get_user_public_info_null_email_value_returns_none_email( sample_username, sample_token ): - """Test that missing email key in response returns None.""" + """Test that null email value in response returns None for email.""" mock_response = MagicMock() mock_response.raise_for_status.return_value = None mock_response.json.return_value = { "login": sample_username, + "email": None, "name": "Test User", - } # Missing "email" key + } + + with patch( + "services.github.users.get_user_public_email.requests.get", + return_value=mock_response, + ): + result = get_user_public_info(username=sample_username, token=sample_token) + assert result.email is None + assert result.display_name == "Test User" + + +def test_get_user_public_info_missing_name_key_returns_empty_display_name( + sample_username, sample_token +): + """Test that missing name key in response returns empty string for display_name.""" + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = { + "login": sample_username, + "email": "test@example.com", + } + + with patch( + "services.github.users.get_user_public_email.requests.get", + return_value=mock_response, + ): + result = get_user_public_info(username=sample_username, token=sample_token) + assert result.email == "test@example.com" + assert result.display_name == "" + + +def test_get_user_public_info_title_cases_lowercase_name(sample_username, sample_token): + """Test that all-lowercase names are title-cased (e.g., 'wes nishio' -> 'Wes Nishio').""" + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = { + "email": "wes@example.com", + "name": "wes nishio", + } + + with patch( + "services.github.users.get_user_public_email.requests.get", + return_value=mock_response, + ): + result = get_user_public_info(username=sample_username, token=sample_token) + assert result.display_name == "Wes Nishio" + + +def test_get_user_public_info_title_cases_uppercase_name(sample_username, sample_token): + """Test that all-uppercase names are title-cased (e.g., 'HIROSHI' -> 'Hiroshi').""" + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = { + "email": "hiroshi@example.com", + "name": "HIROSHI", + } + + with patch( + "services.github.users.get_user_public_email.requests.get", + return_value=mock_response, + ): + result = get_user_public_info(username=sample_username, token=sample_token) + assert result.display_name == "Hiroshi" + + +def test_get_user_public_info_preserves_mixed_case_name(sample_username, sample_token): + """Test that mixed-case names are preserved (e.g., 'Wes Nishio' stays 'Wes Nishio').""" + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = { + "email": "wes@example.com", + "name": "Wes Nishio", + } with patch( "services.github.users.get_user_public_email.requests.get", return_value=mock_response, ): - result = get_user_public_email(username=sample_username, token=sample_token) - assert result is None + result = get_user_public_info(username=sample_username, token=sample_token) + assert result.display_name == "Wes Nishio" -def test_get_user_public_email_null_email_value_returns_none( +def test_get_user_public_info_empty_name_returns_empty_display_name( sample_username, sample_token ): - """Test that null email value in response returns None.""" + """Test that empty string name returns empty display_name.""" mock_response = MagicMock() mock_response.raise_for_status.return_value = None - mock_response.json.return_value = {"login": sample_username, "email": None} + mock_response.json.return_value = { + "email": "test@example.com", + "name": "", + } + + with patch( + "services.github.users.get_user_public_email.requests.get", + return_value=mock_response, + ): + result = get_user_public_info(username=sample_username, token=sample_token) + assert result.display_name == "" + + +def test_get_user_public_info_null_name_returns_empty_display_name( + sample_username, sample_token +): + """Test that null name value returns empty display_name (via 'or' fallback).""" + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = { + "email": "test@example.com", + "name": None, + } with patch( "services.github.users.get_user_public_email.requests.get", return_value=mock_response, ): - result = get_user_public_email(username=sample_username, token=sample_token) - assert result is None + result = get_user_public_info(username=sample_username, token=sample_token) + assert result.display_name == "" diff --git a/services/github/utils/deconstruct_github_payload.py b/services/github/utils/deconstruct_github_payload.py index b2f1aaed7..d7e2d90c4 100644 --- a/services/github/utils/deconstruct_github_payload.py +++ b/services/github/utils/deconstruct_github_payload.py @@ -6,7 +6,7 @@ from services.github.branches.check_branch_exists import check_branch_exists from services.github.types.github_types import BaseArgs, PrLabeledPayload from services.github.token.get_installation_token import get_installation_access_token -from services.github.users.get_user_public_email import get_user_public_email +from services.github.users.get_user_public_email import get_user_public_info from services.supabase.repositories.get_repository import get_repository from utils.error.handle_exceptions import handle_exceptions from utils.logging.logging_config import logger @@ -75,7 +75,9 @@ def deconstruct_github_payload( # Extract other information github_urls, other_urls = extract_urls(text=pr_body) - sender_email = get_user_public_email(username=sender_name, token=token) + sender_info = get_user_public_info(username=sender_name, token=token) + sender_email = sender_info.email + sender_display_name = sender_info.display_name base_args: BaseArgs = { "owner_type": owner_type, @@ -97,6 +99,7 @@ def deconstruct_github_payload( "sender_id": sender_id, "sender_name": sender_name, "sender_email": sender_email, + "sender_display_name": sender_display_name, "is_automation": is_automation, "reviewers": reviewers, "github_urls": github_urls, diff --git a/services/github/utils/test_deconstruct_github_payload.py b/services/github/utils/test_deconstruct_github_payload.py index 64794f7b0..96409de49 100644 --- a/services/github/utils/test_deconstruct_github_payload.py +++ b/services/github/utils/test_deconstruct_github_payload.py @@ -6,6 +6,7 @@ import pytest from config import GITHUB_APP_USER_ID from services.github.types.github_types import PrLabeledPayload +from services.github.users.get_user_public_email import UserPublicInfo from services.github.utils.deconstruct_github_payload import deconstruct_github_payload @@ -64,9 +65,9 @@ def create_mock_payload( @patch("services.github.utils.deconstruct_github_payload.get_repository") @patch("services.github.utils.deconstruct_github_payload.check_branch_exists") @patch("services.github.utils.deconstruct_github_payload.extract_urls") -@patch("services.github.utils.deconstruct_github_payload.get_user_public_email") +@patch("services.github.utils.deconstruct_github_payload.get_user_public_info") def test_deconstruct_github_payload_basic_functionality( - mock_get_user_public_email, + mock_get_user_public_info, mock_extract_urls, mock_check_branch_exists, mock_get_repository, @@ -78,7 +79,9 @@ def test_deconstruct_github_payload_basic_functionality( mock_get_repository.return_value = {"target_branch": None} mock_check_branch_exists.return_value = False mock_extract_urls.return_value = (["https://github.com"], ["https://example.com"]) - mock_get_user_public_email.return_value = "test@example.com" + mock_get_user_public_info.return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) # Create test payload payload = create_mock_payload(pr_body="Test PR body") @@ -106,6 +109,7 @@ def test_deconstruct_github_payload_basic_functionality( assert base_args["sender_id"] == 12345 assert base_args["sender_name"] == "test-sender" assert base_args["sender_email"] == "test@example.com" + assert base_args["sender_display_name"] == "Test Sender" assert base_args["is_automation"] is False assert set(base_args["reviewers"]) == {"test-sender", "test-creator"} assert base_args["github_urls"] == ["https://github.com"] @@ -140,9 +144,9 @@ def test_deconstruct_github_payload_no_token_raises_error( @patch("services.github.utils.deconstruct_github_payload.get_repository") @patch("services.github.utils.deconstruct_github_payload.check_branch_exists") @patch("services.github.utils.deconstruct_github_payload.extract_urls") -@patch("services.github.utils.deconstruct_github_payload.get_user_public_email") +@patch("services.github.utils.deconstruct_github_payload.get_user_public_info") def test_deconstruct_github_payload_with_empty_pr_body( - mock_get_user_public_email, + mock_get_user_public_info, mock_extract_urls, mock_check_branch_exists, mock_get_repository, @@ -154,7 +158,9 @@ def test_deconstruct_github_payload_with_empty_pr_body( mock_get_repository.return_value = {"target_branch": None} mock_check_branch_exists.return_value = False mock_extract_urls.return_value = ([], []) - mock_get_user_public_email.return_value = "test@example.com" + mock_get_user_public_info.return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) # Create test payload with None PR body payload = create_mock_payload(pr_body=None) @@ -170,9 +176,9 @@ def test_deconstruct_github_payload_with_empty_pr_body( @patch("services.github.utils.deconstruct_github_payload.get_repository") @patch("services.github.utils.deconstruct_github_payload.check_branch_exists") @patch("services.github.utils.deconstruct_github_payload.extract_urls") -@patch("services.github.utils.deconstruct_github_payload.get_user_public_email") +@patch("services.github.utils.deconstruct_github_payload.get_user_public_info") def test_deconstruct_github_payload_with_fork_repository( - mock_get_user_public_email, + mock_get_user_public_info, mock_extract_urls, mock_check_branch_exists, mock_get_repository, @@ -184,7 +190,9 @@ def test_deconstruct_github_payload_with_fork_repository( mock_get_repository.return_value = {"target_branch": None} mock_check_branch_exists.return_value = False mock_extract_urls.return_value = ([], []) - mock_get_user_public_email.return_value = "test@example.com" + mock_get_user_public_info.return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) # Create test payload with fork=True payload = create_mock_payload(fork=True) @@ -200,9 +208,9 @@ def test_deconstruct_github_payload_with_fork_repository( @patch("services.github.utils.deconstruct_github_payload.get_repository") @patch("services.github.utils.deconstruct_github_payload.check_branch_exists") @patch("services.github.utils.deconstruct_github_payload.extract_urls") -@patch("services.github.utils.deconstruct_github_payload.get_user_public_email") +@patch("services.github.utils.deconstruct_github_payload.get_user_public_info") def test_deconstruct_github_payload_with_bot_users( - mock_get_user_public_email, + mock_get_user_public_info, mock_extract_urls, mock_check_branch_exists, mock_get_repository, @@ -214,7 +222,9 @@ def test_deconstruct_github_payload_with_bot_users( mock_get_repository.return_value = {"target_branch": None} mock_check_branch_exists.return_value = False mock_extract_urls.return_value = ([], []) - mock_get_user_public_email.return_value = "test@example.com" + mock_get_user_public_info.return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) # Create test payload with bot users payload = create_mock_payload( @@ -232,9 +242,9 @@ def test_deconstruct_github_payload_with_bot_users( @patch("services.github.utils.deconstruct_github_payload.get_repository") @patch("services.github.utils.deconstruct_github_payload.check_branch_exists") @patch("services.github.utils.deconstruct_github_payload.extract_urls") -@patch("services.github.utils.deconstruct_github_payload.get_user_public_email") +@patch("services.github.utils.deconstruct_github_payload.get_user_public_info") def test_deconstruct_github_payload_with_target_branch_exists( - mock_get_user_public_email, + mock_get_user_public_info, mock_extract_urls, mock_check_branch_exists, mock_get_repository, @@ -246,7 +256,9 @@ def test_deconstruct_github_payload_with_target_branch_exists( mock_get_repository.return_value = {"target_branch": "develop"} mock_check_branch_exists.return_value = True # Target branch exists mock_extract_urls.return_value = ([], []) - mock_get_user_public_email.return_value = "test@example.com" + mock_get_user_public_info.return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) # Create test payload payload = create_mock_payload() @@ -262,9 +274,9 @@ def test_deconstruct_github_payload_with_target_branch_exists( @patch("services.github.utils.deconstruct_github_payload.get_repository") @patch("services.github.utils.deconstruct_github_payload.check_branch_exists") @patch("services.github.utils.deconstruct_github_payload.extract_urls") -@patch("services.github.utils.deconstruct_github_payload.get_user_public_email") +@patch("services.github.utils.deconstruct_github_payload.get_user_public_info") def test_deconstruct_github_payload_with_target_branch_not_exists( - mock_get_user_public_email, + mock_get_user_public_info, mock_extract_urls, mock_check_branch_exists, mock_get_repository, @@ -276,7 +288,9 @@ def test_deconstruct_github_payload_with_target_branch_not_exists( mock_get_repository.return_value = {"target_branch": "develop"} mock_check_branch_exists.return_value = False # Target branch doesn't exist mock_extract_urls.return_value = ([], []) - mock_get_user_public_email.return_value = "test@example.com" + mock_get_user_public_info.return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) # Create test payload payload = create_mock_payload() @@ -292,9 +306,9 @@ def test_deconstruct_github_payload_with_target_branch_not_exists( @patch("services.github.utils.deconstruct_github_payload.get_repository") @patch("services.github.utils.deconstruct_github_payload.check_branch_exists") @patch("services.github.utils.deconstruct_github_payload.extract_urls") -@patch("services.github.utils.deconstruct_github_payload.get_user_public_email") +@patch("services.github.utils.deconstruct_github_payload.get_user_public_info") def test_deconstruct_github_payload_with_automation_user( - mock_get_user_public_email, + mock_get_user_public_info, mock_extract_urls, mock_check_branch_exists, mock_get_repository, @@ -306,7 +320,9 @@ def test_deconstruct_github_payload_with_automation_user( mock_get_repository.return_value = {"target_branch": None} mock_check_branch_exists.return_value = False mock_extract_urls.return_value = ([], []) - mock_get_user_public_email.return_value = "test@example.com" + mock_get_user_public_info.return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) # Create test payload with automation user (using GITHUB_APP_USER_ID from config) payload = create_mock_payload(sender_id=GITHUB_APP_USER_ID) @@ -322,9 +338,9 @@ def test_deconstruct_github_payload_with_automation_user( @patch("services.github.utils.deconstruct_github_payload.get_repository") @patch("services.github.utils.deconstruct_github_payload.check_branch_exists") @patch("services.github.utils.deconstruct_github_payload.extract_urls") -@patch("services.github.utils.deconstruct_github_payload.get_user_public_email") +@patch("services.github.utils.deconstruct_github_payload.get_user_public_info") def test_deconstruct_github_payload_no__( - mock_get_user_public_email, + mock_get_user_public_info, mock_extract_urls, mock_check_branch_exists, mock_get_repository, @@ -336,7 +352,9 @@ def test_deconstruct_github_payload_no__( mock_get_repository.return_value = None # No repo settings mock_check_branch_exists.return_value = False mock_extract_urls.return_value = ([], []) - mock_get_user_public_email.return_value = "test@example.com" + mock_get_user_public_info.return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) # Create test payload payload = create_mock_payload() @@ -353,9 +371,9 @@ def test_deconstruct_github_payload_no__( @patch("services.github.utils.deconstruct_github_payload.get_repository") @patch("services.github.utils.deconstruct_github_payload.check_branch_exists") @patch("services.github.utils.deconstruct_github_payload.extract_urls") -@patch("services.github.utils.deconstruct_github_payload.get_user_public_email") +@patch("services.github.utils.deconstruct_github_payload.get_user_public_info") def test_deconstruct_github_payload_no_target_branch_in_settings( - mock_get_user_public_email, + mock_get_user_public_info, mock_extract_urls, mock_check_branch_exists, mock_get_repository, @@ -367,7 +385,9 @@ def test_deconstruct_github_payload_no_target_branch_in_settings( mock_get_repository.return_value = {"target_branch": None} mock_check_branch_exists.return_value = False mock_extract_urls.return_value = ([], []) - mock_get_user_public_email.return_value = "test@example.com" + mock_get_user_public_info.return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) # Create test payload payload = create_mock_payload() @@ -385,9 +405,9 @@ def test_deconstruct_github_payload_no_target_branch_in_settings( @patch("services.github.utils.deconstruct_github_payload.get_repository") @patch("services.github.utils.deconstruct_github_payload.check_branch_exists") @patch("services.github.utils.deconstruct_github_payload.extract_urls") -@patch("services.github.utils.deconstruct_github_payload.get_user_public_email") +@patch("services.github.utils.deconstruct_github_payload.get_user_public_info") def test_deconstruct_github_payload_duplicate_reviewers( - mock_get_user_public_email, + mock_get_user_public_info, mock_extract_urls, mock_check_branch_exists, mock_get_repository, @@ -399,7 +419,9 @@ def test_deconstruct_github_payload_duplicate_reviewers( mock_get_repository.return_value = {"target_branch": None} mock_check_branch_exists.return_value = False mock_extract_urls.return_value = ([], []) - mock_get_user_public_email.return_value = "test@example.com" + mock_get_user_public_info.return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) # Create test payload where sender and PR creator are the same payload = create_mock_payload(pr_creator_name="same-user", sender_name="same-user") @@ -415,9 +437,9 @@ def test_deconstruct_github_payload_duplicate_reviewers( @patch("services.github.utils.deconstruct_github_payload.get_repository") @patch("services.github.utils.deconstruct_github_payload.check_branch_exists") @patch("services.github.utils.deconstruct_github_payload.extract_urls") -@patch("services.github.utils.deconstruct_github_payload.get_user_public_email") +@patch("services.github.utils.deconstruct_github_payload.get_user_public_info") def test_deconstruct_github_payload_missing_fork_key( - mock_get_user_public_email, + mock_get_user_public_info, mock_extract_urls, mock_check_branch_exists, mock_get_repository, @@ -429,7 +451,9 @@ def test_deconstruct_github_payload_missing_fork_key( mock_get_repository.return_value = {"target_branch": None} mock_check_branch_exists.return_value = False mock_extract_urls.return_value = ([], []) - mock_get_user_public_email.return_value = "test@example.com" + mock_get_user_public_info.return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) payload: Any = create_mock_payload() del payload["repository"]["fork"] @@ -444,9 +468,9 @@ def test_deconstruct_github_payload_missing_fork_key( @patch("services.github.utils.deconstruct_github_payload.get_repository") @patch("services.github.utils.deconstruct_github_payload.check_branch_exists") @patch("services.github.utils.deconstruct_github_payload.extract_urls") -@patch("services.github.utils.deconstruct_github_payload.get_user_public_email") +@patch("services.github.utils.deconstruct_github_payload.get_user_public_info") def test_deconstruct_github_payload_target_branch_used( - mock_get_user_public_email, + mock_get_user_public_info, mock_extract_urls, mock_check_branch_exists, mock_get_repository, @@ -458,7 +482,9 @@ def test_deconstruct_github_payload_target_branch_used( mock_get_repository.return_value = {"target_branch": "develop"} mock_check_branch_exists.return_value = True # Target branch exists mock_extract_urls.return_value = ([], []) - mock_get_user_public_email.return_value = "test@example.com" + mock_get_user_public_info.return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) # Create test payload payload = create_mock_payload() @@ -474,9 +500,9 @@ def test_deconstruct_github_payload_target_branch_used( @patch("services.github.utils.deconstruct_github_payload.get_repository") @patch("services.github.utils.deconstruct_github_payload.check_branch_exists") @patch("services.github.utils.deconstruct_github_payload.extract_urls") -@patch("services.github.utils.deconstruct_github_payload.get_user_public_email") +@patch("services.github.utils.deconstruct_github_payload.get_user_public_info") def test_deconstruct_github_payload_schedule_trigger_uses_assignees_as_reviewers( - mock_get_user_public_email, + mock_get_user_public_info, mock_extract_urls, mock_check_branch_exists, mock_get_repository, @@ -488,7 +514,9 @@ def test_deconstruct_github_payload_schedule_trigger_uses_assignees_as_reviewers mock_get_repository.return_value = {"target_branch": None} mock_check_branch_exists.return_value = False mock_extract_urls.return_value = ([], []) - mock_get_user_public_email.return_value = "test@example.com" + mock_get_user_public_info.return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) # Schedule trigger: both sender and PR creator are bots, but PR has a human assignee payload = create_mock_payload( @@ -507,9 +535,9 @@ def test_deconstruct_github_payload_schedule_trigger_uses_assignees_as_reviewers @patch("services.github.utils.deconstruct_github_payload.get_repository") @patch("services.github.utils.deconstruct_github_payload.check_branch_exists") @patch("services.github.utils.deconstruct_github_payload.extract_urls") -@patch("services.github.utils.deconstruct_github_payload.get_user_public_email") +@patch("services.github.utils.deconstruct_github_payload.get_user_public_info") def test_deconstruct_github_payload_branch_from_pr_head( - mock_get_user_public_email, + mock_get_user_public_info, mock_extract_urls, mock_check_branch_exists, mock_get_repository, @@ -521,7 +549,9 @@ def test_deconstruct_github_payload_branch_from_pr_head( mock_get_repository.return_value = {"target_branch": None} mock_check_branch_exists.return_value = False mock_extract_urls.return_value = ([], []) - mock_get_user_public_email.return_value = "test@example.com" + mock_get_user_public_info.return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) expected_branch = "gitauto/schedule-456-20241225-143000-XYZW" payload = create_mock_payload(pr_number=456, branch_name=expected_branch) diff --git a/services/supabase/create_user_request.py b/services/supabase/create_user_request.py index 31a320865..05da88b95 100644 --- a/services/supabase/create_user_request.py +++ b/services/supabase/create_user_request.py @@ -18,6 +18,7 @@ def create_user_request( source: str, trigger: Trigger, email: str | None, + display_name: str, lambda_info: dict[str, str | None] | None = None, ): # Extract Lambda context info if provided @@ -42,5 +43,7 @@ def create_user_request( lambda_request_id=lambda_request_id, ) - upsert_user(user_id=user_id, user_name=user_name, email=email) + upsert_user( + user_id=user_id, user_name=user_name, email=email, display_name=display_name + ) return usage_id diff --git a/services/supabase/test_create_user_request.py b/services/supabase/test_create_user_request.py index df53f51d4..cfef465f4 100644 --- a/services/supabase/test_create_user_request.py +++ b/services/supabase/test_create_user_request.py @@ -26,6 +26,7 @@ def sample_params(self): "source": "github", "trigger": "dashboard", "email": "test@example.com", + "display_name": "Test User", } @pytest.fixture @@ -73,6 +74,7 @@ def test_create_user_request_returns_usage_id( user_id=12345, user_name="test_user", email="test@example.com", + display_name="Test User", ) def test_create_user_request_without_pr_number( @@ -100,6 +102,7 @@ def test_create_user_request_without_email(self, sample_params, mock_dependencie user_id=12345, user_name="test_user", email=None, + display_name="Test User", ) def test_create_user_request_different_trigger_types( diff --git a/services/supabase/users/test_upsert_user.py b/services/supabase/users/test_upsert_user.py index 37f270bc2..3ca7f0928 100644 --- a/services/supabase/users/test_upsert_user.py +++ b/services/supabase/users/test_upsert_user.py @@ -40,7 +40,9 @@ def test_upsert_user_with_valid_email(mock_supabase, mock_check_email_is_valid): mock_check_email_is_valid.return_value = True # Execute - upsert_user(user_id=user_id, user_name=user_name, email=email) + upsert_user( + user_id=user_id, user_name=user_name, email=email, display_name="Test User" + ) # Assert mock_check_email_is_valid.assert_called_once_with(email=email) @@ -49,6 +51,7 @@ def test_upsert_user_with_valid_email(mock_supabase, mock_check_email_is_valid): json={ "user_id": user_id, "user_name": user_name, + "display_name": "Test User", "email": email, "created_by": f"{user_id}:{user_name}", }, @@ -66,7 +69,9 @@ def test_upsert_user_with_invalid_email(mock_supabase, mock_check_email_is_valid mock_check_email_is_valid.return_value = False # Execute - upsert_user(user_id=user_id, user_name=user_name, email=email) + upsert_user( + user_id=user_id, user_name=user_name, email=email, display_name="Test User" + ) # Assert mock_check_email_is_valid.assert_called_once_with(email=email) @@ -75,6 +80,7 @@ def test_upsert_user_with_invalid_email(mock_supabase, mock_check_email_is_valid json={ "user_id": user_id, "user_name": user_name, + "display_name": "Test User", "created_by": f"{user_id}:{user_name}", }, on_conflict="user_id", @@ -91,7 +97,9 @@ def test_upsert_user_with_none_email(mock_supabase, mock_check_email_is_valid): mock_check_email_is_valid.return_value = False # Execute - upsert_user(user_id=user_id, user_name=user_name, email=email) + upsert_user( + user_id=user_id, user_name=user_name, email=email, display_name="Test User" + ) # Assert mock_check_email_is_valid.assert_called_once_with(email=email) @@ -100,6 +108,7 @@ def test_upsert_user_with_none_email(mock_supabase, mock_check_email_is_valid): json={ "user_id": user_id, "user_name": user_name, + "display_name": "Test User", "created_by": f"{user_id}:{user_name}", }, on_conflict="user_id", @@ -117,7 +126,9 @@ def test_upsert_user_exception_handling(mock_supabase, mock_check_email_is_valid mock_supabase.table.side_effect = Exception("Database error") # Execute - should not raise an exception due to handle_exceptions decorator - result = upsert_user(user_id=user_id, user_name=user_name, email=email) + result = upsert_user( + user_id=user_id, user_name=user_name, email=email, display_name="Test User" + ) # Assert assert result is None # Default return value from handle_exceptions diff --git a/services/supabase/users/upsert_user.py b/services/supabase/users/upsert_user.py index 2ea15f355..7de8b5d01 100644 --- a/services/supabase/users/upsert_user.py +++ b/services/supabase/users/upsert_user.py @@ -3,18 +3,20 @@ from utils.error.handle_exceptions import handle_exceptions +# Cross-ref: website/app/actions/supabase/users/upsert-user.ts @handle_exceptions(default_return_value=None, raise_on_error=False) -def upsert_user(user_id: int, user_name: str, email: str | None): +def upsert_user(user_id: int, user_name: str, email: str | None, display_name: str): # Check if email is valid email = email if check_email_is_valid(email=email) else None # Upsert user + json: dict = { + "user_id": user_id, + "user_name": user_name, + **({"display_name": display_name} if display_name else {}), + **({"email": email} if email else {}), + "created_by": f"{user_id}:{user_name}", + } supabase.table(table_name="users").upsert( - json={ - "user_id": user_id, - "user_name": user_name, - **({"email": email} if email else {}), - "created_by": f"{user_id}:{user_name}", - }, - on_conflict="user_id", + json=json, on_conflict="user_id" ).execute() diff --git a/services/test_chat_with_agent.py b/services/test_chat_with_agent.py index fb704b1ea..03407499b 100644 --- a/services/test_chat_with_agent.py +++ b/services/test_chat_with_agent.py @@ -344,7 +344,15 @@ async def test_unavailable_tool_sends_slack_notification( 10, ) - base_args = cast(BaseArgs, {"sender_id": 1, "sender_name": "test-user", "owner": "test-owner", "repo": "test-repo"}) + base_args = cast( + BaseArgs, + { + "sender_id": 1, + "sender_name": "test-user", + "owner": "test-owner", + "repo": "test-repo", + }, + ) with patch("services.chat_with_agent.tools_to_call") as mock_tools: mock_tools.__contains__.return_value = False diff --git a/services/webhook/check_suite_handler.py b/services/webhook/check_suite_handler.py index 4ba460094..a7dc686cf 100644 --- a/services/webhook/check_suite_handler.py +++ b/services/webhook/check_suite_handler.py @@ -48,6 +48,7 @@ from services.github.workflow_runs.get_workflow_run_logs import get_workflow_run_logs from services.claude.tools.tools import TOOLS_FOR_PRS from services.slack.slack_notify import slack_notify +from services.github.users.get_user_public_email import get_user_public_info from services.supabase.check_suites.insert_check_suite import insert_check_suite from services.supabase.codecov_tokens.get_codecov_token import get_codecov_token from services.supabase.create_user_request import create_user_request @@ -171,6 +172,7 @@ async def handle_check_suite( # Extract sender related variables sender_id = payload["sender"]["id"] sender_name = payload["sender"]["login"] + sender_info = get_user_public_info(username=sender_name, token=token) # Extract PR related variables and return if no PR is associated with this check suite pull_requests = check_suite["pull_requests"] @@ -218,7 +220,8 @@ async def handle_check_suite( "token": token, "sender_id": sender_id, "sender_name": sender_name, - "sender_email": f"{sender_name}@users.noreply.github.com", + "sender_email": sender_info.email, + "sender_display_name": sender_info.display_name, "is_automation": True, "reviewers": [], "github_urls": [], @@ -315,7 +318,8 @@ async def handle_check_suite( pr_number=pr_number, source="github", trigger="test_failure", - email=None, + email=sender_info.email, + display_name=sender_info.display_name, lambda_info=lambda_info, ) diff --git a/services/webhook/handle_installation.py b/services/webhook/handle_installation.py index 590de9b71..a1b5fd1ce 100644 --- a/services/webhook/handle_installation.py +++ b/services/webhook/handle_installation.py @@ -1,6 +1,6 @@ from services.github.token.get_installation_token import get_installation_access_token from services.github.types.github_types import InstallationPayload -from services.github.users.get_user_public_email import get_user_public_email +from services.github.users.get_user_public_email import get_user_public_info from services.supabase.credits.check_grant_exists import check_grant_exists from services.supabase.credits.insert_credit import insert_credit from services.supabase.installations.insert_installation import insert_installation @@ -26,7 +26,7 @@ async def handle_installation_created(payload: InstallationPayload): user_id = payload["sender"]["id"] sender_name = payload["sender"]["login"] token = get_installation_access_token(installation_id=installation_id) - email = get_user_public_email(username=sender_name, token=token) + user_info = get_user_public_info(username=sender_name, token=token) if not check_owner_exists(owner_id=owner_id): customer_id = create_stripe_customer( @@ -57,7 +57,12 @@ async def handle_installation_created(payload: InstallationPayload): user_name=sender_name, ) - upsert_user(user_id=user_id, user_name=sender_name, email=email) + upsert_user( + user_id=user_id, + user_name=sender_name, + email=user_info.email, + display_name=user_info.display_name, + ) await process_repositories( owner_id=owner_id, diff --git a/services/webhook/new_pr_handler.py b/services/webhook/new_pr_handler.py index b5c6ff6a6..a3f825497 100644 --- a/services/webhook/new_pr_handler.py +++ b/services/webhook/new_pr_handler.py @@ -130,6 +130,7 @@ async def handle_new_pr( new_branch_name = base_args["new_branch"] sender_id = base_args["sender_id"] sender_email = base_args["sender_email"] + sender_display_name = base_args["sender_display_name"] github_urls = base_args["github_urls"] # other_urls = base_args["other_urls"] is_automation = base_args["is_automation"] @@ -230,6 +231,7 @@ async def handle_new_pr( source="github", trigger=trigger, email=sender_email, + display_name=sender_display_name, lambda_info=lambda_info, ) diff --git a/services/webhook/review_run_handler.py b/services/webhook/review_run_handler.py index 832b930c8..468ae59b6 100644 --- a/services/webhook/review_run_handler.py +++ b/services/webhook/review_run_handler.py @@ -32,6 +32,7 @@ from services.github.pulls.get_pull_request_files import get_pull_request_files from services.github.pulls.get_review_thread_comments import get_review_thread_comments from services.github.token.get_installation_token import get_installation_access_token +from services.github.users.get_user_public_email import get_user_public_info from services.github.trees.get_local_file_tree import get_local_file_tree from services.github.types.github_types import ReviewBaseArgs from services.claude.tools.tools import TOOLS_FOR_PRS @@ -105,6 +106,7 @@ async def handle_review_run( # Extract other information installation_id: int = payload["installation"]["id"] token = get_installation_access_token(installation_id=installation_id) + sender_info = get_user_public_info(username=sender_name, token=token) # Get all comments in the review thread thread_comments = get_review_thread_comments( @@ -146,7 +148,8 @@ async def handle_review_run( "token": token, "sender_id": sender_id, "sender_name": sender_name, - "sender_email": f"{sender_name}@users.noreply.github.com", + "sender_email": sender_info.email, + "sender_display_name": sender_info.display_name, "is_automation": False, "reviewers": [], "github_urls": [], @@ -201,7 +204,8 @@ async def handle_review_run( pr_number=pr_number, source="github", trigger="review_comment", - email=None, + email=sender_info.email, + display_name=sender_info.display_name, lambda_info=lambda_info, ) diff --git a/services/webhook/test_handle_installation.py b/services/webhook/test_handle_installation.py index 16f1b7084..05561dbcf 100644 --- a/services/webhook/test_handle_installation.py +++ b/services/webhook/test_handle_installation.py @@ -8,6 +8,7 @@ import pytest # Local imports +from services.github.users.get_user_public_email import UserPublicInfo from services.webhook.handle_installation import handle_installation_created pytestmark = pytest.mark.asyncio @@ -56,9 +57,9 @@ def mock_get_installation_access_token(): @pytest.fixture -def mock_get_user_public_email(): - """Mock get_user_public_email function.""" - with patch("services.webhook.handle_installation.get_user_public_email") as mock: +def mock_get_user_public_info(): + """Mock get_user_public_info function.""" + with patch("services.webhook.handle_installation.get_user_public_info") as mock: yield mock @@ -124,7 +125,7 @@ def mock_process_repositories(): @pytest.fixture def all_mocks( mock_get_installation_access_token, - mock_get_user_public_email, + mock_get_user_public_info, mock_check_owner_exists, mock_create_stripe_customer, mock_insert_owner, @@ -137,7 +138,7 @@ def all_mocks( """Fixture providing all mocked dependencies.""" return { "get_installation_access_token": mock_get_installation_access_token, - "get_user_public_email": mock_get_user_public_email, + "get_user_public_info": mock_get_user_public_info, "check_owner_exists": mock_check_owner_exists, "create_stripe_customer": mock_create_stripe_customer, "insert_owner": mock_insert_owner, @@ -158,7 +159,9 @@ async def test_handle_installation_created_new_owner_with_grant( """Test successful handling of installation created for new owner with grant.""" # Setup all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = False # New owner all_mocks["create_stripe_customer"].return_value = "cus_test123" all_mocks["check_grant_exists"].return_value = False # No existing grant @@ -170,7 +173,7 @@ async def test_handle_installation_created_new_owner_with_grant( all_mocks["get_installation_access_token"].assert_called_once_with( installation_id=12345 ) - all_mocks["get_user_public_email"].assert_called_once_with( + all_mocks["get_user_public_info"].assert_called_once_with( username="test-sender", token="ghs_test_token" ) @@ -210,7 +213,10 @@ async def test_handle_installation_created_new_owner_with_grant( # Verify user upsert all_mocks["upsert_user"].assert_called_once_with( - user_id=11111, user_name="test-sender", email="test@example.com" + user_id=11111, + user_name="test-sender", + email="test@example.com", + display_name="Test Sender", ) # Verify repository processing @@ -230,7 +236,9 @@ async def test_handle_installation_created_existing_owner_with_existing_grant( """Test handling of installation created for existing owner with existing grant.""" # Setup all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = True # Existing owner all_mocks["check_grant_exists"].return_value = True # Existing grant @@ -241,7 +249,7 @@ async def test_handle_installation_created_existing_owner_with_existing_grant( all_mocks["get_installation_access_token"].assert_called_once_with( installation_id=12345 ) - all_mocks["get_user_public_email"].assert_called_once_with( + all_mocks["get_user_public_info"].assert_called_once_with( username="test-sender", token="ghs_test_token" ) @@ -266,7 +274,10 @@ async def test_handle_installation_created_existing_owner_with_existing_grant( # Verify user upsert still happens all_mocks["upsert_user"].assert_called_once_with( - user_id=11111, user_name="test-sender", email="test@example.com" + user_id=11111, + user_name="test-sender", + email="test@example.com", + display_name="Test Sender", ) # Verify repository processing still happens @@ -286,7 +297,9 @@ async def test_handle_installation_created_new_owner_existing_grant( """Test handling of installation created for new owner with existing grant.""" # Setup all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = False # New owner all_mocks["create_stripe_customer"].return_value = "cus_test123" all_mocks["check_grant_exists"].return_value = True # Existing grant @@ -322,7 +335,9 @@ async def test_handle_installation_created_existing_owner_new_grant( """Test handling of installation created for existing owner with new grant.""" # Setup all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = True # Existing owner all_mocks["check_grant_exists"].return_value = False # No existing grant @@ -347,7 +362,9 @@ async def test_handle_installation_created_with_user_type_owner( # Setup mock_installation_payload["installation"]["account"]["type"] = "User" all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = False all_mocks["create_stripe_customer"].return_value = "cus_test123" all_mocks["check_grant_exists"].return_value = False @@ -381,7 +398,9 @@ async def test_handle_installation_created_with_none_email( """Test handling when user email is None.""" # Setup all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = None + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email=None, display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = False all_mocks["create_stripe_customer"].return_value = "cus_test123" all_mocks["check_grant_exists"].return_value = False @@ -391,7 +410,10 @@ async def test_handle_installation_created_with_none_email( # Verify user upsert with None email all_mocks["upsert_user"].assert_called_once_with( - user_id=11111, user_name="test-sender", email=None + user_id=11111, + user_name="test-sender", + email=None, + display_name="Test Sender", ) async def test_handle_installation_created_with_empty_email( @@ -400,7 +422,9 @@ async def test_handle_installation_created_with_empty_email( """Test handling when user email is empty string.""" # Setup all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = False all_mocks["create_stripe_customer"].return_value = "cus_test123" all_mocks["check_grant_exists"].return_value = False @@ -410,7 +434,7 @@ async def test_handle_installation_created_with_empty_email( # Verify user upsert with empty email all_mocks["upsert_user"].assert_called_once_with( - user_id=11111, user_name="test-sender", email="" + user_id=11111, user_name="test-sender", email="", display_name="Test Sender" ) async def test_handle_installation_created_with_token_error( @@ -428,7 +452,7 @@ async def test_handle_installation_created_with_token_error( # Verify - should return early when token retrieval fails all_mocks["get_installation_access_token"].assert_called_once() - all_mocks["get_user_public_email"].assert_not_called() + all_mocks["get_user_public_info"].assert_not_called() all_mocks["process_repositories"].assert_not_called() async def test_handle_installation_created_with_empty_repositories( @@ -438,7 +462,9 @@ async def test_handle_installation_created_with_empty_repositories( # Setup mock_installation_payload["repositories"] = [] all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = False all_mocks["create_stripe_customer"].return_value = "cus_test123" all_mocks["check_grant_exists"].return_value = False @@ -465,7 +491,9 @@ async def test_handle_installation_created_with_single_repository( single_repo = [{"id": 333, "name": "single-repo"}] mock_installation_payload["repositories"] = single_repo all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = False all_mocks["create_stripe_customer"].return_value = "cus_test123" all_mocks["check_grant_exists"].return_value = False @@ -500,15 +528,15 @@ async def test_handle_installation_created_with_exception_in_get_token( all_mocks["get_installation_access_token"].assert_called_once_with( installation_id=12345 ) - all_mocks["get_user_public_email"].assert_not_called() + all_mocks["get_user_public_info"].assert_not_called() async def test_handle_installation_created_with_exception_in_get_email( self, mock_installation_payload, all_mocks ): - """Test handling when get_user_public_email raises exception.""" + """Test handling when get_user_public_info raises exception.""" # Setup all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].side_effect = Exception("Email error") + all_mocks["get_user_public_info"].side_effect = Exception("Email error") # Execute - exception is re-raised due to raise_on_error=True with pytest.raises(Exception, match="Email error"): @@ -517,7 +545,7 @@ async def test_handle_installation_created_with_exception_in_get_email( all_mocks["get_installation_access_token"].assert_called_once_with( installation_id=12345 ) - all_mocks["get_user_public_email"].assert_called_once_with( + all_mocks["get_user_public_info"].assert_called_once_with( username="test-sender", token="ghs_test_token" ) all_mocks["check_owner_exists"].assert_not_called() @@ -528,7 +556,9 @@ async def test_handle_installation_created_with_exception_in_check_owner_exists( """Test handling when check_owner_exists raises exception.""" # Setup all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].side_effect = Exception("Database error") # Execute - exception is re-raised due to raise_on_error=True @@ -544,7 +574,9 @@ async def test_handle_installation_created_with_exception_in_create_stripe_custo """Test handling when create_stripe_customer raises exception.""" # Setup all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = False all_mocks["create_stripe_customer"].side_effect = Exception("Stripe error") @@ -561,7 +593,9 @@ async def test_handle_installation_created_with_exception_in_insert_owner( """Test handling when insert_owner raises exception.""" # Setup all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = False all_mocks["create_stripe_customer"].return_value = "cus_test123" all_mocks["insert_owner"].side_effect = Exception("Insert error") @@ -579,7 +613,9 @@ async def test_handle_installation_created_with_exception_in_check_grant_exists( """Test handling when check_grant_exists raises exception.""" # Setup all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = True # Skip owner creation all_mocks["check_grant_exists"].side_effect = Exception("Grant check error") @@ -596,7 +632,9 @@ async def test_handle_installation_created_with_exception_in_insert_credit( """Test handling when insert_credit raises exception.""" # Setup all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = True # Skip owner creation all_mocks["check_grant_exists"].return_value = False all_mocks["insert_credit"].side_effect = Exception("Credit insert error") @@ -614,7 +652,9 @@ async def test_handle_installation_created_with_exception_in_insert_installation """Test handling when insert_installation raises exception.""" # Setup all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = True # Skip owner creation all_mocks["check_grant_exists"].return_value = True # Skip grant creation all_mocks["insert_installation"].side_effect = Exception( @@ -634,7 +674,9 @@ async def test_handle_installation_created_with_exception_in_upsert_user( """Test handling when upsert_user raises exception.""" # Setup all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = True # Skip owner creation all_mocks["check_grant_exists"].return_value = True # Skip grant creation all_mocks["upsert_user"].side_effect = Exception("User upsert error") @@ -652,7 +694,9 @@ async def test_handle_installation_created_with_exception_in_process_repositorie """Test handling when process_repositories raises exception.""" # Setup all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = True # Skip owner creation all_mocks["check_grant_exists"].return_value = True # Skip grant creation all_mocks["process_repositories"].side_effect = Exception( @@ -674,7 +718,9 @@ async def test_handle_installation_created_with_string_ids( mock_installation_payload["installation"]["account"]["id"] = "67890" mock_installation_payload["sender"]["id"] = "11111" all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = False all_mocks["create_stripe_customer"].return_value = "cus_test123" all_mocks["check_grant_exists"].return_value = False @@ -703,7 +749,9 @@ async def test_handle_installation_created_with_unicode_names( mock_installation_payload["installation"]["account"]["login"] = "tëst-öwnér" mock_installation_payload["sender"]["login"] = "tëst-sëndér" all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "tëst@ëxämplë.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="tëst@ëxämplë.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = False all_mocks["create_stripe_customer"].return_value = "cus_test123" all_mocks["check_grant_exists"].return_value = False @@ -712,7 +760,7 @@ async def test_handle_installation_created_with_unicode_names( await handle_installation_created(mock_installation_payload) # Verify calls with unicode names - all_mocks["get_user_public_email"].assert_called_once_with( + all_mocks["get_user_public_info"].assert_called_once_with( username="tëst-sëndér", token="ghs_test_token" ) all_mocks["create_stripe_customer"].assert_called_once_with( @@ -723,7 +771,10 @@ async def test_handle_installation_created_with_unicode_names( user_name="tëst-sëndér", ) all_mocks["upsert_user"].assert_called_once_with( - user_id=11111, user_name="tëst-sëndér", email="tëst@ëxämplë.com" + user_id=11111, + user_name="tëst-sëndér", + email="tëst@ëxämplë.com", + display_name="Test Sender", ) async def test_handle_installation_created_with_zero_ids( @@ -735,7 +786,9 @@ async def test_handle_installation_created_with_zero_ids( mock_installation_payload["installation"]["account"]["id"] = 0 mock_installation_payload["sender"]["id"] = 0 all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = False all_mocks["create_stripe_customer"].return_value = "cus_test123" all_mocks["check_grant_exists"].return_value = False @@ -765,7 +818,9 @@ async def test_handle_installation_created_with_negative_ids( mock_installation_payload["installation"]["account"]["id"] = -2 mock_installation_payload["sender"]["id"] = -3 all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = False all_mocks["create_stripe_customer"].return_value = "cus_test123" all_mocks["check_grant_exists"].return_value = False @@ -794,7 +849,9 @@ async def test_handle_installation_created_with_large_repository_list( large_repo_list = [{"id": i, "name": f"repo-{i}"} for i in range(100)] mock_installation_payload["repositories"] = large_repo_list all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = False all_mocks["create_stripe_customer"].return_value = "cus_test123" all_mocks["check_grant_exists"].return_value = False @@ -820,7 +877,9 @@ async def test_handle_installation_created_with_none_repositories( # Setup mock_installation_payload["repositories"] = None all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = False all_mocks["create_stripe_customer"].return_value = "cus_test123" all_mocks["check_grant_exists"].return_value = False @@ -868,7 +927,9 @@ async def test_handle_installation_created_with_complex_repository_data( ] mock_installation_payload["repositories"] = complex_repos all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = False all_mocks["create_stripe_customer"].return_value = "cus_test123" all_mocks["check_grant_exists"].return_value = False @@ -897,7 +958,9 @@ async def test_handle_installation_created_with_special_characters_in_names( mock_installation_payload["installation"]["account"]["login"] = special_owner mock_installation_payload["sender"]["login"] = special_sender all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = False all_mocks["create_stripe_customer"].return_value = "cus_test123" all_mocks["check_grant_exists"].return_value = False @@ -906,7 +969,7 @@ async def test_handle_installation_created_with_special_characters_in_names( await handle_installation_created(mock_installation_payload) # Verify calls with special characters - all_mocks["get_user_public_email"].assert_called_once_with( + all_mocks["get_user_public_info"].assert_called_once_with( username=special_sender, token="ghs_test_token" ) all_mocks["create_stripe_customer"].assert_called_once_with( @@ -926,7 +989,9 @@ async def test_handle_installation_created_with_very_long_names( mock_installation_payload["installation"]["account"]["login"] = long_name mock_installation_payload["sender"]["login"] = long_name all_mocks["get_installation_access_token"].return_value = "ghs_test_token" - all_mocks["get_user_public_email"].return_value = "test@example.com" + all_mocks["get_user_public_info"].return_value = UserPublicInfo( + email="test@example.com", display_name="Test Sender" + ) all_mocks["check_owner_exists"].return_value = False all_mocks["create_stripe_customer"].return_value = "cus_test123" all_mocks["check_grant_exists"].return_value = False @@ -935,7 +1000,7 @@ async def test_handle_installation_created_with_very_long_names( await handle_installation_created(mock_installation_payload) # Verify calls with long names - all_mocks["get_user_public_email"].assert_called_once_with( + all_mocks["get_user_public_info"].assert_called_once_with( username=long_name, token="ghs_test_token" ) all_mocks["create_stripe_customer"].assert_called_once_with( From b2f51b6e35b3b86a06246fa2e0d7dbb21467927b Mon Sep 17 00:00:00 2001 From: Hiroshi Nishio Date: Sun, 22 Feb 2026 22:14:13 -0800 Subject: [PATCH 2/2] Add missing sender_display_name to test_new_pr_handler base_args fixtures --- services/webhook/test_new_pr_handler.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/webhook/test_new_pr_handler.py b/services/webhook/test_new_pr_handler.py index 3658185bb..594226864 100644 --- a/services/webhook/test_new_pr_handler.py +++ b/services/webhook/test_new_pr_handler.py @@ -44,6 +44,7 @@ def _get_base_args(): "token": "github_token_123", "new_branch": "gitauto/dashboard-20250101-120000-Ab1C", "sender_email": "test@example.com", + "sender_display_name": "Test Sender", "github_urls": { "issues": "https://api.github.com/repos/test_owner/test_repo/issues", "pulls": "https://api.github.com/repos/test_owner/test_repo/pulls", @@ -1604,6 +1605,7 @@ async def test_new_pr_handler_token_accumulation( "token": "github_token_123", "new_branch": "gitauto/dashboard-20250101-120000-Ab1C", "sender_email": "test@example.com", + "sender_display_name": "Test Sender", "github_urls": { "issues": "https://api.github.com/repos/test_owner/test_repo/issues", "pulls": "https://api.github.com/repos/test_owner/test_repo/pulls", @@ -1774,6 +1776,7 @@ async def test_restrict_edit_to_target_test_file_only_passed_to_chat_with_agent( "token": "github_token_123", "new_branch": "gitauto/dashboard-20250101-120000-Ab1C", "sender_email": "test@example.com", + "sender_display_name": "Test Sender", "github_urls": {}, "is_automation": False, "clone_url": "https://github.com/test_owner/test_repo.git", @@ -1929,6 +1932,7 @@ async def test_few_test_files_include_contents_in_prompt( "token": "github_token_123", "new_branch": "gitauto/dashboard-20250101-120000-Ab1C", "sender_email": "test@example.com", + "sender_display_name": "Test Sender", "github_urls": {}, "is_automation": False, "clone_url": "https://github.com/test_owner/test_repo.git", @@ -2076,6 +2080,7 @@ async def test_many_test_files_include_paths_only_in_prompt( "token": "github_token_123", "new_branch": "gitauto/dashboard-20250101-120000-Ab1C", "sender_email": "test@example.com", + "sender_display_name": "Test Sender", "github_urls": {}, "is_automation": False, "clone_url": "https://github.com/test_owner/test_repo.git",