f8e02290f4
0006 定「叫什么、什么形状」:五个接缝的 Protocol 名与签名、公共类型的英文名与 字段清单、类型分到 types / ports / tools 三个模块的判据。 0007 定「同一个签名下什么算对」:三个动作状态的触发条件、动作被拒绝时观察由库 合成而不取执行器那段、解释器不许抛异常、read_log 读不存在的运行返回空日志。 两份拆开是因为后者的权威处按 §0 是 tests/contract/,design doc 只记当初为什么这么定。 这两份改动了 0003 四处,全部在文首登记:记录集合是六种东西不是五类; 参数视图是方法不是字段;预算是四项不是两个计数;ports 装「Protocol 与它们的 入参/返回结构体」那半句写不出来——照它写 types 会反向依赖 ports。 四处全是「把字段类型逐个写出来」这个动作本身逼出来的,纯读文档看不见。 四轮评审:两轮硕士生冷读报了约 45 条,两轮 Codex 对抗审查报了 13 条, 逐条核实后基本全部成立并修完。最后一轮是唯一一次契约测试与文档互相抓到对方的错—— 文档改了方法名测试没跟,测试把 dissect 的动作语言写死成输入会误杀 GovDoc 的实现。 结论回写 architecture.md:第七节补类型归属判据,第八节改 ports 那一行, 第九节补五个 Protocol 的英文名,第十四节把「英文名还没定」那条缺口换成指向; 决策索引加两行。字段表刻意不回写——按 §0 那是代码的权威。 CLAUDE.md 与 README.md 开头的「一次 Agent Session」是术语漂移,改成「一次运行」。 CLAUDE.md §7 加两条工作方式:能压成一段结论的活尽量交给 subagent、 委托出去的活交证据不交判断;以及持续往下做,只在人类门和真判断不了的岔路停。 §8 那句「讲完停下来等回应」与后者打架,收窄到只管说话方式。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
89 lines
3.6 KiB
Python
89 lines
3.6 KiB
Python
"""契约套件的装配点。
|
||
|
||
这套测试**不针对任何具体实现**。它写的是「不管你怎么实现,都必须满足这些行为」,所以它
|
||
自己不造实现,只声明「你得提供什么」。任何一个下游写完自己的存储或适配器,把它接到这里
|
||
的 fixture 上跑一遍,全绿就算合格——这是 `CLAUDE.md` §0 说的「任何新适配器的准入标准」。
|
||
|
||
**接法**:下游在自己的 `conftest.py` 里覆盖同名 fixture,返回自己的实现。
|
||
|
||
**现在这套测试全部跳过**,因为 `polyloop` 下还没有任何公共类型与 Protocol
|
||
(`research-wiki/design/0006-public-names-and-signatures.md` 还没过人类门)。跳过的理由写在
|
||
每个 fixture 里,读跳过原因就能知道缺的是哪一块。
|
||
|
||
**为什么在实现之前就写它**:写一条契约测试要求把每一次调用逐字写出来——方法叫什么、参数
|
||
填什么、返回值怎么取。散文里读着通顺的地方,落到这一步就会露出来。前三轮文档评审抓不到的
|
||
洞,几乎全是这么冒出来的。
|
||
"""
|
||
|
||
import pytest
|
||
|
||
#: 公共类型与 Protocol 落地之前,套件里的每一条都缺同一样东西。
|
||
_NOT_YET = (
|
||
"polyloop 的公共类型与 Protocol 还没落地(design/0006 待确认)。"
|
||
"这条契约要断言的行为已经写在测试的 docstring 里,落地之后去掉这个跳过即可。"
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def store():
|
||
"""被测的存储接缝实现。
|
||
|
||
下游覆盖这个 fixture,返回自己的实例。每次调用应当返回一个**空的**存储——套件里的每条
|
||
测试都假设自己面对一份干净的日志,共用状态会让测试之间的顺序变成隐式依赖。
|
||
"""
|
||
pytest.skip(_NOT_YET)
|
||
|
||
|
||
@pytest.fixture
|
||
def records():
|
||
"""构造各类记录的辅助工厂。
|
||
|
||
它不是被测对象,是让测试正文读得懂的一层薄封装:`records.model_call_intent(...)` 比
|
||
直接写一长串构造参数更能看出这条测试在断言什么。工厂本身由库提供,因为记录类的字段
|
||
是库的公共承诺,下游不该为了跑契约测试去手写构造。
|
||
"""
|
||
pytest.skip(_NOT_YET)
|
||
|
||
|
||
@pytest.fixture
|
||
def samples():
|
||
"""被测解释器认得的几段模型输出,由实现方提供。
|
||
|
||
**套件不许自己写死输入。** 库不带默认解释器实现,也就不认识任何一家的动作语言:拿
|
||
dissect 的 Python 代码围栏去喂 GovDoc 的 JSON 解析器,后者正确地返回「无效决策」,
|
||
而写死输入的套件会把这个正确行为判成失败。
|
||
|
||
实现方要提供两段:`yields_an_action`(一段能被解释成动作的模型回复)与 `yields_invalid`
|
||
(一段解释不出动作的)。这不是给套件开后门——「我这套语言里什么算合法动作」本来就只有
|
||
实现方答得出,套件断言的是**拿到之后的形状**,不是输入长什么样。
|
||
"""
|
||
pytest.skip(_NOT_YET)
|
||
|
||
|
||
@pytest.fixture
|
||
def action_executor():
|
||
"""被测的动作执行接缝实现。"""
|
||
pytest.skip(_NOT_YET)
|
||
|
||
|
||
@pytest.fixture
|
||
def decision_parser():
|
||
"""被测的决策解释接缝实现。"""
|
||
pytest.skip(_NOT_YET)
|
||
|
||
|
||
@pytest.fixture
|
||
def model_client():
|
||
"""被测的模型调用接缝实现。
|
||
|
||
注意这一层的契约测试**不打真实网关**——那是 e2e 的事。这里断言的是返回结构体的形状
|
||
与失败时的表达方式,用一个受控的替身就能验。
|
||
"""
|
||
pytest.skip(_NOT_YET)
|
||
|
||
|
||
@pytest.fixture
|
||
def event_sink():
|
||
"""被测的事件出口实现。"""
|
||
pytest.skip(_NOT_YET)
|