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

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

457 lines
18 KiB
Python

"""方法检索的局部失效、同事务派生及带变更守卫的缓存。"""
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