"""Unit tests for the per-tier PDF parse + escalation gate (Deck #323). ``processor._parse_pdf_tier`` runs one tier and either returns the result to index or raises ``EscalateError`` (a queue-hop). These exercise the decision without standing up the full ingest pipeline. """ from unittest.mock import AsyncMock, MagicMock import pytest from nextcloud_mcp_server.document_processors.base import ProcessingResult from nextcloud_mcp_server.document_processors.escalation import EscalateError from nextcloud_mcp_server.vector import processor pytestmark = pytest.mark.unit def _registry(result: ProcessingResult, decision): reg = MagicMock() reg.process_tier = AsyncMock(return_value=result) reg.evaluate_escalation = MagicMock(return_value=decision) return reg async def test_good_parse_returns_result(monkeypatch): rec = MagicMock() monkeypatch.setattr(processor, "record_document_escalation", rec) result = ProcessingResult(text="clean", metadata={}, processor="fast") reg = _registry(result, decision=None) out = await processor._parse_pdf_tier( reg, b"%PDF", "application/pdf", "f.pdf", "fast", settings=object() ) assert out is result rec.assert_not_called() async def test_low_quality_parse_raises_escalate(monkeypatch): rec = MagicMock() monkeypatch.setattr(processor, "record_document_escalation", rec) result = ProcessingResult(text="", metadata={}, processor="fast") reg = _registry(result, decision=("ocr", "empty_text")) with pytest.raises(EscalateError) as ei: await processor._parse_pdf_tier( reg, b"%PDF", "application/pdf", "f.pdf", "fast", settings=object() ) assert ei.value.from_tier == "fast" assert ei.value.to_tier == "ocr" assert ei.value.reason == "empty_text" # The escalation is recorded at the decision point. rec.assert_called_once_with("fast", "ocr", "empty_text") async def test_hard_failure_returns_result_without_escalating(monkeypatch): rec = MagicMock() monkeypatch.setattr(processor, "record_document_escalation", rec) result = ProcessingResult( text="", metadata={"parse_failed_reason": "oversize"}, processor="size_guard", success=False, ) reg = _registry(result, decision=("ocr", "empty_text")) out = await processor._parse_pdf_tier( reg, b"%PDF", "application/pdf", "big.pdf", "fast", settings=object() ) # success=False short-circuits: the gate is never consulted, no escalation. assert out is result reg.evaluate_escalation.assert_not_called() rec.assert_not_called()