test(ingest): cover ocr-incluster routing + fix scan-gate & batch guard

Address claude-review on #922:

Blocking — test coverage for the new tier2 rung:
- test_registry_tiering.py: inline empty-text routes to ocr-incluster before
  ocr-upstream; only-incluster-enabled routes to incluster; disabled-incluster
  skips to upstream; evaluate_escalation empty_text hops to ocr-incluster (and
  falls through to upstream when incluster off); next_available_tier walks the
  full fast→structured→ocr-incluster→ocr-upstream ladder + ignore_ocr_enabled
  ideal-target.
- test_escalation_signature.py: enabling document_ocr_incluster_enabled changes
  the dead-letter signature (independent of the upstream rung).
- test_tiered_escalation_strategy.py: structured→ocr-incluster hops to
  INGEST_QUEUE_OCR_INCLUSTER; tier_for_queue covers the in-cluster queue.

Important — real fixes:
- registry.py: run scan detection (image_coverage_per_page) when EITHER OCR rung
  is enabled, not just the upstream one — a tenant with only in-cluster OCR on
  was missing image-coverage scan signals.
- ocr.py: the gateway_only (in-cluster) processor never enters batch mode — the
  GPU is synchronous/low-latency; batch OCR is the upstream Mistral async path.
  _get_batch_client short-circuits to None. Covered by a new test.

Nit:
- cli.py: worker --tier help lists ocr-incluster/ocr-upstream as separate fleets.

Left as-is: the lazy anyio.Lock init in OcrProcessor — instances ARE created at
module import (document_processors/__init__.py), so deferring lock creation off
import time is still required; moving it into __init__ would reintroduce the
import-time-primitive issue the comment guards against.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-18 00:06:46 +02:00
co-authored by Claude Opus 4.8
parent c21804fbbc
commit 87b8edd139
7 changed files with 209 additions and 4 deletions
+37
View File
@@ -435,6 +435,43 @@ def _wire_batch(monkeypatch, *, client, store, settings=None):
monkeypatch.setattr(_bos.BatchOcrJobStore, "shared", classmethod(_shared))
async def test_gateway_only_processor_never_uses_batch_mode(monkeypatch):
"""The in-cluster (gateway_only) rung targets the synchronous GPU; batch mode
is the upstream Mistral async-job path. _get_batch_client returns None and
never builds a batch client even with DOCUMENT_OCR_MODE=batch set globally —
while the upstream (gateway_only=False) processor still resolves one."""
called = {"n": 0}
def _spy(settings, **kw):
called["n"] += 1
return _FakeBatchClient()
monkeypatch.setattr(
ocr,
"get_settings",
lambda: _settings(
document_ocr_mode="batch",
document_ocr_provider="gateway",
embedding_gateway_url="https://gw",
document_ocr_incluster_model="surya/surya-ocr-2",
),
)
monkeypatch.setattr(ocr, "build_gateway_batch_client", _spy)
incluster = ocr.OcrProcessor(
name="ocr-incluster",
tier="ocr-incluster",
model_setting="document_ocr_incluster_model",
gateway_only=True,
)
assert await incluster._get_batch_client() is None
assert called["n"] == 0 # short-circuited before building anything
upstream = ocr.OcrProcessor() # gateway_only=False
assert await upstream._get_batch_client() is not None
assert called["n"] == 1
async def test_batch_first_run_submits_and_returns_pending_sentinel(monkeypatch):
client = _FakeBatchClient()
store = _FakeStore()