Files
crawler-plugin/backend/tests/test_python_java_http_contract.py
T
huangzd1997 02cff8ea63 task-175: Python 回调/代理超时与重试契约冻结(只读确认,Python 零改动)+ 8 条断言
- 锁定 Python→Java HTTP 面(admin_api._get_backend_java_session/_proxy_backend_java):默认 timeout=10、HTTPAdapter(max_retries=0)、超时/失败转 502
- 断言 Java aiimage.http-client.* 治理不外溢到 Python(非测试 Python 无引用、代理签名超时为字面量)
- 纯测试,不改任何生产 Python 代码
2026-09-04 23:25:59 +08:00

118 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""只读契约冻结:Python→Java HTTP 回调/代理侧超时与重试不受 Java 配置影响(task-175)。
背景:模块 10 的 Java 外部客户端配置治理只动 Java 内部客户端(Coze/品牌/紫鸟/图片下载),
spec §3 明确「不改 Python Worker 的 requests 调用 / 不改 Python 回调请求超时和重试约定」。
本仓库内 Python 对 Java 的 HTTP 面是 blueprints.admin_api 的 _get_backend_java_session /
_proxy_backend_javarequests.Session + HTTPAdapter(max_retries=0),默认 timeout=10s
全部为 Python 侧字面量/参数默认值,不读取任何 aiimage.http-client.* Java 配置。
本文件把这些约定固化为快照断言,防止将来误把 Java 配置引进来改变 Python 行为。
只读任务:不改任何生产 Python 代码,只新增本测试。
"""
import inspect
import sys
import unittest
from pathlib import Path
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from blueprints import admin_api
class _FakeSession:
"""记录请求的伪 requests.Session:捕获 kwargs,返回 Java ApiResponse 成功体。"""
def __init__(self, raise_on_request=False):
self.calls = []
self.raise_on_request = raise_on_request
def request(self, method, url, **kwargs):
self.calls.append((method, url, kwargs))
if self.raise_on_request:
import requests
raise requests.RequestException("backend-java 服务不可用")
class _Resp:
status_code = 200
def json(self):
return {"success": True, "data": []}
return _Resp()
class PythonJavaHttpContractTest(unittest.TestCase):
def _proxy_default_http_timeout(self):
return inspect.signature(admin_api._proxy_backend_java).parameters["timeout"].default
def test_default_timeout_is_python_side_ten_seconds(self):
# 契约:未显式传 timeout 时默认 (连接/读) 10s,且是签名里的字面量,非来自任何配置
self.assertEqual(10, self._proxy_default_http_timeout())
self.assertTrue(isinstance(self._proxy_default_http_timeout(), int))
def test_override_timeout_forwarded_verbatim(self):
fake = _FakeSession()
with patch.object(admin_api, "_get_backend_java_session", return_value=fake):
result, error_response, status = admin_api._proxy_backend_java(
"GET", "/api/foo", params={"a": "1"}, timeout=(10, 1800))
self.assertIsNone(error_response)
method, url, kwargs = fake.calls[0]
self.assertEqual((10, 1800), kwargs["timeout"], "显式超时应原样转发给 requests")
def test_session_never_auto_retries(self):
# 契约:Python→Java session 不自动重试(max_retries=0),失败即报错由上层处理
session = admin_api._get_backend_java_session()
http_adapter = session.get_adapter("http://")
self.assertEqual(0, http_adapter.max_retries.total)
def test_session_mounts_http_and_https(self):
session = admin_api._get_backend_java_session()
self.assertIsNotNone(session.get_adapter("http://"))
self.assertIsNotNone(session.get_adapter("https://"))
def test_timeout_is_not_read_from_java_http_client_config(self):
# 契约:Python 侧无 aiimage.http-client.* 读取点;超时仅来自参数/字面量
source = inspect.getsource(admin_api._proxy_backend_java)
self.assertNotIn("http-client", source)
self.assertNotIn("AIIMAGE_HTTP_CLIENT", source)
self.assertNotIn("connect-timeout", source)
def test_timeout_or_connection_failure_maps_to_502(self):
# 契约:Python→Java 超时/连接失败统一转 502,不静默吞掉也不自动重试
fake = _FakeSession(raise_on_request=True)
with patch.object(admin_api, "_get_backend_java_session", return_value=fake):
with admin_api_app_context():
result, error_response, status = admin_api._proxy_backend_java("GET", "/api/foo")
self.assertIsNone(result)
self.assertEqual(502, status)
def test_java_config_namespace_absent_in_python_sources(self):
# 快照:仓库非测试 Python 代码不存在 aiimage.http-client 配置引用,Java 治理不会外溢
repo_py_root = Path(admin_api.__file__).resolve().parents[2]
hits = []
for py in repo_py_root.rglob("*.py"):
if "__pycache__" in str(py) or "/tests/" in str(py).replace("\\", "/"):
continue
text = py.read_text(encoding="utf-8", errors="ignore")
if "aiimage.http-client" in text or "aiimage_http_client" in text:
hits.append(str(py))
self.assertEqual([], hits, f"Python 侧不应引用 Java 统一命名空间: {hits}")
def test_contract_frozen_documented_values_match_code(self):
# 自检快照:文档化的 Python 回调契约(timeout=10 / max_retries=0)与代码一致
self.assertEqual(10, self._proxy_default_http_timeout())
session = admin_api._get_backend_java_session()
self.assertEqual(0, session.get_adapter("http://").max_retries.total)
self.assertEqual(0, session.get_adapter("https://").max_retries.total)
def admin_api_app_context():
from flask import Flask
app = Flask(__name__)
return app.app_context()
if __name__ == "__main__":
unittest.main()