实现侧: - 上下文:任务范围拆分为 范围校验/范围授权;索引按可发现口径重建、索引新鲜度改对称差;依赖校验统一快照漂移说明。 - 知识方法:方法与材料读取口径统一;超限方法材料按可选省略,核对路径不再二次计费;删除无合同的读时重算。 - 任务运行:新增 context.usage/tool.denied 事件类型;连接池常驻并在装配生命周期内开关;调用结算与核对分列。 - 效果评测/审校修订/交付连载/作者经验/作品规划:凭据冻结、标定消费、导出补证、事实引文核对等收尾修复。 - 资源加载:能力正文不再夹带索引用的导航注记(该注记此前进入角色与技能的模型提示)。 - 元数据:受保护骨架与代码保护属性对齐;字段校验与内置结构口径同步。 - 基础设施:环境预检进入装配生命周期;数据库连接运行期字段不参与相等比较;索引指纹归一化 jsonb 浮点。 - 删除被替代实现:7 份旧提示词模板与空壳 资料来源 读取器。 用例侧: - 用例身份与导航元信息迁移;夹具补生命周期、同库暴露与模板封存; - 本轮定向修复:方法材料省略、事实引文、迁移回执、额度与暂停用例、慢用例超时预算等。
413 lines
16 KiB
Python
413 lines
16 KiB
Python
"""逐条承接引文行为;不执行旧Skill脚本、不规范化正文。"""
|
||
|
||
from dataclasses import asdict, replace
|
||
|
||
import pytest
|
||
from pydantic import TypeAdapter, ValidationError
|
||
|
||
from muse.元数据.接口 import (
|
||
值规则,
|
||
元数据错误,
|
||
合成结构,
|
||
字段定义,
|
||
引用规则,
|
||
类型定义,
|
||
结构定义,
|
||
读取内置种子,
|
||
)
|
||
from muse.故事世界.接口 import (
|
||
事实条目,
|
||
事实错误,
|
||
保留名称提及,
|
||
叙事窗口,
|
||
定位字面引文,
|
||
引用对象,
|
||
校验事实批次,
|
||
核对原文引文,
|
||
解析类型选择,
|
||
)
|
||
from muse.正文写作.接口 import 换行节点, 文本节点, 正文草稿, 段落
|
||
|
||
原增量正文 = "林深把黑纹缠上手臂,异种核心在胸腔里低鸣。何岚站在舱门口没有说话。"
|
||
原抽取正文 = "林深走进舰桥,何岚把黑色钥匙交给他。"
|
||
|
||
|
||
def 单段(内容):
|
||
return 正文草稿((段落("p1", (文本节点(内容),)),))
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-3554e51a0d7a",
|
||
environment="离线,固定原文与引用",
|
||
when="解析事实或关系提案并解析其证据和端点。",
|
||
contract="docs/系统架构/新版设计/数据模型/状态与来源有效性.md",
|
||
)
|
||
def test_quote_not_found_fails_closed__3554e5():
|
||
with pytest.raises(事实错误) as 错误:
|
||
定位字面引文(单段(原增量正文), "正文里不存在的句子")
|
||
assert 错误.value.错误码 == "FACT_QUOTE_NOT_FOUND"
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-5058648ccd62",
|
||
environment="离线,固定原文与引用",
|
||
given="原增量BODY追加字面反斜杠n与原quote真实换行;另给格式1显式换行正文作对照",
|
||
when="对固定格式1正文定位并核对引文",
|
||
then=["字面反斜杠输入不改写,错误换行引文拒绝;真实换行正文可定位且码点区间准确"],
|
||
contract="docs/系统架构/新版设计/数据模型/状态与来源有效性.md",
|
||
)
|
||
def test_quote_is_normalized_before_matching__505864():
|
||
# 当前合成样本:格式1不允许把字面反斜杠改成真正换行。
|
||
escaped_body = 原增量正文 + "\\n舱灯闪了一下"
|
||
quote = "\n舱灯闪了一下"
|
||
with pytest.raises(事实错误) as 错误:
|
||
定位字面引文(单段(escaped_body), quote)
|
||
assert 错误.value.错误码 == "FACT_QUOTE_NOT_FOUND"
|
||
字面证据 = 定位字面引文(单段(escaped_body), "\\n舱灯闪了一下")
|
||
assert asdict(字面证据) == {
|
||
"paragraph_id": "p1",
|
||
"start": len(原增量正文),
|
||
"end": len(escaped_body),
|
||
"quote": "\\n舱灯闪了一下",
|
||
}
|
||
assert 核对原文引文(单段(escaped_body), 字面证据) == "\\n舱灯闪了一下"
|
||
assert 单段(escaped_body).format_version == 1
|
||
原文 = 正文草稿((段落("p1", (文本节点(原增量正文), 换行节点(), 文本节点("舱灯闪了一下"))),))
|
||
证据 = 定位字面引文(原文, quote)
|
||
assert 核对原文引文(原文, 证据) == quote
|
||
assert 证据.start == len(原增量正文) and 证据.end == len(原增量正文 + quote)
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-9e18584a8a92",
|
||
environment="离线,固定原文与引用",
|
||
when="解析事实或关系提案并解析其证据和端点。",
|
||
contract="docs/系统架构/新版设计/数据模型/状态与来源有效性.md",
|
||
)
|
||
def test_normalize_rejects_fabricated_evidence__9e1858():
|
||
原输入 = {
|
||
"type": "item",
|
||
"name": "黑钥匙",
|
||
"brief": "钥匙",
|
||
"fields": {},
|
||
"evidence": "正文没有这句",
|
||
}
|
||
with pytest.raises(事实错误) as 错误:
|
||
定位字面引文(单段(原抽取正文), 原输入["evidence"])
|
||
assert 错误.value.错误码 == "FACT_QUOTE_NOT_FOUND"
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-1b9888594fd1",
|
||
environment="离线,固定原文与引用",
|
||
when="解析事实或关系提案并解析其证据和端点。",
|
||
contract="docs/系统架构/新版设计/数据模型/状态与来源有效性.md",
|
||
)
|
||
def test_normalize_binds_evidence_and_aliases__1b9888():
|
||
原输入 = {
|
||
"type": "人物",
|
||
"name": "林深",
|
||
"brief": "机师",
|
||
"fields": {},
|
||
"evidence": "林深走进舰桥",
|
||
}
|
||
种子 = next(s for s in 读取内置种子() if s.类型.type_id == "character")
|
||
# 此旧场景中的中文类型名在输入目录登记;解析代码没有固定中文别名表。
|
||
类型 = replace(种子.类型, 名称="人物")
|
||
选中 = 解析类型选择(原输入["type"], (类型,))
|
||
正文 = 单段(原抽取正文)
|
||
提案 = 事实条目(
|
||
"proposal-lin",
|
||
"character-lin",
|
||
选中.type_id,
|
||
{"名称": 原输入["name"], "一句话摘要": 原输入["brief"], **原输入["fields"]},
|
||
(定位字面引文(正文, 原输入["evidence"]),),
|
||
)
|
||
结果 = 校验事实批次(
|
||
(提案,),
|
||
作品ID="synthetic-work",
|
||
结构目录={"character": (类型, 合成结构(种子.结构))},
|
||
正文=正文,
|
||
可用对象={},
|
||
)
|
||
assert 结果[0].type_id == "character"
|
||
assert asdict(结果[0].evidence[0]) == {
|
||
"paragraph_id": "p1",
|
||
"start": 0,
|
||
"end": len("林深走进舰桥"),
|
||
"quote": "林深走进舰桥",
|
||
}
|
||
assert 核对原文引文(正文, 结果[0].evidence[0]) == "林深走进舰桥"
|
||
assert 正文.format_version == 1
|
||
assert 结果[0].content["名称"] == "林深"
|
||
|
||
|
||
旧增量样本 = {
|
||
"character_location_changed": (
|
||
{"characterName": "林深", "toLocation": "舱室底层"},
|
||
"异种核心在胸腔里低鸣",
|
||
),
|
||
"character_knowledge_added": (
|
||
{"characterName": "何岚", "knowledge": "林深体内有异种核心"},
|
||
"何岚站在舱门口没有说话",
|
||
),
|
||
"relationship_changed": (
|
||
{"fromName": "林深", "toName": "何岚", "relation": "互相提防"},
|
||
"何岚站在舱门口没有说话",
|
||
),
|
||
"hook_advanced": (
|
||
{
|
||
"hookId": "hook-voice",
|
||
"action": "advanced",
|
||
"dueWindow": {"fromChapter": 5, "toChapter": 8},
|
||
},
|
||
"异种核心在胸腔里低鸣",
|
||
),
|
||
"timeline_event_added": ({"event": "隔离舱首次审讯结束"}, "何岚站在舱门口没有说话"),
|
||
"setting_added": ({"factType": "污染规则", "text": "黑纹扩散不可逆"}, "林深把黑纹缠上手臂"),
|
||
}
|
||
名称映射 = {
|
||
"林深": "character-lin",
|
||
"何岚": "character-he",
|
||
"舱室底层": "location-bottom",
|
||
"指挥舱": "location-command",
|
||
"hook-voice": "hook-voice",
|
||
"h1": "hook-one",
|
||
}
|
||
|
||
|
||
@pytest.fixture
|
||
def 旧增量环境():
|
||
# 六行保留旧输入种类;它们仅是本用例的动态登记,不是生产类型白名单。
|
||
字符 = 值规则("text")
|
||
|
||
def 引用(type_id):
|
||
return 值规则("reference", 引用=引用规则((type_id,)))
|
||
|
||
角色, 地点, 伏笔 = 引用("character"), 引用("location"), 引用("foreshadowing")
|
||
字段表 = {
|
||
"character_location_changed": [
|
||
("角色", "characterName", 角色),
|
||
("目标地点", "toLocation", 地点),
|
||
],
|
||
"character_knowledge_added": [
|
||
("角色", "characterName", 角色),
|
||
("新增认知", "knowledge", 字符),
|
||
],
|
||
"relationship_changed": [
|
||
("甲方", "fromName", 角色),
|
||
("乙方", "toName", 角色),
|
||
("关系", "relation", 字符),
|
||
],
|
||
"hook_advanced": [
|
||
("伏笔", "hookId", 伏笔),
|
||
("动作", "action", 值规则("enum", 枚举=("planted", "advanced", "paid"))),
|
||
],
|
||
"timeline_event_added": [("事件", "event", 字符)],
|
||
"setting_added": [("设定类型", "factType", 字符), ("内容", "text", 字符)],
|
||
}
|
||
结构目录 = {}
|
||
for id_, fields in 字段表.items():
|
||
家族 = "relation" if id_ == "relationship_changed" else "entity"
|
||
类型 = 类型定义(id_, id_, "world", 家族, (家族,))
|
||
结构 = 结构定义(
|
||
id_,
|
||
id_,
|
||
1,
|
||
tuple(
|
||
字段定义(id_ + "." + key, key, key, rule, 必填=True, 别名=(alias,))
|
||
for key, alias, rule in fields
|
||
),
|
||
)
|
||
结构目录[id_] = (类型, 合成结构(结构))
|
||
对象 = {
|
||
id_: 引用对象(
|
||
id_,
|
||
"synthetic-work",
|
||
"character"
|
||
if id_.startswith("character-")
|
||
else "location"
|
||
if id_.startswith("location-")
|
||
else "foreshadowing",
|
||
)
|
||
for id_ in 名称映射.values()
|
||
}
|
||
return {
|
||
"作品ID": "synthetic-work",
|
||
"结构目录": 结构目录,
|
||
"正文": 单段(原增量正文),
|
||
"可用对象": 对象,
|
||
"章节顺序": {f"chapter-{i}": i for i in range(1, 13)},
|
||
}
|
||
|
||
|
||
def 旧样本提案(
|
||
类型名="character_location_changed", *, payload=None, quote=None, delta_id="delta-1"
|
||
):
|
||
原字段, 原引文 = 旧增量样本.get(类型名, 旧增量样本["character_location_changed"])
|
||
内容 = dict(原字段 if payload is None else payload)
|
||
窗口 = 内容.pop("dueWindow", None)
|
||
# 已知旧名称在输入边界转换为固定对象ID;生产事实不由显示名拼接身份。
|
||
for key in ("characterName", "fromName", "toName", "toLocation", "hookId"):
|
||
if key in 内容:
|
||
内容[key] = 名称映射[内容[key]]
|
||
return 事实条目(
|
||
delta_id,
|
||
内容.get("hookId", "object-" + delta_id),
|
||
类型名,
|
||
内容,
|
||
(定位字面引文(单段(原增量正文), 原引文 if quote is None else quote),),
|
||
instance_family="relation" if 类型名 == "relationship_changed" else "entity",
|
||
planned_window=叙事窗口(f"chapter-{窗口['fromChapter']}", f"chapter-{窗口['toChapter']}")
|
||
if 窗口
|
||
else None,
|
||
)
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-d0e17d43037f",
|
||
environment="离线,固定原文与引用",
|
||
when="解析事实或关系提案并解析其证据和端点。",
|
||
contract="docs/系统架构/新版设计/数据模型/状态与来源有效性.md",
|
||
)
|
||
@pytest.mark.parametrize("delta_type", tuple(旧增量样本), ids=tuple(旧增量样本))
|
||
def test_each_delta_type_has_working_contract__d0e17d(旧增量环境, delta_type):
|
||
原字段, 引文 = 旧增量样本[delta_type]
|
||
提案 = 旧样本提案(delta_type, payload=原字段, quote=引文)
|
||
结果 = 校验事实批次((提案,), **旧增量环境)
|
||
assert 结果[0].type_id == delta_type and 结果[0].object_id
|
||
assert 结果[0].evidence[0].quote == 引文
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-e42f4bc89b7b",
|
||
environment="离线,固定原文与引用",
|
||
when="解析事实或关系提案并解析其证据和端点。",
|
||
contract="docs/系统架构/新版设计/数据模型/状态与来源有效性.md",
|
||
)
|
||
def test_subject_key_derivation__e42f4b(旧增量环境):
|
||
提案 = 旧样本提案(
|
||
"relationship_changed",
|
||
payload={"fromName": "林深", "toName": "何岚", "relation": "同盟"},
|
||
quote="何岚站在舱门口没有说话",
|
||
)
|
||
结果 = 校验事实批次((提案,), **旧增量环境)[0]
|
||
assert (结果.content["甲方"], 结果.content["乙方"]) == ("character-lin", "character-he")
|
||
hook = 旧样本提案(
|
||
"hook_advanced",
|
||
payload={"hookId": "hook-voice", "action": "planted"},
|
||
quote="异种核心在胸腔里低鸣",
|
||
)
|
||
assert 校验事实批次((hook,), **旧增量环境)[0].object_id == "hook-voice"
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-42dd3be8765a",
|
||
environment="离线,固定原文与引用",
|
||
when="解析事实或关系提案并解析其证据和端点。",
|
||
contract="docs/系统架构/新版设计/数据模型/状态与来源有效性.md",
|
||
)
|
||
def test_closed_field_set_and_type_enum__42dd3b(旧增量环境):
|
||
extra = asdict(旧样本提案())
|
||
extra["extraField"] = 1
|
||
with pytest.raises(ValidationError):
|
||
TypeAdapter(事实条目).validate_python(extra)
|
||
bad_type = 旧样本提案("character_resurrected")
|
||
with pytest.raises(事实错误) as 错误:
|
||
校验事实批次((bad_type,), **旧增量环境)
|
||
assert 错误.value.错误码 == "FACT_TYPE_UNKNOWN"
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-a30962eff4a0",
|
||
environment="离线,固定原文与引用",
|
||
when="解析事实或关系提案并解析其证据和端点。",
|
||
contract="docs/系统架构/新版设计/数据模型/状态与来源有效性.md",
|
||
)
|
||
def test_payload_contract_enforced_per_type__a30962(旧增量环境):
|
||
missing = 旧样本提案(payload={"characterName": "林深"})
|
||
with pytest.raises(元数据错误) as 错误:
|
||
校验事实批次((missing,), **旧增量环境)
|
||
assert 错误.value.错误码 == "FIELD_INVALID"
|
||
assert 错误.value.上下文 == {"字段": ["目标地点"], "路径": "$"}
|
||
action = 旧样本提案(
|
||
"hook_advanced",
|
||
payload={"hookId": "h1", "action": "detonated"},
|
||
quote="异种核心在胸腔里低鸣",
|
||
)
|
||
with pytest.raises(元数据错误) as 错误:
|
||
校验事实批次((action,), **旧增量环境)
|
||
assert 错误.value.错误码 == "FIELD_INVALID"
|
||
assert 错误.value.上下文 == {"路径": "$.动作", "类型": "enum"}
|
||
window = 旧样本提案(
|
||
"hook_advanced",
|
||
payload={
|
||
"hookId": "h1",
|
||
"action": "planted",
|
||
"dueWindow": {"fromChapter": 9, "toChapter": 3},
|
||
},
|
||
quote="异种核心在胸腔里低鸣",
|
||
)
|
||
with pytest.raises(事实错误) as 错误:
|
||
校验事实批次((window,), **旧增量环境)
|
||
assert 错误.value.错误码 == "FACT_TIME_INVALID"
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-b430df49f1e4",
|
||
environment="离线,固定原文与引用",
|
||
when="解析事实或关系提案并解析其证据和端点。",
|
||
contract="docs/系统架构/新版设计/数据模型/状态与来源有效性.md",
|
||
)
|
||
def test_batch_rejects_duplicate_ids__b430df(旧增量环境):
|
||
首 = 旧样本提案(delta_id="delta-1")
|
||
后 = 旧样本提案(delta_id="delta-1", payload={"characterName": "何岚", "toLocation": "指挥舱"})
|
||
with pytest.raises(事实错误) as 错误:
|
||
校验事实批次((首, 后), **旧增量环境)
|
||
assert 错误.value.错误码 == "FACT_DUPLICATE_ID"
|
||
assert [
|
||
x.proposal_id for x in 校验事实批次((首, replace(后, proposal_id="delta-2")), **旧增量环境)
|
||
] == ["delta-1", "delta-2"]
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-8101bb76825a",
|
||
environment="离线,固定原文与引用",
|
||
when="解析事实或关系提案并解析其证据和端点。",
|
||
contract="docs/系统架构/新版设计/数据模型/状态与来源有效性.md",
|
||
)
|
||
def test_salvage_drops_unbound_relation_and_keeps_exact_name_evidence__8101bb():
|
||
原输出 = {
|
||
"entities": [
|
||
{
|
||
"type": "item",
|
||
"name": "黑色钥匙",
|
||
"brief": "钥匙",
|
||
"fields": {},
|
||
"evidence": "模型编造",
|
||
},
|
||
{
|
||
"type": "item",
|
||
"name": "不存在的物件",
|
||
"brief": "?",
|
||
"fields": {},
|
||
"evidence": "模型编造",
|
||
},
|
||
],
|
||
"relations": [
|
||
{
|
||
"source": "林深",
|
||
"target": "何岚",
|
||
"type": "伙伴",
|
||
"description": "?",
|
||
"evidence": "模型编造",
|
||
}
|
||
],
|
||
"state": {},
|
||
}
|
||
结果 = 保留名称提及(原输出, 单段(原抽取正文), tuple(s.类型 for s in 读取内置种子()))
|
||
assert [x["name"] for x in 结果["mentions"]] == ["黑色钥匙"]
|
||
assert 结果["mentions"][0]["evidence"].quote == "黑色钥匙"
|
||
assert 结果["relations"] == [] and 结果["mechanical_drops"] == {"entities": 1, "relations": 1}
|
||
assert "brief" not in 结果["mentions"][0] and 结果["state"] == "needs_confirmation"
|