"""契约套件的装配点。 这套测试**不针对任何具体实现**。它写的是「不管你怎么实现,都必须满足这些行为」,所以它 自己不造实现,只声明「你得提供什么」。任何一个下游写完自己的存储或适配器,把它接到这里 的 fixture 上跑一遍,全绿就算合格——这是 `CLAUDE.md` §0 说的「任何新适配器的准入标准」。 **接法**:下游在自己的 `conftest.py` 里覆盖同名 fixture,返回自己的实现。 **为什么在实现之前就写它**:写一条契约测试要求把每一次调用逐字写出来——方法叫什么、参数 填什么、返回值怎么取。散文里读着通顺的地方,落到这一步就会露出来。前三轮文档评审抓不到的 洞,几乎全是这么冒出来的。 """ from collections.abc import Mapping import pytest from polyloop.ports import Action, Event, ToolCall from polyloop.stores import JsonlRunStore from polyloop.types import ( ActionOutcome, ActionStatus, Intent, IntentKind, ModelCallResult, ModelReply, ReplayPolicy, RunFinished, RunResult, RunStarted, StepCompleted, StepRecord, StopReason, ) #: 还没有默认实现的那几个接缝,套件里对应的测试全部跳过。 _NO_IMPLEMENTATION = ( "库不带这个接缝的默认实现——带了就等于替某一家定了它的协议。" "下游在自己的 conftest.py 里覆盖这个 fixture,把自己的实现接进来跑。" ) class _Records: """构造各类记录的工厂。 它不是被测对象,是让测试正文读得懂的一层薄封装:`records.model_call_intent(...)` 比直接 写一长串构造参数更能看出这条测试在断言什么。工厂随套件一起走,因为记录类的字段是库的 公共承诺,下游不该为了跑契约测试去手写构造。 """ #: 让测试写 `records.action_status.EXECUTED` 而不必自己 import 那个枚举。 action_status = ActionStatus def reply(self, *, call_id: str | None = "call-1", content: str = "hi") -> ModelReply: return ModelReply(call_id=call_id, content=content, thinking="") def model_call_intent(self, *, run_id: str, call_index: int, result_id: str) -> Intent: return Intent( run_id=run_id, kind=IntentKind.MODEL_CALL, call_index=call_index, result_id=result_id, replay_policy=ReplayPolicy.NEVER, ) def action_intent( self, *, run_id: str, call_index: int, result_id: str, replay_policy: ReplayPolicy = ReplayPolicy.NEVER, ) -> Intent: return Intent( run_id=run_id, kind=IntentKind.ACTION, call_index=call_index, result_id=result_id, replay_policy=replay_policy, ) def model_call_result( self, *, run_id: str, result_id: str, reply: ModelReply | None = None, failure: str | None = None, ) -> ModelCallResult: """`reply` 与 `failure` 恰好一个有值,两个都不给时默认造一条成功的。""" if reply is None and failure is None: reply = self.reply() return ModelCallResult(run_id=run_id, result_id=result_id, reply=reply, failure=failure) def outcome( self, *, status: ActionStatus = ActionStatus.EXECUTED, observation: str = "环境的输出", env_reported_completion: bool = False, ) -> ActionOutcome: return ActionOutcome( status=status, observation=observation, observation_is_synthetic=False, env_reported_completion=env_reported_completion, observation_truncated_chars=0, ) def step(self, *, step_idx: int = 0, parse_ok: bool = True) -> StepRecord: return StepRecord( step_idx=step_idx, raw_output="模型说的话", content_chars=5, thinking_chars=0, action="做点事" if parse_ok else None, parse_ok=parse_ok, parse_error=None if parse_ok else "解释不出动作", observation="环境的输出", observation_is_synthetic=False, observation_truncated_chars=0, prompt_chars=100, call_id="call-1", step_wall_ms=12, ) def step_completed( self, *, run_id: str, result_id: str | None, action_outcome: ActionOutcome | None, step: StepRecord, ) -> StepCompleted: return StepCompleted( run_id=run_id, result_id=result_id, action_outcome=action_outcome, step=step ) def run_started(self, *, run_id: str, parameter_snapshot: Mapping[str, str]) -> RunStarted: return RunStarted(run_id=run_id, parameter_snapshot=dict(parameter_snapshot)) def result( self, *, run_id: str, stop_reason: StopReason = StopReason.TASK_COMPLETED ) -> RunResult: return RunResult( run_id=run_id, stop_reason=stop_reason, final_answer="42", steps=(self.step(),) ) def run_finished(self, *, run_id: str, result: RunResult) -> RunFinished: return RunFinished(run_id=run_id, result=result) def action(self, *, text: str = "做点事", tool_name: str | None = None) -> Action: return Action( text=text, tool_call=None if tool_name is None else ToolCall(name=tool_name, arguments={}), ) def event(self) -> Event: return Event() @pytest.fixture def records() -> _Records: return _Records() @pytest.fixture def store(tmp_path) -> JsonlRunStore: """被测的存储接缝实现。 默认接的是库自带的那个逐行追加实现。下游覆盖这个 fixture,返回自己的实例。 每次调用返回一个**空的**存储:套件里每条测试都假设自己面对一份干净的日志,共用状态会让 测试之间的顺序变成隐式依赖。`tmp_path` 每条测试一个新目录,这一条自动成立。 """ return JsonlRunStore(directory=tmp_path) @pytest.fixture def samples(): """被测解释器认得的几段模型输出,由实现方提供。 **套件不许自己写死输入。** 库不带默认解释器实现,也就不认识任何一家的动作语言:拿一家的 代码围栏去喂另一家的 JSON 解析器,后者正确地返回「无效决策」,而写死输入的套件会把这个 正确行为判成失败。 实现方要提供两段:`yields_an_action`(一段能被解释成动作的模型回复)与 `yields_invalid` (一段解释不出动作的)。这不是给套件开后门——「我这套语言里什么算合法动作」本来就只有 实现方答得出,套件断言的是**拿到之后的形状**,不是输入长什么样。 """ pytest.skip(_NO_IMPLEMENTATION) @pytest.fixture def action_executor(): """被测的动作执行接缝实现。 库自带一个由工具注册表派生的分发器(`polyloop.tools.ToolRegistry.executor`),但它只覆盖 「工具调用」那一种动作语言;把它接在这里会让套件只验得了那一种,所以默认仍然留空。 """ pytest.skip(_NO_IMPLEMENTATION) @pytest.fixture def decision_parser(): """被测的决策解释接缝实现。""" pytest.skip(_NO_IMPLEMENTATION) @pytest.fixture def model_client(): """被测的模型调用接缝实现。 注意这一层的契约测试**不打真实网关**——那是 e2e 的事。这里断言的是返回结构体的形状与 失败时的表达方式,用一个受控的替身就能验。 """ pytest.skip(_NO_IMPLEMENTATION) @pytest.fixture def event_sink(): """被测的事件出口实现。""" pytest.skip(_NO_IMPLEMENTATION)