Files
PolyLoop/tests/contract/test_event_sink_double.py
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

48 lines
1.8 KiB
Python
Raw Permalink 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.
"""把事件出口契约接到一个测试替身上。
库不带这个接缝的实现——带了就等于替某一家定了投递协议。这里造一个最小的替身,它存在的唯一
目的是让套件的每一条用例至少被真的求值一次。
**这个替身住在 `tests/` 里,不进 wheel,任何下游都拿不到它。** 「库不带默认实现」那条禁的是
`src/` 下出现一个能用的实现——下游装了包就拿得到,就会有人直接用。判据是下游拿不拿得到,
不是代码库里有没有一个能跑的实现
`research-wiki/design/0014-contract-suite-distribution.md` 决策六)。
`contract` 这个标记打在本文件上,不打在套件里,理由见 `test_run_stores.py`。
"""
from collections.abc import Mapping
import pytest
from polyloop.ports import Event
from polyloop.testing import EventSinkContract
pytestmark = pytest.mark.contract
class _CollectingEventSink:
"""收下事件,攒进一个列表。
**不做别的**:这套契约只断言「收得下一个事件」,而「投递失败之后库还在跑」「库没有把失败
再发一次」都是整次运行的行为,由库自己的测试守着,不由一个出口实现验。往这里加重试、加
过滤、加计数,验的就变成这个替身自己了。
它满足 `polyloop.ports.EventSink`,但不显式继承那个 Protocol:结构化子类型不需要继承。
"""
def __init__(self) -> None:
self.events: list[Event] = []
async def emit(self, event: Event) -> None:
self.events.append(event)
def parameters(self) -> Mapping[str, str]:
return {"kind": "collecting"}
class TestCollectingEventSink(EventSinkContract):
@pytest.fixture
def event_sink(self) -> _CollectingEventSink:
return _CollectingEventSink()