Files
PolyLoop/tests/contract/test_suspending_executor_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

95 lines
4.5 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.
"""把动作执行契约接到一个有挂起点的测试替身上。
同一套用例在两种形态上各跑一遍:`test_registry_executor.py` 接的分发器是纯计算的,取消那条
用例在它身上永远走跳过分支——取消发出去时它已经跑完,没有机会吞掉取消。那条用例的断言半边
因此在本仓库一次都没被执行过,而契约套件的目标是每一条用例都至少被真的执行一次
`research-wiki/design/0014-contract-suite-distribution.md` 决策六)。这个替身补的就是「有挂起
点」那一档。
**不是给分发器塞一句人为的等待。** 那样换来的通过验的是那句等待,不是分发器有没有吞掉取消
`test_registry_executor.py` 里那个工具的 docstring 说的就是这件事)。补一个另外的实现,验的
是真有等待点时取消穿不穿得过去。
**这个替身住在 `tests/` 里,不进 wheel,任何下游都拿不到它。** 「库不带默认实现」那条禁的是
`src/` 下出现一个能用的实现——下游装了包就拿得到,就会有人直接用,于是动作语言被库替它定了。
判据是下游拿不拿得到,不是代码库里有没有一个能跑的实现(同上,决策六)。
`contract` 这个标记打在本文件上,不打在套件里,理由见 `test_run_stores.py`。
"""
import asyncio
from collections.abc import Mapping
from dataclasses import dataclass
import pytest
from polyloop.ports import Action
from polyloop.testing import ActionExecutorContract, RecordFactory
from polyloop.types import ActionOutcome, ActionStatus
pytestmark = pytest.mark.contract
#: 这个替身认得的全部动作语言:正文以它开头的那个动作,跑完之后自己报错。
_FAILS_PREFIX = "FAIL "
#: 挂起窗口的长度。**不能用 `asyncio.sleep(0)`**:那只是让出一次,取消赶不赶得上就取决于事件
#: 循环就绪队列里两个回调的先后,而那个顺序不是承诺。给一个真的定时器,套件让出一次之后这个
#: 替身一定还没跑完,`cancel()` 一定返回 `True`,取消那条用例才稳定地走到断言那一半。
_PAUSE_SECONDS = 0.001
class _SuspendingActionExecutor:
"""每个动作都先真的挂起一小段,再返回一个「已执行」的结果。
那次挂起模拟的是真实实现里的等待点——网络往返、子进程、容器会话。**它不捕获任何异常**,
所以挂起期间收到的 `CancelledError` 原样穿出去,这正是契约要断言的行为
`CLAUDE.md` §1.6)。没有 in-flight 资源要放,所以也没有 `finally`。
它满足 `polyloop.ports.ActionExecutor`,但不显式继承那个 Protocol:结构化子类型不需要继承。
"""
async def execute(self, action: Action) -> ActionOutcome:
await asyncio.sleep(_PAUSE_SECONDS)
if action.text.startswith(_FAILS_PREFIX):
return ActionOutcome(
status=ActionStatus.EXECUTED,
observation=f"动作自己报错了:{action.text[len(_FAILS_PREFIX) :]}",
observation_is_synthetic=False,
env_reported_completion=False,
observation_truncated_chars=0,
)
return ActionOutcome(
status=ActionStatus.EXECUTED,
observation=f"动作的输出:{action.text}",
observation_is_synthetic=False,
env_reported_completion=False,
observation_truncated_chars=0,
)
def parameters(self) -> Mapping[str, str]:
return {"kind": "suspending", "pause_seconds": str(_PAUSE_SECONDS)}
@dataclass(frozen=True, slots=True, kw_only=True)
class _ActionSamples:
executes_cleanly: Action
executes_but_errors: Action
class TestSuspendingActionExecutor(ActionExecutorContract):
@pytest.fixture
def action_executor(self) -> _SuspendingActionExecutor:
return _SuspendingActionExecutor()
@pytest.fixture
def action_samples(self, records: RecordFactory) -> _ActionSamples:
"""两个样本的状态都是「已执行」,动作本身报错的那个也是。
这个替身的动作语言里没有「没进执行」和「环境坏了」这两档,报错的动作照样跑完了,只是
观察里多一句错误说明——套件对样本的要求就是这个(`action_samples` 的退化情况那一段)。
"""
return _ActionSamples(
executes_cleanly=records.action(text="做点事"),
executes_but_errors=records.action(text=f"{_FAILS_PREFIX}除以零"),
)