fix(review): lock OCR backend init, warn on rollback fallthrough, zero-page metric

Address PR #858 review round 2:

- OcrProcessor backend resolution is now guarded by an anyio.Lock (lazy-init,
  double-checked) so a burst of concurrent first-OCR calls resolves the backend
  once instead of each fetching its own gateway M2M token.
- The document_tier1_engine=pymupdf rollback now logs a warning when it falls
  back to the fast processor (no 'structured' registered) instead of silently
  using the very engine the operator opted out of.
- classify_from_text defaults ocr_frac to 0.0 (not 1.0) for a zero-page PDF, so
  the recorded classification metric is "fast" (no OCR evidence) rather than a
  misleading "ocr"; the no_text_layer/bad_text_layer flags are gated on having
  sampled at least one page.

New tests: zero-page classify routes fast, rollback-fallback warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-05 02:24:08 +02:00
co-authored by Claude Opus 4.8
parent 1634e8adc2
commit f1272dfe84
5 changed files with 55 additions and 11 deletions
+9
View File
@@ -176,3 +176,12 @@ def test_classify_from_text_empty_routes_ocr():
assert c.recommended_tier == "ocr"
assert "no_text_layer" in c.flags
assert c.total_chars == 0
def test_classify_from_text_no_pages_routes_fast():
# An empty/corrupt PDF (no page boundaries) is not OCR evidence -> "fast",
# so the recorded classification metric isn't a misleading "ocr".
c = clf.classify_from_text("", [])
assert c.recommended_tier == "fast"
assert c.ocr_page_fraction == pytest.approx(0.0)
assert c.flags == set()
+14
View File
@@ -92,6 +92,20 @@ async def test_engine_rollback_uses_structured(monkeypatch):
assert res.processor == "structured"
async def test_engine_rollback_warns_when_no_structured(monkeypatch, caplog):
# pymupdf rollback with no structured processor registered: it falls back to
# the fast processor but must warn (it silently used what the user opted out
# of otherwise).
monkeypatch.setattr(reg_mod, "get_settings", lambda: _Settings(engine="pymupdf"))
r = _registry((_Fake("fast", "fast"), 20))
with caplog.at_level(
"WARNING", logger="nextcloud_mcp_server.document_processors.registry"
):
res = await r.process(b"%PDF-1.7", "application/pdf")
assert res.processor == "fast"
assert any("no 'structured' processor" in rec.message for rec in caplog.records)
async def test_records_classification(monkeypatch):
monkeypatch.setattr(reg_mod, "get_settings", lambda: _Settings())
rec = MagicMock()