8aa430c7df
解释器不许抛异常是接缝自己的行为,改成正面断言(等实现接进来才跑)。另两条的答案 落在库这一侧不在接缝上:动作状态的触发条件是执行器自己的判断,套件面对任意实现逼 不出后两档,硬探会把 dissect 那种状态恒为 EXECUTED 的合法实现判成不合格;观察由谁 合成同理。两条留成不断言的说明,指向 tests/unit/test_session.py 里真正验它们的地方。 273 passed / 15 skipped / 4 xfailed。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
85 lines
4.0 KiB
Python
85 lines
4.0 KiB
Python
"""决策解释接缝的行为契约。
|
|
|
|
两个已知形态:一个从代码围栏里抽 Python 源码,一个从 JSON 里抽工具名与参数。库不带任何
|
|
默认实现——带了就等于替某一家定了动作语言。
|
|
|
|
## 写这份文件时撞出来的那个问题,`design/0007` 决策三答了
|
|
|
|
模型输出完全无法解释时,解释器返回「无效决策」,不抛异常。下面最后一条断言它。
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from polyloop.ports import InvalidDecision
|
|
|
|
pytestmark = pytest.mark.contract
|
|
|
|
|
|
def test_parse_is_synchronous(decision_parser, samples):
|
|
"""`parse` 是同步的,不是协程。
|
|
|
|
解释一次模型回复是纯计算,没有等待点。写成协程会让每个只想写测试替身的下游多套一层
|
|
`async def`,也会诱导实现方在里面做 I/O——而这个接缝一旦做起 I/O,「恢复时重新解释
|
|
被打断的那一步」就不再是安全操作了。
|
|
"""
|
|
parsed = decision_parser.parse(samples.yields_an_action)
|
|
|
|
assert not hasattr(parsed, "__await__")
|
|
|
|
|
|
def test_history_text_is_what_goes_back_into_the_conversation(decision_parser, samples):
|
|
"""`history_text` 是这一步回填进历史的那段文本,可以与模型原文不同。
|
|
|
|
解释器有权改写它:dissect 的解析器把第一个代码围栏之后的内容整段丢掉,因为模型常在
|
|
代码块后面编造「执行结果」。库这边只有模型原文,照它回填,模型下一轮会看见自己编的
|
|
那段,而迁移前它看不见。
|
|
|
|
**输入由被测实现自己提供**,不由套件写死。库不带默认实现,也就不认识任何一家的动作
|
|
语言——拿 dissect 的代码围栏去喂 GovDoc 的 JSON 解析器,它正确地返回「无效决策」,
|
|
而套件会把这个正确行为判成失败。
|
|
"""
|
|
reply = samples.yields_an_action
|
|
parsed = decision_parser.parse(reply)
|
|
|
|
assert isinstance(parsed.history_text, str)
|
|
assert len(parsed.history_text) <= len(reply.content)
|
|
|
|
|
|
def test_invalid_decision_explanation_is_what_is_fed_back(decision_parser, samples):
|
|
"""无效决策的说明文本**就是**回喂给模型的那段观察,不是从一个固定串里取。
|
|
|
|
dissect 的解析器对五种解析失败各有一条对症说明(没有代码块、空的未闭合块、闭合围栏后
|
|
跟了别的内容、多块策略下第一块为空、拼接策略下全空)。压成一句会改掉它的实验条件——
|
|
模型收到的纠错信息变了,它的纠错行为也就变了。
|
|
"""
|
|
parsed = decision_parser.parse(samples.yields_invalid)
|
|
|
|
assert isinstance(parsed.decision.explanation, str)
|
|
assert parsed.decision.explanation != ""
|
|
|
|
|
|
def test_action_carries_its_trace_form(decision_parser, samples):
|
|
"""动作分支要带「这一步的动作在轨迹里长什么样」,由实现方决定内容,库原样填进步记录。
|
|
|
|
dissect 传那段 Python 源码,GovDoc 传序列化后的参数。没有这个字段,dissect 轨迹里那一列
|
|
会被库改写,而那个文件是它的反思模型的唯一输入界面。
|
|
"""
|
|
parsed = decision_parser.parse(samples.yields_an_action)
|
|
|
|
assert isinstance(parsed.decision.text, str)
|
|
|
|
|
|
def test_unparseable_output_returns_invalid_decision_rather_than_raising(decision_parser, samples):
|
|
"""模型输出完全无法解释时返回「无效决策」,不抛异常(`design/0007` 决策三)。
|
|
|
|
两条路后果完全不同:返回无效决策,那一步照常留痕、说明文本回喂给模型、循环继续;抛
|
|
异常,库要么把它翻译成某个停止原因终止整次运行,要么让它穿出去炸掉调用方。
|
|
|
|
dissect 的解析器不抛异常,所以它撞不到这个分歧。但契约测试是**任何新适配器的准入
|
|
标准**,所以这条要正面断言,不能靠「反正没人这么写」。
|
|
"""
|
|
parsed = decision_parser.parse(samples.yields_invalid)
|
|
|
|
assert isinstance(parsed.decision, InvalidDecision)
|
|
assert parsed.decision.explanation != ""
|