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 代码
This commit is contained in:
2026-09-04 23:25:59 +08:00
parent 2a2d23e0dc
commit 02cff8ea63
@@ -0,0 +1,117 @@
"""只读契约冻结: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()