8c642e7881
它们函数体是空的,名字却承诺了一个行为,读起来像「这条已经验过了」——而契约套件是新适配器 的准入标准,下游跑一遍看见绿的会以为自己那一条被验过。名字改成说清楚断言在哪一层, 函数体那段解释保持不动。 Codex 另报「测试 docstring 里出现 GovDoc 是业务词汇」,不采纳:§1.1 那道扫描挡的是领域词 (公文、超声、招标),说明一条测试被哪个消费者的需求逼出来不构成业务假设,仓库里这类引用 本来就有十几处。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
59 lines
3.1 KiB
Python
59 lines
3.1 KiB
Python
"""事件出口的行为契约。
|
|
|
|
两个已知形态差别在可靠性要求上:一个把进度逐步回写业务数据库供前端轮询(要求低延迟、
|
|
可以丢),一个把审计事件送进日志管道(要求不丢、可以慢)。
|
|
|
|
## 写这份文件时撞出来的问题,`design/0013` 答了
|
|
|
|
「发出去的事件里有什么」当时验不了,因为 `Event` 只有一个名字没有字段。现在事件集定下来了,
|
|
而答案把这份文件里的两条测试都挪走了——它们要断言的行为都在库那一侧,不在出口这一侧,见文末
|
|
那两条说明。
|
|
"""
|
|
|
|
import pytest
|
|
|
|
pytestmark = pytest.mark.contract
|
|
|
|
|
|
async def test_emit_accepts_an_event(event_sink, records):
|
|
"""能收下一个事件,正常路径不抛异常。"""
|
|
await event_sink.emit(records.event())
|
|
|
|
|
|
def test_a_raising_sink_is_compliant_so_this_layer_asserts_nothing():
|
|
"""**这一层不断言「emit 不抛」——一个后端连不上时抛异常的出口是合规实现。**
|
|
|
|
契约写的是「投递失败由**库**捕获、记日志、把失败计数加一,然后继续跑」,所以要断言的
|
|
行为在库那一侧,不在出口这一侧。原来这里写了一条 `await emit(...)` 不抛的断言,那会把
|
|
一个完全合法的审计 sink 判失败——它在日志管道不可用时抛 `ConnectionError`,而库本来就
|
|
该接住。
|
|
|
|
「库接住了失败并继续跑」属于整次运行的行为,落在驱动入口那一层的测试里,不在这个接缝的
|
|
契约里。这条留成一个说明,是为了让下一个想在这儿加断言的人先看到这段。
|
|
"""
|
|
|
|
|
|
def test_the_no_re_emission_guarantee_is_asserted_in_the_library_not_here():
|
|
"""投递失败不再转成一条事件从同一个出口发出去(`design/0013` 决策七)。
|
|
|
|
那会自我喂食:一个持续失败的出口会让失败处理路径变成递归,而递归的表现是进程卡住或
|
|
栈溢出,不是一条错误日志。
|
|
|
|
**要断言的是库有没有再发一次,那是整次运行的行为**,所以断言在
|
|
`tests/unit/test_session.py` 里——那边用一个恒抛异常的出口跑完一次运行,验出口收到的
|
|
条数恰好等于步数。这个接缝自己看不到「库发了几次」。
|
|
"""
|
|
|
|
|
|
def test_the_audit_trail_is_asserted_against_the_log_not_here():
|
|
"""审计纪律由存储承担,不由事件流承担(`design/0013` 决策二)。
|
|
|
|
GovDoc 有一条硬纪律:agent 的原始输出、修复后的输出、恢复来源全程留痕,禁止静默修复。
|
|
这条测试原来断言「事件要同时带原文与修复后的文本」,而那个前提是错的——事件流可丢,
|
|
一件只存在于可丢通道里的事实撑不起「禁止静默修复」。
|
|
|
|
两份文本在意图日志里各有位置:原文在模型调用结果记录的回复里,修复后的那份是步记录的
|
|
`raw_output`。断言落在 `tests/unit/test_session.py`,因为要跑完一次完整运行再把日志读
|
|
回来,而这个接缝的契约只看得见一个出口实现。
|
|
"""
|