"""成品身份、只读下载、缺失格式复用和显式旧成品审计的独立机制验证。""" 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 == []