实现侧: - 上下文:任务范围拆分为 范围校验/范围授权;索引按可发现口径重建、索引新鲜度改对称差;依赖校验统一快照漂移说明。 - 知识方法:方法与材料读取口径统一;超限方法材料按可选省略,核对路径不再二次计费;删除无合同的读时重算。 - 任务运行:新增 context.usage/tool.denied 事件类型;连接池常驻并在装配生命周期内开关;调用结算与核对分列。 - 效果评测/审校修订/交付连载/作者经验/作品规划:凭据冻结、标定消费、导出补证、事实引文核对等收尾修复。 - 资源加载:能力正文不再夹带索引用的导航注记(该注记此前进入角色与技能的模型提示)。 - 元数据:受保护骨架与代码保护属性对齐;字段校验与内置结构口径同步。 - 基础设施:环境预检进入装配生命周期;数据库连接运行期字段不参与相等比较;索引指纹归一化 jsonb 浮点。 - 删除被替代实现:7 份旧提示词模板与空壳 资料来源 读取器。 用例侧: - 用例身份与导航元信息迁移;夹具补生命周期、同库暴露与模板封存; - 本轮定向修复:方法材料省略、事实引文、迁移回执、额度与暂停用例、慢用例超时预算等。
402 lines
15 KiB
Python
402 lines
15 KiB
Python
"""成品身份、只读下载、缺失格式复用和显式旧成品审计的独立机制验证。"""
|
||
|
||
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 == []
|