2025-06-07 20:35:47 +02:00
|
|
|
import pytest
|
|
|
|
from unittest.mock import MagicMock, patch, AsyncMock
|
|
|
|
from pr_agent.tools.pr_update_changelog import PRUpdateChangelog
|
|
|
|
|
|
|
|
|
|
|
|
class TestPRUpdateChangelog:
|
|
|
|
"""Test suite for the PR Update Changelog functionality."""
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def mock_git_provider(self):
|
|
|
|
"""Create a mock git provider."""
|
|
|
|
provider = MagicMock()
|
|
|
|
provider.get_pr_branch.return_value = "feature-branch"
|
|
|
|
provider.get_pr_file_content.return_value = ""
|
|
|
|
provider.pr.title = "Test PR"
|
|
|
|
provider.get_pr_description.return_value = "Test description"
|
|
|
|
provider.get_commit_messages.return_value = "fix: test commit"
|
|
|
|
provider.get_languages.return_value = {"Python": 80, "JavaScript": 20}
|
|
|
|
provider.get_files.return_value = ["test.py", "test.js"]
|
|
|
|
return provider
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def mock_ai_handler(self):
|
|
|
|
"""Create a mock AI handler."""
|
|
|
|
handler = MagicMock()
|
|
|
|
handler.chat_completion = AsyncMock(return_value=("Test changelog entry", "stop"))
|
|
|
|
return handler
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def changelog_tool(self, mock_git_provider, mock_ai_handler):
|
|
|
|
"""Create a PRUpdateChangelog instance with mocked dependencies."""
|
|
|
|
with patch('pr_agent.tools.pr_update_changelog.get_git_provider', return_value=lambda url: mock_git_provider), \
|
|
|
|
patch('pr_agent.tools.pr_update_changelog.get_main_pr_language', return_value="Python"), \
|
|
|
|
patch('pr_agent.tools.pr_update_changelog.get_settings') as mock_settings:
|
|
|
|
|
|
|
|
# Configure mock settings
|
|
|
|
mock_settings.return_value.pr_update_changelog.push_changelog_changes = False
|
|
|
|
mock_settings.return_value.pr_update_changelog.extra_instructions = ""
|
|
|
|
mock_settings.return_value.pr_update_changelog_prompt.system = "System prompt"
|
|
|
|
mock_settings.return_value.pr_update_changelog_prompt.user = "User prompt"
|
|
|
|
mock_settings.return_value.config.temperature = 0.2
|
|
|
|
|
|
|
|
tool = PRUpdateChangelog("https://gitlab.com/test/repo/-/merge_requests/1", ai_handler=lambda: mock_ai_handler)
|
|
|
|
return tool
|
|
|
|
|
|
|
|
def test_get_changelog_file_with_existing_content(self, changelog_tool, mock_git_provider):
|
|
|
|
"""Test retrieving existing changelog content."""
|
|
|
|
# Arrange
|
|
|
|
existing_content = "# Changelog\n\n## v1.0.0\n- Initial release\n- Bug fixes"
|
|
|
|
mock_git_provider.get_pr_file_content.return_value = existing_content
|
|
|
|
|
|
|
|
# Act
|
|
|
|
changelog_tool._get_changelog_file()
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
assert changelog_tool.changelog_file == existing_content
|
|
|
|
assert "# Changelog" in changelog_tool.changelog_file_str
|
|
|
|
|
|
|
|
def test_get_changelog_file_with_no_existing_content(self, changelog_tool, mock_git_provider):
|
|
|
|
"""Test handling when no changelog file exists."""
|
|
|
|
# Arrange
|
|
|
|
mock_git_provider.get_pr_file_content.return_value = ""
|
|
|
|
|
|
|
|
# Act
|
|
|
|
changelog_tool._get_changelog_file()
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
assert changelog_tool.changelog_file == ""
|
|
|
|
assert "Example:" in changelog_tool.changelog_file_str # Default template
|
|
|
|
|
|
|
|
def test_get_changelog_file_with_bytes_content(self, changelog_tool, mock_git_provider):
|
|
|
|
"""Test handling when git provider returns bytes instead of string."""
|
|
|
|
# Arrange
|
|
|
|
content_bytes = b"# Changelog\n\n## v1.0.0\n- Initial release"
|
|
|
|
mock_git_provider.get_pr_file_content.return_value = content_bytes
|
|
|
|
|
|
|
|
# Act
|
|
|
|
changelog_tool._get_changelog_file()
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
assert isinstance(changelog_tool.changelog_file, str)
|
|
|
|
assert changelog_tool.changelog_file == "# Changelog\n\n## v1.0.0\n- Initial release"
|
|
|
|
|
|
|
|
def test_get_changelog_file_with_exception(self, changelog_tool, mock_git_provider):
|
|
|
|
"""Test handling exceptions during file retrieval."""
|
|
|
|
# Arrange
|
|
|
|
mock_git_provider.get_pr_file_content.side_effect = Exception("Network error")
|
|
|
|
|
|
|
|
# Act
|
|
|
|
changelog_tool._get_changelog_file()
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
assert changelog_tool.changelog_file == ""
|
2025-06-07 21:13:16 +02:00
|
|
|
assert changelog_tool.changelog_file_str == "" # Exception should result in empty string, no default template
|
2025-06-07 20:35:47 +02:00
|
|
|
|
|
|
|
def test_prepare_changelog_update_with_existing_content(self, changelog_tool):
|
|
|
|
"""Test preparing changelog update when existing content exists."""
|
|
|
|
# Arrange
|
|
|
|
changelog_tool.prediction = "## v1.1.0\n- New feature\n- Bug fix"
|
|
|
|
changelog_tool.changelog_file = "# Changelog\n\n## v1.0.0\n- Initial release"
|
|
|
|
changelog_tool.commit_changelog = True
|
|
|
|
|
|
|
|
# Act
|
|
|
|
new_content, answer = changelog_tool._prepare_changelog_update()
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
assert new_content.startswith("## v1.1.0\n- New feature\n- Bug fix\n\n")
|
|
|
|
assert "# Changelog\n\n## v1.0.0\n- Initial release" in new_content
|
|
|
|
assert answer == "## v1.1.0\n- New feature\n- Bug fix"
|
|
|
|
|
|
|
|
def test_prepare_changelog_update_without_existing_content(self, changelog_tool):
|
|
|
|
"""Test preparing changelog update when no existing content."""
|
|
|
|
# Arrange
|
|
|
|
changelog_tool.prediction = "## v1.0.0\n- Initial release"
|
|
|
|
changelog_tool.changelog_file = ""
|
|
|
|
changelog_tool.commit_changelog = True
|
|
|
|
|
|
|
|
# Act
|
|
|
|
new_content, answer = changelog_tool._prepare_changelog_update()
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
assert new_content == "## v1.0.0\n- Initial release"
|
|
|
|
assert answer == "## v1.0.0\n- Initial release"
|
|
|
|
|
|
|
|
def test_prepare_changelog_update_no_commit(self, changelog_tool):
|
|
|
|
"""Test preparing changelog update when not committing."""
|
|
|
|
# Arrange
|
|
|
|
changelog_tool.prediction = "## v1.1.0\n- New feature"
|
|
|
|
changelog_tool.changelog_file = ""
|
|
|
|
changelog_tool.commit_changelog = False
|
|
|
|
|
|
|
|
# Act
|
|
|
|
new_content, answer = changelog_tool._prepare_changelog_update()
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
assert new_content == "## v1.1.0\n- New feature"
|
|
|
|
assert "to commit the new content" in answer
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
async def test_run_without_push_support(self, changelog_tool, mock_git_provider):
|
|
|
|
"""Test running changelog update when git provider doesn't support pushing."""
|
|
|
|
# Arrange
|
|
|
|
delattr(mock_git_provider, 'create_or_update_pr_file') # Remove the method
|
|
|
|
changelog_tool.commit_changelog = True
|
|
|
|
|
|
|
|
with patch('pr_agent.tools.pr_update_changelog.get_settings') as mock_settings:
|
|
|
|
mock_settings.return_value.pr_update_changelog.push_changelog_changes = True
|
|
|
|
mock_settings.return_value.config.publish_output = True
|
|
|
|
|
|
|
|
# Act
|
|
|
|
await changelog_tool.run()
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
mock_git_provider.publish_comment.assert_called_once()
|
|
|
|
assert "not currently supported" in str(mock_git_provider.publish_comment.call_args)
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
async def test_run_with_push_support(self, changelog_tool, mock_git_provider):
|
|
|
|
"""Test running changelog update when git provider supports pushing."""
|
|
|
|
# Arrange
|
|
|
|
mock_git_provider.create_or_update_pr_file = MagicMock()
|
|
|
|
changelog_tool.commit_changelog = True
|
|
|
|
changelog_tool.prediction = "## v1.1.0\n- New feature"
|
|
|
|
|
|
|
|
with patch('pr_agent.tools.pr_update_changelog.get_settings') as mock_settings, \
|
|
|
|
patch('pr_agent.tools.pr_update_changelog.retry_with_fallback_models') as mock_retry, \
|
|
|
|
patch('pr_agent.tools.pr_update_changelog.sleep'):
|
|
|
|
|
|
|
|
mock_settings.return_value.pr_update_changelog.push_changelog_changes = True
|
|
|
|
mock_settings.return_value.pr_update_changelog.get.return_value = True
|
|
|
|
mock_settings.return_value.config.publish_output = True
|
|
|
|
mock_settings.return_value.config.git_provider = "gitlab"
|
|
|
|
mock_retry.return_value = None
|
|
|
|
|
|
|
|
# Act
|
|
|
|
await changelog_tool.run()
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
mock_git_provider.create_or_update_pr_file.assert_called_once()
|
|
|
|
call_args = mock_git_provider.create_or_update_pr_file.call_args
|
|
|
|
assert call_args[1]['file_path'] == 'CHANGELOG.md'
|
|
|
|
assert call_args[1]['branch'] == 'feature-branch'
|
|
|
|
|
|
|
|
def test_push_changelog_update(self, changelog_tool, mock_git_provider):
|
|
|
|
"""Test the push changelog update functionality."""
|
|
|
|
# Arrange
|
|
|
|
mock_git_provider.create_or_update_pr_file = MagicMock()
|
|
|
|
mock_git_provider.get_pr_branch.return_value = "feature-branch"
|
|
|
|
new_content = "# Updated changelog content"
|
|
|
|
answer = "Changes made"
|
|
|
|
|
|
|
|
with patch('pr_agent.tools.pr_update_changelog.get_settings') as mock_settings, \
|
|
|
|
patch('pr_agent.tools.pr_update_changelog.sleep'):
|
|
|
|
|
|
|
|
mock_settings.return_value.pr_update_changelog.get.return_value = True
|
|
|
|
|
|
|
|
# Act
|
|
|
|
changelog_tool._push_changelog_update(new_content, answer)
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
mock_git_provider.create_or_update_pr_file.assert_called_once_with(
|
|
|
|
file_path="CHANGELOG.md",
|
|
|
|
branch="feature-branch",
|
|
|
|
contents=new_content,
|
|
|
|
message="[skip ci] Update CHANGELOG.md"
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_gitlab_provider_method_detection(self, changelog_tool, mock_git_provider):
|
|
|
|
"""Test that the tool correctly detects GitLab provider method availability."""
|
|
|
|
# Arrange
|
|
|
|
mock_git_provider.create_or_update_pr_file = MagicMock()
|
|
|
|
|
|
|
|
# Act & Assert
|
|
|
|
assert hasattr(mock_git_provider, "create_or_update_pr_file")
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("existing_content,new_entry,expected_order", [
|
|
|
|
(
|
|
|
|
"# Changelog\n\n## v1.0.0\n- Old feature",
|
|
|
|
"## v1.1.0\n- New feature",
|
|
|
|
["v1.1.0", "v1.0.0"]
|
|
|
|
),
|
|
|
|
(
|
|
|
|
"",
|
|
|
|
"## v1.0.0\n- Initial release",
|
|
|
|
["v1.0.0"]
|
|
|
|
),
|
|
|
|
(
|
|
|
|
"Some existing content",
|
|
|
|
"## v1.0.0\n- New entry",
|
|
|
|
["v1.0.0", "Some existing content"]
|
|
|
|
),
|
|
|
|
])
|
|
|
|
def test_changelog_order_preservation(self, changelog_tool, existing_content, new_entry, expected_order):
|
|
|
|
"""Test that changelog entries are properly ordered (newest first)."""
|
|
|
|
# Arrange
|
|
|
|
changelog_tool.prediction = new_entry
|
|
|
|
changelog_tool.changelog_file = existing_content
|
|
|
|
changelog_tool.commit_changelog = True
|
|
|
|
|
|
|
|
# Act
|
|
|
|
new_content, _ = changelog_tool._prepare_changelog_update()
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
for i, expected in enumerate(expected_order[:-1]):
|
|
|
|
current_pos = new_content.find(expected)
|
|
|
|
next_pos = new_content.find(expected_order[i + 1])
|
|
|
|
assert current_pos < next_pos, f"Expected {expected} to come before {expected_order[i + 1]}"
|