mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-30 19:02:16 +00:00
- Rewrite .github/workflows/desktop.yml: 2-job pipeline (validate + build-and-release) with rolling desktop-latest pre-release on push to main and stable desktop-v* releases - Add UpdateChecker component: checks for updates on startup + every 30 min, background download with progress bar, one-click relaunch - Configure Tauri updater: endpoints pointing to desktop-latest release, pubkey placeholder - Add tauri-plugin-process for relaunch support (Cargo.toml, lib.rs, package.json) - Add macOS Entitlements.plist for notarization (network + file access, no sandbox) - Add scripts/bump-desktop-version.sh for atomic version bumps across 3 config files - Add desktop/README.md with dev setup, auto-update architecture, signing docs - Update .gitignore for desktop/node_modules, dist, target - Configure macOS minimumSystemVersion, Windows timestampUrl - Include all Phase 14-21 work: agent hardening, RBAC, taint tracking, workflows, skills, knowledge graph, sessions, A2A, MCP templates, WASM sandbox, TUI dashboard, production tools, CLI expansion, API expansion, learning productionization, Tauri desktop app, and 10 new channels Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
198 lines
6.9 KiB
Python
198 lines
6.9 KiB
Python
"""Tests for the pdf_extract tool."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import builtins
|
|
import sys
|
|
from unittest.mock import MagicMock
|
|
|
|
from openjarvis.tools.pdf_tool import PDFExtractTool, _parse_pages
|
|
|
|
|
|
class TestPDFExtractTool:
|
|
def test_spec(self):
|
|
tool = PDFExtractTool()
|
|
assert tool.spec.name == "pdf_extract"
|
|
assert tool.spec.category == "media"
|
|
assert "file_path" in tool.spec.parameters["properties"]
|
|
assert "file_path" in tool.spec.parameters["required"]
|
|
assert tool.spec.required_capabilities == ["file:read"]
|
|
|
|
def test_tool_id(self):
|
|
tool = PDFExtractTool()
|
|
assert tool.tool_id == "pdf_extract"
|
|
|
|
def test_no_file_path(self):
|
|
tool = PDFExtractTool()
|
|
result = tool.execute(file_path="")
|
|
assert result.success is False
|
|
assert "No file_path" in result.content
|
|
|
|
def test_no_file_path_param(self):
|
|
tool = PDFExtractTool()
|
|
result = tool.execute()
|
|
assert result.success is False
|
|
assert "No file_path" in result.content
|
|
|
|
def test_file_not_found(self):
|
|
tool = PDFExtractTool()
|
|
result = tool.execute(file_path="/nonexistent/doc.pdf")
|
|
assert result.success is False
|
|
assert "File not found" in result.content
|
|
|
|
def test_not_a_pdf(self, tmp_path):
|
|
f = tmp_path / "document.txt"
|
|
f.write_text("not a pdf", encoding="utf-8")
|
|
tool = PDFExtractTool()
|
|
result = tool.execute(file_path=str(f))
|
|
assert result.success is False
|
|
assert "Not a PDF" in result.content
|
|
|
|
def test_pdfplumber_not_installed(self, tmp_path, monkeypatch):
|
|
f = tmp_path / "doc.pdf"
|
|
f.write_bytes(b"%PDF-1.4 fake pdf content")
|
|
|
|
monkeypatch.delitem(sys.modules, "pdfplumber", raising=False)
|
|
original_import = builtins.__import__
|
|
|
|
def _mock_import(name, *args, **kwargs):
|
|
if name == "pdfplumber":
|
|
raise ImportError("No module named 'pdfplumber'")
|
|
return original_import(name, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(builtins, "__import__", _mock_import)
|
|
|
|
tool = PDFExtractTool()
|
|
result = tool.execute(file_path=str(f))
|
|
assert result.success is False
|
|
assert "pdfplumber package not installed" in result.content
|
|
|
|
def test_successful_extraction(self, tmp_path, monkeypatch):
|
|
f = tmp_path / "doc.pdf"
|
|
f.write_bytes(b"%PDF-1.4 fake")
|
|
|
|
# Mock pdfplumber
|
|
mock_page1 = MagicMock()
|
|
mock_page1.extract_text.return_value = "Page one text."
|
|
mock_page2 = MagicMock()
|
|
mock_page2.extract_text.return_value = "Page two text."
|
|
|
|
mock_pdf = MagicMock()
|
|
mock_pdf.pages = [mock_page1, mock_page2]
|
|
mock_pdf.__enter__ = MagicMock(return_value=mock_pdf)
|
|
mock_pdf.__exit__ = MagicMock(return_value=False)
|
|
|
|
mock_pdfplumber = MagicMock()
|
|
mock_pdfplumber.open.return_value = mock_pdf
|
|
monkeypatch.setitem(sys.modules, "pdfplumber", mock_pdfplumber)
|
|
|
|
tool = PDFExtractTool()
|
|
result = tool.execute(file_path=str(f))
|
|
assert result.success is True
|
|
assert "Page one text." in result.content
|
|
assert "Page two text." in result.content
|
|
assert result.metadata["total_pages"] == 2
|
|
assert result.metadata["pages_extracted"] == 2
|
|
|
|
def test_extraction_with_page_range(self, tmp_path, monkeypatch):
|
|
f = tmp_path / "doc.pdf"
|
|
f.write_bytes(b"%PDF-1.4 fake")
|
|
|
|
mock_page1 = MagicMock()
|
|
mock_page1.extract_text.return_value = "First page."
|
|
mock_page2 = MagicMock()
|
|
mock_page2.extract_text.return_value = "Second page."
|
|
mock_page3 = MagicMock()
|
|
mock_page3.extract_text.return_value = "Third page."
|
|
|
|
mock_pdf = MagicMock()
|
|
mock_pdf.pages = [mock_page1, mock_page2, mock_page3]
|
|
mock_pdf.__enter__ = MagicMock(return_value=mock_pdf)
|
|
mock_pdf.__exit__ = MagicMock(return_value=False)
|
|
|
|
mock_pdfplumber = MagicMock()
|
|
mock_pdfplumber.open.return_value = mock_pdf
|
|
monkeypatch.setitem(sys.modules, "pdfplumber", mock_pdfplumber)
|
|
|
|
tool = PDFExtractTool()
|
|
# Extract only pages 1 and 3 (1-indexed)
|
|
result = tool.execute(file_path=str(f), pages="1,3")
|
|
assert result.success is True
|
|
assert "First page." in result.content
|
|
assert "Third page." in result.content
|
|
assert "Second page." not in result.content
|
|
assert result.metadata["pages_extracted"] == 2
|
|
|
|
def test_max_chars_truncation(self, tmp_path, monkeypatch):
|
|
f = tmp_path / "doc.pdf"
|
|
f.write_bytes(b"%PDF-1.4 fake")
|
|
|
|
mock_page = MagicMock()
|
|
mock_page.extract_text.return_value = "A" * 1000
|
|
|
|
mock_pdf = MagicMock()
|
|
mock_pdf.pages = [mock_page]
|
|
mock_pdf.__enter__ = MagicMock(return_value=mock_pdf)
|
|
mock_pdf.__exit__ = MagicMock(return_value=False)
|
|
|
|
mock_pdfplumber = MagicMock()
|
|
mock_pdfplumber.open.return_value = mock_pdf
|
|
monkeypatch.setitem(sys.modules, "pdfplumber", mock_pdfplumber)
|
|
|
|
tool = PDFExtractTool()
|
|
result = tool.execute(file_path=str(f), max_chars=100)
|
|
assert result.success is True
|
|
assert "[Content truncated]" in result.content
|
|
# The content before truncation marker should be <= max_chars
|
|
truncated_idx = result.content.index("\n\n[Content truncated]")
|
|
assert truncated_idx == 100
|
|
|
|
def test_pdf_extraction_error(self, tmp_path, monkeypatch):
|
|
f = tmp_path / "bad.pdf"
|
|
f.write_bytes(b"%PDF-1.4 corrupt")
|
|
|
|
mock_pdfplumber = MagicMock()
|
|
mock_pdfplumber.open.side_effect = RuntimeError("Corrupt PDF")
|
|
monkeypatch.setitem(sys.modules, "pdfplumber", mock_pdfplumber)
|
|
|
|
tool = PDFExtractTool()
|
|
result = tool.execute(file_path=str(f))
|
|
assert result.success is False
|
|
assert "PDF extraction error" in result.content
|
|
|
|
def test_to_openai_function(self):
|
|
tool = PDFExtractTool()
|
|
fn = tool.to_openai_function()
|
|
assert fn["type"] == "function"
|
|
assert fn["function"]["name"] == "pdf_extract"
|
|
|
|
|
|
class TestParsePages:
|
|
def test_single_page(self):
|
|
assert _parse_pages("3", 10) == [2]
|
|
|
|
def test_range(self):
|
|
assert _parse_pages("1-5", 10) == [0, 1, 2, 3, 4]
|
|
|
|
def test_comma_separated(self):
|
|
assert _parse_pages("1,3,5", 10) == [0, 2, 4]
|
|
|
|
def test_mixed(self):
|
|
result = _parse_pages("1-3,5", 10)
|
|
assert result == [0, 1, 2, 4]
|
|
|
|
def test_out_of_range_clamped(self):
|
|
# Page 20 is out of range for a 5-page doc
|
|
assert _parse_pages("20", 5) == []
|
|
|
|
def test_range_clamped_to_total(self):
|
|
# "1-100" on a 3-page doc should only return 3 pages
|
|
assert _parse_pages("1-100", 3) == [0, 1, 2]
|
|
|
|
def test_duplicates_removed(self):
|
|
result = _parse_pages("1,1,2,2", 5)
|
|
assert result == [0, 1]
|
|
|
|
def test_empty_string(self):
|
|
assert _parse_pages("", 5) == []
|