Files
pocketpaw/tests/test_tools.py
Ajeet dfafb4f149 test: make agent and config tests robust against environment variables
Updated Tests to use Settings(_env_file=None) to prevent leakage
from local .env files, and added monkeypatching for POCKETPAW_ env
vars to ensure consistent expected defaults across environments.
2026-04-16 17:13:05 +05:30

277 lines
9.0 KiB
Python

"""Unit tests for PocketPaw tools."""
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
class TestStatusTool:
"""Tests for status tool."""
def test_get_system_status_returns_string(self):
"""Status should return a formatted string."""
from pocketpaw.tools import status
result = status.get_system_status()
assert isinstance(result, str)
assert "System Status" in result
assert "CPU" in result
assert "RAM" in result
assert "Disk" in result
def test_get_system_status_contains_percentages(self):
"""Status should contain percentage values."""
from pocketpaw.tools import status
result = status.get_system_status()
# Should have percentage signs
assert "%" in result
class TestFetchTool:
"""Tests for fetch tool."""
def test_is_safe_path_within_jail(self, tmp_path):
"""Paths within jail should be safe."""
from pocketpaw.tools.fetch import is_safe_path
jail = tmp_path
safe_path = tmp_path / "subdir"
safe_path.mkdir()
assert is_safe_path(safe_path, jail) is True
def test_is_safe_path_outside_jail(self, tmp_path):
"""Paths outside jail should be unsafe."""
from pocketpaw.tools.fetch import is_safe_path
jail = tmp_path / "jail"
jail.mkdir()
outside_path = tmp_path / "outside"
outside_path.mkdir()
assert is_safe_path(outside_path, jail) is False
def test_is_safe_path_parent_traversal(self, tmp_path):
"""Parent traversal should be blocked."""
from pocketpaw.tools.fetch import is_safe_path
jail = tmp_path / "jail"
jail.mkdir()
traversal_path = jail / ".." / "outside"
assert is_safe_path(traversal_path, jail) is False
def test_is_safe_path_prefix_bypass(self, tmp_path):
"""Prefix-matching bypass should be blocked."""
from pocketpaw.tools.fetch import is_safe_path
jail = tmp_path / "jail"
jail.mkdir()
outside_with_prefix = tmp_path / "jail_outside"
outside_with_prefix.mkdir()
assert is_safe_path(outside_with_prefix, jail) is False
@pytest.mark.asyncio
async def test_handle_path_directory(self, tmp_path):
"""Should handle directory paths."""
from pocketpaw.tools.fetch import handle_path
result = await handle_path(str(tmp_path), tmp_path)
assert result["type"] == "directory"
assert "keyboard" in result
@pytest.mark.asyncio
async def test_handle_path_file(self, tmp_path):
"""Should handle file paths."""
from pocketpaw.tools.fetch import handle_path
test_file = tmp_path / "test.txt"
test_file.write_text("content")
result = await handle_path(str(test_file), tmp_path)
assert result["type"] == "file"
assert result["filename"] == "test.txt"
@pytest.mark.asyncio
async def test_handle_path_outside_jail(self, tmp_path):
"""Should reject paths outside jail."""
from pocketpaw.tools.fetch import handle_path
jail = tmp_path / "jail"
jail.mkdir()
outside = tmp_path / "outside"
outside.mkdir()
result = await handle_path(str(outside), jail)
assert result["type"] == "error"
class TestScreenshotTool:
"""Tests for screenshot tool."""
def test_take_screenshot_returns_bytes_or_str(self):
"""Screenshot should return bytes on success, or an error string on failure."""
from pocketpaw.tools import screenshot
result = screenshot.take_screenshot()
# Should be bytes (success) or str (error message — e.g. on headless CI)
assert isinstance(result, bytes | str)
@patch("pocketpaw.tools.screenshot.PYAUTOGUI_AVAILABLE", False)
def test_take_screenshot_without_pyautogui(self):
"""Should return a descriptive error string when pyautogui is unavailable."""
from pocketpaw.tools import screenshot
with patch.object(screenshot, "PYAUTOGUI_AVAILABLE", False):
result = screenshot.take_screenshot()
assert isinstance(result, str)
assert "pyautogui" in result.lower()
def test_take_screenshot_handles_exception(self):
"""Should return a descriptive error string when pyautogui raises."""
from pocketpaw.tools import screenshot
mock_pyautogui = MagicMock()
mock_pyautogui.screenshot.side_effect = OSError("No display")
with (
patch.object(screenshot, "PYAUTOGUI_AVAILABLE", True),
patch.object(screenshot, "pyautogui", mock_pyautogui, create=True),
):
result = screenshot.take_screenshot()
assert isinstance(result, str)
assert "Screenshot failed" in result
class TestConfig:
"""Tests for configuration."""
def test_settings_defaults(self, monkeypatch):
"""Settings should have sensible defaults."""
from pocketpaw.config import Settings
monkeypatch.delenv("POCKETPAW_LLM_PROVIDER", raising=False)
monkeypatch.delenv("POCKETPAW_OLLAMA_HOST", raising=False)
monkeypatch.delenv("POCKETPAW_AGENT_BACKEND", raising=False)
settings = Settings(_env_file=None)
assert settings.agent_backend == "claude_agent_sdk" # New default
assert settings.llm_provider == "auto"
assert settings.web_port == 8888
assert settings.ollama_model == "llama3.2"
def test_settings_save_and_load(self, tmp_path, monkeypatch):
"""Settings should persist to disk."""
from pocketpaw.config import Settings
from pocketpaw.credentials import CredentialStore
# Mock config path to use temp directory
config_file = tmp_path / "config.json"
monkeypatch.setattr("pocketpaw.config.get_config_path", lambda: config_file)
# Mock credential store to use temp directory (avoid polluting real secrets)
test_store = CredentialStore(config_dir=tmp_path)
monkeypatch.setattr("pocketpaw.credentials.get_credential_store", lambda: test_store)
# Create and save settings
settings = Settings(telegram_bot_token="test-token", allowed_user_id=12345)
settings.save()
# Verify file exists
assert config_file.exists()
# Load and verify
loaded = Settings.load()
assert loaded.telegram_bot_token == "test-token"
assert loaded.allowed_user_id == 12345
def test_get_config_dir_creates_directory(self, tmp_path, monkeypatch):
"""Config dir should be created if not exists."""
from pocketpaw.config import get_config_dir
# Mock home to use temp
new_home = tmp_path / "home"
new_home.mkdir()
monkeypatch.setattr(Path, "home", lambda: new_home)
result = get_config_dir()
assert result.exists()
assert result.name == ".pocketpaw"
class TestLLMRouter:
"""Tests for LLM router."""
def test_router_initialization(self):
"""Router should initialize without errors."""
from pocketpaw.config import Settings
from pocketpaw.llm.router import LLMRouter
settings = Settings()
router = LLMRouter(settings)
assert router.conversation_history == []
def test_router_clear_history(self):
"""Should clear conversation history."""
from pocketpaw.config import Settings
from pocketpaw.llm.router import LLMRouter
settings = Settings()
router = LLMRouter(settings)
router.conversation_history = [{"role": "user", "content": "test"}]
router.clear_history()
assert router.conversation_history == []
@pytest.mark.asyncio
async def test_router_no_backend_returns_error(self):
"""Should return error when no backend available."""
from pocketpaw.config import Settings
from pocketpaw.llm.router import LLMRouter
settings = Settings(
llm_provider="openai",
openai_api_key=None, # No key
)
router = LLMRouter(settings)
result = await router.chat("Hello")
assert "No LLM backend available" in result
class TestAgentRouter:
"""Tests for agent router."""
def test_router_initializes_claude_agent_sdk(self):
"""Should initialize with claude_agent_sdk backend."""
from pocketpaw.agents.router import AgentRouter
from pocketpaw.config import Settings
settings = Settings(agent_backend="claude_agent_sdk", anthropic_api_key="test")
router = AgentRouter(settings)
assert router._backend is not None
def test_router_legacy_backend_falls_back(self):
"""Legacy backend names should fall back to claude_agent_sdk."""
from pocketpaw.agents.router import AgentRouter
from pocketpaw.config import Settings
settings = Settings(agent_backend="open_interpreter", anthropic_api_key="test")
router = AgentRouter(settings)
# Should have fallen back to claude_agent_sdk
assert router._backend is not None