"""把动作执行契约接到一个有挂起点的测试替身上。 同一套用例在两种形态上各跑一遍:`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}除以零"), )