muse-agent-example/tests/契约/test_流式终态.py
zizi b9af240b68 R2 收尾:W30 唯一写入观察与最终验收(C08 章后接通 / 构建身份重建 / 最终备份与恢复演练)
- 章后处理:事实提案命令身份加入类型、批内去重改为先校验后去重(REPEAT_OBJECT_TYPE 记跳过、不再静默丢重复);查询章后状态回传事实跳过;可按需发布/核对可登记任务与 config_not_declared 语义收紧
- 模型治理:推理字符数计量(ChatCompletions/Messages/执行合同)、探针在声明关闭推理却仍有推理时拒绝、计价支持缓存协议与未知模型兜底
- 运行态:资源包重建(发布身份 21d850f7…),三处配置同步,post-extract v5 代次 5 启用,服务 PID 96675(health/ready 通过)
- 门禁:make 检查通过;离线 1008 passed;定点数据库 101 passed;最终完整备份 bkp-1f5591d8… 与空库恢复演练 verified(152 表/158 关系/迁移 45,演练库已 DROP)
- 文档与证据:运行手册、功能覆盖、工作包清单 W30 证据、测试用例清单(17 条 execution_evidence 回填 + 历史死指针说明)、目标文件清单
- 旧实现与旧库直连材料退出(含明文凭据文件移除);新配置 配置/本机正式.toml、配置/本机维护.toml、配置/计费/、配置/运行配置/ 入库,均只含受控引用
- R2 执行证据在 .agents.local/改造/R2-20260909/(不入库)
2026-09-16 08:23:39 +08:00

245 lines
7.9 KiB
Python

"""合成流验证模型协议;不发送网络请求,不作为真实模型证据。"""
import asyncio
import json
import pytest
from muse.任务运行.接口 import 模型协议错误
from muse.基础设施.模型.HTTP传输 import 解码SSE
from muse.基础设施.模型.Responses import 解析响应
def 完成事件(*, status="completed", usage=True):
return {
"type": "response." + status,
"response": {
"id": "response-1",
"status": status,
"model": "test-model",
"output": [
{
"type": "message",
"content": [{"type": "output_text", "text": "中文\u0085保持。"}],
}
],
"usage": {"input_tokens": 10, "output_tokens": 4} if usage else None,
},
}
def test_Responses明确终态与UTF8字节边界__a61001() -> None:
帧 = ("data: " + json.dumps(完成事件(), ensure_ascii=False) + "\r\n\r\n").encode()
async def 字节流():
for b in 帧:
yield bytes([b])
async def 收集():
return [项 async for 项 in 解码SSE(字节流())]
结果 = 解析响应(asyncio.run(收集()))
assert 结果.状态 == "completed" and 结果.文本 == "中文\u0085保持。"
assert 结果.实际模型 == "test-model"
assert 结果.用量.输入token == 10 and 结果.用量.输出token == 4
@pytest.mark.parametrize(
"事件,期望",
[
([{"type": "response.output_text.delta", "delta": "只到一半"}], "incomplete"),
([完成事件(status="incomplete")], "incomplete"),
([完成事件(status="failed")], "failed"),
([{"type": "error", "code": "server_error"}], "failed"),
],
ids=["no-terminal", "incomplete", "failed", "error"],
)
def test_断流与失败不计模型完成__a61002(事件, 期望) -> None:
assert 解析响应(事件).状态 == 期望
def test_完成但用量未知独立保留__a61003() -> None:
结果 = 解析响应([完成事件(usage=False)])
assert 结果.状态 == "completed" and 结果.用量 is None
def test_错误SSE帧和UTF8拒绝__a61004() -> None:
async def 字节流():
yield b'data: {"text":"\xff"}\n\n'
async def 收集():
return [项 async for 项 in 解码SSE(字节流())]
with pytest.raises(模型协议错误):
asyncio.run(收集())
@pytest.mark.parametrize(
"结束,原因,状态",
[
(True, "end_turn", "completed"),
(False, "end_turn", "incomplete"),
(True, "max_tokens", "incomplete"),
],
ids=["complete", "missing-stop", "token-limit"],
)
def test_Anthropic结束消息与停止原因共同判定__a61005(结束, 原因, 状态) -> None:
from muse.基础设施.模型.Messages import 解析响应 as 解析
事件 = [
{
"type": "message_start",
"message": {"id": "a1", "model": "opus", "usage": {"input_tokens": 3}},
},
{"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}},
{
"type": "content_block_delta",
"index": 0,
"delta": {"type": "text_delta", "text": "甲乙。"},
},
{"type": "message_delta", "delta": {"stop_reason": 原因}, "usage": {"output_tokens": 2}},
]
if 结束:
事件.append({"type": "message_stop"})
结果 = 解析(事件)
assert 结果.状态 == 状态 and 结果.文本 == "甲乙。"
assert 结果.用量.输入token == 3
def test_ChatCompletions完成与工具回合分开__a61006() -> None:
from muse.基础设施.模型.ChatCompletions import 解析响应 as 解析
结果 = 解析(
[
{
"id": "c1",
"model": "test",
"choices": [{"index": 0, "delta": {"content": "完成。"}, "finish_reason": "stop"}],
}
]
)
assert 结果.状态 == "completed" and 结果.用量 is None
工具 = 解析(
[
{
"id": "c2",
"model": "test",
"choices": [
{
"index": 0,
"delta": {
"tool_calls": [
{
"index": 0,
"id": "tool1",
"function": {"name": "read", "arguments": "{}"},
}
]
},
"finish_reason": "tool_calls",
}
],
}
]
)
assert 工具.状态 == "tool_calls" and 工具.工具调用[0].名称 == "read"
def test_推理内容只计量不进入可见文本__a61008() -> None:
from muse.基础设施.模型.ChatCompletions import 解析响应 as 解析对话
from muse.基础设施.模型.Messages import 解析响应 as 解析消息
对话 = 解析对话(
[
{
"id": "c3",
"model": "test",
"choices": [
{
"index": 0,
"delta": {"reasoning_content": "先想两字"},
"finish_reason": None,
}
],
},
{
"id": "c3",
"model": "test",
"choices": [{"index": 0, "delta": {"content": "答案。"}, "finish_reason": "stop"}],
},
]
)
assert 对话.文本 == "答案。" and 对话.推理字符数 == 4
消息 = 解析消息(
[
{
"type": "message_start",
"message": {"id": "a3", "model": "opus", "usage": {"input_tokens": 3}},
},
{
"type": "content_block_start",
"index": 0,
"content_block": {"type": "thinking", "thinking": ""},
},
{
"type": "content_block_delta",
"index": 0,
"delta": {"type": "thinking_delta", "thinking": "推理三字"},
},
{
"type": "content_block_start",
"index": 1,
"content_block": {"type": "text", "text": ""},
},
{
"type": "content_block_delta",
"index": 1,
"delta": {"type": "text_delta", "text": "答案。"},
},
{
"type": "message_delta",
"delta": {"stop_reason": "end_turn"},
"usage": {"output_tokens": 9},
},
{"type": "message_stop"},
]
)
assert 消息.文本 == "答案。" and 消息.推理字符数 == 4
@pytest.mark.parametrize(
"模型,文本,合法",
[
("test", '{"正文":"中文"}', True),
("other", '{"正文":"中文"}', False),
("test", '{"正文":1}', False),
("test", '{"正文":"中文","state":"confirmed"}', False),
],
ids=["valid", "model-drift", "wrong-type", "extra-authority"],
)
def test_实际模型与输出合同同时满足才产候选__a61007(模型, 文本, 合法) -> None:
from muse.任务运行.接口 import 校验模型输出, 模型结果, 模型请求
请求 = 模型请求(
"call",
"synthetic",
"test",
"合成系统说明",
"合成输入",
{
"type": "object",
"properties": {"正文": {"type": "string"}},
"required": ["正文"],
"additionalProperties": False,
},
100,
30,
)
结果 = 模型结果("completed", 文本, 模型, None)
if 合法:
assert 校验模型输出(请求, 结果) == {"正文": "中文"}
else:
with pytest.raises(模型协议错误):
校验模型输出(请求, 结果)