"""把动作执行契约接到库自带的分发器上。 `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"), )