1fac387e75
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 才验得出来。
73 lines
3.0 KiB
Python
73 lines
3.0 KiB
Python
"""把决策解释契约接到一个测试替身上。
|
||
|
||
库不带这个接缝的实现——带了就等于替某一家定了动作语言。所以这里造一个最小的替身,它存在的
|
||
唯一目的是让套件的每一条用例至少被真的求值一次:一条引用了记录工厂里不存在的方法的用例,
|
||
只有在被执行的时候才会红。
|
||
|
||
**这个替身住在 `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="我先想想。"),
|
||
)
|