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 才验得出来。
This commit is contained in:
+38
-3
@@ -21,6 +21,11 @@ dependencies = []
|
||||
# 模型适配器单独成一个 extra:不用它的人不该被迫装上网关,也不该在 import 时把网关连同
|
||||
# 它的 provider 目录一起拉起来(依赖规则 9)。PolyGateway 不在公共 PyPI 上,装它要配私有源。
|
||||
gateway = ["polygateway>=1.1,<2"]
|
||||
# 契约套件(polyloop.testing)跑起来要的两个包。**版本只写下界,不钉死**——这和下面 dev 那组
|
||||
# 正好相反,理由也正好相反:dev 是本仓库自己的工具链,钉死是为了本地和 CI 跑的是同一套;
|
||||
# testing 装进的是下游自己的环境,钉死会和下游已经在用的 pytest 打架,而下游没有第二个
|
||||
# 虚拟环境可以放我们钉的那一版。下次想「顺手统一一下」这两组的写法时,先看这段。
|
||||
testing = ["pytest>=8", "pytest-asyncio>=0.24"]
|
||||
# dev 工具链的版本必须钉死,不用下界。教训来自 CHSAnalyzer:本地 conda 是 ruff 0.15.1、
|
||||
# CI 装到刚发布的 0.16.0,新版本多了几条规则,于是「本地全绿、CI 全红」。不钉的话 CI 会在
|
||||
# 没有任何人改代码的情况下随上游发版随机变红,而随机的红叉很快就会训练所有人忽略红叉。
|
||||
@@ -43,6 +48,13 @@ Homepage = "https://gitea.iomgaa.online/iomgaa/PolyLoop"
|
||||
Changelog = "https://gitea.iomgaa.online/iomgaa/PolyLoop/src/branch/main/CHANGELOG.md"
|
||||
Issues = "https://gitea.iomgaa.online/iomgaa/PolyLoop/issues"
|
||||
|
||||
# pytest 插件入口。它换回来的是**断言重写**:契约模块不在下游的 `python_files` 匹配范围里,
|
||||
# 默认不被重写,于是一条契约用例失败时下游只看到光秃秃的 AssertionError,没有等号两边的值。
|
||||
# 声明了 pytest11 入口的发行包,它的每个 .py 文件都会被 pytest 标记为可重写。
|
||||
# 不做的话没有任何东西会报错——纯静默退化,理由与边界写在 polyloop/testing/_plugin.py 里。
|
||||
[project.entry-points.pytest11]
|
||||
polyloop = "polyloop.testing._plugin"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
@@ -94,13 +106,14 @@ markers = [
|
||||
addopts = "--strict-markers --import-mode=importlib -m 'not e2e'"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 依赖规则的机器形式。九条规则本身与它们各自的理由在
|
||||
# 依赖规则的机器形式。十条规则本身与它们各自的理由在
|
||||
# research-wiki/design/0003-public-api-shape.md 决策八,当前形状在
|
||||
# research-wiki/explanation/architecture.md 第七节。这里只写契约,不复述理由。
|
||||
#
|
||||
# 九条里有两条写不成 import-linter 契约,它们是 tests/ 里的测试:
|
||||
# 十条里有两条写不成 import-linter 契约,还有一条只有前半写得成;写不成的那些是 tests/ 里的测试:
|
||||
# 规则 6(types 与 ports 禁止 import 任何第三方包)——「任何第三方」不是一份可枚举的清单。
|
||||
# 规则 9(import polyloop 之后 sys.modules 里没有 polygateway)——那是运行时事实,不是静态图。
|
||||
# 规则 10 的后半(import polyloop 之后 sys.modules 里没有 pytest)——同上。前半在下面。
|
||||
# ---------------------------------------------------------------------------
|
||||
[tool.importlinter]
|
||||
root_packages = ["polyloop"]
|
||||
@@ -115,7 +128,7 @@ include_external_packages = true
|
||||
name = "分层:装配层 > 逻辑层 > ports > types,且同层互不 import"
|
||||
type = "layers"
|
||||
layers = [
|
||||
"polyloop.session | polyloop.stores | polyloop.adapters",
|
||||
"polyloop.session | polyloop.stores | polyloop.adapters | polyloop.testing",
|
||||
"polyloop.tools | polyloop._assembly | polyloop._stopping | polyloop._recovery | polyloop.serialization",
|
||||
"polyloop.ports",
|
||||
"polyloop.types",
|
||||
@@ -131,6 +144,7 @@ forbidden_modules = [
|
||||
"polyloop.session",
|
||||
"polyloop.stores",
|
||||
"polyloop.adapters",
|
||||
"polyloop.testing",
|
||||
"polyloop.tools",
|
||||
"polyloop._assembly",
|
||||
"polyloop._stopping",
|
||||
@@ -153,6 +167,7 @@ type = "forbidden"
|
||||
source_modules = [
|
||||
"polyloop.session",
|
||||
"polyloop.stores",
|
||||
"polyloop.testing",
|
||||
"polyloop.tools",
|
||||
"polyloop._assembly",
|
||||
"polyloop._stopping",
|
||||
@@ -163,6 +178,26 @@ source_modules = [
|
||||
]
|
||||
forbidden_modules = ["polygateway"]
|
||||
|
||||
# 规则 10 的前半。契约套件是 polyloop 里唯一允许碰 pytest 的地方,形状照着上面那条 polygateway 写。
|
||||
# 别处 import pytest 的后果是每个装了本库的下游都被迫装上 pytest 才 import 得动 polyloop,
|
||||
# 而 pytest 只在 testing 这个 extra 里,核心的 dependencies 是空的。
|
||||
[[tool.importlinter.contracts]]
|
||||
name = "除 testing 外一切禁止 import pytest"
|
||||
type = "forbidden"
|
||||
source_modules = [
|
||||
"polyloop.session",
|
||||
"polyloop.stores",
|
||||
"polyloop.adapters",
|
||||
"polyloop.tools",
|
||||
"polyloop._assembly",
|
||||
"polyloop._stopping",
|
||||
"polyloop._recovery",
|
||||
"polyloop.serialization",
|
||||
"polyloop.ports",
|
||||
"polyloop.types",
|
||||
]
|
||||
forbidden_modules = ["pytest"]
|
||||
|
||||
# 规则 7。这一条是烟雾报警,不是纯度契约——os、subprocess、sqlite3 都能绕过它,
|
||||
# 而 open() 是内置函数,import-linter 根本看不见。它拦得住最常见的那种偷懒
|
||||
# (写着写着顺手 await 一下存储、顺手读个文件),拦不住存心的。
|
||||
|
||||
Reference in New Issue
Block a user