From 7e7dd2496286b445dc6e5423be98d169ee858b93 Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Sat, 13 Jun 2026 15:35:06 +0200 Subject: [PATCH] =?UTF-8?q?refactor(ingest):=20rename=20ignore=5Fenabled?= =?UTF-8?q?=E2=86=92ignore=5Focr=5Fenabled=20+=20empty/structured=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review round 4 (both nits): - Rename the flag to ignore_ocr_enabled so its OCR-specific scope is explicit at the call sites (the gate only bypasses the OCR-enabled check). - Add test_evaluate_escalation_empty_suppressed_even_when_structured_registered: empty_text (minimum='ocr') skips a registered structured tier and suppresses to ocr when OCR is off, never hopping to structured. Deck #324. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../document_processors/registry.py | 20 ++++++++++------ tests/unit/test_registry_tiering.py | 24 +++++++++++++++++++ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/nextcloud_mcp_server/document_processors/registry.py b/nextcloud_mcp_server/document_processors/registry.py index b7529dd7..33174409 100644 --- a/nextcloud_mcp_server/document_processors/registry.py +++ b/nextcloud_mcp_server/document_processors/registry.py @@ -398,7 +398,7 @@ class ProcessorRegistry: return classification def _tier_available( - self, tier: str, settings: Any, *, ignore_enabled: bool = False + self, tier: str, settings: Any, *, ignore_ocr_enabled: bool = False ) -> bool: """Whether ``tier`` can run a PDF parse right now. @@ -407,7 +407,7 @@ class ProcessorRegistry: (so OCR stays opt-in and a misconfigured tenant never escalates to a backend it hasn't turned on). - ``ignore_enabled`` drops only the OCR-enabled gate (not the registered- + ``ignore_ocr_enabled`` drops only the OCR-enabled gate (not the registered- processor requirement): it answers "would this tier run if OCR were turned on?" — used to compute the *ideal* escalation target for the what-if-OCR suppressed-escalation signal. (Today only ``ocr`` has an enabled gate; a @@ -415,7 +415,11 @@ class ProcessorRegistry: """ if self._pdf_processor_for_tier(tier) is None: return False - if not ignore_enabled and tier == "ocr" and not settings.document_ocr_enabled: + if ( + not ignore_ocr_enabled + and tier == "ocr" + and not settings.document_ocr_enabled + ): return False return True @@ -425,7 +429,7 @@ class ProcessorRegistry: settings: Any, *, minimum: str | None = None, - ignore_enabled: bool = False, + ignore_ocr_enabled: bool = False, ) -> str | None: """First escalation target above ``current_tier`` that can actually run. @@ -433,7 +437,7 @@ class ProcessorRegistry: ``minimum``'s rung, when given) and returns the first :meth:`_tier_available` tier. ``None`` means no higher tier can run -- ``current_tier`` is then terminal and its result is indexed as-is. - ``ignore_enabled`` is forwarded to :meth:`_tier_available` to find the + ``ignore_ocr_enabled`` is forwarded to :meth:`_tier_available` to find the *ideal* target ignoring the OCR-enabled gate (see ``evaluate_escalation``). """ try: @@ -447,7 +451,9 @@ class ProcessorRegistry: except ValueError: pass for tier in TIER_LADDER[start_idx:]: - if self._tier_available(tier, settings, ignore_enabled=ignore_enabled): + if self._tier_available( + tier, settings, ignore_ocr_enabled=ignore_ocr_enabled + ): return tier return None @@ -549,7 +555,7 @@ class ProcessorRegistry: # No tier can run as configured. Distinguish "disabled (e.g. OCR off)" # from "no such tier at all" by re-resolving ignoring the enabled gate. ideal = self.next_available_tier( - current_tier, settings, minimum=minimum, ignore_enabled=True + current_tier, settings, minimum=minimum, ignore_ocr_enabled=True ) if ideal is not None: return EscalationDecision("suppressed", ideal, reason) diff --git a/tests/unit/test_registry_tiering.py b/tests/unit/test_registry_tiering.py index 828dfa73..65d08768 100644 --- a/tests/unit/test_registry_tiering.py +++ b/tests/unit/test_registry_tiering.py @@ -498,3 +498,27 @@ def test_evaluate_escalation_terminal_when_ocr_unregistered_and_off(monkeypatch) processor="fast", ) assert r.evaluate_escalation(res, b"%PDF", "fast", _Settings(ocr=False)) is None + + +def test_evaluate_escalation_empty_suppressed_even_when_structured_registered( + monkeypatch, +): + """empty_text uses minimum='ocr', so it skips structured even when structured + IS registered: with OCR off it suppresses to ocr, never hops to structured + (a text extractor can't conjure text from a raster scan).""" + monkeypatch.setattr(reg_mod, "record_document_classification", MagicMock()) + r = _registry( + (_Fake("fast", "fast"), 20), + (_Fake("structured", "structured"), 10), # registered but skipped for empty + (_Fake("ocr", "ocr"), 5), + ) + res = ProcessingResult( + text="", + metadata={ + "page_count": 1, + "page_boundaries": [{"page": 1, "start_offset": 0, "end_offset": 0}], + }, + processor="fast", + ) + decision = r.evaluate_escalation(res, b"%PDF", "fast", _Settings(ocr=False)) + assert decision == EscalationDecision("suppressed", "ocr", "empty_text")