契约测试套件不随包发布,第一个下游拿不到准入标准 #1
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
我们是谁
dissect2(
/home/iomgaa/Projects/dissect2)——dissect 的第二版,正在重建。它要接 PolyLoop 当执行内核,会是 PolyLoop 的第一个真实使用者。分工先说清楚:我们只提需求,不改这个仓库。下面每一条都带了
文件:行号,方便你们核。问题
tests/contract/那套公共行为一致性套件不随包发布,pip install polyloop之后拿不到。pyproject.toml:46-47:tests/不在src/下,所以不进 wheel。而
tests/contract/conftest.py:1-12写明了这套套件的定位:下游在自己的conftest.py里覆盖同名 fixture、返回自己的实现,跑一遍就算合格。也就是说它是下游接缝实现的准入标准。两件事凑在一起的结果是:准入标准存在,但要通过准入的人拿不到它。
为什么这对我们是第一天就要用的东西
我们至少要自己实现三个接缝:
DecisionParser(我们的动作语言是「模型回复里的一个 Python 代码围栏」,不是结构化 tool call)、ActionExecutor(AppWorld 会话)、EventSink。如果后面换掉存储,就是四个。我们是论文项目,一次运行的轨迹就是论文的原始数据。接缝实现有 bug 的表现不是报错,是数据静默地不对——比如某一步该记没记、观察被替换了没标记。这类问题只有契约套件查得出来,而我们自己写的测试查不出来,因为我们不知道库那边保证了什么。
绕过去的代价
把
tests/contract/拷贝一份到我们仓库里。代价是拷出去的那份从此不跟着升级——你们改了契约、加了用例,我们不会知道。半年后我们那份和你们的实际行为已经不是一回事,而它还在绿着。一个可能的做法(不强加,你们定)
把契约用例从
tests/contract/搬进src/polyloop/testing/,作为一个正式的公开子包和实现一起发布、一起升版本。下游写:另一种是注册成 pytest plugin 的 entry point,下游装了库就能直接
pytest --polyloop-contract。外部参考:Pi(
reference/pi/)的 telemetry 包就是这么做的——在exports里多开一个./testing子路径,一致性测试和实现同包发布。相关
这一条和另一个 issue(stores 的两个欠账)是连着的:README 阶段⑤说关系数据库那种形态由下游自己实现、契约套件是它的准入标准,而准入标准正是这里拿不到的这个。
1.0.2 已发布,这一条做完了。
套件搬进了
polyloop.testing,跟着 wheel 一起装到你们那边:那个 extra 只装 pytest 与 pytest-asyncio,版本只写下界,不会和你们已经在用的 pytest 打架。
接法换了,不是「覆盖同名 fixture」
你们提的那个做法在发布之后走不通,这一点值得单独说:pytest 的
conftest.py只沿着被收集文件的目录链往上找,而套件装在 site-packages 里,那条链上没有你们仓库的 conftest。就算我们把文件原样发出去,你们还是得先拷进自己的tests/才接得上——而拷贝正是这个 issue 说不想要的。改成继承契约基类,子类定义在你们自己的测试文件里,fixture 在类作用域内覆盖是 pytest 的原生行为:
顺带一个好处对你们直接有用:一个接缝可以接多个实现,各写一个子类。你们那边如果 SQLite 和别的形态并存,同一套契约同时验两个。
用法、每个基类要哪些 fixture、
asyncio_mode要怎么设、PYTEST_DISABLE_PLUGIN_AUTOLOAD=1的环境要补哪一行,全写在polyloop/testing/__init__.py的 docstring 里——和套件同一个文件,不会漂。三件你们接上去之前该知道的
一、这套东西在此之前一次都没有被执行过。
test_model_client.py有四条用例调用records.model_call(...),而那个工厂里根本没有这个方法——它没炸是因为那个 fixture 默认pytest.skip。五个接缝里只有存储那套被真跑过。你们接上去的第一件事会是AttributeError。 已经补上;而且这一版把五套契约全部接了实现(库自带三个 + 两个测试替身),每一条用例至少被真求值过一次。二、契约层原来那 15 条「通过」里有 5 条是假绿——函数体只有 docstring、一个断言都没有,pytest 照样报 PASSED。已经和另外两条
xfail一起统一成无条件pytest.skip,理由字符串写全三件事:这条承诺是什么、为什么这一层验不了、你们该在哪儿自己验。所以你们跑出来会看到若干跳过,那是诚实的「这条没验」,pytest -rs把每条理由逐行列出来。三、撤掉了一条我们从没承诺过的断言。 原来有一条
len(history_text) <= len(reply.content)——而端口对history_text只承诺「解释器有权改写它」,一个字都没提长度。一个把 JSON 工具调用规范化成文本、或者补回被 stop 序列截断的三反引号的解析器都会超长。这条以前从没被执行过,搬进包里等于第一次把它变成对你们有约束力的条款,所以撤了。(我们自己的压测场景就为了迁就它刻意偏离过被复刻的实现,注释里白纸黑字写着「补一个字符就违约」。)一处缺口先说在前面
决策解释与事件出口那两套契约,我们这边接的是测试替身,不是真实现——库不带这两个接缝的实现,带了就等于替某一家定了动作语言或投递协议。替身证明用例本身写得对,证明不了「一个真实的第三方实现接上来也说得通」。你们是第一个接的,撞到问题请回来说,那是套件的问题不是你们的。
设计与全部取舍在
research-wiki/design/0014-contract-suite-distribution.md。