mirror of
https://github.com/pocketpaw/pocketpaw.git
synced 2026-05-19 16:31:15 +00:00
193 lines
6.0 KiB
Python
193 lines
6.0 KiB
Python
# Tests for Tool System
|
|
# Created: 2026-02-02
|
|
|
|
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from pocketpaw.config import Settings
|
|
from pocketpaw.tools.builtin.filesystem import ListDirTool, ReadFileTool, WriteFileTool
|
|
from pocketpaw.tools.builtin.shell import ShellTool
|
|
from pocketpaw.tools.protocol import BaseTool
|
|
from pocketpaw.tools.registry import ToolRegistry
|
|
|
|
|
|
class MockTool(BaseTool):
|
|
"""Mock tool for testing registry."""
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return "mock_tool"
|
|
|
|
@property
|
|
def description(self) -> str:
|
|
return "A mock tool."
|
|
|
|
async def execute(self, param: str) -> str:
|
|
return f"Executed with {param}"
|
|
|
|
|
|
class TestToolRegistry:
|
|
"""Tests for ToolRegistry."""
|
|
|
|
def test_register_and_get(self):
|
|
registry = ToolRegistry()
|
|
tool = MockTool()
|
|
registry.register(tool)
|
|
|
|
assert registry.has("mock_tool")
|
|
assert registry.get("mock_tool") == tool
|
|
assert "mock_tool" in registry.tool_names
|
|
|
|
def test_unregister(self):
|
|
registry = ToolRegistry()
|
|
tool = MockTool()
|
|
registry.register(tool)
|
|
registry.unregister("mock_tool")
|
|
|
|
assert not registry.has("mock_tool")
|
|
|
|
def test_get_definitions(self):
|
|
registry = ToolRegistry()
|
|
tool = MockTool()
|
|
registry.register(tool)
|
|
|
|
defs = registry.get_definitions("openai")
|
|
assert len(defs) == 1
|
|
assert defs[0]["function"]["name"] == "mock_tool"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute(self):
|
|
registry = ToolRegistry()
|
|
tool = MockTool()
|
|
registry.register(tool)
|
|
|
|
result = await registry.execute("mock_tool", param="test")
|
|
assert result == "Executed with test"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_missing(self):
|
|
registry = ToolRegistry()
|
|
result = await registry.execute("missing_tool")
|
|
assert "Error: Tool 'missing_tool' not found" in result
|
|
|
|
|
|
class TestShellTool:
|
|
"""Tests for ShellTool."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_simple(self):
|
|
tool = ShellTool()
|
|
result = (
|
|
await tool.execute_command("echo 'hello'")
|
|
if hasattr(tool, "execute_command")
|
|
else await tool.execute(command="echo 'hello'")
|
|
)
|
|
assert "hello" in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_security_check(self):
|
|
tool = ShellTool()
|
|
result = await tool.execute(command="rm -rf /")
|
|
assert "Dangerous command blocked" in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_timeout(self):
|
|
# Create tool with short timeout
|
|
tool = ShellTool(timeout=1)
|
|
# Use a cross-platform command that runs longer than the timeout
|
|
cmd = "ping -n 5 127.0.0.1" if sys.platform == "win32" else "sleep 2"
|
|
result = await tool.execute(command=cmd)
|
|
assert "Command timed out" in result
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_jail():
|
|
"""Create a temporary directory for file jail."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
yield Path(tmpdir)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_settings(temp_jail):
|
|
"""Mock settings with custom file jail."""
|
|
settings = Settings(file_jail_path=temp_jail)
|
|
with (
|
|
patch("pocketpaw.tools.builtin.filesystem.get_settings", return_value=settings),
|
|
patch("pocketpaw.tools.builtin.shell.get_settings", return_value=settings),
|
|
):
|
|
yield settings
|
|
|
|
|
|
class TestFilesystemTools:
|
|
"""Tests for filesystem tools."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_and_read(self, temp_jail, mock_settings):
|
|
write_tool = WriteFileTool()
|
|
read_tool = ReadFileTool()
|
|
|
|
# Write
|
|
file_path = str(temp_jail / "test.txt")
|
|
result = await write_tool.execute(path=file_path, content="Hello World")
|
|
assert "Successfully wrote" in result
|
|
|
|
# Read
|
|
content = await read_tool.execute(path=file_path)
|
|
assert content == "Hello World"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_jail_break_attempt(self, temp_jail, mock_settings):
|
|
read_tool = ReadFileTool()
|
|
|
|
# Try to read outside jail
|
|
outside = str(temp_jail.parent / "secret.txt")
|
|
result = await read_tool.execute(path=outside)
|
|
if "Access denied" not in result:
|
|
# Depending on how resolve works, it might be same dir if parent is tmp
|
|
# Let's try explicit relative path
|
|
outside = str(temp_jail / "../secret.txt")
|
|
result = await read_tool.execute(path=outside)
|
|
|
|
# It's possible for temp dir to be weird, but let's check basic protection
|
|
# If result is "File not found" it might mean it resolved but didn't error on jail
|
|
# We want explicit jail error
|
|
assert "Access denied" in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_jail_prefix_bypass_blocked(self, temp_jail, mock_settings):
|
|
read_tool = ReadFileTool()
|
|
write_tool = WriteFileTool()
|
|
list_tool = ListDirTool()
|
|
|
|
outside_prefix_dir = temp_jail.parent / f"{temp_jail.name}_outside"
|
|
outside_prefix_dir.mkdir(exist_ok=True)
|
|
outside_prefix_file = outside_prefix_dir / "secret.txt"
|
|
outside_prefix_file.write_text("secret")
|
|
|
|
read_result = await read_tool.execute(path=str(outside_prefix_file))
|
|
write_result = await write_tool.execute(path=str(outside_prefix_file), content="overwrite")
|
|
list_result = await list_tool.execute(path=str(outside_prefix_dir))
|
|
|
|
assert "Access denied" in read_result
|
|
assert "Access denied" in write_result
|
|
assert "Access denied" in list_result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_dir(self, temp_jail, mock_settings):
|
|
list_tool = ListDirTool()
|
|
write_tool = WriteFileTool()
|
|
|
|
# Create some files
|
|
await write_tool.execute(path=str(temp_jail / "a.txt"), content="a")
|
|
await write_tool.execute(path=str(temp_jail / "b.txt"), content="b")
|
|
|
|
# List
|
|
result = await list_tool.execute(path=str(temp_jail))
|
|
assert "a.txt" in result
|
|
assert "b.txt" in result
|