Files
PolyLoop/tests/contract/conftest.py
T
iomgaa f8e02290f4 docs(design): 落成 0006 与 0007,公共 API 的名字、签名与接缝行为
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>
2026-08-09 23:56:38 -04:00

89 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""契约套件的装配点。
这套测试**不针对任何具体实现**。它写的是「不管你怎么实现,都必须满足这些行为」,所以它
自己不造实现,只声明「你得提供什么」。任何一个下游写完自己的存储或适配器,把它接到这里
的 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)