Files
PolyLoop/tests/contract/test_decision_parser_double.py
T
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

73 lines
3.0 KiB
Python
Raw 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.
"""把决策解释契约接到一个测试替身上。
库不带这个接缝的实现——带了就等于替某一家定了动作语言。所以这里造一个最小的替身,它存在的
唯一目的是让套件的每一条用例至少被真的求值一次:一条引用了记录工厂里不存在的方法的用例,
只有在被执行的时候才会红。
**这个替身住在 `tests/` 里,不进 wheel,任何下游都拿不到它。** 「库不带默认实现」那条禁的是
`src/` 下出现一个能用的实现——下游装了包就拿得到,就会有人直接用,于是动作语言被库替它定了。
判据是下游拿不拿得到,不是代码库里有没有一个能跑的实现
`research-wiki/design/0014-contract-suite-distribution.md` 决策六)。
`contract` 这个标记打在本文件上,不打在套件里,理由见 `test_run_stores.py`。
"""
from collections.abc import Mapping
from dataclasses import dataclass
import pytest
from polyloop.ports import Action, InvalidDecision, ParsedReply
from polyloop.testing import DecisionParserContract, RecordFactory
from polyloop.types import ModelReply
#: 这个替身认得的全部动作语言:正文以它开头就是一个动作,剩下的部分是动作本身。
_ACTION_PREFIX = "DO "
pytestmark = pytest.mark.contract
class _PrefixDecisionParser:
"""认一种一行前缀的动作语言,别的一律解释不出动作。
**两支都要走得到**,这是契约对样本的要求在替身这一侧的对应:一个「什么都解释得出来」的
实现会让无效决策那一支变成死代码,而套件照样绿,绿的含义从「这一支对」变成「这一支没验」。
它满足 `polyloop.ports.DecisionParser`,但不显式继承那个 Protocol:结构化子类型不需要继承。
"""
def parse(self, reply: ModelReply) -> ParsedReply:
if reply.content.startswith(_ACTION_PREFIX):
return ParsedReply(
history_text=reply.content,
decision=Action(text=reply.content[len(_ACTION_PREFIX) :], tool_call=None),
)
return ParsedReply(
history_text=reply.content,
decision=InvalidDecision(
explanation=f"这段回复没有以 {_ACTION_PREFIX!r} 开头,解释不出动作"
),
)
def parameters(self) -> Mapping[str, str]:
return {"prefix": _ACTION_PREFIX}
@dataclass(frozen=True, slots=True, kw_only=True)
class _ReplySamples:
yields_an_action: ModelReply
yields_invalid: ModelReply
class TestPrefixDecisionParser(DecisionParserContract):
@pytest.fixture
def decision_parser(self) -> _PrefixDecisionParser:
return _PrefixDecisionParser()
@pytest.fixture
def reply_samples(self, records: RecordFactory) -> _ReplySamples:
return _ReplySamples(
yields_an_action=records.reply(content=f"{_ACTION_PREFIX}做点事"),
yields_invalid=records.reply(content="我先想想。"),
)