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:
@@ -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` 必须在包根里。
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user