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:
@@ -9,6 +9,7 @@
|
||||
"""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -21,6 +22,7 @@ from polygateway.errors import AllSourcesExhausted # noqa: E402
|
||||
|
||||
from polyloop.adapters import GatewayModelClient # noqa: E402
|
||||
from polyloop.ports import ModelCall # noqa: E402
|
||||
from polyloop.testing import ModelClientContract # noqa: E402
|
||||
from polyloop.types import Message, Role, TextBlock # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
@@ -207,3 +209,42 @@ def test_changing_a_sampling_parameter_changes_the_parameters() -> None:
|
||||
).parameters()
|
||||
|
||||
assert before["sources"] != after["sources"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 模型调用契约(`polyloop.testing.ModelClientContract`)接在这一层,不在契约层。
|
||||
#
|
||||
# 分层判据是「依赖什么」(`CLAUDE.md` §1.9):这里的配置、装配守卫、响应类型全是网关真的
|
||||
# 那套,所以它是 integration。`research-wiki/design/0014-contract-suite-distribution.md`
|
||||
# 决策六那张表里,五个接缝只有这一行落在契约层之外,就是这个原因。
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True, kw_only=True)
|
||||
class _UnsupportedBlock:
|
||||
"""一种适配器不认得的内容块。
|
||||
|
||||
它是 `failing_call` 用来让适配器在入参这一关就挂掉的东西。适配器把每个块翻译成文本,
|
||||
碰到不认得的类型直接抛——那是它自己的守卫,不是替身编出来的失败。
|
||||
"""
|
||||
|
||||
|
||||
class TestGatewayModelClient(ModelClientContract):
|
||||
"""网关适配器要满足模型调用接缝的全部契约。"""
|
||||
|
||||
@pytest.fixture
|
||||
def model_client(self) -> GatewayModelClient:
|
||||
return GatewayModelClient(client=_StubClient(_response()), settings=_settings())
|
||||
|
||||
@pytest.fixture
|
||||
def failing_call(self) -> ModelCall:
|
||||
"""一次带着适配器不认得的内容块的调用。
|
||||
|
||||
**失败发生在请求打出去之前**:适配器逐块翻译消息,碰到不是文本块的东西直接抛。所以这
|
||||
条路径不碰替身客户端、不产生任何副作用,客户端实例失败之后照样能接着服务——这三件事
|
||||
正是 `failing_call` 那份 docstring 要求的。
|
||||
|
||||
另一条路是让替身客户端认一个暗号、见到就抛,那等于在实现这一侧重新造出套件刚刚扔掉的
|
||||
那个约定,而它验的会变成替身的分支写对没有。
|
||||
"""
|
||||
return _call(messages=(Message(role=Role.USER, content=(_UnsupportedBlock(),)),))
|
||||
|
||||
Reference in New Issue
Block a user