"""MonkeyOCR 真实服务集成测试(M3 计划 T7;ROADMAP §4 验收出口)。 前置: LAN 服务 10.77.0.20:7866/7867(不可达即 FAIL——验收必打真实,不 skip); 语料 `data/soak/chs_images/`(仅实验室机器有,缺失 skip); Redis 组合用例沿既有约定: 缺 REDIS_URL 时 skip。 与 soak 不并跑(Redis db3 会被 FLUSHDB)。 """ import os import time from pathlib import Path import httpx import pytest from dotenv import dotenv_values from polygateway.ocr import OcrClient from polygateway.transports.monkey_ocr import _parse_middle_json _IMAGES = Path("data/soak/chs_images") _TABLE_IMAGE = _IMAGES / "chs_0001.jpg" # 取证已核: 有表 _PLAIN_IMAGE = _IMAGES / "chs_0002.jpg" # 取证已核: 无表 _PRIMARY = "http://10.77.0.20:7866" _SECONDARY = "http://10.77.0.20:7867" _BLACKHOLE = "http://10.255.255.1:7866" _REDIS_URL = os.environ.get("REDIS_URL") or dotenv_values(".env").get("REDIS_URL") pytestmark = pytest.mark.skipif( not _TABLE_IMAGE.exists(), reason="需实验室语料 data/soak/chs_images(见 findings §6)" ) def _env(sources: dict[int, str], **extra: str) -> dict[str, str]: env = { "LLM_MAX_RETRIES": "3", "LLM_RETRY_BASE_DELAY": "1.0", "LLM_RETRY_MAX_DELAY": "5.0", "LLM_CIRCUIT_BREAKER_THRESHOLD": "2", "LLM_CIRCUIT_BREAKER_COOLDOWN": "30", "PGW_CACHE_BACKEND": "none", "PGW_TELEMETRY_BACKEND": "none", } for n, base_url in sources.items(): env |= { f"OCR__MONKEY__{n}__BASE_URL": base_url, f"OCR__MONKEY__{n}__API_KEY": "none", f"OCR__MONKEY__{n}__MODEL": "monkey-ocr", f"OCR__MONKEY__{n}__TIMEOUT_S": "300", } env.update(extra) return env class TestLiveEndpoints: async def test_recognize_text_returns_content(self): async with OcrClient.from_env("OCR", env=_env({1: _PRIMARY})) as client: result = await client.recognize_text(_TABLE_IMAGE.read_bytes()) assert result.text.strip() # 真实护理记录图必有文字 assert result.source_name == "monkey_1" assert result.usage.prompt_tokens == 0 and result.latency_ms > 0 async def test_parse_layout_table_and_plain(self): async with OcrClient.from_env("OCR", env=_env({1: _PRIMARY})) as client: with_table = await client.parse_layout(_TABLE_IMAGE.read_bytes()) plain = await client.parse_layout(_PLAIN_IMAGE.read_bytes()) tables = [e for e in with_table.elements if e.type == "table"] assert tables, "取证样本 chs_0001 应检出表格" x1, y1, x2, y2 = tables[0].bbox assert x2 > x1 and y2 > y1 and with_table.page_sizes[0][0] > 0 # 无表样本: elements 无 table 且不抛异常(合法"无表"语义) assert all(e.type != "table" for e in plain.elements) async def test_middle_json_consistency_guard(self): """设计 §1.2 持续护栏: tables 列表与库全元素提取的 table bbox 一致。""" async with httpx.AsyncClient(base_url=_PRIMARY, trust_env=False, timeout=300) as raw: resp = await raw.post( "/parse", files={"file": ("image.jpg", _TABLE_IMAGE.read_bytes(), "image/jpeg")} ) resp.raise_for_status() zip_resp = await raw.get(resp.json()["download_url"]) zip_resp.raise_for_status() elements, _ = _parse_middle_json(zip_resp.content) lib_tables = {e.bbox for e in elements if e.type == "table"} import io import json import zipfile with zipfile.ZipFile(io.BytesIO(zip_resp.content)) as archive: middle = next(n for n in archive.namelist() if n.endswith("_middle.json")) payload = json.load(archive.open(middle)) raw_tables = { tuple(float(v) for v in t["bbox"]) for page in payload["pdf_info"] for t in page.get("tables", []) } assert lib_tables == raw_tables and raw_tables async def test_check_health_per_source(self): env = _env({1: _PRIMARY, 2: _SECONDARY, 3: _BLACKHOLE}) started = time.monotonic() async with OcrClient.from_env("OCR", env=env) as client: health = await client.check_health() elapsed = time.monotonic() - started assert health["monkey_1"] is True assert health["monkey_2"] is True assert health["monkey_3"] is False # 黑洞: 5s 探测超时判 False assert elapsed < 30 # 走探测常量 5s,不等 TIMEOUT_S=300 @pytest.mark.skipif(not _REDIS_URL, reason="需实验室远程 Redis: 在 .env 设置 REDIS_URL") class TestRedisGovernanceCombo: def _redis_env(self, sources): return _env( sources, PGW_LIMITER_BACKEND="redis", PGW_BREAKER_BACKEND="redis", REDIS_URL=_REDIS_URL, ) async def test_real_call_through_redis_backends(self): async with OcrClient.from_env("OCR", env=self._redis_env({1: _PRIMARY})) as client: result = await client.recognize_text(_PLAIN_IMAGE.read_bytes()) assert result.text is not None async def test_blackhole_pool_circuit_opens_with_retry_after(self): """G1 真实后端联调: 全池熔断后 retry_after_s 来自 Redis gate。""" from polygateway.errors import GatewayUnavailableError env = self._redis_env({1: _BLACKHOLE}) env["OCR__MONKEY__1__TIMEOUT_S"] = "3" env["OCR__RETRY__MAX_ATTEMPTS"] = "2" async with OcrClient.from_env("OCR", env=env) as client: with pytest.raises(GatewayUnavailableError) as ei: await client.recognize_text(_PLAIN_IMAGE.read_bytes()) exc = ei.value assert exc.per_source_reasons # G1: 逐源原因非空 assert exc.retry_after_s >= 0