Files
PolyLoop/src/polyloop/testing/__init__.py
T
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

82 lines
4.6 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.
"""五个接缝的契约套件,随包发布,给下游当准入标准用。
每个接缝一个基类。这套用例不针对任何具体实现,写的是「不管你怎么实现,都必须满足这些
行为」——所以它自己不造实现,只声明「你得提供什么」。写完自己的存储或适配器之后,在自己的
测试文件里继承对应的基类、覆盖那几个必需 fixture,跑一遍全绿就算合格。
# tests/test_my_store.py
import pytest
from polyloop.testing import RunStoreContract
from myproject.storage import MyPostgresStore
class TestMyPostgresStore(RunStoreContract):
@pytest.fixture
def store(self, pg_pool):
return MyPostgresStore(pg_pool)
子类的类名要以 `Test` 开头,pytest 才收集它。基类自己叫 `...Contract` 正是为了不被收集:
pytest 按 `Test` 前缀匹配测试类,一个叫 `RunStoreContract` 的基类不会被当成测试类跑一遍,
于是它那些 `raise NotImplementedError` 的默认 fixture 也就不会失败。
**装它**`pip install "polyloop[testing]"`。
**必需的 fixture 在基类里都有一个默认实现,函数体是 `raise NotImplementedError`**,报错消息
写着该覆盖什么、该返回什么。忘了覆盖时看到的是这条消息,而不是 pytest 那句
`fixture 'store' not found` 加一整屏 available fixtures 列表——后者指向的是 site-packages 里
的库文件,第一反应会是库坏了。
**样本输入由实现方提供**,见 `DecisionParserContract.reply_samples`、
`ActionExecutorContract.action_samples` 与 `ModelClientContract.failing_call` 的 docstring。
套件不认识任何一家的动作语言,也不知道一家实现要怎样才会失败:拿一家的代码围栏去喂另一家的
JSON 解析器,后者正确地返回「无效决策」,而写死输入的套件会把这个正确行为判成失败。
**实现之间的能力差异走运行期跳过,不走失败。** 取消那两条就是这么处理的——一个在单个事件
循环 tick 之内就返回的实现根本没有机会吞掉取消,那条契约对它无从谈起,跳过的理由字符串会
说清楚这一点。看到这种跳过不必去改自己的实现。
**async 用例的事件循环归下游管。** 套件里的用例照常写成 `async def`,套件不做任何事件循环
安排。所以要么把 `asyncio_mode` 设成 `"auto"``pyproject.toml` 的
`[tool.pytest.ini_options]` 里),要么自己给子类打上对应的标记。没配对的失败是响亮的:
pytest 会明说 async def 函数不被原生支持,那条信息直接指向解法。
库不替下游安排循环,是因为下游的 fixture 很可能是异步的——一个数据库存储实现的连接池就是。
那个 fixture 在下游的循环里创建,库自己开的循环是另一个,跨循环使用 asyncio 对象会炸,而且
炸得很难查。
**`PYTEST_DISABLE_PLUGIN_AUTOLOAD=1` 的环境要多写一行。** 本包声明了一个 pytest 插件入口,
它唯一的作用是让 pytest 重写这些模块里的 `assert`,失败时打印出等号两边的实际值。那个环境
变量一设上,入口就不加载了,契约失败会退化成光秃秃的 `AssertionError`。这时在自己的
`conftest.py` 顶上补一行:
pytest.register_assert_rewrite("polyloop.testing")
**套件里一个自定义 marker 都不用。** 开了 `--strict-markers` 而没注册那个 marker 的话,炸掉
的是整个测试文件的收集,不是一条失败,而报错指向的同样是库的文件。要给这些用例分组就在自己
的子类文件上打标记,那个文件和那个 marker 都在你自己的仓库里。
**这里的每个名字都是公共承诺**:基类名、基类上的 fixture 名、每一条用例的方法名。下游的子类
按它们写,改名的代价和改公共类型的字段一样。用例方法名尤其要紧——下游要豁免某一条时按名字
重绑它,改名之后那个豁免会悄悄失效,跑出来照样全绿。
设计与取舍见 `research-wiki/design/0014-contract-suite-distribution.md`。
"""
from polyloop.testing._action_executor import ActionExecutorContract
from polyloop.testing._decision_parser import DecisionParserContract
from polyloop.testing._event_sink import EventSinkContract
from polyloop.testing._model_client import ModelClientContract
from polyloop.testing._records import RecordFactory
from polyloop.testing._run_store import RunStoreContract
__all__ = [
"ActionExecutorContract",
"DecisionParserContract",
"EventSinkContract",
"ModelClientContract",
"RecordFactory",
"RunStoreContract",
]