From e2c7dd72ae982a29ebcbe837ab29a2fe047ba862 Mon Sep 17 00:00:00 2001 From: Igor Udot Date: Fri, 16 May 2025 21:26:24 +0800 Subject: [PATCH] feat: block editing --- src/core/pre_checks.py | 19 +++++++++ src/core/settings.py | 2 + src/main.py | 5 +++ tests/test_main.py | 36 ++++++++++++++++ tests/test_pre_checks.py | 92 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 154 insertions(+) create mode 100644 src/core/pre_checks.py create mode 100644 tests/test_pre_checks.py diff --git a/src/core/pre_checks.py b/src/core/pre_checks.py new file mode 100644 index 0000000..e763bf6 --- /dev/null +++ b/src/core/pre_checks.py @@ -0,0 +1,19 @@ +def block_user_title_edit(event_data, skip_label, github_client, issue): + if not isinstance(event_data, dict): + return False + if event_data["action"] != "edited": + return False + if event_data["sender"]["type"] != "User": + return False + + previous_title = event_data["changes"]["title"]["from"] + if not previous_title: + return False + if skip_label not in [label["name"] for label in event_data["issue"]["labels"]]: + return False + + github_client.add_issue_comment( + issue, "This issue has already been processed. Please do not change the title." + ) + issue.edit(title=previous_title) + return True diff --git a/src/core/settings.py b/src/core/settings.py index 8251ff8..333fb8c 100644 --- a/src/core/settings.py +++ b/src/core/settings.py @@ -35,6 +35,7 @@ def __init__(self): self.event_path = os.environ.get("GITHUB_EVENT_PATH") self.is_issue_event = self.event_name == "issues" self.issue_number = None + self.event_data = None # If it's an issue event, get the issue number if self.is_issue_event and self.event_path and os.path.exists(self.event_path): @@ -43,6 +44,7 @@ def __init__(self): with open(self.event_path) as f: event_data = json.load(f) + self.event_data = event_data if "issue" in event_data and "number" in event_data["issue"]: self.issue_number = event_data["issue"]["number"] except Exception as e: diff --git a/src/main.py b/src/main.py index 3e70dc6..43fe692 100644 --- a/src/main.py +++ b/src/main.py @@ -5,6 +5,7 @@ from core.github_client import GitHubClient from core.issue_service import IssueProcessor from core.llm import create_ai_client +from core.pre_checks import block_user_title_edit from core.settings import Config from core.verbose import set_verbose @@ -13,6 +14,10 @@ def open_issue_event(config, repo_obj, ai_client, github_client): print(f"Processing single issue #{config.issue_number} from event trigger") try: issue = repo_obj.get_issue(config.issue_number) + + if block_user_title_edit(config.event_data, config.skip_label, github_client, issue): + return [] + issue_processor = IssueProcessor( ai_client, github_client, config.prompt, config.skip_label, config.required_labels ) diff --git a/tests/test_main.py b/tests/test_main.py index 3cd2901..81f7938 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -44,6 +44,7 @@ def mock_config(): config.issue_number = None config.required_labels = [] config.apply_to_closed = False + config.event_data = None return config @@ -72,6 +73,41 @@ def test_open_issue_event(mock_config, mock_ai_client, mock_github_client, mock_ assert results[0]["issue_number"] == 1 +def test_open_issue_event_with_editing( + mock_config, mock_ai_client, mock_github_client, mock_repo, mock_issue +): + mock_config.issue_number = 1 + mock_repo.get_issue.return_value = mock_issue + + # Setup event_data to simulate a title edit + mock_config.event_data = { + "action": "edited", + "sender": {"type": "User"}, + "changes": {"title": {"from": "Previous title"}}, + "issue": {"labels": [{"name": "titled"}]}, + } + + # Add the 'titled' label to the mock issue to match the event_data + mock_issue.labels = [{"name": "titled"}] + + results = open_issue_event(mock_config, mock_repo, mock_ai_client, mock_github_client) + + # Should have called get_issue + mock_repo.get_issue.assert_called_once_with(1) + + # Since the issue has the 'titled' label and was edited, block_user_title_edit should have + # returned True and no results should be returned + assert len(results) == 0 + + # Verify that the issue was edited back to its previous title + mock_issue.edit.assert_called_once_with(title="Previous title") + + # Verify that a comment was added explaining the title change was not allowed + mock_github_client.add_issue_comment.assert_called_once_with( + mock_issue, "This issue has already been processed. Please do not change the title." + ) + + def test_open_issue_event_error(mock_config, mock_ai_client, mock_github_client, mock_repo): mock_config.issue_number = 1 mock_repo.get_issue.side_effect = Exception("Issue not found") diff --git a/tests/test_pre_checks.py b/tests/test_pre_checks.py new file mode 100644 index 0000000..63de01b --- /dev/null +++ b/tests/test_pre_checks.py @@ -0,0 +1,92 @@ +from unittest.mock import Mock, patch + +import pytest + +from src.core.pre_checks import block_user_title_edit + + +@pytest.fixture +def mock_event_data(): + """Create mock event data for edited issue title.""" + return { + "action": "edited", + "sender": {"type": "User"}, + "changes": {"title": {"from": "Original Title"}}, + "issue": {"labels": [{"name": "titled"}]}, + } + + +@pytest.fixture +def mock_github_client(): + """Create a mock GitHub client.""" + client = Mock() + client.add_issue_comment = Mock() + return client + + +@pytest.fixture +def mock_issue(): + """Create a mock GitHub issue.""" + issue = Mock() + issue.edit = Mock() + return issue + + +def test_block_user_title_edit_success(mock_event_data, mock_github_client, mock_issue): + """Test successful blocking of a user title edit.""" + result = block_user_title_edit(mock_event_data, "titled", mock_github_client, mock_issue) + + # Assert the function returns True + assert result is True + + # Assert comment was added with correct message + mock_github_client.add_issue_comment.assert_called_once_with( + mock_issue, "This issue has already been processed. Please do not change the title." + ) + + # Assert title was reverted back to the original + mock_issue.edit.assert_called_once_with(title="Original Title") + + +def test_block_user_title_edit_not_edited_action(mock_event_data, mock_github_client, mock_issue): + """Test when action is not 'edited'.""" + mock_event_data["action"] = "created" + + result = block_user_title_edit(mock_event_data, "titled", mock_github_client, mock_issue) + + assert result is False + mock_github_client.add_issue_comment.assert_not_called() + mock_issue.edit.assert_not_called() + + +def test_block_user_title_edit_not_user(mock_event_data, mock_github_client, mock_issue): + """Test when sender is not a User.""" + mock_event_data["sender"]["type"] = "Bot" + + result = block_user_title_edit(mock_event_data, "titled", mock_github_client, mock_issue) + + assert result is False + mock_github_client.add_issue_comment.assert_not_called() + mock_issue.edit.assert_not_called() + + +def test_block_user_title_edit_no_previous_title(mock_event_data, mock_github_client, mock_issue): + """Test when there is no previous title.""" + mock_event_data["changes"]["title"]["from"] = "" + + result = block_user_title_edit(mock_event_data, "titled", mock_github_client, mock_issue) + + assert result is False + mock_github_client.add_issue_comment.assert_not_called() + mock_issue.edit.assert_not_called() + + +def test_block_user_title_edit_no_skip_label(mock_event_data, mock_github_client, mock_issue): + """Test when the issue doesn't have the skip label.""" + mock_event_data["issue"]["labels"] = [{"name": "bug"}] + + result = block_user_title_edit(mock_event_data, "titled", mock_github_client, mock_issue) + + assert result is False + mock_github_client.add_issue_comment.assert_not_called() + mock_issue.edit.assert_not_called()