muse-agent-example/tests/单元/test_交付读取与复用.py
zizi d909d1bd1b 后端实现与用例身份:19 包集成落地并修复收尾缺陷
实现侧:
- 上下文:任务范围拆分为 范围校验/范围授权;索引按可发现口径重建、索引新鲜度改对称差;依赖校验统一快照漂移说明。
- 知识方法:方法与材料读取口径统一;超限方法材料按可选省略,核对路径不再二次计费;删除无合同的读时重算。
- 任务运行:新增 context.usage/tool.denied 事件类型;连接池常驻并在装配生命周期内开关;调用结算与核对分列。
- 效果评测/审校修订/交付连载/作者经验/作品规划:凭据冻结、标定消费、导出补证、事实引文核对等收尾修复。
- 资源加载:能力正文不再夹带索引用的导航注记(该注记此前进入角色与技能的模型提示)。
- 元数据:受保护骨架与代码保护属性对齐;字段校验与内置结构口径同步。
- 基础设施:环境预检进入装配生命周期;数据库连接运行期字段不参与相等比较;索引指纹归一化 jsonb 浮点。
- 删除被替代实现:7 份旧提示词模板与空壳 资料来源 读取器。

用例侧:
- 用例身份与导航元信息迁移;夹具补生命周期、同库暴露与模板封存;
- 本轮定向修复:方法材料省略、事实引文、迁移回执、额度与暂停用例、慢用例超时预算等。
2026-09-18 01:15:00 +08:00

402 lines
15 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.

"""成品身份、只读下载、缺失格式复用和显式旧成品审计的独立机制验证。"""
import copy
import hashlib
from contextlib import contextmanager, nullcontext
from types import SimpleNamespace
import pytest
from muse.交付连载 import 发布登记, 定稿版本, 导出格式, 接口, 文稿导出
from muse.交付连载.存储 import 交付存储
from muse.交付连载.模型 import 交付错误, 发布请求
from muse.共享.调用身份 import 用途
from muse.正式变更.接口 import 固定哈希
@pytest.fixture
def 环境(monkeypatch):
manifest = {
"work_id": "work",
"materials": {
"title": "合成书",
"author_name": "作者",
"introduction": "简介",
"synopsis": "梗概",
"characters": "人物",
},
"chapters": [{"chapter_id": "c", "title": "第一章", "position": 1}],
}
metadata = {
"delivery_id": "00000000-0000-4000-8000-000000000010",
"work_id": "work",
"author_id": "author",
"manifest": manifest,
"manifest_hash": 固定哈希(manifest),
}
content = {
**metadata,
"chapters": [
{**manifest["chapters"][0], "paragraphs": ["合成正文。"], "text": "合成正文。"}
],
}
state = SimpleNamespace(
rows={},
source_reads=0,
payload_reads=0,
active=0,
checkpoints=[],
renders=[],
supplements=[],
)
class Store:
def __init__(self, conn):
pass
def 读定稿(self, delivery_id):
if 固定哈希(metadata["manifest"]) != metadata["manifest_hash"]:
raise 交付错误("DELIVERY_CORRUPT", "合成清单哈希错误")
return copy.deepcopy(metadata) if str(delivery_id) == metadata["delivery_id"] else None
def 读产物(self, delivery_id, format, version):
row = state.rows.get(format)
if row:
state.payload_reads += 1
return copy.deepcopy(row)
def 读产物头(self, delivery_id, formats, version):
return [
{k: copy.deepcopy(v) for k, v in row.items() if k != "data"}
for f, row in state.rows.items()
if f in formats
]
def 写产物(self, delivery_id, format, version, data, digest, verification, task_id):
assert state.active == 1
state.rows.setdefault(
format,
{
"artifact_id": format,
"delivery_id": delivery_id,
"format": format,
"formatter_version": version,
"data": data,
"content_hash": digest,
"verification": verification,
},
)
return copy.deepcopy(state.rows[format])
def 追加成品验证(self, artifact_id, author, proof):
assert author == metadata["author_id"]
state.rows[artifact_id]["supplemental_verification"] = proof
state.rows[artifact_id]["supplemental_author"] = author
state.supplements.append(proof)
class Database:
用途 = 用途.生产
@contextmanager
def 连接(self, **kwargs):
state.active += 1
try:
yield SimpleNamespace(transaction=lambda: nullcontext())
finally:
state.active -= 1
def read_source(*args):
state.source_reads += 1
raise AssertionError("快捷路径不得读取源正文")
def full_content(*args):
state.source_reads += 1
return copy.deepcopy(content)
for module in (定稿版本, 接口, 文稿导出, 发布登记):
monkeypatch.setattr(module, "交付存储", Store)
monkeypatch.setattr(定稿版本, "读取正文依据", read_source)
monkeypatch.setattr(文稿导出, "读取定稿内容", full_content)
svc = 接口.交付服务(Database(), None)
identity = SimpleNamespace(作者="author", 允许写正式内容=True)
task = SimpleNamespace(
任务ID="task",
作者="author",
流程=SimpleNamespace(流程ID="定稿交付"),
冻结输入={
"输入": {
"delivery_id": metadata["delivery_id"],
"manifest_hash": metadata["manifest_hash"],
"formatter_version": 导出格式.格式版本,
"formats": ["txt", "md", "docx", "epub"],
}
},
)
ctx = SimpleNamespace(任务=task, 领取="claim")
tasks = SimpleNamespace(
核对领取于=lambda c, claim: task,
保存检查点=lambda claim, result: state.checkpoints.append(result),
)
real_render, real_verify = 导出格式.导出, 导出格式.校验
def render(format, source):
assert state.active == 0
state.renders.append(format)
return real_render(format, source)
def verify(format, data, source):
assert state.active == 0
return real_verify(format, data, source)
monkeypatch.setattr(导出格式, "导出", render)
monkeypatch.setattr(导出格式, "校验", verify)
return SimpleNamespace(
svc=svc,
identity=identity,
metadata=metadata,
content=content,
state=state,
task=task,
tasks=tasks,
ctx=ctx,
)
def _seed(env, formats=("txt",), *, legacy=False):
for f in formats:
data = 导出格式.导出(f, env.content)
proof = 导出格式.校验(f, data, env.content)
if not legacy:
proof = 文稿导出.绑定验证(proof, env.metadata, f, 导出格式.格式版本)
env.state.rows[f] = {
"artifact_id": f,
"delivery_id": env.metadata["delivery_id"],
"format": f,
"formatter_version": 导出格式.格式版本,
"data": data,
"content_hash": hashlib.sha256(data).hexdigest(),
"verification": proof,
}
env.state.renders.clear()
@pytest.mark.case_id("NC-O10-001")
def test_下载在源读取故障时只读一次成品并核验一次哈希(环境, monkeypatch):
_seed(环境)
real_hash, hashes = hashlib.sha256, []
monkeypatch.setattr(
文稿导出.hashlib, "sha256", lambda data: (hashes.append(data), real_hash(data))[1]
)
row = 环境.svc.读取导出(环境.identity, 环境.metadata["delivery_id"], "txt")
assert row["data"] == 环境.state.rows["txt"]["data"]
assert 环境.state.payload_reads == 1 and 环境.state.source_reads == 0
# 清单固定哈希也用SHA256,成品字节只校验一次。
assert hashes.count(row["data"]) == 1
@pytest.mark.case_id("NC-O10-002")
@pytest.mark.parametrize(
"fault,code",
[
("missing", "EXPORT_NOT_READY"),
("legacy", "EXPORT_VERIFICATION_UPGRADE_REQUIRED"),
("bytes", "EXPORT_CORRUPT"),
("manifest", "EXPORT_CORRUPT"),
("version", "EXPORT_CORRUPT"),
("validation", "EXPORT_CORRUPT"),
("author", "DELIVERY_NOT_FOUND"),
("metadata", "DELIVERY_CORRUPT"),
],
)
def test_快捷下载拒绝缺失损坏错身份与旧证明且不读源(环境, fault, code):
_seed(环境, legacy=fault == "legacy")
row = 环境.state.rows["txt"]
if fault == "missing":
环境.state.rows.clear()
elif fault == "bytes":
row["data"] += b"x"
elif fault == "manifest":
row["verification"]["binding"]["manifest_hash"] = "0" * 64
elif fault == "version":
row["formatter_version"] = "wrong"
elif fault == "validation":
row["verification"]["validation_version"] = "wrong"
elif fault == "author":
环境.identity.作者 = "stranger"
elif fault == "metadata":
环境.metadata["manifest"]["work_id"] = "other"
with pytest.raises(交付错误) as error:
环境.svc.读取导出(环境.identity, 环境.metadata["delivery_id"], "txt")
assert error.value.错误码 == code
assert 环境.state.source_reads == 0
if fault == "missing":
assert 环境.state.payload_reads == 0
@pytest.mark.case_id("NC-O10-003")
@pytest.mark.parametrize("existing", [(), ("txt", "md"), ("txt", "md", "docx", "epub")])
def test_多格式源只读取一次且恢复仅渲染缺失格式(环境, existing):
_seed(环境, existing)
before = copy.deepcopy(环境.state.rows)
result = 文稿导出.执行导出(环境.svc, 环境.tasks, 环境.ctx)
assert 环境.state.source_reads == (0 if len(existing) == 4 else 1)
assert 环境.state.renders == [
f for f in 环境.task.冻结输入["输入"]["formats"] if f not in existing
]
assert len(result["artifacts"]) == 4
assert all(环境.state.rows[f] == before[f] for f in existing)
环境.state.checkpoints.clear() # 检查点丢失仍以成品为准。
环境.state.source_reads = 0
环境.state.renders.clear()
assert 文稿导出.执行导出(环境.svc, 环境.tasks, 环境.ctx) == result
assert 环境.state.source_reads == 0 and 环境.state.renders == []
@pytest.mark.case_id("NC-O10-004")
def test_准备发布和任务恢复检查仅核元数据(环境):
request = 发布请求(
"release",
环境.metadata["delivery_id"],
("c",),
"2026-09-17T00:00:00+00:00",
"平台",
"https://example.invalid/release",
)
work, payload = 发布登记.准备发布(None, "author", request)
assert work == "work" and payload["chapter_versions"] == 环境.metadata["manifest"]["chapters"]
assert 文稿导出.导出任务内容(None, 环境.task)[0] == 环境.metadata
assert 环境.state.source_reads == 环境.state.payload_reads == 0
@pytest.mark.case_id("NC-O10-005")
def test_显式审计旧成品追加证明而不改旧字节及原回执(环境):
_seed(环境, legacy=True)
before = copy.deepcopy(环境.state.rows["txt"])
result = 环境.svc.审计导出(环境.identity, 环境.metadata["delivery_id"], "txt")
assert result["persisted"] is False and 环境.state.supplements == []
with pytest.raises(交付错误, match="旧成品"):
环境.svc.读取导出(环境.identity, 环境.metadata["delivery_id"], "txt")
receipt = 环境.svc.审计导出(环境.identity, 环境.metadata["delivery_id"], "txt", 补充验证=True)
assert receipt["persisted"] is True
环境.state.source_reads = 0
row = 环境.svc.读取导出(环境.identity, 环境.metadata["delivery_id"], "txt")
assert {
k: v
for k, v in row.items()
if k not in {"supplemental_verification", "supplemental_author"}
} == before
assert 环境.state.source_reads == 0
@pytest.mark.case_id("NC-O10-006")
def test_显式审计实际文本不符不能复制清单补证(环境):
_seed(环境, legacy=True)
环境.content["chapters"][0]["text"] = "另一正文"
环境.content["chapters"][0]["paragraphs"] = ["另一正文"]
with pytest.raises(交付错误) as error:
环境.svc.审计导出(环境.identity, 环境.metadata["delivery_id"], "txt", 补充验证=True)
assert error.value.错误码 == "EXPORT_VERIFICATION_FAILED"
assert 环境.state.supplements == []
@pytest.mark.case_id("NC-O10-007")
def test_真实存储清单哈希不符必须拒绝():
cursor = SimpleNamespace(
execute=lambda *a: SimpleNamespace(
fetchone=lambda: {"manifest": {"work_id": "wrong"}, "manifest_hash": "0" * 64}
)
)
with pytest.raises(交付错误) as error:
交付存储(SimpleNamespace(cursor=lambda **k: cursor)).读定稿("delivery")
assert error.value.错误码 == "DELIVERY_CORRUPT"
@pytest.mark.case_id("NC-O10-011")
def test_格式已存但被损坏不会依据检查点跳过或重渲染(环境):
_seed(环境, ("txt", "md", "docx", "epub"))
环境.state.rows["txt"]["data"] += b"bad"
环境.state.checkpoints.append({"artifacts": [{"format": "txt", "verified": True}]})
with pytest.raises(交付错误) as error:
文稿导出.执行导出(环境.svc, 环境.tasks, 环境.ctx)
assert error.value.错误码 == "EXPORT_CORRUPT"
assert 环境.state.source_reads == 0 and 环境.state.renders == []
@pytest.mark.case_id("NC-O10-012")
def test_显式审计源不可用不追加证明(环境, monkeypatch):
_seed(环境, legacy=True)
def fail(*args):
raise 交付错误("DELIVERY_SOURCE_CHANGED", "合成源故障")
monkeypatch.setattr(文稿导出, "读取定稿内容", fail)
with pytest.raises(交付错误) as error:
环境.svc.审计导出(环境.identity, 环境.metadata["delivery_id"], "txt", 补充验证=True)
assert error.value.错误码 == "DELIVERY_SOURCE_CHANGED"
assert 环境.state.supplements == []
@pytest.mark.case_id("NC-O10-013")
def test_读取产物头不查询成品载荷():
queries = []
cursor = SimpleNamespace(
execute=lambda query, args: (queries.append(query), SimpleNamespace(fetchall=lambda: []))[1]
)
store = 交付存储(SimpleNamespace(cursor=lambda **k: cursor))
assert store.读产物头("delivery", ["txt", "md"], 导出格式.格式版本) == []
projection = queries[0].split("FROM", 1)[0]
assert "data" not in projection and "a.*" not in projection
assert "content_hash" in projection and "verification" in projection
@pytest.mark.case_id("NC-O10-015")
def test_渲染后租约失效不写成品(环境):
calls = 0
def claim(conn, receipt):
nonlocal calls
calls += 1
if calls >= 3:
raise 交付错误("LEASE_EXPIRED", "合成租约过期")
return 环境.task
环境.tasks.核对领取于 = claim
with pytest.raises(交付错误) as error:
文稿导出.执行导出(环境.svc, 环境.tasks, 环境.ctx)
assert error.value.错误码 == "LEASE_EXPIRED"
assert 环境.state.rows == {} and 环境.state.checkpoints == []
assert 环境.state.renders == ["txt"]
@pytest.mark.case_id("NC-O10-016")
def test_中断后复用已落库格式而不信检查点(环境, monkeypatch):
real_render = 导出格式.导出
def fail(format, source):
if format == "docx":
raise RuntimeError("合成中断")
return real_render(format, source)
monkeypatch.setattr(导出格式, "导出", fail)
with pytest.raises(RuntimeError, match="合成中断"):
文稿导出.执行导出(环境.svc, 环境.tasks, 环境.ctx)
assert set(环境.state.rows) == {"txt", "md"}
prior = copy.deepcopy(环境.state.rows)
环境.state.checkpoints.clear()
环境.state.renders.clear()
环境.state.source_reads = 0
monkeypatch.setattr(导出格式, "导出", real_render)
文稿导出.执行导出(环境.svc, 环境.tasks, 环境.ctx)
assert 环境.state.renders == ["docx", "epub"] and 环境.state.source_reads == 1
assert all(环境.state.rows[f] == row for f, row in prior.items())
@pytest.mark.case_id("NC-O10-017")
def test_审计不得把损坏的新证明当旧记录补证(环境):
_seed(环境)
环境.state.rows["txt"]["verification"]["binding"]["manifest_hash"] = "0" * 64
with pytest.raises(交付错误) as error:
环境.svc.审计导出(环境.identity, 环境.metadata["delivery_id"], "txt", 补充验证=True)
assert error.value.错误码 == "EXPORT_CORRUPT"
assert 环境.state.source_reads == 0 and 环境.state.supplements == []