"""资料导入与原文版本:幂等去重、版本链、证据校验、用途授权与清理对照。""" import pytest from muse.共享.调用身份 import 用途 from muse.资料研究.接口 import 资料服务 from muse.资料研究.模型 import 导入请求, 资料错误 pytestmark = pytest.mark.数据库 @pytest.fixture def 服务(应用测试库): return 资料服务() @pytest.mark.case_id( "NC-w17-w17a01", environment="真实隔离 PG", given="隔离数据库", when="同源同内容幂等返回既有版本;同源异内容追加版本且授权只增不减", then=["重复可辨认", "版本链完整", "授权持久"], contract="docs/系统架构/新版设计/模块设计/B03-资料研究.md", ) def test_导入去重与版本链__17a01a(服务, 应用测试库): 库 = 应用测试库[用途.生产] 请求 = 导入请求( kind="reference", title="封神演义", origin="file:///资料/封神演义.txt", content="第一回 纣王女娲宫进香。\n\n最新章节请访问 example.com\n", authorized_uses=("analysis",), ) with 库.连接() as 连, 连.transaction(): 首次 = 服务.导入(连, "importer", 请求) assert 首次.重复 is False and 首次.revision == 1 # 同源同内容:幂等返回既有版本,重复可辨认。 再次 = 服务.导入(连, "importer", 请求) assert 再次.重复 is True and 再次.revision == 1 and 再次.source_id == 首次.source_id # 同源异内容:追加新版本,旧版本仍可回读。 新版 = 服务.导入( 连, "importer", 导入请求( kind="reference", title="封神演义(校对版)", origin=请求.origin, content="第一回 纣王女娲宫进香。(校对)\n", authorized_uses=("analysis",), ), ) assert 新版.重复 is False and 新版.revision == 2 and 新版.source_id == 首次.source_id 版本 = 服务.读取版本(连, 首次.source_id, 1) assert "最新章节" in 版本["content"] # 内容重导入不改变授权版本和用途。 源 = 服务.核对来源授权(连, 首次.source_id, "analysis") assert 源["authorized_uses"] == ["analysis"] assert 源["authorization_revision"] == 1 @pytest.mark.case_id( "NC-w17-w17a02", environment="真实隔离 PG", given="隔离数据库", when="按码点区间取原文片段并校验内容哈希;越界拒绝", then=["片段正确", "越界拒绝"], contract="docs/系统架构/新版设计/模块设计/B03-资料研究.md", ) def test_证据读取与哈希校验__17a02b(服务, 应用测试库): 库 = 应用测试库[用途.生产] 文本 = "林深把伞收在了门边。雨还没停。" with 库.连接() as 连, 连.transaction(): 结果 = 服务.导入( 连, "importer", 导入请求("note", "码头笔记", "note://local/1", 文本, ("analysis",)), ) 证据 = 服务.读取证据(连, 结果.source_id, 结果.revision, 0, 10) assert 证据["片段"] == "林深把伞收在了门边。" with pytest.raises(资料错误, match="区间越界"): 服务.读取证据(连, 结果.source_id, 结果.revision, 0, 999) @pytest.mark.case_id( "NC-w17-w17a03", environment="真实隔离 PG", given="隔离数据库", when="来源未授权的用途被拒绝", then=["未授权拒绝"], contract="docs/系统架构/新版设计/模块设计/B03-资料研究.md", ) def test_用途未授权拒绝消费__17a03c(服务, 应用测试库): 库 = 应用测试库[用途.生产] with 库.连接() as 连, 连.transaction(): 结果 = 服务.导入( 连, "importer", 导入请求("web", "某网页", "https://example.com/a", "网页内容", ("reference_reading",)), ) with pytest.raises(资料错误, match="未授权用途"): 服务.核对来源授权(连, 结果.source_id, "analysis") @pytest.mark.case_id( "NC-w17-w17a04", environment="真实隔离 PG", given="隔离数据库", when="清理生成对照而不改写原文;处置不可改写", then=["原文不变", "对照生成", "处置终态"], contract="docs/系统架构/新版设计/模块设计/B03-资料研究.md", ) def test_清理候选对照且原文不变__17a04d(服务, 应用测试库): 库 = 应用测试库[用途.生产] 原文 = "第一回 纣王女娲宫进香。\n最新章节请访问 example.com\n本章完。" with 库.连接() as 连, 连.transaction(): 结果 = 服务.导入( 连, "importer", 导入请求("reference", "样章", "file:///样章.txt", 原文, ("analysis",)), ) 候选 = 服务.生成清理候选(连, 结果.source_id, 结果.revision) assert 候选.removals and all(项["理由"] for 项 in 候选.removals) 应用后 = 服务.应用清理候选(连, 候选.candidate_id) assert "最新章节" not in 应用后.cleaned_text assert "第一回" in 应用后.cleaned_text # 原文版本不受清理影响。 版本 = 服务.读取版本(连, 结果.source_id, 结果.revision) assert 版本["content"] == 原文 # 重复应用与处置不可改写。 with pytest.raises(资料错误, match="已处置"): 服务.应用清理候选(连, 候选.candidate_id) 新候选 = 服务.生成清理候选(连, 结果.source_id, 结果.revision) 服务.拒绝清理候选(连, 新候选.candidate_id) with pytest.raises(资料错误, match="已处置"): 服务.应用清理候选(连, 新候选.candidate_id) @pytest.mark.case_id("NC-O09-PG-REVOKE") def test_撤权命令幂等与历史内容保留(应用测试库): from dataclasses import replace from muse.共享.标识 import 任务标识 from muse.共享.调用身份 import 内容用途, 调用身份 from muse.正式变更.接口 import 参与者目录, 变更错误, 正式变更服务 from muse.资料研究.接口 import 来源用途变更, 登记分析参与者 库 = 应用测试库[用途.生产] 目录 = 参与者目录() 登记分析参与者(目录) 服务 = 资料服务(正式变更服务(库, 目录)) 身份 = 调用身份("author", 任务标识("revoke"), 用途.生产, 内容用途.规划) 导入 = 导入请求("reference", "合成样本", "fixture://revoke", "合成文字", ("analysis", "sample")) with 库.连接() as 连, 连.transaction(): 旧 = 服务.导入(连, "author", 导入) 请求 = 来源用途变更(旧.source_id, 1, ("analysis",), "撤回样本") 回执 = 服务.变更来源用途(身份, "revoke", 请求) assert 服务.变更来源用途(身份, "revoke", 请求) == 回执 with pytest.raises(变更错误): 服务.变更来源用途(身份, "revoke", replace(请求, uses=())) with pytest.raises(变更错误): 服务.变更来源用途(身份, "stale", 请求) with 库.连接() as 连, 连.transaction(): assert 服务.读取版本(连, 旧.source_id, 1)["content"] == "合成文字" assert 服务.核对来源授权(连, 旧.source_id, "analysis")["authorization_revision"] == 2 with pytest.raises(资料错误): 服务.核对来源授权(连, 旧.source_id, "sample") with pytest.raises(资料错误, match="重导入不能修改"): 服务.导入(连, "author", 导入) 片段 = 服务.读取案例证据(连, "author", 旧.source_id, 1, 0, 2) assert 片段["excerpt"] is None and not 片段["text_allowed"] assert ( 连.execute( "SELECT count(*) FROM muse_source_version WHERE source_id=%s", (旧.source_id,) ).fetchone()[0] == 1 ) 全撤 = replace(请求, expected_authorization_revision=2, uses=(), reason="撤回全部") 服务.变更来源用途(身份, "revoke-all", 全撤) with 库.连接() as 连: with pytest.raises(资料错误): 服务.读取版本(连, 旧.source_id, 1) assert ( 连.execute( "SELECT content FROM muse_source_version WHERE source_id=%s", (旧.source_id,) ).fetchone()[0] == "合成文字" ) 他人 = replace(身份, 作者="other") with pytest.raises(资料错误): 服务.变更来源用途(他人, "other-grant", replace(请求, expected_authorization_revision=3)) @pytest.mark.case_id("NC-O09-PG-AUTH-RACE") def test_并发授权变更只有一个当前版本成功(应用测试库): from concurrent.futures import ThreadPoolExecutor from threading import Barrier from muse.共享.标识 import 任务标识 from muse.共享.调用身份 import 内容用途, 调用身份 from muse.正式变更.接口 import 参与者目录, 变更错误, 正式变更服务 from muse.资料研究.接口 import 来源用途变更, 登记分析参与者 库 = 应用测试库[用途.生产] 目录 = 参与者目录() 登记分析参与者(目录) 服务 = 资料服务(正式变更服务(库, 目录)) 身份 = 调用身份("author", 任务标识("revoke-race"), 用途.生产, 内容用途.规划) with 库.连接() as 连, 连.transaction(): 源 = 服务.导入( 连, "author", 导入请求( "note", "并发合成", "fixture://revoke-race", "合成文字", ("analysis", "sample") ), ) 屏障 = Barrier(2) def 改用途(name, uses): 屏障.wait(timeout=5) try: return 服务.变更来源用途(身份, name, 来源用途变更(源.source_id, 1, uses, name)) except (变更错误, 资料错误): return None with ThreadPoolExecutor(max_workers=2) as pool: 第一 = pool.submit(改用途, "revoke-sample", ("analysis",)) 第二 = pool.submit(改用途, "revoke-all", ()) 结果 = [第一.result(timeout=10), 第二.result(timeout=10)] 成功 = [r for r in 结果 if r is not None] assert len(成功) == 1 with 库.连接() as 连: 行 = 连.execute( "SELECT authorized_uses,authorization_revision FROM muse_source WHERE source_id=%s", (源.source_id,), ).fetchone() assert 行 == (成功[0]["results"][0]["authorized_uses"], 2) assert ( 连.execute( "SELECT count(*) FROM muse_source_version WHERE source_id=%s", (源.source_id,) ).fetchone()[0] == 1 )