From 6d57cf55165b49132d3e74ed34980bd0010b4f39 Mon Sep 17 00:00:00 2001 From: "Abuzar Mahmood (aider)" Date: Wed, 2 Apr 2025 16:18:47 -0400 Subject: [PATCH 1/5] feat: Add comprehensive test suite for GitHub bot functionality --- requirements-dev.txt | 4 + run_tests.sh | 10 ++ tests/__init__.py | 1 + tests/conftest.py | 86 +++++++++++++++++ tests/test_agents.py | 109 +++++++++++++++++++++ tests/test_bot_tools.py | 130 +++++++++++++++++++++++++ tests/test_branch_handler.py | 147 ++++++++++++++++++++++++++++ tests/test_git_utils.py | 172 +++++++++++++++++++++++++++++++++ tests/test_integration.py | 113 ++++++++++++++++++++++ tests/test_response_agent.py | 180 +++++++++++++++++++++++++++++++++++ tests/test_triggers.py | 164 +++++++++++++++++++++++++++++++ 11 files changed, 1116 insertions(+) create mode 100644 requirements-dev.txt create mode 100644 run_tests.sh create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_agents.py create mode 100644 tests/test_bot_tools.py create mode 100644 tests/test_branch_handler.py create mode 100644 tests/test_git_utils.py create mode 100644 tests/test_integration.py create mode 100644 tests/test_response_agent.py create mode 100644 tests/test_triggers.py diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..953a87b --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,4 @@ +# Development dependencies +pytest==7.4.0 +pytest-cov==4.1.0 +pytest-mock==3.11.1 diff --git a/run_tests.sh b/run_tests.sh new file mode 100644 index 0000000..348dc4f --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Run tests with coverage +python -m pytest tests/ -v --cov=src --cov-report=term-missing + +# To run specific test files: +# python -m pytest tests/test_agents.py -v + +# To run integration tests only: +# python -m pytest tests/test_integration.py -v -m integration diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..011100a --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# This file is intentionally empty to make the directory a Python package diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..f8f7476 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,86 @@ +""" +Pytest configuration file with fixtures +""" +import os +import sys +import pytest +from unittest.mock import MagicMock + +# Add src directory to path +src_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(src_dir) + + +@pytest.fixture +def mock_issue(): + """Create a mock GitHub issue""" + issue = MagicMock() + issue.number = 123 + issue.title = "Test Issue" + issue.body = "This is a test issue" + issue.state = "open" + issue.html_url = "https://github.com/test/repo/issues/123" + + # Add labels + label = MagicMock() + label.name = "bug" + issue.labels = [label] + + return issue + + +@pytest.fixture +def mock_issue_with_label(label_name="bug"): + """Create a mock GitHub issue with specific label""" + def _create_issue(label_name): + issue = MagicMock() + issue.number = 123 + issue.title = "Test Issue" + issue.body = "This is a test issue" + issue.state = "open" + + # Add label + label = MagicMock() + label.name = label_name + issue.labels = [label] + + return issue + + return _create_issue(label_name) + + +@pytest.fixture +def mock_pull_request(): + """Create a mock GitHub pull request""" + pr = MagicMock() + pr.number = 456 + pr.title = "Test PR" + pr.body = "This is a test pull request" + pr.state = "open" + pr.html_url = "https://github.com/test/repo/pull/456" + + # Add head reference + head = MagicMock() + head.ref = "feature-branch" + pr.head = head + + return pr + + +@pytest.fixture +def mock_repository(): + """Create a mock GitHub repository""" + repo = MagicMock() + repo.full_name = "test/repo" + repo.name = "repo" + repo.clone_url = "https://github.com/test/repo.git" + repo.default_branch = "main" + + return repo + + +@pytest.fixture +def mock_github_client(): + """Create a mock GitHub client""" + client = MagicMock() + return client diff --git a/tests/test_agents.py b/tests/test_agents.py new file mode 100644 index 0000000..288ff97 --- /dev/null +++ b/tests/test_agents.py @@ -0,0 +1,109 @@ +""" +Tests for the agents module +""" +import os +import sys +import pytest +from unittest.mock import MagicMock, patch + +# Add src directory to path +src_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(src_dir) + +from src.agents import ( + create_user_agent, + create_agent, + generate_prompt, + is_terminate_msg, + register_functions +) +from autogen import UserProxyAgent, AssistantAgent + + +def test_create_user_agent(): + """Test creation of user agent""" + user_agent = create_user_agent() + assert isinstance(user_agent, UserProxyAgent) + assert user_agent.name == "User" + assert user_agent.human_input_mode == "NEVER" + + +def test_create_agent(): + """Test creation of assistant agent""" + llm_config = {"model": "test-model", "api_key": "test-key"} + agent = create_agent("file_assistant", llm_config) + assert isinstance(agent, AssistantAgent) + assert agent.name == "file_assistant" + assert "analyze this GitHub issue" in agent.system_message.lower() + + +def test_is_terminate_msg(): + """Test terminate message detection""" + assert is_terminate_msg({"content": "Done. TERMINATE"}) is True + assert is_terminate_msg({"content": "Not done yet"}) is False + assert is_terminate_msg({"content": "TERMINATE."}) is True + assert is_terminate_msg({"content": ""}) is False + + +@patch('src.agents.parse_comments') +def test_generate_prompt(mock_parse_comments): + """Test prompt generation for different agent types""" + # Setup mock + mock_parse_comments.return_value = ("Last comment", "Comments string", ["comment1"]) + + # Mock issue and details + issue = MagicMock() + issue.number = 123 + issue.title = "Test Issue" + + details = { + "title": "Test Issue", + "body": "This is a test issue" + } + + # Test file_assistant prompt + prompt = generate_prompt("file_assistant", "test/repo", "/path/to/repo", details, issue) + assert "Please analyze this GitHub issue" in prompt + assert "Repository: test/repo" in prompt + + # Test edit_assistant prompt + prompt = generate_prompt("edit_assistant", "test/repo", "/path/to/repo", details, issue) + assert "Suggest what changes can be made to resolve this issue" in prompt + + # Test feedback_assistant prompt + prompt = generate_prompt( + "feedback_assistant", + "test/repo", + "/path/to/repo", + details, + issue, + original_response="Original response", + feedback_text="Feedback text" + ) + assert "Process this user feedback" in prompt + assert "Original response" in prompt + assert "Feedback text" in prompt + + +def test_register_functions(): + """Test function registration with agent""" + agent = MagicMock() + agent.register_for_llm = MagicMock(return_value=lambda x: x) + agent.register_for_execution = MagicMock(return_value=lambda x: x) + + # Define test function + def test_func(): + """Test function docstring""" + pass + + # Test LLM registration + result = register_functions(agent, "llm", [test_func]) + assert agent.register_for_llm.called + assert agent.register_for_llm.call_args[1]["name"] == "test_func" + assert "Test function docstring" in agent.register_for_llm.call_args[1]["description"] + + # Test execution registration + agent.reset_mock() + result = register_functions(agent, "execution", [test_func]) + assert agent.register_for_execution.called + assert agent.register_for_execution.call_args[1]["name"] == "test_func" diff --git a/tests/test_bot_tools.py b/tests/test_bot_tools.py new file mode 100644 index 0000000..1cc0868 --- /dev/null +++ b/tests/test_bot_tools.py @@ -0,0 +1,130 @@ +""" +Tests for the bot_tools module +""" +import os +import sys +import pytest +from unittest.mock import MagicMock, patch + +# Add src directory to path +src_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(src_dir) + +from src.bot_tools import ( + get_local_repo_path, + search_for_pattern, + search_for_file, + estimate_tokens, + readfile, + readlines +) + + +def test_get_local_repo_path(): + """Test getting local repository path""" + # Test with existing repo + with patch('os.path.exists', return_value=True): + path = get_local_repo_path("owner/repo") + assert "owner/repo" in path + assert path.endswith("owner/repo") + + # Test with non-existing repo + with patch('os.path.exists', return_value=False): + result = get_local_repo_path("nonexistent/repo") + assert "not found" in result + + +@patch('os.popen') +def test_search_for_pattern(mock_popen): + """Test searching for pattern in files""" + # Setup mock + mock_popen.return_value.read.return_value = "/path/to/file1.py\n/path/to/file2.py" + + # Call function + result = search_for_pattern("/search/dir", "pattern") + + # Verify + mock_popen.assert_called_with("grep -irl pattern /search/dir --include='*.py'") + assert result == "/path/to/file1.py\n/path/to/file2.py" + + +@patch('os.popen') +def test_search_for_file(mock_popen): + """Test searching for file by name""" + # Setup mock + mock_popen.return_value.read.return_value = "/path/to/file.py" + + # Call function + result = search_for_file("/search/dir", "file.py") + + # Verify + mock_popen.assert_called_with("find /search/dir -iname '*file.py*'") + assert result == "/path/to/file.py" + + # Test file not found + mock_popen.return_value.read.return_value = "" + result = search_for_file("/search/dir", "nonexistent.py") + assert result == "File not found" + + +def test_estimate_tokens(): + """Test token estimation""" + # Test with normal text + text = "This is a test string with multiple words." + assert estimate_tokens(text) == 8 + + # Test with empty text + assert estimate_tokens("") == 0 + + # Test with None + assert estimate_tokens(None) == 0 + + +@patch('builtins.open') +def test_readfile(mock_open): + """Test reading file with line numbers""" + # Setup mock + mock_file = MagicMock() + mock_file.__enter__.return_value.readlines.return_value = ["Line 1\n", "Line 2\n", "Line 3\n"] + mock_open.return_value = mock_file + + # Call function + result = readfile("/path/to/file.py") + + # Verify + mock_open.assert_called_with("/path/to/file.py", 'r') + assert "0000: Line 1" in result + assert "0001: Line 2" in result + assert "0002: Line 3" in result + + # Test file not found + mock_open.side_effect = FileNotFoundError() + result = readfile("/nonexistent/file.py") + assert "File not found" in result + + # Test other error + mock_open.side_effect = Exception("Test error") + result = readfile("/error/file.py") + assert "Error reading file" in result + + +@patch('builtins.open') +def test_readlines(mock_open): + """Test reading specific lines from file""" + # Setup mock + mock_file = MagicMock() + mock_file.__enter__.return_value.readlines.return_value = [ + "Line 1\n", "Line 2\n", "Line 3\n", "Line 4\n", "Line 5\n" + ] + mock_open.return_value = mock_file + + # Call function + result = readlines("/path/to/file.py", 1, 4) + + # Verify + mock_open.assert_called_with("/path/to/file.py", 'r') + assert "0001: Line 2" in result + assert "0002: Line 3" in result + assert "0003: Line 4" in result + assert "0000: Line 1" not in result + assert "0004: Line 5" not in result diff --git a/tests/test_branch_handler.py b/tests/test_branch_handler.py new file mode 100644 index 0000000..ce2d99c --- /dev/null +++ b/tests/test_branch_handler.py @@ -0,0 +1,147 @@ +""" +Tests for the branch_handler module +""" +import os +import sys +import pytest +from unittest.mock import MagicMock, patch + +# Add src directory to path +src_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(src_dir) + +from src.branch_handler import ( + get_issue_related_branches, + get_current_branch, + checkout_branch, + delete_branch, + back_to_master_branch, + push_changes +) + + +@patch('src.branch_handler.os') +@patch('src.branch_handler.git.Repo') +def test_get_issue_related_branches(mock_repo, mock_os): + """Test finding branches related to an issue""" + # Setup mocks + mock_issue = MagicMock() + mock_issue.number = 123 + mock_issue.title = "Test Issue" + + mock_os.getcwd.return_value = "/original/dir" + mock_os.popen.return_value.read.return_value = "branch-123\thttp://github.com/test/repo" + + # Call function + branches = get_issue_related_branches("/path/to/repo", mock_issue) + + # Verify + mock_os.chdir.assert_any_call("/path/to/repo") + mock_os.chdir.assert_called_with("/original/dir") + mock_os.popen.assert_called_with("gh issue develop -l 123") + assert branches == [("branch-123", "http://github.com/test/repo")] + + # Test fallback when gh command returns no branches + mock_os.popen.return_value.read.return_value = "" + mock_repo_instance = MagicMock() + mock_repo.return_value = mock_repo_instance + mock_repo_instance.heads = [] + mock_repo_instance.git.ls_remote.return_value = "" + + branches = get_issue_related_branches("/path/to/repo", mock_issue) + assert branches == [] + + +@patch('src.branch_handler.git.Repo') +def test_get_current_branch(mock_repo): + """Test getting current branch name""" + # Setup mock + mock_repo_instance = MagicMock() + mock_repo.return_value = mock_repo_instance + mock_repo_instance.active_branch.name = "test-branch" + + # Call function + branch = get_current_branch("/path/to/repo") + + # Verify + mock_repo.assert_called_with("/path/to/repo") + assert branch == "test-branch" + + +@patch('src.branch_handler.git.Repo') +def test_checkout_branch(mock_repo): + """Test branch checkout functionality""" + # Setup mock + mock_repo_instance = MagicMock() + mock_repo.return_value = mock_repo_instance + mock_repo_instance.heads = ["main"] + + # Test creating new branch + checkout_branch("/path/to/repo", "test-branch", create=True) + + # Verify + mock_repo.assert_called_with("/path/to/repo") + mock_repo_instance.git.clean.assert_called_with('-f') + mock_repo_instance.create_head.assert_called_with("test-branch") + mock_repo_instance.git.checkout.assert_called_with("test-branch") + mock_repo_instance.git.reset.assert_called_with('--hard', 'origin/test-branch') + + +@patch('src.branch_handler.git.Repo') +def test_delete_branch(mock_repo): + """Test branch deletion""" + # Setup mock + mock_repo_instance = MagicMock() + mock_repo.return_value = mock_repo_instance + mock_repo_instance.heads = ["test-branch"] + + # Call function + delete_branch("/path/to/repo", "test-branch", force=True) + + # Verify + mock_repo.assert_called_with("/path/to/repo") + mock_repo_instance.delete_head.assert_called_with("test-branch", force=True) + + +@patch('src.branch_handler.git.Repo') +def test_back_to_master_branch(mock_repo): + """Test switching back to master/main branch""" + # Setup mock + mock_repo_instance = MagicMock() + mock_repo.return_value = mock_repo_instance + + # Test with master branch + mock_repo_instance.heads = ["master", "other"] + back_to_master_branch("/path/to/repo") + mock_repo_instance.git.checkout.assert_called_with("master") + + # Test with main branch + mock_repo_instance.heads = ["main", "other"] + back_to_master_branch("/path/to/repo") + mock_repo_instance.git.checkout.assert_called_with("main") + + # Test with neither branch + mock_repo_instance.heads = ["develop", "other"] + with pytest.raises(ValueError): + back_to_master_branch("/path/to/repo") + + +@patch('src.branch_handler.git.Repo') +def test_push_changes(mock_repo): + """Test pushing changes to remote""" + # Setup mock + mock_repo_instance = MagicMock() + mock_repo.return_value = mock_repo_instance + mock_repo_instance.active_branch.name = "test-branch" + + # Test normal push + push_changes("/path/to/repo") + mock_repo_instance.git.push.assert_called_with('origin', 'test-branch') + + # Test force push + push_changes("/path/to/repo", force=True) + mock_repo_instance.git.push.assert_called_with('origin', 'test-branch', '--force') + + # Test with specific branch + push_changes("/path/to/repo", branch_name="feature-branch") + mock_repo_instance.git.push.assert_called_with('origin', 'feature-branch') diff --git a/tests/test_git_utils.py b/tests/test_git_utils.py new file mode 100644 index 0000000..3324417 --- /dev/null +++ b/tests/test_git_utils.py @@ -0,0 +1,172 @@ +""" +Tests for the git_utils module +""" +import os +import sys +import pytest +from unittest.mock import MagicMock, patch + +# Add src directory to path +src_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(src_dir) + +from src.git_utils import ( + clean_response, + get_github_client, + get_repository, + get_issue_comments, + get_issue_details, + write_issue_response, + is_pull_request +) +from github import Github +from github.Repository import Repository + + +def test_clean_response(): + """Test cleaning response text""" + # Test removing TERMINATE + response = "This is a response. TERMINATE" + cleaned = clean_response(response) + assert "TERMINATE" not in cleaned + assert cleaned == "This is a response." + + # Test handling signatures + response = "Response\n\n---\n*This response was automatically generated by blech_bot*" + cleaned = clean_response(response) + assert cleaned == response + + # Test handling duplicate signatures + response = "Response\n\n---\n*This response was automatically generated by blech_bot*\n\n---\n*This response was automatically generated by blech_bot*" + cleaned = clean_response(response) + assert cleaned.count("*This response was automatically generated by blech_bot*") == 1 + + +@patch('src.git_utils.Github') +@patch('src.git_utils.os') +def test_get_github_client(mock_os, mock_github): + """Test GitHub client initialization""" + # Setup mock + mock_os.getenv.return_value = "test-token" + mock_github.return_value = "github-client" + + # Call function + client = get_github_client() + + # Verify + mock_os.getenv.assert_called_with('GITHUB_TOKEN') + mock_github.assert_called_with("test-token") + assert client == "github-client" + + # Test error case + mock_os.getenv.return_value = None + with pytest.raises(ValueError): + get_github_client() + + +@patch('src.git_utils.get_github_client') +def test_get_repository(mock_get_client): + """Test repository retrieval""" + # Setup mock + mock_client = MagicMock() + mock_repo = MagicMock(spec=Repository) + mock_client.get_repo.return_value = mock_repo + mock_get_client.return_value = mock_client + + # Call function + repo = get_repository(mock_client, "test/repo") + + # Verify + mock_client.get_repo.assert_called_with("test/repo") + assert repo == mock_repo + + # Test error case + mock_client.get_repo.side_effect = Exception("Repo not found") + with pytest.raises(ValueError): + get_repository(mock_client, "nonexistent/repo") + + +def test_get_issue_comments(): + """Test filtering issue comments""" + # Create mock issue with comments + issue = MagicMock() + comment1 = MagicMock() + comment1.body = "Normal comment" + comment2 = MagicMock() + comment2.body = "Comment with app.grapite.dev reference" + + # Setup mock comments + issue.get_comments.return_value = [comment1, comment2] + + # Call function + comments = get_issue_comments(issue) + + # Verify filtering + assert len(comments) == 1 + assert comments[0].body == "Normal comment" + + +def test_get_issue_details(): + """Test extracting issue details""" + # Create mock issue + issue = MagicMock() + issue.number = 123 + issue.title = "Test Issue" + issue.body = "Issue body" + issue.state = "open" + issue.created_at = "2023-01-01" + issue.updated_at = "2023-01-02" + issue.comments = 5 + + label = MagicMock() + label.name = "bug" + issue.labels = [label] + + assignee = MagicMock() + assignee.login = "testuser" + issue.assignees = [assignee] + + # Call function + details = get_issue_details(issue) + + # Verify + assert details["number"] == 123 + assert details["title"] == "Test Issue" + assert details["body"] == "Issue body" + assert details["state"] == "open" + assert details["created_at"] == "2023-01-01" + assert details["updated_at"] == "2023-01-02" + assert details["comments_count"] == 5 + assert details["labels"] == ["bug"] + assert details["assignees"] == ["testuser"] + + +@patch('src.git_utils.create_issue_comment') +def test_write_issue_response(mock_create_comment): + """Test writing response to issue""" + # Setup + issue = MagicMock() + response = "Test response" + + # Call function + write_issue_response(issue, response) + + # Verify signature was added + mock_create_comment.assert_called_once() + called_with = mock_create_comment.call_args[0][1] + assert "Test response" in called_with + assert "*This response was automatically generated by blech_bot" in called_with + + +def test_is_pull_request(): + """Test detection of pull requests""" + # Create mock issue and PR + issue = MagicMock() + issue.html_url = "https://github.com/test/repo/issues/1" + + pr = MagicMock() + pr.html_url = "https://github.com/test/repo/pull/2" + + # Test + assert is_pull_request(issue) is False + assert is_pull_request(pr) is True diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..15cdcc1 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,113 @@ +""" +Integration tests for the GitHub bot +""" +import os +import sys +import pytest +from unittest.mock import MagicMock, patch +import tempfile +import shutil + +# Add src directory to path +src_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(src_dir) + +from src.response_agent import process_issue, process_repository +from src.git_utils import get_github_client, get_repository + + +@pytest.mark.integration +@patch('src.git_utils.get_github_client') +@patch('src.git_utils.get_repository') +@patch('src.response_agent.write_issue_response') +def test_process_issue_with_blech_bot_tag(mock_write_response, mock_get_repo, mock_get_client, mock_issue_with_label): + """Test processing an issue with blech_bot tag""" + # Setup mocks + issue = mock_issue_with_label("blech_bot") + + # Mock triggers + with patch('src.triggers.has_bot_response', return_value=False), \ + patch('src.triggers.has_pr_creation_comment', return_value=(False, None)), \ + patch('src.triggers.has_develop_issue_trigger', return_value=False), \ + patch('src.response_agent.generate_new_response', return_value=("Test response", [])): + + # Call function + success, error = process_issue(issue, "test/repo") + + # Verify + assert success is True + assert error is None + mock_write_response.assert_called_once() + + +@pytest.mark.integration +@patch('src.git_utils.get_github_client') +@patch('src.git_utils.get_repository') +def test_process_issue_without_blech_bot_tag(mock_get_repo, mock_get_client, mock_issue): + """Test processing an issue without blech_bot tag""" + # Call function + success, error = process_issue(mock_issue, "test/repo") + + # Verify + assert success is False + assert "does not have blech_bot label" in error + + +@pytest.mark.integration +@patch('src.git_utils.get_github_client') +@patch('src.git_utils.get_repository') +@patch('src.response_agent.bot_tools.get_local_repo_path') +@patch('src.branch_handler.checkout_branch') +@patch('src.response_agent.update_repository') +def test_process_repository(mock_update, mock_checkout, mock_get_path, mock_get_repo, mock_get_client, + mock_repository, mock_issue_with_label): + """Test processing a repository""" + # Setup mocks + client = MagicMock() + mock_get_client.return_value = client + + repo = mock_repository + mock_get_repo.return_value = repo + + # Create a temporary directory for the repo + temp_dir = tempfile.mkdtemp() + mock_get_path.return_value = temp_dir + + # Mock repository having one issue with blech_bot tag + issue = mock_issue_with_label("blech_bot") + repo.get_issues.return_value = [issue] + + # Mock process_issue to avoid actual processing + with patch('src.response_agent.process_issue', return_value=(True, None)): + try: + # Call function + process_repository("test/repo") + + # Verify + mock_get_client.assert_called_once() + mock_get_repo.assert_called_with(client, "test/repo") + mock_get_path.assert_called_with("test/repo") + mock_checkout.assert_called_once() + mock_update.assert_called_once() + finally: + # Clean up + shutil.rmtree(temp_dir) + + +@pytest.mark.integration +@patch('src.git_utils.get_github_client') +def test_real_github_client(mock_get_client): + """Test creating a real GitHub client with environment variables""" + # Setup environment variable + os.environ['GITHUB_TOKEN'] = 'test-token' + + # Mock the Github class + mock_github = MagicMock() + mock_get_client.return_value = mock_github + + # Call function + client = get_github_client() + + # Verify + assert client == mock_github + mock_get_client.assert_called_once() diff --git a/tests/test_response_agent.py b/tests/test_response_agent.py new file mode 100644 index 0000000..2a2a03d --- /dev/null +++ b/tests/test_response_agent.py @@ -0,0 +1,180 @@ +""" +Tests for the response_agent module +""" +import os +import sys +import pytest +from unittest.mock import MagicMock, patch + +# Add src directory to path +src_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(src_dir) + +from src.response_agent import ( + clean_response, + check_not_empty, + check_triggers, + response_selector, + extract_urls_from_issue, + scrape_text_from_url, + summarize_text, + get_tracked_repos +) + + +def test_clean_response(): + """Test cleaning response text""" + # Test removing TERMINATE + response = "This is a response. TERMINATE" + cleaned = clean_response(response) + assert "TERMINATE" not in cleaned + assert cleaned == "This is a response." + + # Test handling model signatures + response = "Response\n\n---\n*This response was automatically generated by blech_bot using model gpt-4*" + cleaned = clean_response(response) + assert cleaned == response + + # Test handling basic signatures + response = "Response\n\n---\n*This response was automatically generated by blech_bot*" + cleaned = clean_response(response) + assert cleaned == response + + +def test_check_not_empty(): + """Test checking if response is not empty""" + assert check_not_empty("This is a response") is True + assert check_not_empty("") is False + assert check_not_empty("terminate") is False + assert check_not_empty("TERMINATE") is False + assert check_not_empty("This is a response. TERMINATE") is True + + +@patch('src.response_agent.triggers') +def test_check_triggers(mock_triggers): + """Test checking for response triggers""" + issue = MagicMock() + + # Test generate_edit_command trigger + mock_triggers.has_generate_edit_command_trigger.return_value = True + mock_triggers.has_user_feedback.return_value = False + mock_triggers.has_bot_response.return_value = False + assert check_triggers(issue) == "generate_edit_command" + + # Test user feedback trigger + mock_triggers.has_generate_edit_command_trigger.return_value = False + mock_triggers.has_user_feedback.return_value = True + mock_triggers.has_bot_response.return_value = True + assert check_triggers(issue) == "feedback" + + # Test new response trigger + mock_triggers.has_generate_edit_command_trigger.return_value = False + mock_triggers.has_user_feedback.return_value = False + mock_triggers.has_bot_response.return_value = False + assert check_triggers(issue) == "new_response" + + # Test no trigger + mock_triggers.has_generate_edit_command_trigger.return_value = False + mock_triggers.has_user_feedback.return_value = False + mock_triggers.has_bot_response.return_value = True + assert check_triggers(issue) is None + + +def test_response_selector(): + """Test selecting response function based on trigger""" + # Test feedback trigger + func = response_selector("feedback") + assert func.__name__ == "generate_feedback_response" + + # Test generate_edit_command trigger + func = response_selector("generate_edit_command") + assert func.__name__ == "generate_edit_command_response" + + # Test new_response trigger + func = response_selector("new_response") + assert func.__name__ == "generate_new_response" + + # Test invalid trigger + assert response_selector("invalid") is None + + +@patch('src.response_agent.get_issue_comments') +def test_extract_urls_from_issue(mock_get_comments): + """Test extracting URLs from issue and comments""" + # Setup mock + issue = MagicMock() + issue.body = "Issue with URL: https://example.com" + + comment1 = MagicMock() + comment1.body = "Comment with URL: https://github.com" + comment2 = MagicMock() + comment2.body = "Another comment with same URL: https://github.com" + + mock_get_comments.return_value = [comment1, comment2] + + # Call function + urls = extract_urls_from_issue(issue) + + # Verify + assert len(urls) == 2 + assert "https://example.com" in urls + assert "https://github.com" in urls + + +@patch('src.response_agent.requests.get') +def test_scrape_text_from_url(mock_get): + """Test scraping text content from URL""" + # Setup mock for HTML response + mock_response = MagicMock() + mock_response.text = "

Test content

" + mock_response.headers = {"Content-Type": "text/html"} + mock_get.return_value = mock_response + + # Call function + result = scrape_text_from_url("https://example.com") + + # Verify + assert "Test content" in result + + # Test non-text content + mock_response.headers = {"Content-Type": "application/pdf"} + result = scrape_text_from_url("https://example.com/file.pdf") + assert "Non-text content detected" in result + + # Test request exception + mock_get.side_effect = Exception("Connection error") + result = scrape_text_from_url("https://example.com") + assert "Error fetching URL" in result + + +def test_summarize_text(): + """Test summarizing text to maximum length""" + # Test text under max length + short_text = "This is a short text." + assert summarize_text(short_text, 100) == short_text + + # Test text over max length + long_text = "This is a very long text that exceeds the maximum length." + summary = summarize_text(long_text, 20) + assert len(summary) > 20 # Account for ellipsis and message + assert "..." in summary + assert "[Text truncated" in summary + + +@patch('builtins.open') +def test_get_tracked_repos(mock_open): + """Test getting tracked repositories""" + # Setup mock + mock_file = MagicMock() + mock_file.__enter__.return_value.readlines.return_value = [ + "owner1/repo1\n", "owner2/repo2\n" + ] + mock_open.return_value = mock_file + + # Call function + repos = get_tracked_repos() + + # Verify + assert len(repos) == 2 + assert "owner1/repo1" in repos + assert "owner2/repo2" in repos diff --git a/tests/test_triggers.py b/tests/test_triggers.py new file mode 100644 index 0000000..1807e03 --- /dev/null +++ b/tests/test_triggers.py @@ -0,0 +1,164 @@ +""" +Tests for the triggers module +""" +import os +import sys +import pytest +from unittest.mock import MagicMock, patch + +# Add src directory to path +src_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(src_dir) + +from src.triggers import ( + has_blech_bot_tag, + has_generate_edit_command_trigger, + has_bot_response, + has_user_feedback, + has_develop_issue_trigger, + has_pull_request_trigger, + has_pr_creation_comment +) + + +def test_has_blech_bot_tag(): + """Test detection of blech_bot tag""" + # Create mock issue with blech_bot label + issue_with_tag = MagicMock() + label = MagicMock() + label.name = "blech_bot" + issue_with_tag.labels = [label] + + # Create mock issue without blech_bot label + issue_without_tag = MagicMock() + other_label = MagicMock() + other_label.name = "bug" + issue_without_tag.labels = [other_label] + + # Test + assert has_blech_bot_tag(issue_with_tag) is True + assert has_blech_bot_tag(issue_without_tag) is False + + +@patch('src.triggers.get_issue_comments') +def test_has_generate_edit_command_trigger(mock_get_comments): + """Test detection of generate_edit_command trigger""" + # Setup mock comments + comment1 = MagicMock() + comment1.body = "Normal comment" + comment2 = MagicMock() + comment2.body = "[ generate_edit_command ]" + + # Test with trigger + mock_get_comments.return_value = [comment1, comment2] + assert has_generate_edit_command_trigger(MagicMock()) is True + + # Test without trigger + mock_get_comments.return_value = [comment1] + assert has_generate_edit_command_trigger(MagicMock()) is False + + +@patch('src.triggers.get_issue_comments') +def test_has_bot_response(mock_get_comments): + """Test detection of bot response""" + # Setup mock comments + comment1 = MagicMock() + comment1.body = "Normal comment" + comment2 = MagicMock() + comment2.body = "This comment was generated by blech_bot" + + # Test with bot response + mock_get_comments.return_value = [comment1, comment2] + assert has_bot_response(MagicMock()) is True + + # Test without bot response + mock_get_comments.return_value = [comment1] + assert has_bot_response(MagicMock()) is False + + +@patch('src.triggers.get_issue_comments') +def test_has_user_feedback(mock_get_comments): + """Test detection of user feedback after bot response""" + # Setup mock comments + bot_comment = MagicMock() + bot_comment.body = "This comment was generated by blech_bot" + user_comment = MagicMock() + user_comment.body = "User feedback" + + # Test with user feedback after bot comment + mock_get_comments.return_value = [bot_comment, user_comment] + assert has_user_feedback(MagicMock()) is True + + # Test without user feedback after bot comment + mock_get_comments.return_value = [user_comment, bot_comment] + assert has_user_feedback(MagicMock()) is False + + # Test with no bot comment + mock_get_comments.return_value = [user_comment] + assert has_user_feedback(MagicMock()) is False + + +@patch('src.triggers.get_issue_comments') +def test_has_develop_issue_trigger(mock_get_comments): + """Test detection of develop_issue trigger""" + # Setup mock comments + comment1 = MagicMock() + comment1.body = "Normal comment" + comment2 = MagicMock() + comment2.body = "[ develop_issue ]" + + # Test with trigger in last comment + mock_get_comments.return_value = [comment1, comment2] + assert has_develop_issue_trigger(MagicMock()) is True + + # Test with trigger not in last comment + mock_get_comments.return_value = [comment2, comment1] + assert has_develop_issue_trigger(MagicMock()) is False + + # Test with no comments + mock_get_comments.return_value = [] + assert has_develop_issue_trigger(MagicMock()) is False + + +@patch('src.triggers.get_issue_comments') +def test_has_pull_request_trigger(mock_get_comments): + """Test detection of pull request trigger""" + # Setup mock comments + comment1 = MagicMock() + comment1.body = "Normal comment" + comment2 = MagicMock() + comment2.body = "Created pull request" + + # Test with trigger in last comment + mock_get_comments.return_value = [comment1, comment2] + assert has_pull_request_trigger(MagicMock()) is True + + # Test with trigger not in last comment + mock_get_comments.return_value = [comment2, comment1] + assert has_pull_request_trigger(MagicMock()) is False + + # Test with no comments + mock_get_comments.return_value = [] + assert has_pull_request_trigger(MagicMock()) is False + + +@patch('src.triggers.get_issue_comments') +def test_has_pr_creation_comment(mock_get_comments): + """Test detection of PR creation comment""" + # Setup mock comments + comment1 = MagicMock() + comment1.body = "Normal comment" + comment2 = MagicMock() + comment2.body = "Created pull request https://github.com/test/repo/pull/1" + + # Test with PR creation comment + mock_get_comments.return_value = [comment1, comment2] + has_comment, comment_body = has_pr_creation_comment(MagicMock()) + assert has_comment is True + assert comment_body == "Created pull request https://github.com/test/repo/pull/1" + + # Test without PR creation comment + mock_get_comments.return_value = [comment1] + has_comment, comment_body = has_pr_creation_comment(MagicMock()) + assert has_comment is False + assert comment_body is None From 9ac52d3489b6964edc41d3f81ba507bbed64b4b6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 2 Apr 2025 20:19:17 +0000 Subject: [PATCH 2/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/conftest.py | 16 +++++----- tests/test_agents.py | 50 +++++++++++++++-------------- tests/test_bot_tools.py | 51 ++++++++++++++--------------- tests/test_branch_handler.py | 60 +++++++++++++++++----------------- tests/test_git_utils.py | 62 ++++++++++++++++++------------------ tests/test_integration.py | 39 +++++++++++------------ tests/test_response_agent.py | 57 ++++++++++++++++----------------- tests/test_triggers.py | 51 +++++++++++++++-------------- 8 files changed, 194 insertions(+), 192 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index f8f7476..1692209 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,12 +20,12 @@ def mock_issue(): issue.body = "This is a test issue" issue.state = "open" issue.html_url = "https://github.com/test/repo/issues/123" - + # Add labels label = MagicMock() label.name = "bug" issue.labels = [label] - + return issue @@ -38,14 +38,14 @@ def _create_issue(label_name): issue.title = "Test Issue" issue.body = "This is a test issue" issue.state = "open" - + # Add label label = MagicMock() label.name = label_name issue.labels = [label] - + return issue - + return _create_issue(label_name) @@ -58,12 +58,12 @@ def mock_pull_request(): pr.body = "This is a test pull request" pr.state = "open" pr.html_url = "https://github.com/test/repo/pull/456" - + # Add head reference head = MagicMock() head.ref = "feature-branch" pr.head = head - + return pr @@ -75,7 +75,7 @@ def mock_repository(): repo.name = "repo" repo.clone_url = "https://github.com/test/repo.git" repo.default_branch = "main" - + return repo diff --git a/tests/test_agents.py b/tests/test_agents.py index 288ff97..328bfd3 100644 --- a/tests/test_agents.py +++ b/tests/test_agents.py @@ -1,6 +1,14 @@ """ Tests for the agents module """ +from autogen import UserProxyAgent, AssistantAgent +from src.agents import ( + create_user_agent, + create_agent, + generate_prompt, + is_terminate_msg, + register_functions +) import os import sys import pytest @@ -10,15 +18,6 @@ src_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(src_dir) -from src.agents import ( - create_user_agent, - create_agent, - generate_prompt, - is_terminate_msg, - register_functions -) -from autogen import UserProxyAgent, AssistantAgent - def test_create_user_agent(): """Test creation of user agent""" @@ -49,33 +48,36 @@ def test_is_terminate_msg(): def test_generate_prompt(mock_parse_comments): """Test prompt generation for different agent types""" # Setup mock - mock_parse_comments.return_value = ("Last comment", "Comments string", ["comment1"]) - + mock_parse_comments.return_value = ( + "Last comment", "Comments string", ["comment1"]) + # Mock issue and details issue = MagicMock() issue.number = 123 issue.title = "Test Issue" - + details = { "title": "Test Issue", "body": "This is a test issue" } - + # Test file_assistant prompt - prompt = generate_prompt("file_assistant", "test/repo", "/path/to/repo", details, issue) + prompt = generate_prompt( + "file_assistant", "test/repo", "/path/to/repo", details, issue) assert "Please analyze this GitHub issue" in prompt assert "Repository: test/repo" in prompt - + # Test edit_assistant prompt - prompt = generate_prompt("edit_assistant", "test/repo", "/path/to/repo", details, issue) + prompt = generate_prompt( + "edit_assistant", "test/repo", "/path/to/repo", details, issue) assert "Suggest what changes can be made to resolve this issue" in prompt - + # Test feedback_assistant prompt prompt = generate_prompt( - "feedback_assistant", - "test/repo", - "/path/to/repo", - details, + "feedback_assistant", + "test/repo", + "/path/to/repo", + details, issue, original_response="Original response", feedback_text="Feedback text" @@ -90,18 +92,18 @@ def test_register_functions(): agent = MagicMock() agent.register_for_llm = MagicMock(return_value=lambda x: x) agent.register_for_execution = MagicMock(return_value=lambda x: x) - + # Define test function def test_func(): """Test function docstring""" pass - + # Test LLM registration result = register_functions(agent, "llm", [test_func]) assert agent.register_for_llm.called assert agent.register_for_llm.call_args[1]["name"] == "test_func" assert "Test function docstring" in agent.register_for_llm.call_args[1]["description"] - + # Test execution registration agent.reset_mock() result = register_functions(agent, "execution", [test_func]) diff --git a/tests/test_bot_tools.py b/tests/test_bot_tools.py index 1cc0868..a26870e 100644 --- a/tests/test_bot_tools.py +++ b/tests/test_bot_tools.py @@ -1,15 +1,6 @@ """ Tests for the bot_tools module """ -import os -import sys -import pytest -from unittest.mock import MagicMock, patch - -# Add src directory to path -src_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.append(src_dir) - from src.bot_tools import ( get_local_repo_path, search_for_pattern, @@ -18,6 +9,14 @@ readfile, readlines ) +import os +import sys +import pytest +from unittest.mock import MagicMock, patch + +# Add src directory to path +src_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(src_dir) def test_get_local_repo_path(): @@ -27,7 +26,7 @@ def test_get_local_repo_path(): path = get_local_repo_path("owner/repo") assert "owner/repo" in path assert path.endswith("owner/repo") - + # Test with non-existing repo with patch('os.path.exists', return_value=False): result = get_local_repo_path("nonexistent/repo") @@ -39,12 +38,13 @@ def test_search_for_pattern(mock_popen): """Test searching for pattern in files""" # Setup mock mock_popen.return_value.read.return_value = "/path/to/file1.py\n/path/to/file2.py" - + # Call function result = search_for_pattern("/search/dir", "pattern") - + # Verify - mock_popen.assert_called_with("grep -irl pattern /search/dir --include='*.py'") + mock_popen.assert_called_with( + "grep -irl pattern /search/dir --include='*.py'") assert result == "/path/to/file1.py\n/path/to/file2.py" @@ -53,14 +53,14 @@ def test_search_for_file(mock_popen): """Test searching for file by name""" # Setup mock mock_popen.return_value.read.return_value = "/path/to/file.py" - + # Call function result = search_for_file("/search/dir", "file.py") - + # Verify mock_popen.assert_called_with("find /search/dir -iname '*file.py*'") assert result == "/path/to/file.py" - + # Test file not found mock_popen.return_value.read.return_value = "" result = search_for_file("/search/dir", "nonexistent.py") @@ -72,10 +72,10 @@ def test_estimate_tokens(): # Test with normal text text = "This is a test string with multiple words." assert estimate_tokens(text) == 8 - + # Test with empty text assert estimate_tokens("") == 0 - + # Test with None assert estimate_tokens(None) == 0 @@ -85,23 +85,24 @@ def test_readfile(mock_open): """Test reading file with line numbers""" # Setup mock mock_file = MagicMock() - mock_file.__enter__.return_value.readlines.return_value = ["Line 1\n", "Line 2\n", "Line 3\n"] + mock_file.__enter__.return_value.readlines.return_value = [ + "Line 1\n", "Line 2\n", "Line 3\n"] mock_open.return_value = mock_file - + # Call function result = readfile("/path/to/file.py") - + # Verify mock_open.assert_called_with("/path/to/file.py", 'r') assert "0000: Line 1" in result assert "0001: Line 2" in result assert "0002: Line 3" in result - + # Test file not found mock_open.side_effect = FileNotFoundError() result = readfile("/nonexistent/file.py") assert "File not found" in result - + # Test other error mock_open.side_effect = Exception("Test error") result = readfile("/error/file.py") @@ -117,10 +118,10 @@ def test_readlines(mock_open): "Line 1\n", "Line 2\n", "Line 3\n", "Line 4\n", "Line 5\n" ] mock_open.return_value = mock_file - + # Call function result = readlines("/path/to/file.py", 1, 4) - + # Verify mock_open.assert_called_with("/path/to/file.py", 'r') assert "0001: Line 2" in result diff --git a/tests/test_branch_handler.py b/tests/test_branch_handler.py index ce2d99c..7139db3 100644 --- a/tests/test_branch_handler.py +++ b/tests/test_branch_handler.py @@ -1,15 +1,6 @@ """ Tests for the branch_handler module """ -import os -import sys -import pytest -from unittest.mock import MagicMock, patch - -# Add src directory to path -src_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.append(src_dir) - from src.branch_handler import ( get_issue_related_branches, get_current_branch, @@ -18,6 +9,14 @@ back_to_master_branch, push_changes ) +import os +import sys +import pytest +from unittest.mock import MagicMock, patch + +# Add src directory to path +src_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(src_dir) @patch('src.branch_handler.os') @@ -28,26 +27,26 @@ def test_get_issue_related_branches(mock_repo, mock_os): mock_issue = MagicMock() mock_issue.number = 123 mock_issue.title = "Test Issue" - + mock_os.getcwd.return_value = "/original/dir" mock_os.popen.return_value.read.return_value = "branch-123\thttp://github.com/test/repo" - + # Call function branches = get_issue_related_branches("/path/to/repo", mock_issue) - + # Verify mock_os.chdir.assert_any_call("/path/to/repo") mock_os.chdir.assert_called_with("/original/dir") mock_os.popen.assert_called_with("gh issue develop -l 123") assert branches == [("branch-123", "http://github.com/test/repo")] - + # Test fallback when gh command returns no branches mock_os.popen.return_value.read.return_value = "" mock_repo_instance = MagicMock() mock_repo.return_value = mock_repo_instance mock_repo_instance.heads = [] mock_repo_instance.git.ls_remote.return_value = "" - + branches = get_issue_related_branches("/path/to/repo", mock_issue) assert branches == [] @@ -59,10 +58,10 @@ def test_get_current_branch(mock_repo): mock_repo_instance = MagicMock() mock_repo.return_value = mock_repo_instance mock_repo_instance.active_branch.name = "test-branch" - + # Call function branch = get_current_branch("/path/to/repo") - + # Verify mock_repo.assert_called_with("/path/to/repo") assert branch == "test-branch" @@ -75,16 +74,17 @@ def test_checkout_branch(mock_repo): mock_repo_instance = MagicMock() mock_repo.return_value = mock_repo_instance mock_repo_instance.heads = ["main"] - + # Test creating new branch checkout_branch("/path/to/repo", "test-branch", create=True) - + # Verify mock_repo.assert_called_with("/path/to/repo") mock_repo_instance.git.clean.assert_called_with('-f') mock_repo_instance.create_head.assert_called_with("test-branch") mock_repo_instance.git.checkout.assert_called_with("test-branch") - mock_repo_instance.git.reset.assert_called_with('--hard', 'origin/test-branch') + mock_repo_instance.git.reset.assert_called_with( + '--hard', 'origin/test-branch') @patch('src.branch_handler.git.Repo') @@ -94,13 +94,14 @@ def test_delete_branch(mock_repo): mock_repo_instance = MagicMock() mock_repo.return_value = mock_repo_instance mock_repo_instance.heads = ["test-branch"] - + # Call function delete_branch("/path/to/repo", "test-branch", force=True) - + # Verify mock_repo.assert_called_with("/path/to/repo") - mock_repo_instance.delete_head.assert_called_with("test-branch", force=True) + mock_repo_instance.delete_head.assert_called_with( + "test-branch", force=True) @patch('src.branch_handler.git.Repo') @@ -109,17 +110,17 @@ def test_back_to_master_branch(mock_repo): # Setup mock mock_repo_instance = MagicMock() mock_repo.return_value = mock_repo_instance - + # Test with master branch mock_repo_instance.heads = ["master", "other"] back_to_master_branch("/path/to/repo") mock_repo_instance.git.checkout.assert_called_with("master") - + # Test with main branch mock_repo_instance.heads = ["main", "other"] back_to_master_branch("/path/to/repo") mock_repo_instance.git.checkout.assert_called_with("main") - + # Test with neither branch mock_repo_instance.heads = ["develop", "other"] with pytest.raises(ValueError): @@ -133,15 +134,16 @@ def test_push_changes(mock_repo): mock_repo_instance = MagicMock() mock_repo.return_value = mock_repo_instance mock_repo_instance.active_branch.name = "test-branch" - + # Test normal push push_changes("/path/to/repo") mock_repo_instance.git.push.assert_called_with('origin', 'test-branch') - + # Test force push push_changes("/path/to/repo", force=True) - mock_repo_instance.git.push.assert_called_with('origin', 'test-branch', '--force') - + mock_repo_instance.git.push.assert_called_with( + 'origin', 'test-branch', '--force') + # Test with specific branch push_changes("/path/to/repo", branch_name="feature-branch") mock_repo_instance.git.push.assert_called_with('origin', 'feature-branch') diff --git a/tests/test_git_utils.py b/tests/test_git_utils.py index 3324417..0ffefd3 100644 --- a/tests/test_git_utils.py +++ b/tests/test_git_utils.py @@ -1,15 +1,8 @@ """ Tests for the git_utils module """ -import os -import sys -import pytest -from unittest.mock import MagicMock, patch - -# Add src directory to path -src_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.append(src_dir) - +from github.Repository import Repository +from github import Github from src.git_utils import ( clean_response, get_github_client, @@ -19,8 +12,14 @@ write_issue_response, is_pull_request ) -from github import Github -from github.Repository import Repository +import os +import sys +import pytest +from unittest.mock import MagicMock, patch + +# Add src directory to path +src_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(src_dir) def test_clean_response(): @@ -30,16 +29,17 @@ def test_clean_response(): cleaned = clean_response(response) assert "TERMINATE" not in cleaned assert cleaned == "This is a response." - + # Test handling signatures response = "Response\n\n---\n*This response was automatically generated by blech_bot*" cleaned = clean_response(response) assert cleaned == response - + # Test handling duplicate signatures response = "Response\n\n---\n*This response was automatically generated by blech_bot*\n\n---\n*This response was automatically generated by blech_bot*" cleaned = clean_response(response) - assert cleaned.count("*This response was automatically generated by blech_bot*") == 1 + assert cleaned.count( + "*This response was automatically generated by blech_bot*") == 1 @patch('src.git_utils.Github') @@ -49,15 +49,15 @@ def test_get_github_client(mock_os, mock_github): # Setup mock mock_os.getenv.return_value = "test-token" mock_github.return_value = "github-client" - + # Call function client = get_github_client() - + # Verify mock_os.getenv.assert_called_with('GITHUB_TOKEN') mock_github.assert_called_with("test-token") assert client == "github-client" - + # Test error case mock_os.getenv.return_value = None with pytest.raises(ValueError): @@ -72,14 +72,14 @@ def test_get_repository(mock_get_client): mock_repo = MagicMock(spec=Repository) mock_client.get_repo.return_value = mock_repo mock_get_client.return_value = mock_client - + # Call function repo = get_repository(mock_client, "test/repo") - + # Verify mock_client.get_repo.assert_called_with("test/repo") assert repo == mock_repo - + # Test error case mock_client.get_repo.side_effect = Exception("Repo not found") with pytest.raises(ValueError): @@ -94,13 +94,13 @@ def test_get_issue_comments(): comment1.body = "Normal comment" comment2 = MagicMock() comment2.body = "Comment with app.grapite.dev reference" - + # Setup mock comments issue.get_comments.return_value = [comment1, comment2] - + # Call function comments = get_issue_comments(issue) - + # Verify filtering assert len(comments) == 1 assert comments[0].body == "Normal comment" @@ -117,18 +117,18 @@ def test_get_issue_details(): issue.created_at = "2023-01-01" issue.updated_at = "2023-01-02" issue.comments = 5 - + label = MagicMock() label.name = "bug" issue.labels = [label] - + assignee = MagicMock() assignee.login = "testuser" issue.assignees = [assignee] - + # Call function details = get_issue_details(issue) - + # Verify assert details["number"] == 123 assert details["title"] == "Test Issue" @@ -147,10 +147,10 @@ def test_write_issue_response(mock_create_comment): # Setup issue = MagicMock() response = "Test response" - + # Call function write_issue_response(issue, response) - + # Verify signature was added mock_create_comment.assert_called_once() called_with = mock_create_comment.call_args[0][1] @@ -163,10 +163,10 @@ def test_is_pull_request(): # Create mock issue and PR issue = MagicMock() issue.html_url = "https://github.com/test/repo/issues/1" - + pr = MagicMock() pr.html_url = "https://github.com/test/repo/pull/2" - + # Test assert is_pull_request(issue) is False assert is_pull_request(pr) is True diff --git a/tests/test_integration.py b/tests/test_integration.py index 15cdcc1..e0df617 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,6 +1,8 @@ """ Integration tests for the GitHub bot """ +from src.git_utils import get_github_client, get_repository +from src.response_agent import process_issue, process_repository import os import sys import pytest @@ -12,9 +14,6 @@ src_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(src_dir) -from src.response_agent import process_issue, process_repository -from src.git_utils import get_github_client, get_repository - @pytest.mark.integration @patch('src.git_utils.get_github_client') @@ -24,16 +23,16 @@ def test_process_issue_with_blech_bot_tag(mock_write_response, mock_get_repo, mo """Test processing an issue with blech_bot tag""" # Setup mocks issue = mock_issue_with_label("blech_bot") - + # Mock triggers with patch('src.triggers.has_bot_response', return_value=False), \ - patch('src.triggers.has_pr_creation_comment', return_value=(False, None)), \ - patch('src.triggers.has_develop_issue_trigger', return_value=False), \ - patch('src.response_agent.generate_new_response', return_value=("Test response", [])): - + patch('src.triggers.has_pr_creation_comment', return_value=(False, None)), \ + patch('src.triggers.has_develop_issue_trigger', return_value=False), \ + patch('src.response_agent.generate_new_response', return_value=("Test response", [])): + # Call function success, error = process_issue(issue, "test/repo") - + # Verify assert success is True assert error is None @@ -47,7 +46,7 @@ def test_process_issue_without_blech_bot_tag(mock_get_repo, mock_get_client, moc """Test processing an issue without blech_bot tag""" # Call function success, error = process_issue(mock_issue, "test/repo") - + # Verify assert success is False assert "does not have blech_bot label" in error @@ -59,30 +58,30 @@ def test_process_issue_without_blech_bot_tag(mock_get_repo, mock_get_client, moc @patch('src.response_agent.bot_tools.get_local_repo_path') @patch('src.branch_handler.checkout_branch') @patch('src.response_agent.update_repository') -def test_process_repository(mock_update, mock_checkout, mock_get_path, mock_get_repo, mock_get_client, - mock_repository, mock_issue_with_label): +def test_process_repository(mock_update, mock_checkout, mock_get_path, mock_get_repo, mock_get_client, + mock_repository, mock_issue_with_label): """Test processing a repository""" # Setup mocks client = MagicMock() mock_get_client.return_value = client - + repo = mock_repository mock_get_repo.return_value = repo - + # Create a temporary directory for the repo temp_dir = tempfile.mkdtemp() mock_get_path.return_value = temp_dir - + # Mock repository having one issue with blech_bot tag issue = mock_issue_with_label("blech_bot") repo.get_issues.return_value = [issue] - + # Mock process_issue to avoid actual processing with patch('src.response_agent.process_issue', return_value=(True, None)): try: # Call function process_repository("test/repo") - + # Verify mock_get_client.assert_called_once() mock_get_repo.assert_called_with(client, "test/repo") @@ -100,14 +99,14 @@ def test_real_github_client(mock_get_client): """Test creating a real GitHub client with environment variables""" # Setup environment variable os.environ['GITHUB_TOKEN'] = 'test-token' - + # Mock the Github class mock_github = MagicMock() mock_get_client.return_value = mock_github - + # Call function client = get_github_client() - + # Verify assert client == mock_github mock_get_client.assert_called_once() diff --git a/tests/test_response_agent.py b/tests/test_response_agent.py index 2a2a03d..0c06f0a 100644 --- a/tests/test_response_agent.py +++ b/tests/test_response_agent.py @@ -1,15 +1,6 @@ """ Tests for the response_agent module """ -import os -import sys -import pytest -from unittest.mock import MagicMock, patch - -# Add src directory to path -src_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.append(src_dir) - from src.response_agent import ( clean_response, check_not_empty, @@ -20,6 +11,14 @@ summarize_text, get_tracked_repos ) +import os +import sys +import pytest +from unittest.mock import MagicMock, patch + +# Add src directory to path +src_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(src_dir) def test_clean_response(): @@ -29,12 +28,12 @@ def test_clean_response(): cleaned = clean_response(response) assert "TERMINATE" not in cleaned assert cleaned == "This is a response." - + # Test handling model signatures response = "Response\n\n---\n*This response was automatically generated by blech_bot using model gpt-4*" cleaned = clean_response(response) assert cleaned == response - + # Test handling basic signatures response = "Response\n\n---\n*This response was automatically generated by blech_bot*" cleaned = clean_response(response) @@ -54,25 +53,25 @@ def test_check_not_empty(): def test_check_triggers(mock_triggers): """Test checking for response triggers""" issue = MagicMock() - + # Test generate_edit_command trigger mock_triggers.has_generate_edit_command_trigger.return_value = True mock_triggers.has_user_feedback.return_value = False mock_triggers.has_bot_response.return_value = False assert check_triggers(issue) == "generate_edit_command" - + # Test user feedback trigger mock_triggers.has_generate_edit_command_trigger.return_value = False mock_triggers.has_user_feedback.return_value = True mock_triggers.has_bot_response.return_value = True assert check_triggers(issue) == "feedback" - + # Test new response trigger mock_triggers.has_generate_edit_command_trigger.return_value = False mock_triggers.has_user_feedback.return_value = False mock_triggers.has_bot_response.return_value = False assert check_triggers(issue) == "new_response" - + # Test no trigger mock_triggers.has_generate_edit_command_trigger.return_value = False mock_triggers.has_user_feedback.return_value = False @@ -85,15 +84,15 @@ def test_response_selector(): # Test feedback trigger func = response_selector("feedback") assert func.__name__ == "generate_feedback_response" - + # Test generate_edit_command trigger func = response_selector("generate_edit_command") assert func.__name__ == "generate_edit_command_response" - + # Test new_response trigger func = response_selector("new_response") assert func.__name__ == "generate_new_response" - + # Test invalid trigger assert response_selector("invalid") is None @@ -104,17 +103,17 @@ def test_extract_urls_from_issue(mock_get_comments): # Setup mock issue = MagicMock() issue.body = "Issue with URL: https://example.com" - + comment1 = MagicMock() comment1.body = "Comment with URL: https://github.com" comment2 = MagicMock() comment2.body = "Another comment with same URL: https://github.com" - + mock_get_comments.return_value = [comment1, comment2] - + # Call function urls = extract_urls_from_issue(issue) - + # Verify assert len(urls) == 2 assert "https://example.com" in urls @@ -129,18 +128,18 @@ def test_scrape_text_from_url(mock_get): mock_response.text = "

Test content

" mock_response.headers = {"Content-Type": "text/html"} mock_get.return_value = mock_response - + # Call function result = scrape_text_from_url("https://example.com") - + # Verify assert "Test content" in result - + # Test non-text content mock_response.headers = {"Content-Type": "application/pdf"} result = scrape_text_from_url("https://example.com/file.pdf") assert "Non-text content detected" in result - + # Test request exception mock_get.side_effect = Exception("Connection error") result = scrape_text_from_url("https://example.com") @@ -152,7 +151,7 @@ def test_summarize_text(): # Test text under max length short_text = "This is a short text." assert summarize_text(short_text, 100) == short_text - + # Test text over max length long_text = "This is a very long text that exceeds the maximum length." summary = summarize_text(long_text, 20) @@ -170,10 +169,10 @@ def test_get_tracked_repos(mock_open): "owner1/repo1\n", "owner2/repo2\n" ] mock_open.return_value = mock_file - + # Call function repos = get_tracked_repos() - + # Verify assert len(repos) == 2 assert "owner1/repo1" in repos diff --git a/tests/test_triggers.py b/tests/test_triggers.py index 1807e03..1e261cd 100644 --- a/tests/test_triggers.py +++ b/tests/test_triggers.py @@ -1,15 +1,6 @@ """ Tests for the triggers module """ -import os -import sys -import pytest -from unittest.mock import MagicMock, patch - -# Add src directory to path -src_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.append(src_dir) - from src.triggers import ( has_blech_bot_tag, has_generate_edit_command_trigger, @@ -19,6 +10,14 @@ has_pull_request_trigger, has_pr_creation_comment ) +import os +import sys +import pytest +from unittest.mock import MagicMock, patch + +# Add src directory to path +src_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(src_dir) def test_has_blech_bot_tag(): @@ -28,13 +27,13 @@ def test_has_blech_bot_tag(): label = MagicMock() label.name = "blech_bot" issue_with_tag.labels = [label] - + # Create mock issue without blech_bot label issue_without_tag = MagicMock() other_label = MagicMock() other_label.name = "bug" issue_without_tag.labels = [other_label] - + # Test assert has_blech_bot_tag(issue_with_tag) is True assert has_blech_bot_tag(issue_without_tag) is False @@ -48,11 +47,11 @@ def test_has_generate_edit_command_trigger(mock_get_comments): comment1.body = "Normal comment" comment2 = MagicMock() comment2.body = "[ generate_edit_command ]" - + # Test with trigger mock_get_comments.return_value = [comment1, comment2] assert has_generate_edit_command_trigger(MagicMock()) is True - + # Test without trigger mock_get_comments.return_value = [comment1] assert has_generate_edit_command_trigger(MagicMock()) is False @@ -66,11 +65,11 @@ def test_has_bot_response(mock_get_comments): comment1.body = "Normal comment" comment2 = MagicMock() comment2.body = "This comment was generated by blech_bot" - + # Test with bot response mock_get_comments.return_value = [comment1, comment2] assert has_bot_response(MagicMock()) is True - + # Test without bot response mock_get_comments.return_value = [comment1] assert has_bot_response(MagicMock()) is False @@ -84,15 +83,15 @@ def test_has_user_feedback(mock_get_comments): bot_comment.body = "This comment was generated by blech_bot" user_comment = MagicMock() user_comment.body = "User feedback" - + # Test with user feedback after bot comment mock_get_comments.return_value = [bot_comment, user_comment] assert has_user_feedback(MagicMock()) is True - + # Test without user feedback after bot comment mock_get_comments.return_value = [user_comment, bot_comment] assert has_user_feedback(MagicMock()) is False - + # Test with no bot comment mock_get_comments.return_value = [user_comment] assert has_user_feedback(MagicMock()) is False @@ -106,15 +105,15 @@ def test_has_develop_issue_trigger(mock_get_comments): comment1.body = "Normal comment" comment2 = MagicMock() comment2.body = "[ develop_issue ]" - + # Test with trigger in last comment mock_get_comments.return_value = [comment1, comment2] assert has_develop_issue_trigger(MagicMock()) is True - + # Test with trigger not in last comment mock_get_comments.return_value = [comment2, comment1] assert has_develop_issue_trigger(MagicMock()) is False - + # Test with no comments mock_get_comments.return_value = [] assert has_develop_issue_trigger(MagicMock()) is False @@ -128,15 +127,15 @@ def test_has_pull_request_trigger(mock_get_comments): comment1.body = "Normal comment" comment2 = MagicMock() comment2.body = "Created pull request" - + # Test with trigger in last comment mock_get_comments.return_value = [comment1, comment2] assert has_pull_request_trigger(MagicMock()) is True - + # Test with trigger not in last comment mock_get_comments.return_value = [comment2, comment1] assert has_pull_request_trigger(MagicMock()) is False - + # Test with no comments mock_get_comments.return_value = [] assert has_pull_request_trigger(MagicMock()) is False @@ -150,13 +149,13 @@ def test_has_pr_creation_comment(mock_get_comments): comment1.body = "Normal comment" comment2 = MagicMock() comment2.body = "Created pull request https://github.com/test/repo/pull/1" - + # Test with PR creation comment mock_get_comments.return_value = [comment1, comment2] has_comment, comment_body = has_pr_creation_comment(MagicMock()) assert has_comment is True assert comment_body == "Created pull request https://github.com/test/repo/pull/1" - + # Test without PR creation comment mock_get_comments.return_value = [comment1] has_comment, comment_body = has_pr_creation_comment(MagicMock()) From b3f295e5ba8e9e2aa8a7a5e83a32fb7b358a29e3 Mon Sep 17 00:00:00 2001 From: "Abuzar Mahmood (aider)" Date: Wed, 2 Apr 2025 16:24:00 -0400 Subject: [PATCH 3/5] ci: Add GitHub Actions workflow for automated testing --- .github/workflows/test.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..967d850 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,32 @@ +name: Run Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -r requirements-dev.txt + + - name: Run tests + run: | + pytest tests/ -v + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} From 7b3d4600fcbb742d3d635f37843c186b089bcd84 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 2 Apr 2025 20:24:59 +0000 Subject: [PATCH 4/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 967d850..e99634a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,21 +9,21 @@ on: jobs: test: runs-on: ubuntu-latest - + steps: - uses: actions/checkout@v3 - + - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.10' - + - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install -r requirements-dev.txt - + - name: Run tests run: | pytest tests/ -v From 523d3d0d63774e4c285b0cc6890ad4b7adcdbd55 Mon Sep 17 00:00:00 2001 From: Abuzar Mahmood Date: Wed, 2 Apr 2025 16:26:11 -0400 Subject: [PATCH 5/5] Update test.yml --- .github/workflows/test.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e99634a..8751554 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,10 +1,7 @@ name: Run Tests on: - push: - branches: [ main ] pull_request: - branches: [ main ] jobs: test: