1fac387e75
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 才验得出来。
80 lines
3.3 KiB
Python
80 lines
3.3 KiB
Python
"""包基线:版本号双写一致,以及 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()
|