Files
pocketpaw/tests/test_ocr.py
Ajeet 213cbb09e3 test(ocr,stt): fix jail-check breakage in existing tests
The new is_safe_path guard in ocr.py and stt.py caused all tests
that place files in tmp_path to fail with 'Access denied' because:
  - _mock_settings did not set file_jail_path on the mock
  - get_settings() was called directly (bypassing the fixture) for jail lookup

Fix:
  - _mock_settings now sets file_jail_path = tmp_path and patches
    is_safe_path to return True for tests that are not testing security
  - Tests that use tmp_path without _mock_settings get the same treatment
  - Add test_ocr_file_jail_rejects_outside_path and
    test_stt_file_jail_rejects_outside_path for explicit jail enforcement
2026-04-16 17:13:05 +05:30

200 lines
6.2 KiB
Python

# Tests for OCR tool (Sprint 28)
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
class TestOCRToolSchema:
"""Test OCRTool properties and schema."""
def test_name(self):
from pocketpaw.tools.builtin.ocr import OCRTool
tool = OCRTool()
assert tool.name == "ocr"
def test_trust_level(self):
from pocketpaw.tools.builtin.ocr import OCRTool
tool = OCRTool()
assert tool.trust_level == "standard"
def test_parameters(self):
from pocketpaw.tools.builtin.ocr import OCRTool
tool = OCRTool()
params = tool.parameters
assert "image_path" in params["properties"]
assert "prompt" in params["properties"]
assert "image_path" in params["required"]
def test_description(self):
from pocketpaw.tools.builtin.ocr import OCRTool
tool = OCRTool()
assert "ocr" in tool.description.lower() or "extract text" in tool.description.lower()
@pytest.fixture
def _mock_settings(tmp_path):
settings = MagicMock()
settings.openai_api_key = "test-key"
settings.ocr_provider = "openai"
settings.file_jail_path = tmp_path
with (
patch("pocketpaw.tools.builtin.ocr.get_settings", return_value=settings),
patch("pocketpaw.tools.builtin.ocr.is_safe_path", return_value=True),
):
yield settings
async def test_ocr_file_not_found(_mock_settings):
from pocketpaw.tools.builtin.ocr import OCRTool
tool = OCRTool()
result = await tool.execute(image_path="/nonexistent/image.png")
assert result.startswith("Error:")
assert "not found" in result
async def test_ocr_file_jail_rejects_outside_path(tmp_path):
"""Files outside the jail directory must be rejected."""
from pocketpaw.tools.builtin.ocr import OCRTool
tool = OCRTool()
jail = tmp_path / "jail"
jail.mkdir()
outside = tmp_path / "outside.png"
outside.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 50)
settings = MagicMock()
settings.file_jail_path = jail
with patch("pocketpaw.tools.builtin.ocr.get_settings", return_value=settings):
result = await tool.execute(image_path=str(outside))
assert result.startswith("Error:")
assert "Access denied" in result or "outside" in result
async def test_ocr_unsupported_format(_mock_settings, tmp_path):
from pocketpaw.tools.builtin.ocr import OCRTool
tool = OCRTool()
bad_file = tmp_path / "test.xyz"
bad_file.write_bytes(b"\x00")
result = await tool.execute(image_path=str(bad_file))
assert result.startswith("Error:")
assert "Unsupported" in result
async def test_ocr_file_too_large(_mock_settings, tmp_path):
from pocketpaw.tools.builtin.ocr import OCRTool
tool = OCRTool()
big_file = tmp_path / "big.png"
big_file.write_bytes(b"\x00" * (21 * 1024 * 1024))
result = await tool.execute(image_path=str(big_file))
assert result.startswith("Error:")
assert "too large" in result
async def test_ocr_openai_success(_mock_settings, tmp_path):
from pocketpaw.tools.builtin.ocr import OCRTool
tool = OCRTool()
img = tmp_path / "test.png"
img.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 100)
mock_resp = MagicMock()
mock_resp.json.return_value = {"choices": [{"message": {"content": "Hello World"}}]}
mock_resp.raise_for_status = MagicMock()
with patch("httpx.AsyncClient") as mock_cls:
mock_client = AsyncMock()
mock_client.post = AsyncMock(return_value=mock_resp)
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)
mock_cls.return_value = mock_client
result = await tool.execute(image_path=str(img))
assert "Hello World" in result
assert "OCR result" in result
async def test_ocr_no_text_detected(_mock_settings, tmp_path):
from pocketpaw.tools.builtin.ocr import OCRTool
tool = OCRTool()
img = tmp_path / "blank.png"
img.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 50)
mock_resp = MagicMock()
mock_resp.json.return_value = {"choices": [{"message": {"content": ""}}]}
mock_resp.raise_for_status = MagicMock()
with patch("httpx.AsyncClient") as mock_cls:
mock_client = AsyncMock()
mock_client.post = AsyncMock(return_value=mock_resp)
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)
mock_cls.return_value = mock_client
result = await tool.execute(image_path=str(img))
assert "No text detected" in result
async def test_ocr_no_api_key_no_tesseract(tmp_path):
from pocketpaw.tools.builtin.ocr import OCRTool
tool = OCRTool()
img = tmp_path / "test.jpg"
img.write_bytes(b"\xff\xd8\xff" + b"\x00" * 50)
settings = MagicMock()
settings.openai_api_key = None
settings.ocr_provider = "openai"
settings.file_jail_path = tmp_path
with (
patch("pocketpaw.tools.builtin.ocr.get_settings", return_value=settings),
patch("pocketpaw.tools.builtin.ocr.is_safe_path", return_value=True),
):
# Mock pytesseract as not installed
with patch.dict("sys.modules", {"pytesseract": None}):
result = await tool.execute(image_path=str(img))
assert result.startswith("Error:")
assert "No OCR provider" in result or "pytesseract" in result.lower()
async def test_ocr_api_error(_mock_settings, tmp_path):
from pocketpaw.tools.builtin.ocr import OCRTool
tool = OCRTool()
img = tmp_path / "test.png"
img.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 50)
import httpx as httpx_mod
mock_resp = MagicMock()
mock_resp.status_code = 429
mock_resp.request = MagicMock()
with patch("httpx.AsyncClient") as mock_cls:
mock_client = AsyncMock()
mock_client.post = AsyncMock(
side_effect=httpx_mod.HTTPStatusError(
"rate limited", request=mock_resp.request, response=mock_resp
)
)
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)
mock_cls.return_value = mock_client
result = await tool.execute(image_path=str(img))
assert result.startswith("Error:")
assert "429" in result