Files
iomgaa 1fac387e75 feat(testing): 契约套件搬进 polyloop.testing 随包发布,五套全部接上实现
tests/ 不进 wheel,所以那套被 CLAUDE.md §0 称作「任何新适配器的准入标准」的用例,第一个
下游根本拿不到。**接法同时换掉**:pytest 的 conftest 只沿被收集文件的目录链查找,装在
site-packages 里的测试模块看不见下游的 conftest,原来那个「在自己的 conftest 里覆盖同名
fixture」的接法在发布之后走不通。改成继承契约基类,下游的子类定义在自己的目录链上。

**搬的过程中发现这套准入标准从来没被执行过。** test_model_client.py 有四条用例调用
records.model_call(...),而工厂里根本没有这个方法——它没炸是因为那个 fixture 默认 skip。
五个接缝里只有存储那套被真跑过(15 条跳过里有 15 条是这四套)。

所以这个提交的另一半是让它真的跑起来。存储接两个实现(一份契约同时验多个实现,正是换接法
换来的);动作执行接注册表分发器,外加一个有真实等待点的替身,否则那条取消用例的断言半边
永远走不到;模型调用接网关适配器,落在 integration,它连的是真网关;决策解释与事件出口各
接一个测试替身——替身住在 tests/ 里不进 wheel,下游拿不到,所以不违反「库不带默认实现」,
判据是下游拿不拿得到。

**一并清掉两类坏用例。** 五条函数体只有 docstring、一个断言都没有却报 PASSED 的假绿——一个
准入标准里出现假绿比出现跳过糟得多,下游看到全绿会以为验过了。以及一条端口从没承诺过的
长度断言(len(history_text) <= len(reply.content)):压测的 AppWorld 场景为了迁就它,刻意
不补被复刻的实现真的会补的三个反引号,注释里写着「补一个字符就违约」。七条「这一层验不了」
统一成无条件 skip,理由字符串写全「承诺是什么/为什么验不了/你该在哪儿自己验」。

**发一个 pytest11 entry point,只为换回断言重写。** 契约模块不在下游的 python_files 里,
默认不被重写,于是一条契约失败时下游看到的是光秃秃的 AssertionError。不做的话没有任何东西
会报错,纯静默退化。实测过:editable 安装下 entry point 注册了但重写不生效(RECORD 里没有
包文件),要装真 wheel 才验得出来。
2026-08-27 03:59:16 -04:00

80 lines
3.3 KiB
Python
Raw Permalink 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.
"""包基线:版本号双写一致,以及 import 本库不会把网关拉起来。
**这个文件读源码文本和 `pyproject.toml`,不是只跑被测代码。** 它仍然属于 unit,因为
分层判据是「依赖什么」(`CLAUDE.md` §1.9),而它不连任何外部服务。
"""
import subprocess
import sys
import tomllib
from pathlib import Path
import pytest
import polyloop
REPO_ROOT = Path(__file__).resolve().parents[2]
pytestmark = pytest.mark.unit
def test_version_matches_pyproject() -> None:
"""`__version__` 与 `pyproject.toml` 的 version 必须一致。
两处双写是刻意的(见 `polyloop/__init__.py`),代价就是会漂移,所以用这条断言钉住。
漂移的表现是下游报 bug 时说的版本号和实际装的不是一个,而那种错查起来要绕很远。
"""
declared = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
assert polyloop.__version__ == declared["project"]["version"]
def test_importing_polyloop_does_not_import_polygateway() -> None:
"""依赖规则 9`import polyloop` 之后 `sys.modules` 里不许出现 `polygateway`。
这条写不成 import-linter 契约——它是运行时事实,不是静态图上的边。适配器模块允许
import 网关(规则 5),所以静态图上那条边合法;这里要禁的是**顶层 import 时就把它
拉起来**。一个顺手提供的默认模型客户端会让每个进程在 import 本库时,把网关连同它的
provider 目录一起加载。
在子进程里跑,因为本进程早就 import 过别的东西了,`sys.modules` 不干净。
"""
code = "import polyloop, sys; print('polygateway' in sys.modules)"
result = subprocess.run( # noqa: S603
[sys.executable, "-c", code],
capture_output=True,
text=True,
check=True,
cwd=REPO_ROOT,
)
assert result.stdout.strip() == "False", result.stdout
def test_importing_polyloop_does_not_import_pytest() -> None:
"""`import polyloop` 之后 `sys.modules` 里不许出现 `pytest`。
契约套件 `polyloop.testing` 顶层就 import pytest,而顶层包不 re-export 它——它和
`stores`、`adapters` 同一档,必须显式 import。少了这条断言,哪天有人顺手把 `testing`
加进 `polyloop/__init__.py`,每个下游的运行时就都被拽上一个 pytest 依赖,而 pytest 在
生产环境里通常根本没装,表现是下游一 import 本库就 `ModuleNotFoundError`。
和上面那条 polygateway 同构,也同样在子进程里跑:本进程早就 import 过 pytest 了。
"""
code = "import polyloop, sys; print('pytest' in sys.modules)"
result = subprocess.run( # noqa: S603
[sys.executable, "-c", code],
capture_output=True,
text=True,
check=True,
cwd=REPO_ROOT,
)
assert result.stdout.strip() == "False", result.stdout
def test_py_typed_marker_ships_with_the_package() -> None:
"""`py.typed` 必须在包根里。
少了它,下游的类型检查器把整个包当成无注解的黑盒——而这个库对下游的承诺大半写在
签名里。这个文件是空的,很容易在某次移动目录时丢掉且不报错。
"""
assert (REPO_ROOT / "src" / "polyloop" / "py.typed").is_file()