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 才验得出来。
This commit is contained in:
2026-08-27 03:59:16 -04:00
parent 3492a2994a
commit 1fac387e75
22 changed files with 1538 additions and 632 deletions
+21
View File
@@ -49,6 +49,27 @@ def test_importing_polyloop_does_not_import_polygateway() -> None:
assert result.stdout.strip() == "False", result.stdout
def test_importing_polyloop_does_not_import_pytest() -> None:
"""`import polyloop` 之后 `sys.modules` 里不许出现 `pytest`。
契约套件 `polyloop.testing` 顶层就 import pytest,而顶层包不 re-export 它——它和
`stores`、`adapters` 同一档,必须显式 import。少了这条断言,哪天有人顺手把 `testing`
加进 `polyloop/__init__.py`,每个下游的运行时就都被拽上一个 pytest 依赖,而 pytest 在
生产环境里通常根本没装,表现是下游一 import 本库就 `ModuleNotFoundError`。
和上面那条 polygateway 同构,也同样在子进程里跑:本进程早就 import 过 pytest 了。
"""
code = "import polyloop, sys; print('pytest' in sys.modules)"
result = subprocess.run( # noqa: S603
[sys.executable, "-c", code],
capture_output=True,
text=True,
check=True,
cwd=REPO_ROOT,
)
assert result.stdout.strip() == "False", result.stdout
def test_py_typed_marker_ships_with_the_package() -> None:
"""`py.typed` 必须在包根里。
+177
View File
@@ -0,0 +1,177 @@
"""记录工厂造出来的东西必须合法:构造得出,而且能过一遍编解码往返。
**它守的是工厂造的记录合不合法,不是「契约用例引用的方法存不存在」。** 后者只有真的执行那条
用例才查得出——一条用例调了工厂上不存在的方法,工厂自己的单元测试怎么写都看不见它,因为那
条引用根本不在这个文件里。所以这份测试全绿不代表契约套件接上了实现;把五套契约都接到实现上
是另一件事,做在 `tests/contract/` 与 `tests/integration/`
`research-wiki/design/0014-contract-suite-distribution.md` 决策六)。
往返用的是 `polyloop.serialization`,因为那是记录进日志的唯一通道:一条编不出来或者解回来
不等于自己的记录,在契约套件里表现成某个存储实现的用例红,而红的原因其实在工厂这一侧。
"""
import pytest
from polyloop.serialization import (
decode_intent,
decode_model_call_result,
decode_run_finished,
decode_run_result,
decode_run_started,
decode_step_completed,
decode_step_record,
encode,
)
from polyloop.testing import RecordFactory
from polyloop.types import ActionStatus, ReplayPolicy, StopReason
pytestmark = pytest.mark.unit
@pytest.fixture
def records() -> RecordFactory:
return RecordFactory()
def test_run_started_round_trips(records: RecordFactory) -> None:
record = records.run_started(run_id="r1", parameter_snapshot={"model": "m-1"})
assert decode_run_started(encode(record)) == record
def test_model_call_intent_round_trips(records: RecordFactory) -> None:
record = records.model_call_intent(run_id="r1", call_index=0, result_id="m0")
assert decode_intent(encode(record)) == record
def test_action_intent_round_trips_with_a_non_default_replay_policy(
records: RecordFactory,
) -> None:
"""重放策略跟着走一遍。它是恢复时「这一步要不要重跑」的输入,编码里丢了就会静默降级。"""
record = records.action_intent(
run_id="r1", call_index=1, result_id="a0", replay_policy=ReplayPolicy.SAFE
)
assert decode_intent(encode(record)) == record
assert record.replay_policy is ReplayPolicy.SAFE
def test_successful_model_call_result_round_trips(records: RecordFactory) -> None:
record = records.model_call_result(run_id="r1", result_id="m0", reply=records.reply())
assert decode_model_call_result(encode(record)) == record
def test_failed_model_call_result_round_trips(records: RecordFactory) -> None:
"""失败那一档单独走一遍:回复为空、失败说明有值,两个可空字段的组合和成功那档相反。"""
record = records.model_call_result(run_id="r1", result_id="m0", failure="连接超时")
decoded = decode_model_call_result(encode(record))
assert decoded == record
assert decoded.reply is None
assert decoded.failure == "连接超时"
def test_step_record_round_trips(records: RecordFactory) -> None:
record = records.step(step_idx=3)
assert decode_step_record(encode(record)) == record
def test_unparsed_step_record_round_trips(records: RecordFactory) -> None:
"""解析失败那一档:动作为空、解析说明有值。"""
record = records.step(parse_ok=False)
decoded = decode_step_record(encode(record))
assert decoded == record
assert decoded.action is None
assert decoded.parse_error is not None
def test_step_completed_round_trips(records: RecordFactory) -> None:
record = records.step_completed(
run_id="r1",
result_id="a0",
action_outcome=records.outcome(),
step=records.step(),
)
assert decode_step_completed(encode(record)) == record
def test_step_completed_without_an_action_round_trips(records: RecordFactory) -> None:
"""没有动作的那一步:结果标识与动作结果同时为空,这个组合存储要能原样存下来。"""
record = records.step_completed(
run_id="r1", result_id=None, action_outcome=None, step=records.step()
)
decoded = decode_step_completed(encode(record))
assert decoded == record
assert decoded.result_id is None
assert decoded.action_outcome is None
def test_outcome_carries_a_non_default_status(records: RecordFactory) -> None:
"""状态取值跟着记录走一遍。默认那档和显式传的那档在编码里长得一样,各验一次。"""
record = records.step_completed(
run_id="r1",
result_id="a0",
action_outcome=records.outcome(status=ActionStatus.ENV_ERROR),
step=records.step(),
)
decoded = decode_step_completed(encode(record))
assert decoded == record
assert decoded.action_outcome is not None
assert decoded.action_outcome.status is ActionStatus.ENV_ERROR
def test_run_result_round_trips(records: RecordFactory) -> None:
record = records.result(run_id="r1", stop_reason=StopReason.STEP_BUDGET)
assert decode_run_result(encode(record)) == record
def test_run_finished_round_trips(records: RecordFactory) -> None:
record = records.run_finished(run_id="r1", result=records.result(run_id="r1"))
assert decode_run_finished(encode(record)) == record
def test_model_call_defaults_to_one_user_message(records: RecordFactory) -> None:
"""一次模型调用不是记录,编解码不管它,但它的默认消息序列是契约用例的隐式输入。
默认给一条用户消息而不是空序列:一个真实的实现拿到空消息序列多半直接拒绝,于是那几条
用例验的就变成了它的入参校验,不是它的返回结构。
"""
call = records.model_call(call_index=0, result_id="m0")
assert len(call.messages) == 1
assert call.messages[0].content[0].text != ""
def test_action_with_a_tool_name_carries_a_tool_call(records: RecordFactory) -> None:
"""带工具名的动作要真的带上工具调用,按工具名分发的执行器靠它才走得进分发。"""
action = records.action(text="echo", tool_name="echo")
assert action.tool_call is not None
assert action.tool_call.name == "echo"
def test_action_without_a_tool_name_carries_no_tool_call(records: RecordFactory) -> None:
"""不带工具名的那种是代码执行型动作,工具调用必须为空,否则会被分发器当成工具调用收下。"""
assert records.action().tool_call is None
def test_event_carries_the_step_it_reports(records: RecordFactory) -> None:
"""事件不是记录,但它带着的那条步记录要和工厂造的其他步记录同形。"""
event = records.event(step_idx=2)
assert event.step is not None
assert event.step.step_idx == 2
assert decode_step_record(encode(event.step)) == event.step