Files
PolyGateway/tests/integration/test_monkey_ocr_live.py
T
iomgaa 91671a77df test: pin OCR live tests to trust_env=false
httpx reads the macOS system proxy config (not just env vars) when
trust_env is on, and the proxy answers 403 for the LAN service at
10.77.0.20. The raw-httpx case in this file already passed
trust_env=False; the OcrClient cases relied on the default and failed on
any machine with a system proxy enabled.
2026-07-29 23:57:44 -04:00

143 lines
6.0 KiB
Python

"""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",
# 服务在 LAN,开发机若开着系统代理(httpx trust_env 读 macOS 系统配置,
# 不是环境变量),代理会对内网地址回 403 —— 与本文件 raw httpx 用例
# 显式传 trust_env=False 同因
f"OCR__MONKEY__{n}__TRUST_ENV": "false",
}
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