"""方法检索的局部失效、同事务派生及带变更守卫的缓存。""" from copy import deepcopy from types import SimpleNamespace from unittest.mock import Mock import pytest from muse.上下文 import 混合检索 as 检索模块 from muse.上下文 import 索引维护 from muse.基础设施.模型.嵌入 import 特征哈希嵌入器 from muse.正式变更.接口 import 作者动作, 参与者目录 from muse.知识方法.接口 import 方法错误 from muse.知识方法.方法版本 import 方法决定入口 class 索引夹具: def __init__(self): self.嵌入器 = 特征哈希嵌入器() self.行 = [ { "method_version_id": "v1", "检索文本": "伏笔埋设回收", "检索文本哈希": 索引维护.固定哈希(["伏笔埋设回收"]), "vector": self.嵌入器.嵌入("伏笔埋设回收"), "embedder": self.嵌入器.身份, } ] self.头 = { "ledger_id": 1, "索引身份": 索引维护.索引指纹(self.行, self.嵌入器.身份), "embedder": self.嵌入器.身份, "method_count": len(self.行), "generation": 1, } self.整代读取 = 0 def 当前索引版本(self): return dict(self.头) def 读取索引行(self): return deepcopy(self.行) def 读取索引状态(self): return dict(self.头) def 读取索引快照(self, **_): self.整代读取 += 1 return {**self.头, "entries": deepcopy(self.行)} def 替换方法条目(self, 旧版本, 新行): self.行 = [条 for 条 in self.行 if 条["method_version_id"] not in 旧版本] self.行.append( dict( zip( ("method_version_id", "检索文本", "检索文本哈希", "vector", "embedder"), 新行, strict=True, ) ) ) self.头["generation"] += 1 def 登记索引版本(self, 指纹, 嵌入器, 数量): self.头.update( ledger_id=self.头["ledger_id"] + 1, 索引身份=指纹, embedder=嵌入器, method_count=数量, generation=self.头["generation"] + 1, ) return self.头["ledger_id"] @pytest.fixture def 索引环境(monkeypatch): 存储 = 索引夹具() 连 = SimpleNamespace( info=SimpleNamespace(host="isolated-test", port=1, dbname="methods", user="muse_app"), execute=Mock(), ) monkeypatch.setattr(索引维护, "索引存储", lambda _: 存储) if hasattr(索引维护, "_清空索引缓存"): 索引维护._清空索引缓存() return 连, 存储 @pytest.mark.case_id("NC-O07-INDEX-001") def test_普通读取只验索引字节不展开所有方法授权(索引环境, monkeypatch): 连, 存储 = 索引环境 def 禁止全库展开(_): raise 方法错误("SOURCE_USE_DENIED", "另一张方法卡的来源已撤权") monkeypatch.setattr(索引维护, "读取方法检索资料", 禁止全库展开) 头, 行, _ = 索引维护.读取新鲜索引(连) assert 头["索引身份"] == 存储.头["索引身份"] assert [条["method_version_id"] for 条 in 行] == ["v1"] @pytest.mark.case_id("NC-O07-INDEX-002") def test_相同守卫代次百次读取仅冷载一次且视图不可改(索引环境): 连, 存储 = 索引环境 for _ in range(100): _, 行, _ = 索引维护.读取新鲜索引(连) assert 存储.整代读取 == 1 with pytest.raises(TypeError): 行[0]["检索文本"] = "不能污染共享视图" with pytest.raises(TypeError): 行[0]["vector"][0] = 3 @pytest.mark.case_id("NC-O07-INDEX-003") @pytest.mark.parametrize( "字段,污染", [("检索文本", "污染"), ("vector", [0.1]), ("embedder", "old")] ) def test_暖缓存后任一索引字节变化都由代次守卫拒绝(索引环境, 字段, 污染): 连, 存储 = 索引环境 索引维护.读取新鲜索引(连) 存储.行[0][字段] = 污染 存储.头["generation"] += 1 with pytest.raises(方法错误) as 错: 索引维护.读取新鲜索引(连) assert 错.value.错误码 == "INDEX_STALE" assert 错.value.上下文["status"] == "blocked" assert 存储.整代读取 == 2 @pytest.mark.case_id("NC-O07-INDEX-004") def test_显式审计即使守卫被测试桩冻结也绕缓存验字节(索引环境): 连, 存储 = 索引环境 索引维护.读取新鲜索引(连) 存储.行[0]["检索文本哈希"] = "伪摘要" 结果 = 索引维护.核对索引新鲜(连) assert 结果["status"] == "blocked" and not 结果["新鲜"] assert 存储.整代读取 == 2 @pytest.mark.case_id("NC-O07-INDEX-005") def test_同代次不同数据库不借用别库视图(索引环境): 连, 存储 = 索引环境 索引维护.读取新鲜索引(连) 另库 = SimpleNamespace( info=SimpleNamespace(host="another-server", port=1, dbname="methods", user="muse_app") ) 索引维护.读取新鲜索引(另库) assert 存储.整代读取 == 2 @pytest.mark.case_id("NC-O07-INDEX-006") def test_回滚后代次复用也必须核对索引指纹(索引环境): 连, 存储 = 索引环境 索引维护.读取新鲜索引(连) 存储.替换方法条目( ["v1"], ("v2", "回收", 索引维护.固定哈希(["回收"]), 存储.嵌入器.嵌入("回收"), 存储.嵌入器.身份), ) 存储.头["索引身份"] = 索引维护.索引指纹(存储.行, 存储.嵌入器.身份) 索引维护.读取新鲜索引(连) # 模拟失败事务的 generation 后来被复用;它不能令不同字节复用旧视图。 存储.行[0]["检索文本"] = "新的回收" 存储.行[0]["检索文本哈希"] = 索引维护.固定哈希(["新的回收"]) 存储.行[0]["vector"] = 存储.嵌入器.嵌入("新的回收") 存储.头["索引身份"] = 索引维护.索引指纹(存储.行, 存储.嵌入器.身份) _, 行, _ = 索引维护.读取新鲜索引(连) assert 行[0]["检索文本"] == "新的回收" assert 存储.整代读取 == 3 def 准备候选(monkeypatch, 行数=2): embedder = 特征哈希嵌入器() 行 = [ {"method_version_id": f"v{i}", "检索文本": f"方法{i}", "vector": embedder.嵌入(f"方法{i}")} for i in range(行数) ] monkeypatch.setattr( 检索模块, "读取新鲜索引", lambda *_, **__: ({"索引身份": "index", "generation": 1}, 行, {}) ) monkeypatch.setattr( 检索模块, "读取方法检索身份", lambda *_, **__: tuple(f"v{i}" for i in range(行数)) ) monkeypatch.setattr(检索模块, "关键词得分", lambda *_: {f"v{i}": 行数 - i for i in range(行数)}) monkeypatch.setattr(检索模块, "向量得分", lambda *_: {}) return embedder def 候选资料(version_id): return { "version_id": version_id, "method_id": "m-" + version_id, "type_id": "craft", "title": "合成方法", "version": 1, "来源回读": [], "检索文本": "方法" + version_id[1:], } @pytest.mark.case_id("NC-O07-INDEX-007") def test_局部撤权不堵有效方法且不会泄露失败内容(monkeypatch): embedder = 准备候选(monkeypatch) def 读取(_, version_id, **__): if version_id == "v0": raise 方法错误("SOURCE_USE_DENIED", "禁止泄露的来源说明") return 候选资料(version_id) monkeypatch.setattr(检索模块, "读取方法检索候选", 读取) 结果 = 检索模块.混合检索(object(), "查询", embedder, 数量=1) assert [项["version_id"] for 项 in 结果["结果"]] == ["v1"] assert 结果["status"] == "partial" and 结果["evaluated_count"] == 2 assert 结果["candidate_errors"] == [ {"version_id": "v0", "code": "SOURCE_USE_DENIED", "retryable": False} ] assert "禁止泄露" not in repr(结果) @pytest.mark.case_id("NC-O07-INDEX-008") def test_全部候选不可消费不伪装无命中(monkeypatch): embedder = 准备候选(monkeypatch) def 拒绝(*_, **__): raise 方法错误("METHOD_VERSION_STALE", "旧版本") monkeypatch.setattr(检索模块, "读取方法检索候选", 拒绝) with pytest.raises(方法错误) as 错: 检索模块.混合检索(object(), "查询", embedder) assert 错.value.错误码 == "METHOD_CANDIDATES_UNAVAILABLE" assert 错.value.上下文["status"] == "blocked" assert len(错.value.上下文["candidate_errors"]) == 2 @pytest.mark.case_id("NC-O07-INDEX-009") def test_候选上限不因失效而无限继续且标明余项(monkeypatch): embedder = 准备候选(monkeypatch, 250) 调用 = [] def 读取(_, version_id, **__): 调用.append(version_id) if version_id == "v0": return 候选资料(version_id) raise 方法错误("METHOD_STATE_UNAVAILABLE", "已停用") monkeypatch.setattr(检索模块, "读取方法检索候选", 读取) 结果 = 检索模块.混合检索(object(), "查询", embedder, 数量=50) assert len(调用) == 200 assert 结果["has_more_candidates"] and 结果["status"] == "partial" assert 结果["candidate_limit"] == 200 @pytest.mark.case_id("NC-O07-INDEX-010") def test_旧代缺新条目仍返回匹配方法并明确待派生(monkeypatch): embedder = 准备候选(monkeypatch, 1) monkeypatch.setattr(检索模块, "读取方法检索身份", lambda *_, **__: ("v0", "new")) monkeypatch.setattr(检索模块, "读取方法检索候选", lambda _, id_, **__: 候选资料(id_)) 结果 = 检索模块.混合检索(object(), "查询", embedder) assert len(结果["结果"]) == 1 and 结果["pending_count"] == 1 assert 结果["status"] == "partial" @pytest.mark.case_id("NC-O07-INDEX-011") def test_方法确认通过注入把派生操作放在正式版本之后(monkeypatch): from muse.知识方法 import 方法版本 monkeypatch.setattr( 方法版本, "核对提案归属", lambda *_: ({"proposal_hash": "p-hash"}, SimpleNamespace(method_id="m1")), ) 命令 = SimpleNamespace(动作=作者动作.采纳, 业务请求=SimpleNamespace(proposal_id="p1")) 当前 = SimpleNamespace(目标="method:m1", 数据版本=0) 入口 = 方法决定入口(索引维护.方法索引操作) 计划 = 入口.准备(object(), SimpleNamespace(作者="author"), 命令, 当前) assert [项.参与者 for 项 in 计划.操作] == ["B04.write_version", "B09.update_method_index"] assert 计划.操作[1].请求.method_id == "m1" with pytest.raises(方法错误, match="缺少同事务"): 方法决定入口().准备(object(), SimpleNamespace(作者="author"), 命令, 当前) @pytest.mark.case_id("NC-O07-INDEX-012") def test_派生参与者沿原连接替换本方法并保存新头(索引环境, monkeypatch): 连, 存储 = 索引环境 旁 = deepcopy(存储.行[0]) 旁["method_version_id"] = "other" 存储.行.append(旁) 存储.头.update(method_count=2, 索引身份=索引维护.索引指纹(存储.行, 存储.嵌入器.身份)) 读取 = Mock(return_value=({"version_id": "v2", "检索文本": "新的方法"}, ["v1", "v2"])) monkeypatch.setattr(索引维护, "读取方法索引维护资料", 读取) 目录 = 参与者目录() 索引维护.登记方法索引参与者(目录) 操作 = 索引维护.方法索引操作("author", "method") 回执 = 目录.参与者[操作.参与者].提交(连, 操作.请求) 读取.assert_called_once_with(连, "author", "method") assert {条["method_version_id"] for 条 in 存储.行} == {"other", "v2"} assert next(条 for 条 in 存储.行 if 条["method_version_id"] == "other") == 旁 assert 回执["index_ledger_id"] == 2 assert 存储.头["索引身份"] == 索引维护.索引指纹(存储.行, 存储.嵌入器.身份) @pytest.mark.case_id("NC-O07-INDEX-013") def test_条目容量和真实字节超限明确拒绝(索引环境, monkeypatch): 连, 存储 = 索引环境 monkeypatch.setattr(索引维护, "索引数量上限", 0) with pytest.raises(方法错误) as 错: 索引维护.读取新鲜索引(连) assert 错.value.错误码 == "INDEX_CAPACITY_EXCEEDED" monkeypatch.setattr(索引维护, "索引数量上限", 5000) monkeypatch.setattr(索引维护, "索引字节上限", 32) with pytest.raises(方法错误) as 错: 索引维护.读取新鲜索引(连) assert 错.value.错误码 == "INDEX_CAPACITY_EXCEEDED" @pytest.mark.case_id("NC-O07-INDEX-014") @pytest.mark.parametrize( "状态,当前版本,作者,错误码", [ ("disabled", 1, "author", "METHOD_STATE_UNAVAILABLE"), ("withdrawn", 1, "author", "METHOD_STATE_UNAVAILABLE"), ("enabled", 2, "author", "METHOD_VERSION_STALE"), ("enabled", 1, "other-author", "SCOPE_DENIED"), ], ) def test_当前禁用旧版本或越权候选先拒绝而不读材料(monkeypatch, 状态, 当前版本, 作者, 错误码): from muse.知识方法 import 方法卡 存储 = SimpleNamespace( 读取检索版本头=lambda _: { "method_id": "m", "owner": "author", "state": 状态, "version": 1, "current_version": 当前版本, } ) monkeypatch.setattr(方法卡, "方法存储", lambda _: 存储) 读正文 = Mock() monkeypatch.setattr(方法卡, "读取版本材料", 读正文) with pytest.raises(方法错误) as 错: 方法卡.读取方法检索候选(object(), "v1", 作者=作者) assert 错.value.错误码 == 错误码 读正文.assert_not_called() @pytest.mark.case_id("NC-O07-INDEX-015") def test_来源授权同次去重但下次撤权立即拒绝(monkeypatch): from muse.知识方法 import 证据反例 from muse.知识方法.接口 import 证据引用 from muse.资料研究 import 接口 as 资料 当前 = {"allowed": True, "revision": 1} 次数 = {"authorization": 0, "version": 0} def 授权(_, 来源, 用途): 次数["authorization"] += 1 if not 当前["allowed"]: raise 资料.资料错误("撤权") return { "source_id": 来源, "authorized_uses": [用途], "authorization_revision": 当前["revision"], } def 版本(_, 连, 来源, revision): 次数["version"] += 1 return {"source_id": 来源, "revision": revision} monkeypatch.setattr(资料, "核对来源授权", 授权) monkeypatch.setattr(资料.资料服务, "读取版本依据", 版本) 证 = (证据引用(kind="source", source_id="synthetic-source", revision=1),) 缓存 = {} for _ in range(3): 证据反例.核对证据(object(), "author", 证, 来源缓存=缓存) assert 次数 == {"authorization": 1, "version": 1} 当前.update(allowed=False, revision=2) 新缓存 = {} for _ in range(2): with pytest.raises(方法错误) as 错: 证据反例.核对证据(object(), "author", 证, 来源缓存=新缓存) assert 错.value.错误码 == "SOURCE_USE_DENIED" assert 次数 == {"authorization": 2, "version": 1} @pytest.mark.case_id("NC-O07-INDEX-016") def test_候选材料字节预算在正文读取前拒绝(monkeypatch): from muse.知识方法 import 方法卡 存储 = SimpleNamespace( 读取检索版本头=lambda _: { "owner": "author", "state": "confirmed", "version": 1, "current_version": 1, }, 读取版本材料大小=lambda _: 1024, ) monkeypatch.setattr(方法卡, "方法存储", lambda _: 存储) 读正文 = Mock() monkeypatch.setattr(方法卡, "读取版本材料", 读正文) def 预算(字节): assert 字节 == 1024 raise 方法错误("METHOD_INPUT_TOO_LARGE", "本次预算不允许") with pytest.raises(方法错误, match="METHOD_INPUT_TOO_LARGE"): 方法卡.读取方法检索候选(object(), "v1", 作者="author", 读取预算=预算) 读正文.assert_not_called() @pytest.mark.case_id("NC-O07-INDEX-017") def test_向量逐项只算一次且保留零负分过滤(monkeypatch): from muse.基础设施.检索 import 向量 原 = 向量.余弦相似 计数 = Mock(side_effect=原) monkeypatch.setattr(向量, "余弦相似", 计数) 得分 = 向量.向量得分( [1.0, 0.0], { "正一": [1.0, 0.0], "正二": [2.0, 0.0], "零": [0.0, 0.0], "负": [-1.0, 0.0], }, ) assert 得分 == {"正一": 1.0, "正二": 1.0} assert 计数.call_count == 4 with pytest.raises(ValueError, match="维度不一致"): 向量.向量得分([1.0], {"维度错误": [1.0, 0.0]}) @pytest.mark.case_id("NC-O07-INDEX-018") def test_暖索引每次仍核当前授权并在撤权后失败(索引环境, monkeypatch): 连, 存储 = 索引环境 monkeypatch.setattr(检索模块, "读取方法检索身份", lambda *_, **__: ("v1",)) monkeypatch.setattr(检索模块, "关键词得分", lambda *_: {"v1": 1.0}) monkeypatch.setattr(检索模块, "向量得分", lambda *_: {}) 当前 = {"允许": True} 读取次数 = [] def 候选(_, version_id, *, 来源缓存, **__): if "允许" not in 来源缓存: 来源缓存["允许"] = 当前["允许"] 读取次数.append(当前["允许"]) if not 来源缓存["允许"]: raise 方法错误("SOURCE_USE_DENIED", "当前来源已撤权") return {**候选资料(version_id), "检索文本": "伏笔埋设回收"} monkeypatch.setattr(检索模块, "读取方法检索候选", 候选) for _ in range(100): assert 检索模块.混合检索(连, "伏笔", 存储.嵌入器)["status"] == "complete" 当前["允许"] = False with pytest.raises(方法错误, match="METHOD_CANDIDATES_UNAVAILABLE"): 检索模块.混合检索(连, "伏笔", 存储.嵌入器) assert 读取次数 == [True] * 100 + [False] assert 存储.整代读取 == 1