diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..8751554 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: Run Tests + +on: + pull_request: + +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 }} 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..1692209 --- /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..328bfd3 --- /dev/null +++ b/tests/test_agents.py @@ -0,0 +1,111 @@ +""" +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 +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_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..a26870e --- /dev/null +++ b/tests/test_bot_tools.py @@ -0,0 +1,131 @@ +""" +Tests for the bot_tools module +""" +from src.bot_tools import ( + get_local_repo_path, + search_for_pattern, + search_for_file, + estimate_tokens, + 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(): + """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..7139db3 --- /dev/null +++ b/tests/test_branch_handler.py @@ -0,0 +1,149 @@ +""" +Tests for the branch_handler module +""" +from src.branch_handler import ( + get_issue_related_branches, + get_current_branch, + checkout_branch, + delete_branch, + 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') +@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..0ffefd3 --- /dev/null +++ b/tests/test_git_utils.py @@ -0,0 +1,172 @@ +""" +Tests for the git_utils module +""" +from github.Repository import Repository +from github import Github +from src.git_utils import ( + clean_response, + get_github_client, + get_repository, + get_issue_comments, + get_issue_details, + write_issue_response, + is_pull_request +) +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(): + """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..e0df617 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,112 @@ +""" +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 +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) + + +@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..0c06f0a --- /dev/null +++ b/tests/test_response_agent.py @@ -0,0 +1,179 @@ +""" +Tests for the response_agent module +""" +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 +) +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(): + """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..1e261cd --- /dev/null +++ b/tests/test_triggers.py @@ -0,0 +1,163 @@ +""" +Tests for the triggers module +""" +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 +) +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(): + """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