#!/usr/bin/env python3 """ Unit Tests for Stack 2.9 Tools Module Tests all 37+ tools: file operations, git, code execution, web, memory, and task planning. """ import pytest import sys import json import tempfile from pathlib import Path from unittest.mock import MagicMock, patch, mock_open # Add stack_cli to path sys.path.insert(0, str(Path(__file__).parent.parent / "stack_cli")) from stack_cli.tools import ( TOOLS, get_tool, list_tools, get_tool_schemas, tool_read_file, tool_write_file, tool_edit_file, tool_search_files, tool_grep, tool_copy_file, tool_move_file, tool_delete_file, tool_git_status, tool_git_commit, tool_git_push, tool_git_pull, tool_git_branch, tool_git_log, tool_git_diff, tool_run_command, tool_run_tests, tool_lint_code, tool_format_code, tool_check_type, tool_start_server, tool_install_dependencies, tool_web_search, tool_web_fetch, tool_download_file, tool_check_url, tool_screenshot, tool_memory_recall, tool_memory_save, tool_memory_list, tool_context_load, tool_project_scan, tool_create_task, tool_list_tasks, tool_update_task, tool_delete_task, tool_create_plan, tool_execute_plan, ) class TestToolsRegistry: """Test tools registry.""" def test_tools_count(self): """Verify we have 37+ tools.""" tools = list_tools() assert len(tools) >= 37, f"Expected 37+ tools, got {len(tools)}" def test_get_tool_valid(self): """Test getting a valid tool.""" tool = get_tool("read") assert tool is not None assert callable(tool) def test_get_tool_invalid(self): """Test getting an invalid tool.""" tool = get_tool("nonexistent_tool") assert tool is None def test_get_tool_schemas(self): """Test getting tool schemas.""" schemas = get_tool_schemas() assert isinstance(schemas, list) assert len(schemas) > 0 class TestFileOperations: """Test file operation tools.""" def test_read_file_success(self, temp_file): """Test reading a file.""" result = tool_read_file(str(temp_file)) assert result["success"] is True assert "content" in result assert "Line 1" in result["content"] def test_read_file_not_found(self): """Test reading nonexistent file.""" result = tool_read_file("/nonexistent/file.txt") assert result["success"] is False assert "error" in result def test_read_file_with_limit(self, temp_file): """Test reading file with limit.""" result = tool_read_file(str(temp_file), limit=2) assert result["success"] is True lines = result["content"].split('\n') assert len(lines) <= 3 def test_write_file_success(self, temp_workspace): """Test writing a file.""" path = temp_workspace / "written.txt" result = tool_write_file(str(path), "Hello World") assert result["success"] is True assert path.exists() assert path.read_text() == "Hello World" def test_write_file_creates_dirs(self, temp_workspace): """Test writing creates parent directories.""" path = temp_workspace / "subdir" / "nested" / "file.txt" result = tool_write_file(str(path), "content") assert result["success"] is True assert path.exists() def test_edit_file_success(self, temp_file): """Test editing a file.""" result = tool_edit_file(str(temp_file), "Line 1", "Line ONE") assert result["success"] is True content = temp_file.read_text() assert "Line ONE" in content def test_edit_file_not_found(self): """Test editing nonexistent file.""" result = tool_edit_file("/nonexistent/file.txt", "old", "new") assert result["success"] is False def test_edit_file_text_not_found(self, temp_file): """Test editing with non-existent text.""" result = tool_edit_file(str(temp_file), "NonExistentText", "new") assert result["success"] is False def test_search_files(self, temp_project): """Test searching for files.""" result = tool_search_files(str(temp_project), "*.py") assert result["success"] is True assert "matches" in result def test_grep_basic(self, temp_file): """Test grep functionality.""" result = tool_grep(str(temp_file), "Line 1") assert result["success"] is True assert "matches" in result assert result["count"] > 0 def test_grep_with_context(self, temp_file): """Test grep with context.""" result = tool_grep(str(temp_file), "Line", context=1) assert result["success"] is True if result["matches"]: assert "context" in result["matches"][0] def test_copy_file(self, temp_file, temp_workspace): """Test copying a file.""" dest = temp_workspace / "copied.txt" result = tool_copy_file(str(temp_file), str(dest)) assert result["success"] is True assert dest.exists() def test_move_file(self, temp_file, temp_workspace): """Test moving a file.""" dest = temp_workspace / "moved.txt" result = tool_move_file(str(temp_file), str(dest)) assert result["success"] is True assert dest.exists() def test_delete_file_without_force(self, temp_file): """Test delete without force.""" result = tool_delete_file(str(temp_file)) assert result["success"] is True assert "would_delete" in result def test_delete_file_with_force(self, temp_file): """Test delete with force.""" result = tool_delete_file(str(temp_file), force=True) assert result["success"] is True assert not temp_file.exists() class TestGitOperations: """Test git operation tools.""" def test_git_status_no_repo(self): """Test git status on non-repo.""" result = tool_git_status("/nonexistent") assert result["success"] is False assert "error" in result @patch('subprocess.run') def test_git_status_success(self, mock_run, temp_git_repo): """Test git status success.""" mock_result = MagicMock() mock_result.stdout = " M modified.py\nA added.py\n" mock_run.return_value = mock_result result = tool_git_status(str(temp_git_repo)) assert result["success"] is True @patch('subprocess.run') def test_git_commit(self, mock_run, temp_git_repo): """Test git commit.""" mock_result = MagicMock() mock_result.stdout = "[main abc123] Test commit" mock_result.stderr = "" mock_run.return_value = mock_result result = tool_git_commit(str(temp_git_repo), "Test commit") assert result["success"] is True @patch('subprocess.run') def test_git_push(self, mock_run): """Test git push.""" mock_result = MagicMock() mock_result.stdout = "To github.com:test/test.git\n abc123..def456 main -> main\n" mock_run.return_value = mock_result result = tool_git_push(str(temp_git_repo)) assert result["success"] is True @patch('subprocess.run') def test_git_pull(self, mock_run): """Test git pull.""" mock_result = MagicMock() mock_result.stdout = "Updating abc123..def456\n" mock_run.return_value = mock_result result = tool_git_pull(str(temp_git_repo)) assert result["success"] is True @patch('subprocess.run') def test_git_branch_list(self, mock_run): """Test listing branches.""" mock_result = MagicMock() mock_result.stdout = "* main\n develop\n feature/test\n" mock_run.return_value = mock_result result = tool_git_branch(str(temp_git_repo)) assert result["success"] is True assert "branches" in result @patch('subprocess.run') def test_git_log(self, mock_run): """Test git log.""" mock_result = MagicMock() mock_result.stdout = "abc123 Commit message 1\ndef456 Commit message 2\n" mock_run.return_value = mock_result result = tool_git_log(str(temp_git_repo)) assert result["success"] is True @patch('subprocess.run') def test_git_diff(self, mock_run): """Test git diff.""" mock_result = MagicMock() mock_result.stdout = "diff --git a/test.py b/test.py\n+new line\n" mock_run.return_value = mock_result result = tool_git_diff(str(temp_git_repo)) assert result["success"] is True class TestCodeExecution: """Test code execution tools.""" @patch('subprocess.run') def test_run_command_success(self, mock_run): """Test running a command successfully.""" mock_result = MagicMock() mock_result.returncode = 0 mock_result.stdout = "output" mock_result.stderr = "" mock_run.return_value = mock_result result = tool_run_command("echo hello") assert result["success"] is True assert result["stdout"] == "output" @patch('subprocess.run') def test_run_command_failure(self, mock_run): """Test running a failing command.""" mock_result = MagicMock() mock_result.returncode = 1 mock_result.stdout = "" mock_result.stderr = "error" mock_run.return_value = mock_result result = tool_run_command("false") assert result["success"] is False @patch('subprocess.run') def test_run_command_timeout(self, mock_run): """Test command timeout.""" mock_run.side_effect = subprocess.TimeoutExpired("cmd", 1) result = tool_run_command("sleep 100", timeout=1) assert result["success"] is False assert "timeout" in result["error"].lower() @patch('subprocess.run') def test_run_tests(self, mock_run): """Test running tests.""" mock_result = MagicMock() mock_result.returncode = 0 mock_result.stdout = "test output" mock_result.stderr = "" mock_run.return_value = mock_result result = tool_run_tests(".") assert "success" in result @patch('subprocess.run') def test_lint_code(self, mock_run): """Test linting code.""" mock_result = MagicMock() mock_result.returncode = 0 mock_result.stdout = "lint output" mock_run.return_value = mock_result result = tool_lint_code(".") assert "success" in result @patch('subprocess.run') def test_format_code(self, mock_run): """Test formatting code.""" mock_result = MagicMock() mock_result.returncode = 0 mock_run.return_value = mock_result result = tool_format_code(".") assert "success" in result @patch('subprocess.run') def test_check_type(self, mock_run): """Test type checking.""" mock_result = MagicMock() mock_result.returncode = 0 mock_run.return_value = mock_result result = tool_check_type(".") assert "success" in result @patch('subprocess.Popen') def test_start_server_background(self, mock_popen): """Test starting server in background.""" mock_proc = MagicMock() mock_proc.pid = 12345 mock_popen.return_value = mock_proc result = tool_start_server("python server.py", 8000, background=True) assert result["success"] is True assert "pid" in result class TestWebTools: """Test web tools.""" @patch('subprocess.run') def test_web_search(self, mock_run): """Test web search.""" mock_result = MagicMock() mock_result.returncode = 0 mock_result.stdout = '[{"title": "Result", "url": "http://example.com"}]' mock_run.return_value = mock_result result = tool_web_search("python") assert result["success"] is True assert "results" in result @patch('subprocess.run') def test_web_fetch(self, mock_run): """Test web fetch.""" mock_result = MagicMock() mock_result.returncode = 0 mock_result.stdout = "test" mock_run.return_value = mock_result result = tool_web_fetch("http://example.com") assert result["success"] is True assert "content" in result @patch('subprocess.run') def test_check_url(self, mock_run): """Test URL check.""" mock_result = MagicMock() mock_result.stdout = "200" mock_run.return_value = mock_result result = tool_check_url("http://example.com") assert result["success"] is True class TestMemoryTools: """Test memory tools.""" @patch('pathlib.Path.read_text') def test_memory_recall(self, mock_read): """Test memory recall.""" mock_read.return_value = "### test\ntest content" result = tool_memory_recall("test") assert result["success"] is True @patch('pathlib.Path.write_text') def test_memory_save(self, mock_write): """Test memory save.""" with patch('pathlib.Path.exists', return_value=True): result = tool_memory_save("test_key", "test_value") assert result["success"] is True @patch('pathlib.Path.exists') def test_memory_list(self, mock_exists): """Test memory list.""" mock_exists.return_value = False result = tool_memory_list() assert result["success"] is True @patch('pathlib.Path.read_text') def test_context_load(self, mock_read): """Test context load.""" mock_read.return_value = "# Context" result = tool_context_load() assert result["success"] is True def test_project_scan(self, temp_project): """Test project scan.""" result = tool_project_scan(str(temp_project)) assert result["success"] is True assert "project" in result class TestTaskPlanningTools: """Test task planning tools.""" @patch('pathlib.Path.write_text') def test_create_task(self, mock_write): """Test creating a task.""" with patch('pathlib.Path.exists', return_value=False): result = tool_create_task("Test task", "Description", "high") assert result["success"] is True assert "task" in result @patch('pathlib.Path.read_text') def test_list_tasks(self, mock_read): """Test listing tasks.""" mock_read.return_value = "[]" result = tool_list_tasks() assert result["success"] is True @patch('pathlib.Path.read_text') @patch('pathlib.Path.write_text') def test_update_task(self, mock_write, mock_read): """Test updating a task.""" mock_read.return_value = '[{"id": "test123", "title": "Test"}]' result = tool_update_task("test123", status="completed") assert result["success"] is True @patch('pathlib.Path.read_text') @patch('pathlib.Path.write_text') def test_delete_task(self, mock_write, mock_read): """Test deleting a task.""" mock_read.return_value = '[{"id": "test123", "title": "Test"}]' result = tool_delete_task("test123") assert result["success"] is True @patch('pathlib.Path.write_text') def test_create_plan(self, mock_write): """Test creating a plan.""" with patch('pathlib.Path.exists', return_value=False): result = tool_create_plan("Goal", ["step1", "step2"]) assert result["success"] is True @patch('pathlib.Path.read_text') @patch('pathlib.Path.write_text') def test_execute_plan(self, mock_write, mock_read): """Test executing a plan.""" mock_read.return_value = '[{"id": "plan1", "goal": "Goal", "steps": ["step1"]}]' result = tool_execute_plan("plan1") assert result["success"] is True if __name__ == "__main__": pytest.main([__file__, "-v"])