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:
@@ -0,0 +1,81 @@
|
||||
"""把动作执行契约接到库自带的分发器上。
|
||||
|
||||
`RegistryExecutor` 是库里唯一一个动作执行器实现:它按工具名查注册表、校验参数、调那个工具的
|
||||
实现。套件不认识任何一家的动作语言,所以两个样本动作由这里提供——都是工具调用,因为那是这个
|
||||
执行器唯一认得的形状。
|
||||
|
||||
`contract` 这个标记打在本文件上,不打在套件里,理由见 `test_run_stores.py`。
|
||||
"""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
|
||||
from polyloop.ports import Action
|
||||
from polyloop.testing import ActionExecutorContract, RecordFactory
|
||||
from polyloop.tools import RegistryExecutor, ToolRegistry, ToolSpec
|
||||
|
||||
pytestmark = pytest.mark.contract
|
||||
|
||||
#: 两个工具都不收参数,校验那一段因此不参与这套用例的成败。
|
||||
_NO_ARGUMENTS: Mapping[str, object] = {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
|
||||
async def _returns_text(arguments: Mapping[str, object]) -> str:
|
||||
"""跑得完、不报错的那个工具。
|
||||
|
||||
**它里面没有等待点,这是刻意的。** 一个纯计算的工具本来就没有可挂起的地方,而取消那条
|
||||
用例会因此判定「这个实现快到没有可取消的窗口」并跳过——跑得太快不是违约。往这里塞一句
|
||||
`await asyncio.sleep(0)` 能把那条跳过换成通过,但换来的通过验的是这句人为的等待,不是
|
||||
分发器有没有吞掉取消。
|
||||
"""
|
||||
return "工具的输出"
|
||||
|
||||
|
||||
async def _raises(arguments: Mapping[str, object]) -> str:
|
||||
"""跑得完、但动作本身报错的那个工具。
|
||||
|
||||
抛一个普通异常而不是 `ToolEnvironmentError`:后者是「环境坏了」那一档,会被分发器记成
|
||||
环境故障,而这套用例要的恰恰是「动作报错仍然算已执行」。
|
||||
"""
|
||||
raise ValueError("这个工具自己报错了")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True, kw_only=True)
|
||||
class _ActionSamples:
|
||||
executes_cleanly: Action
|
||||
executes_but_errors: Action
|
||||
|
||||
|
||||
class TestRegistryExecutor(ActionExecutorContract):
|
||||
@pytest.fixture
|
||||
def action_executor(self) -> RegistryExecutor:
|
||||
registry = ToolRegistry(
|
||||
[
|
||||
ToolSpec(
|
||||
name="echo",
|
||||
description="回一段固定文本",
|
||||
parameters=_NO_ARGUMENTS,
|
||||
handler=_returns_text,
|
||||
),
|
||||
ToolSpec(
|
||||
name="boom",
|
||||
description="抛一个普通异常",
|
||||
parameters=_NO_ARGUMENTS,
|
||||
handler=_raises,
|
||||
),
|
||||
]
|
||||
)
|
||||
return registry.executor()
|
||||
|
||||
@pytest.fixture
|
||||
def action_samples(self, records: RecordFactory) -> _ActionSamples:
|
||||
return _ActionSamples(
|
||||
executes_cleanly=records.action(text="echo", tool_name="echo"),
|
||||
executes_but_errors=records.action(text="boom", tool_name="boom"),
|
||||
)
|
||||
Reference in New Issue
Block a user