Add comprehensive test suites for all new tools (20+ tests)
This commit is contained in:
82
tests/test_code_analysis.py
Normal file
82
tests/test_code_analysis.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from core.tools.base import ToolContext
|
||||||
|
from tools.code_analysis import CodeAnalysisTool
|
||||||
|
|
||||||
|
|
||||||
|
class TestCodeAnalysisTool:
|
||||||
|
"""Test code analysis tool."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def tool(self) -> CodeAnalysisTool:
|
||||||
|
return CodeAnalysisTool()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_py_file(self) -> Path:
|
||||||
|
"""Create a sample Python file for testing."""
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
||||||
|
f.write(
|
||||||
|
'''"""Sample module."""
|
||||||
|
|
||||||
|
def hello(name: str) -> str:
|
||||||
|
"""Say hello."""
|
||||||
|
return f"Hello {name}"
|
||||||
|
|
||||||
|
class Calculator:
|
||||||
|
"""Simple calculator."""
|
||||||
|
|
||||||
|
def add(self, a: int, b: int) -> int:
|
||||||
|
"""Add two numbers."""
|
||||||
|
return a + b
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
return Path(f.name)
|
||||||
|
|
||||||
|
def test_analyze_file_valid(self, tool: CodeAnalysisTool, sample_py_file: Path) -> None:
|
||||||
|
"""Test analyzing a valid Python file."""
|
||||||
|
result = tool.analyze_file({"path": str(sample_py_file)})
|
||||||
|
assert result["valid_syntax"] is True
|
||||||
|
assert result["functions"] >= 1
|
||||||
|
assert result["classes"] >= 1
|
||||||
|
assert result["imports"] >= 2
|
||||||
|
|
||||||
|
def test_analyze_file_invalid_type(self, tool: CodeAnalysisTool) -> None:
|
||||||
|
"""Test with invalid path type."""
|
||||||
|
with pytest.raises(ValueError, match="path must be string"):
|
||||||
|
tool.analyze_file({"path": 123})
|
||||||
|
|
||||||
|
def test_extract_functions(self, tool: CodeAnalysisTool, sample_py_file: Path) -> None:
|
||||||
|
"""Test extracting functions from file."""
|
||||||
|
result = tool.extract_functions({"path": str(sample_py_file)})
|
||||||
|
assert result["count"] >= 2
|
||||||
|
assert any(f["name"] == "hello" for f in result["functions"])
|
||||||
|
assert any(f["name"] == "add" for f in result["functions"])
|
||||||
|
|
||||||
|
def test_check_complexity(self, tool: CodeAnalysisTool, sample_py_file: Path) -> None:
|
||||||
|
"""Test complexity calculation."""
|
||||||
|
result = tool.check_complexity({"path": str(sample_py_file)})
|
||||||
|
assert "functions" in result
|
||||||
|
assert "avg_complexity" in result
|
||||||
|
assert result["avg_complexity"] > 0
|
||||||
|
|
||||||
|
def test_execute_analyze(self, tool: CodeAnalysisTool, sample_py_file: Path) -> None:
|
||||||
|
"""Test execute with analyze_file action."""
|
||||||
|
ctx = ToolContext(dry_run=False)
|
||||||
|
result = tool.execute(
|
||||||
|
{"action": "analyze_file", "path": str(sample_py_file)},
|
||||||
|
ctx
|
||||||
|
)
|
||||||
|
assert result["valid_syntax"] is True
|
||||||
|
|
||||||
|
def test_execute_invalid_action(self, tool: CodeAnalysisTool) -> None:
|
||||||
|
"""Test with invalid action."""
|
||||||
|
ctx = ToolContext(dry_run=False)
|
||||||
|
with pytest.raises(ValueError, match="Unknown analysis action"):
|
||||||
|
tool.execute({"action": "invalid_action"}, ctx)
|
||||||
76
tests/test_environment.py
Normal file
76
tests/test_environment.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
|
||||||
|
from core.tools.base import ToolContext
|
||||||
|
from tools.environment import EnvironmentTool
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnvironmentTool:
|
||||||
|
"""Test environment tool."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def tool(self) -> EnvironmentTool:
|
||||||
|
return EnvironmentTool()
|
||||||
|
|
||||||
|
def test_get_info(self, tool: EnvironmentTool) -> None:
|
||||||
|
"""Test getting system info."""
|
||||||
|
result = tool.get_info()
|
||||||
|
assert "system" in result
|
||||||
|
assert "machine" in result
|
||||||
|
assert "hostname" in result
|
||||||
|
assert result["system"] in ["Windows", "Linux", "Darwin"]
|
||||||
|
|
||||||
|
def test_python_info(self, tool: EnvironmentTool) -> None:
|
||||||
|
"""Test getting Python info."""
|
||||||
|
result = tool.get_python_info()
|
||||||
|
assert "version" in result
|
||||||
|
assert "implementation" in result
|
||||||
|
assert "executable" in result
|
||||||
|
assert result["version"] == platform.python_version()
|
||||||
|
|
||||||
|
def test_env_var(self, tool: EnvironmentTool) -> None:
|
||||||
|
"""Test getting environment variable."""
|
||||||
|
os.environ["TEST_VAR"] = "test_value"
|
||||||
|
result = tool.get_env_var({"name": "TEST_VAR"})
|
||||||
|
assert result["exists"] is True
|
||||||
|
assert result["value"] == "test_value"
|
||||||
|
|
||||||
|
def test_env_var_nonexistent(self, tool: EnvironmentTool) -> None:
|
||||||
|
"""Test getting nonexistent environment variable."""
|
||||||
|
result = tool.get_env_var({"name": "NONEXISTENT_VAR_12345"})
|
||||||
|
assert result["exists"] is False
|
||||||
|
assert result["value"] is None
|
||||||
|
|
||||||
|
def test_env_var_invalid_type(self, tool: EnvironmentTool) -> None:
|
||||||
|
"""Test with invalid name type."""
|
||||||
|
with pytest.raises(ValueError, match="name must be string"):
|
||||||
|
tool.get_env_var({"name": 123})
|
||||||
|
|
||||||
|
def test_all_env_vars(self, tool: EnvironmentTool) -> None:
|
||||||
|
"""Test getting all environment variables."""
|
||||||
|
result = tool.get_all_env_vars()
|
||||||
|
assert "count" in result
|
||||||
|
assert "vars" in result
|
||||||
|
assert result["count"] > 0
|
||||||
|
|
||||||
|
def test_execute_info(self, tool: EnvironmentTool) -> None:
|
||||||
|
"""Test execute with info action."""
|
||||||
|
ctx = ToolContext(dry_run=False)
|
||||||
|
result = tool.execute({"action": "info"}, ctx)
|
||||||
|
assert "system" in result
|
||||||
|
|
||||||
|
def test_execute_python_info(self, tool: EnvironmentTool) -> None:
|
||||||
|
"""Test execute with python_info action."""
|
||||||
|
ctx = ToolContext(dry_run=False)
|
||||||
|
result = tool.execute({"action": "python_info"}, ctx)
|
||||||
|
assert "version" in result
|
||||||
|
|
||||||
|
def test_execute_invalid_action(self, tool: EnvironmentTool) -> None:
|
||||||
|
"""Test with invalid action."""
|
||||||
|
ctx = ToolContext(dry_run=False)
|
||||||
|
with pytest.raises(ValueError, match="Unknown environment action"):
|
||||||
|
tool.execute({"action": "invalid_action"}, ctx)
|
||||||
111
tests/test_git_ops.py
Normal file
111
tests/test_git_ops.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from core.tools.base import ToolContext
|
||||||
|
from tools.git_ops import GitOpsTool
|
||||||
|
|
||||||
|
|
||||||
|
class TestGitOpsTool:
|
||||||
|
"""Test git operations tool."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def tool(self) -> GitOpsTool:
|
||||||
|
return GitOpsTool()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def git_repo(self) -> Path:
|
||||||
|
"""Create a temporary git repository."""
|
||||||
|
tmpdir = Path(tempfile.mkdtemp())
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
["git", "init"],
|
||||||
|
cwd=str(tmpdir),
|
||||||
|
capture_output=True
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
["git", "config", "user.email", "test@test.com"],
|
||||||
|
cwd=str(tmpdir),
|
||||||
|
capture_output=True
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
["git", "config", "user.name", "Test User"],
|
||||||
|
cwd=str(tmpdir),
|
||||||
|
capture_output=True
|
||||||
|
)
|
||||||
|
|
||||||
|
(tmpdir / "test.txt").write_text("test content")
|
||||||
|
subprocess.run(
|
||||||
|
["git", "add", "test.txt"],
|
||||||
|
cwd=str(tmpdir),
|
||||||
|
capture_output=True
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
["git", "commit", "-m", "Initial commit"],
|
||||||
|
cwd=str(tmpdir),
|
||||||
|
capture_output=True
|
||||||
|
)
|
||||||
|
|
||||||
|
yield tmpdir
|
||||||
|
|
||||||
|
def test_git_status(self, tool: GitOpsTool, git_repo: Path) -> None:
|
||||||
|
"""Test git status."""
|
||||||
|
result = tool.git_status({"repo": str(git_repo)})
|
||||||
|
assert "repo" in result
|
||||||
|
assert "status_lines" in result
|
||||||
|
assert "has_changes" in result
|
||||||
|
|
||||||
|
def test_git_status_not_repo(self, tool: GitOpsTool) -> None:
|
||||||
|
"""Test git status on non-git directory."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
with pytest.raises(ValueError, match="Not a git repository"):
|
||||||
|
tool.git_status({"repo": tmpdir})
|
||||||
|
|
||||||
|
def test_git_log(self, tool: GitOpsTool, git_repo: Path) -> None:
|
||||||
|
"""Test git log."""
|
||||||
|
result = tool.git_log({"repo": str(git_repo), "limit": 5})
|
||||||
|
assert "commits" in result
|
||||||
|
assert "count" in result
|
||||||
|
assert result["count"] >= 1
|
||||||
|
|
||||||
|
def test_git_current_branch(self, tool: GitOpsTool, git_repo: Path) -> None:
|
||||||
|
"""Test getting current branch."""
|
||||||
|
result = tool.git_current_branch({"repo": str(git_repo)})
|
||||||
|
assert "branch" in result
|
||||||
|
assert result["branch"] in ["master", "main"]
|
||||||
|
|
||||||
|
def test_git_list_branches(self, tool: GitOpsTool, git_repo: Path) -> None:
|
||||||
|
"""Test listing branches."""
|
||||||
|
result = tool.git_list_branches({"repo": str(git_repo)})
|
||||||
|
assert "branches" in result
|
||||||
|
assert "count" in result
|
||||||
|
assert result["count"] >= 1
|
||||||
|
|
||||||
|
def test_git_diff(self, tool: GitOpsTool, git_repo: Path) -> None:
|
||||||
|
"""Test git diff."""
|
||||||
|
(git_repo / "test.txt").write_text("modified content")
|
||||||
|
|
||||||
|
result = tool.git_diff({"repo": str(git_repo)})
|
||||||
|
assert "repo" in result
|
||||||
|
assert "diff" in result
|
||||||
|
|
||||||
|
def test_execute_status(self, tool: GitOpsTool, git_repo: Path) -> None:
|
||||||
|
"""Test execute with status action."""
|
||||||
|
ctx = ToolContext(dry_run=False)
|
||||||
|
result = tool.execute(
|
||||||
|
{"action": "status", "repo": str(git_repo)},
|
||||||
|
ctx
|
||||||
|
)
|
||||||
|
assert "status_lines" in result
|
||||||
|
|
||||||
|
def test_execute_invalid_action(self, tool: GitOpsTool, git_repo: Path) -> None:
|
||||||
|
"""Test with invalid action."""
|
||||||
|
ctx = ToolContext(dry_run=False)
|
||||||
|
with pytest.raises(ValueError, match="Unknown git action"):
|
||||||
|
tool.execute(
|
||||||
|
{"action": "invalid_action", "repo": str(git_repo)},
|
||||||
|
ctx
|
||||||
|
)
|
||||||
103
tests/test_python_exec.py
Normal file
103
tests/test_python_exec.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from core.tools.base import ToolContext
|
||||||
|
from tools.python_exec import PythonExecTool
|
||||||
|
|
||||||
|
|
||||||
|
class TestPythonExecTool:
|
||||||
|
"""Test Python execution tool."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def tool(self) -> PythonExecTool:
|
||||||
|
return PythonExecTool()
|
||||||
|
|
||||||
|
def test_execute_snippet_simple(self, tool: PythonExecTool) -> None:
|
||||||
|
"""Test executing a simple code snippet."""
|
||||||
|
ctx = ToolContext(dry_run=False)
|
||||||
|
result = tool.execute_snippet(
|
||||||
|
{"code": "x = 1 + 1\nprint(x)"},
|
||||||
|
ctx
|
||||||
|
)
|
||||||
|
assert result["status"] == "success"
|
||||||
|
assert "2" in result["output"]
|
||||||
|
|
||||||
|
def test_execute_snippet_with_error(self, tool: PythonExecTool) -> None:
|
||||||
|
"""Test executing code with error."""
|
||||||
|
ctx = ToolContext(dry_run=False)
|
||||||
|
result = tool.execute_snippet(
|
||||||
|
{"code": "x = 1 / 0"},
|
||||||
|
ctx
|
||||||
|
)
|
||||||
|
assert result["status"] == "error"
|
||||||
|
assert "ZeroDivisionError" in result["error_type"]
|
||||||
|
|
||||||
|
def test_execute_snippet_invalid_code_type(self, tool: PythonExecTool) -> None:
|
||||||
|
"""Test with invalid code type."""
|
||||||
|
ctx = ToolContext(dry_run=False)
|
||||||
|
with pytest.raises(ValueError, match="code must be string"):
|
||||||
|
tool.execute_snippet({"code": 123}, ctx)
|
||||||
|
|
||||||
|
def test_execute_snippet_dry_run(self, tool: PythonExecTool) -> None:
|
||||||
|
"""Test snippet execution in dry-run mode."""
|
||||||
|
ctx = ToolContext(dry_run=True)
|
||||||
|
result = tool.execute_snippet(
|
||||||
|
{"code": "x = 1 + 1"},
|
||||||
|
ctx
|
||||||
|
)
|
||||||
|
assert result["dry_run"] is True
|
||||||
|
assert result["message"] == "Would execute code (dry-run mode)"
|
||||||
|
|
||||||
|
def test_execute_script(self, tool: PythonExecTool) -> None:
|
||||||
|
"""Test executing a Python script."""
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
||||||
|
f.write("print('hello from script')\n")
|
||||||
|
script_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
ctx = ToolContext(dry_run=False)
|
||||||
|
result = tool.execute_script(
|
||||||
|
{"script": script_path, "timeout": 10},
|
||||||
|
ctx
|
||||||
|
)
|
||||||
|
assert result["status"] == "success"
|
||||||
|
assert "hello from script" in result["stdout"]
|
||||||
|
finally:
|
||||||
|
Path(script_path).unlink()
|
||||||
|
|
||||||
|
def test_execute_script_nonexistent(self, tool: PythonExecTool) -> None:
|
||||||
|
"""Test executing nonexistent script."""
|
||||||
|
with pytest.raises(ValueError, match="Script not found"):
|
||||||
|
tool.execute_script({"script": "/nonexistent/script.py"}, ToolContext())
|
||||||
|
|
||||||
|
def test_execute_script_dry_run(self, tool: PythonExecTool) -> None:
|
||||||
|
"""Test script execution in dry-run mode."""
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
||||||
|
f.write("print('test')\n")
|
||||||
|
script_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
ctx = ToolContext(dry_run=True)
|
||||||
|
result = tool.execute_script({"script": script_path}, ctx)
|
||||||
|
assert result["dry_run"] is True
|
||||||
|
finally:
|
||||||
|
Path(script_path).unlink()
|
||||||
|
|
||||||
|
def test_execute_action_snippet(self, tool: PythonExecTool) -> None:
|
||||||
|
"""Test execute with snippet action."""
|
||||||
|
ctx = ToolContext(dry_run=False)
|
||||||
|
result = tool.execute(
|
||||||
|
{"action": "snippet", "code": "x = 42\nprint(x)"},
|
||||||
|
ctx
|
||||||
|
)
|
||||||
|
assert result["status"] == "success"
|
||||||
|
assert "42" in result["output"]
|
||||||
|
|
||||||
|
def test_execute_invalid_action(self, tool: PythonExecTool) -> None:
|
||||||
|
"""Test with invalid action."""
|
||||||
|
ctx = ToolContext(dry_run=False)
|
||||||
|
with pytest.raises(ValueError, match="Unknown exec action"):
|
||||||
|
tool.execute({"action": "invalid_action"}, ctx)
|
||||||
91
tests/test_search_code.py
Normal file
91
tests/test_search_code.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from core.tools.base import ToolContext
|
||||||
|
from tools.search_code import SearchCodeTool
|
||||||
|
|
||||||
|
|
||||||
|
class TestSearchCodeTool:
|
||||||
|
"""Test code search tool."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def tool(self) -> SearchCodeTool:
|
||||||
|
return SearchCodeTool()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def temp_dir(self) -> Path:
|
||||||
|
"""Create a temporary directory with test files."""
|
||||||
|
tmpdir = Path(tempfile.mkdtemp())
|
||||||
|
|
||||||
|
(tmpdir / "test1.py").write_text("def hello():\n print('hello')\n")
|
||||||
|
(tmpdir / "test2.py").write_text("def world():\n print('world')\n")
|
||||||
|
(tmpdir / "data.txt").write_text("some data\n")
|
||||||
|
|
||||||
|
yield tmpdir
|
||||||
|
|
||||||
|
def test_search_literal(self, tool: SearchCodeTool, temp_dir: Path) -> None:
|
||||||
|
"""Test literal string search."""
|
||||||
|
result = tool.search_literal({
|
||||||
|
"pattern": "def",
|
||||||
|
"path": str(temp_dir),
|
||||||
|
"file_pattern": "*.py"
|
||||||
|
})
|
||||||
|
assert result["pattern"] == "def"
|
||||||
|
assert result["matches_found"] >= 2
|
||||||
|
|
||||||
|
def test_search_literal_no_matches(self, tool: SearchCodeTool, temp_dir: Path) -> None:
|
||||||
|
"""Test literal search with no matches."""
|
||||||
|
result = tool.search_literal({
|
||||||
|
"pattern": "nonexistent_pattern",
|
||||||
|
"path": str(temp_dir),
|
||||||
|
"file_pattern": "*.py"
|
||||||
|
})
|
||||||
|
assert result["matches_found"] == 0
|
||||||
|
|
||||||
|
def test_search_regex(self, tool: SearchCodeTool, temp_dir: Path) -> None:
|
||||||
|
"""Test regex search."""
|
||||||
|
result = tool.search_regex({
|
||||||
|
"pattern": r"def \w+",
|
||||||
|
"path": str(temp_dir),
|
||||||
|
"file_pattern": "*.py"
|
||||||
|
})
|
||||||
|
assert result["matches_found"] >= 2
|
||||||
|
|
||||||
|
def test_search_regex_invalid(self, tool: SearchCodeTool, temp_dir: Path) -> None:
|
||||||
|
"""Test regex search with invalid pattern."""
|
||||||
|
with pytest.raises(ValueError, match="Invalid regex"):
|
||||||
|
tool.search_regex({
|
||||||
|
"pattern": "[invalid",
|
||||||
|
"path": str(temp_dir)
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_search_in_file(self, tool: SearchCodeTool, temp_dir: Path) -> None:
|
||||||
|
"""Test search within a specific file."""
|
||||||
|
test_file = temp_dir / "test1.py"
|
||||||
|
result = tool.search_in_file({
|
||||||
|
"path": str(test_file),
|
||||||
|
"pattern": "hello"
|
||||||
|
})
|
||||||
|
assert result["count"] >= 1
|
||||||
|
|
||||||
|
def test_execute_literal(self, tool: SearchCodeTool, temp_dir: Path) -> None:
|
||||||
|
"""Test execute with literal action."""
|
||||||
|
ctx = ToolContext(dry_run=False)
|
||||||
|
result = tool.execute(
|
||||||
|
{
|
||||||
|
"action": "literal",
|
||||||
|
"pattern": "def",
|
||||||
|
"path": str(temp_dir)
|
||||||
|
},
|
||||||
|
ctx
|
||||||
|
)
|
||||||
|
assert result["matches_found"] >= 2
|
||||||
|
|
||||||
|
def test_execute_invalid_action(self, tool: SearchCodeTool) -> None:
|
||||||
|
"""Test with invalid action."""
|
||||||
|
ctx = ToolContext(dry_run=False)
|
||||||
|
with pytest.raises(ValueError, match="Unknown search action"):
|
||||||
|
tool.execute({"action": "invalid_action"}, ctx)
|
||||||
Reference in New Issue
Block a user
