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