契约测试套件不随包发布,第一个下游拿不到准入标准 #1

Closed
opened 2026-08-26 19:56:40 +08:00 by iomgaa · 1 comment
Owner

我们是谁

dissect2(/home/iomgaa/Projects/dissect2)——dissect 的第二版,正在重建。它要接 PolyLoop 当执行内核,会是 PolyLoop 的第一个真实使用者

分工先说清楚:我们只提需求,不改这个仓库。下面每一条都带了 文件:行号,方便你们核。

问题

tests/contract/ 那套公共行为一致性套件不随包发布,pip install polyloop 之后拿不到。

pyproject.toml:46-47

[tool.setuptools.packages.find]
where = ["src"]

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/,作为一个正式的公开子包和实现一起发布、一起升版本。下游写:

from polyloop.testing import contract_suite   # 具体形状你们定

另一种是注册成 pytest plugin 的 entry point,下游装了库就能直接 pytest --polyloop-contract

外部参考:Pi(reference/pi/)的 telemetry 包就是这么做的——在 exports 里多开一个 ./testing 子路径,一致性测试和实现同包发布。

相关

这一条和另一个 issue(stores 的两个欠账)是连着的:README 阶段⑤说关系数据库那种形态由下游自己实现、契约套件是它的准入标准,而准入标准正是这里拿不到的这个。

## 我们是谁 dissect2(`/home/iomgaa/Projects/dissect2`)——dissect 的第二版,正在重建。它要接 PolyLoop 当执行内核,**会是 PolyLoop 的第一个真实使用者**。 分工先说清楚:我们只提需求,不改这个仓库。下面每一条都带了 `文件:行号`,方便你们核。 ## 问题 `tests/contract/` 那套公共行为一致性套件不随包发布,`pip install polyloop` 之后拿不到。 `pyproject.toml:46-47`: ```toml [tool.setuptools.packages.find] where = ["src"] ``` `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/`,作为一个正式的公开子包和实现一起发布、一起升版本。下游写: ```python from polyloop.testing import contract_suite # 具体形状你们定 ``` 另一种是注册成 pytest plugin 的 entry point,下游装了库就能直接 `pytest --polyloop-contract`。 外部参考:Pi(`reference/pi/`)的 telemetry 包就是这么做的——在 `exports` 里多开一个 `./testing` 子路径,一致性测试和实现同包发布。 ## 相关 这一条和另一个 issue(stores 的两个欠账)是连着的:README 阶段⑤说关系数据库那种形态由下游自己实现、契约套件是它的准入标准,而准入标准正是这里拿不到的这个。
Author
Owner

1.0.2 已发布,这一条做完了。

套件搬进了 polyloop.testing,跟着 wheel 一起装到你们那边:

pip install --extra-index-url https://gitea.iomgaa.online/api/packages/iomgaa/pypi/simple/ \
    "polyloop[testing]==1.0.*"

那个 extra 只装 pytest 与 pytest-asyncio,版本只写下界,不会和你们已经在用的 pytest 打架。

接法换了,不是「覆盖同名 fixture」

你们提的那个做法在发布之后走不通,这一点值得单独说:pytest 的 conftest.py 只沿着被收集文件的目录链往上找,而套件装在 site-packages 里,那条链上没有你们仓库的 conftest。就算我们把文件原样发出去,你们还是得先拷进自己的 tests/ 才接得上——而拷贝正是这个 issue 说不想要的。

改成继承契约基类,子类定义在你们自己的测试文件里,fixture 在类作用域内覆盖是 pytest 的原生行为:

from polyloop.testing import RunStoreContract

class TestMyEpisodeStore(RunStoreContract):
    @pytest.fixture
    def store(self, tmp_path):
        return MyStore(tmp_path)

顺带一个好处对你们直接有用:一个接缝可以接多个实现,各写一个子类。你们那边如果 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

**1.0.2 已发布**,这一条做完了。 套件搬进了 **`polyloop.testing`**,跟着 wheel 一起装到你们那边: ```bash pip install --extra-index-url https://gitea.iomgaa.online/api/packages/iomgaa/pypi/simple/ \ "polyloop[testing]==1.0.*" ``` 那个 extra 只装 pytest 与 pytest-asyncio,版本只写下界,不会和你们已经在用的 pytest 打架。 ## 接法换了,不是「覆盖同名 fixture」 你们提的那个做法在发布之后**走不通**,这一点值得单独说:pytest 的 `conftest.py` 只沿着**被收集文件**的目录链往上找,而套件装在 site-packages 里,那条链上没有你们仓库的 conftest。就算我们把文件原样发出去,你们还是得先拷进自己的 `tests/` 才接得上——而拷贝正是这个 issue 说不想要的。 改成继承契约基类,子类定义在你们自己的测试文件里,fixture 在类作用域内覆盖是 pytest 的原生行为: ```python from polyloop.testing import RunStoreContract class TestMyEpisodeStore(RunStoreContract): @pytest.fixture def store(self, tmp_path): return MyStore(tmp_path) ``` 顺带一个好处对你们直接有用:**一个接缝可以接多个实现,各写一个子类**。你们那边如果 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`。
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: iomgaa/PolyLoop#1