"""记录工厂造出来的东西必须合法:构造得出,而且能过一遍编解码往返。 **它守的是工厂造的记录合不合法,不是「契约用例引用的方法存不存在」。** 后者只有真的执行那条 用例才查得出——一条用例调了工厂上不存在的方法,工厂自己的单元测试怎么写都看不见它,因为那 条引用根本不在这个文件里。所以这份测试全绿不代表契约套件接上了实现;把五套契约都接到实现上 是另一件事,做在 `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