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

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

566 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""元数据的独立字段合同;仅通过公开接口解释结构与候选值。"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from muse.元数据.接口 import (
作品扩展,
值规则,
元数据错误,
合成结构,
字段定义,
校验值,
结构定义,
)
@pytest.mark.case_id(
"NC-meta-extension-compose",
environment="离线,固定结构及合成候选值",
given="人物基础与题材扩展",
when="合成并保存输入",
then=["同源结构解释新增枚举。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_作品新增字段可重复合成并校验__a40001() -> None:
"""given 人物基础与题材扩展;when 合成结构并校验输入;then 同源结构解释新增枚举。"""
基础 = 结构定义(
"character",
"character",
1,
(字段定义("character.name", "姓名", "姓名", 值规则("text"), 必填=True),),
)
扩展 = 作品扩展(
"work-a",
"character",
1,
1,
(字段定义("work-a.weather", "天气亲和", "天气亲和", 值规则("enum", 枚举=("雨", "雪"))),),
)
结构 = 合成结构(基础, 扩展)
assert 结构.effective_schema_hash == 合成结构(基础, 扩展).effective_schema_hash
样例 = json.loads((Path(__file__).resolve().parents[1] / "夹具/动态结构.json").read_text())
结果 = 校验值(结构, 样例["valid"])
assert 结果.值 == {"姓名": "青棠", "天气亲和": "雨"}
with pytest.raises(元数据错误) as 错误:
校验值(结构, 样例["invalid_enum"])
assert 错误.value.错误码 == "FIELD_INVALID"
@pytest.mark.case_id(
"NC-meta-nested-binding",
environment="离线纯规则",
given="嵌套作品扩展声明 work.title 原生绑定",
when="合成有效结构",
then=["任何嵌套深度的扩展原生绑定均拒绝"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_嵌套扩展不能声明原生存储绑定__a40021() -> None:
基础 = 结构定义("t", "s", 1, ())
伪字段 = 字段定义(
"forged", "书名", "书名", 值规则("text"), 存储="native_column", 绑定身份="work.title"
)
扩展 = 作品扩展(
"work-a",
"s",
1,
1,
(字段定义("outer", "附加", "附加", 值规则("list", 元素=值规则("object", 字段=(伪字段,)))),),
)
with pytest.raises(元数据错误) as 错误:
合成结构(基础, 扩展)
assert 错误.value.错误码 == "FIELD_NOT_ALLOWED"
@pytest.fixture
def 细纲():
from muse.元数据.接口 import 读取内置结构
return 合成结构(读取内置结构("fine_outline"))
@pytest.fixture
def 细纲值():
return {
"targetChapter": 3,
"sourceRef": {"sourceId": "outline-a", "sourceVersion": 2},
"chapterGoal": "寻找雨中的证人",
"hardConstraints": ["上章已经封城"],
"keyEvents": [
{
"id": "event-a",
"order": 1,
"event": "收到口信",
"participants": ["青棠"],
"trigger": "信使抵达",
"resultDirection": "得知桥下有人等候",
}
],
"adjustableBeats": ["可调整停留时间"],
"declaredNewFacts": [],
"mustAppearEntities": ["character-a"],
"chapterEndHook": "证人失约",
"foreshadowing": [],
"entities": [],
"stateChanges": [],
"unknowns": [],
"assumptions": [],
}
@pytest.mark.case_id(
"TC-01cbb1d6b293",
environment="离线,固定结构及合成候选值",
given="原细纲合同",
when="读取固定种子",
then=["九项必填与对象节拍均有明确约束。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_细纲固定结构保留必填与推荐__01cbb1(细纲) -> None:
"""given 原细纲合同;when 读取固定种子;then 九项必填与对象节拍均有明确约束。"""
assert {项.key for 项 in 细纲.字段 if 项.必填} == {
"targetChapter",
"sourceRef",
"chapterGoal",
"hardConstraints",
"keyEvents",
"adjustableBeats",
"declaredNewFacts",
"mustAppearEntities",
"chapterEndHook",
}
按键 = {项.key: 项 for 项 in 细纲.字段}
assert 按键["keyEvents"].值.类型 == "list"
assert 按键["foreshadowing"].值.元素.类型 == "object"
@pytest.mark.case_id(
"TC-9191dd03a6cf",
environment="离线,固定结构及合成候选值",
given="全部必填与五项推荐",
when="校验",
then=["可保存并报告其余两项推荐。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_完整细纲保留缺推荐报告__9191dd(细纲, 细纲值) -> None:
"""given 全部必填与五项推荐;when 校验;then 值校验通过并报告其余两项推荐。"""
结果 = 校验值(细纲, 细纲值)
assert 结果.值 == 细纲值
assert 结果.缺少推荐字段 == ("sourceRefs", "targetChars")
assert any(项.值 == {"sourceId": "outline-a", "sourceVersion": 2} for 项 in 结果.引用需求)
@pytest.mark.case_id(
"TC-d03c31c928c7",
environment="离线,固定结构及合成候选值",
given="九项必填",
when="校验",
then=["缺七项推荐只报告不阻断。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_仅必填不被推荐字段阻断__d03c31(细纲, 细纲值) -> None:
"""given 九项必填;when 校验;then 缺七项推荐只报告不阻断。"""
必填 = {项.key for 项 in 细纲.字段 if 项.必填}
结果 = 校验值(细纲, {键: 值 for 键, 值 in 细纲值.items() if 键 in 必填})
assert len(结果.缺少推荐字段) == 7
@pytest.mark.case_id(
"TC-64236c24059f",
environment="离线,固定结构及合成候选值",
given="每种缺少必填的细纲",
when="校验",
then=["明确指出缺失字段。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
@pytest.mark.parametrize(
"缺少",
[
"targetChapter",
"sourceRef",
"chapterGoal",
"hardConstraints",
"keyEvents",
"adjustableBeats",
"declaredNewFacts",
"mustAppearEntities",
"chapterEndHook",
],
ids=[
"targetChapter",
"sourceRef",
"chapterGoal",
"hardConstraints",
"keyEvents",
"adjustableBeats",
"declaredNewFacts",
"mustAppearEntities",
"chapterEndHook",
],
)
def test_任一细纲必填缺失均拒绝__64236c(细纲, 细纲值, 缺少: str) -> None:
"""given 每种缺少必填的细纲;when 校验;then 明确指出缺失字段。"""
del 细纲值[缺少]
with pytest.raises(元数据错误) as 错误:
校验值(细纲, 细纲值)
assert 错误.value.错误码 == "FIELD_INVALID"
assert 缺少 in 错误.value.上下文["字段"]
@pytest.mark.case_id(
"TC-3b368c564466",
environment="离线,固定结构及合成候选值",
given="旧字段存疑免责形式",
when="缺必填又追加免责键",
then=["按新版拒绝未知字段。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_未登记免责键不能放行必填__3b368c(细纲, 细纲值) -> None:
"""given 旧字段存疑免责形式;when 缺必填又追加免责键;then 按新版拒绝未知字段。"""
del 细纲值["chapterEndHook"]
细纲值["字段存疑"] = {"chapterEndHook": "本章没有钩子"}
with pytest.raises(元数据错误) as 错误:
校验值(细纲, 细纲值)
assert 错误.value.错误码 == "FIELD_NOT_ALLOWED"
@pytest.mark.case_id(
"TC-f5268d6a6e5f",
environment="离线,固定结构及合成候选值",
given="已发布大纲结构",
when="使用未登记旧 title 键",
then=["明确拒绝而非静默跳过。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_旧非强制结构也拒绝未知字段__f5268d() -> None:
"""given 已发布大纲结构;when 使用未登记旧 title 键;then 明确拒绝而非静默跳过。"""
from muse.元数据.接口 import 读取内置结构
结构 = 合成结构(读取内置结构("outline"))
assert 校验值(结构, {"主线一句话": "送信人寻找失踪证人"}).值["主线一句话"]
with pytest.raises(元数据错误) as 错误:
校验值(结构, {"title": "未登记字段"})
assert 错误.value.错误码 == "FIELD_NOT_ALLOWED"
@pytest.mark.case_id(
"TC-7d96a5a7fec3",
environment="离线,固定结构及合成候选值",
given="未登记结构身份",
when="请求加载",
then=["拒绝未知版本。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_未知结构版本拒绝跳过__7d96a5() -> None:
"""given 未登记结构身份;when 请求加载;then 拒绝未知版本。"""
from muse.元数据.接口 import 读取内置结构
with pytest.raises(元数据错误) as 错误:
读取内置结构("no_such_type")
assert 错误.value.错误码 == "SCHEMA_UNKNOWN"
@pytest.mark.case_id(
"NC-meta-extension-protection",
environment="离线,固定结构及合成候选值",
given="基础字段",
when="扩展覆盖、注入原生列或复用别名",
then=["拒绝合成。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
@pytest.mark.parametrize(
"修改",
["base", "identity", "native", "alias"],
ids=["redefine-base", "system-identity", "native-column", "alias-conflict"],
)
def test_扩展不能覆盖基础和系统骨架__a40002(修改: str) -> None:
"""given 基础字段;when 扩展覆盖、注入原生列或复用别名;then 拒绝合成。"""
基础 = 结构定义(
"character",
"character",
1,
(字段定义("name", "姓名", "姓名", 值规则("text"), 别名=("名字",)),),
)
追加 = {
"base": 字段定义("name", "另名", "另名", 值规则("text")),
"identity": 字段定义("mine", "revision", "版本", 值规则("integer")),
"native": 字段定义(
"mine", "年龄", "年龄", 值规则("integer"), 存储="native_column", 绑定身份="user.table"
),
"alias": 字段定义("mine", "另名", "另名", 值规则("text"), 别名=("名字",)),
}[修改]
with pytest.raises(元数据错误) as 错误:
合成结构(基础, 作品扩展("work-a", "character", 1, 1, (追加,)))
assert 错误.value.错误码 == "FIELD_NOT_ALLOWED"
@pytest.mark.case_id(
"NC-meta-field-aliases",
environment="离线,固定结构及合成候选值",
given="已审中文别名",
when="单键或双键提交",
then=["单键规范化,双键冲突拒绝。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_别名规范化与双键冲突__a40003(细纲, 细纲值) -> None:
"""given 已审中文别名;when 单键或双键提交;then 单键规范化,双键冲突拒绝。"""
细纲值["本章目标"] = 细纲值.pop("chapterGoal")
assert 校验值(细纲, 细纲值).值["chapterGoal"] == "寻找雨中的证人"
细纲值["chapterGoal"] = "另一目标"
with pytest.raises(元数据错误) as 错误:
校验值(细纲, 细纲值)
assert 错误.value.错误码 == "FIELD_NOT_ALLOWED"
@pytest.mark.case_id(
"NC-meta-value-types",
environment="离线,固定结构及合成候选值",
given="明确值类型",
when="传入不相容值",
then=["类型错误不能经自由 JSON 放行。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
@pytest.mark.parametrize(
"规则,值",
[
(值规则("integer"), True),
(值规则("boolean"), 1),
(值规则("date"), "赤历三年"),
(值规则("number"), float("nan")),
(值规则("list", 元素=值规则("text")), [1]),
],
ids=["integer-is-not-bool", "boolean-is-not-int", "real-date", "finite-number", "list-element"],
)
def test_动态值类型实际校验__a40004(规则: 值规则, 值) -> None:
"""given 明确值类型;when 传入不相容值;then 类型错误不能经自由 JSON 放行。"""
结构 = 合成结构(
结构定义("new_type", "new_schema", 1, (字段定义("new.field", "字段", "字段", 规则),))
)
with pytest.raises(元数据错误) as 错误:
校验值(结构, {"字段": 值})
assert 错误.value.错误码 == "FIELD_INVALID"
@pytest.mark.case_id(
"NC-meta-conditional-fields",
environment="离线,固定结构及合成候选值",
given="技法形态合同",
when="缺条件必填、禁填或收益超量",
then=["同源校验直接拒绝。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_条件字段和列表数量来自内置合同__a40005() -> None:
"""given 技法形态合同;when 缺条件必填、禁填或收益超量;then 同源校验直接拒绝。"""
from muse.元数据.接口 import 读取内置结构
结构 = 合成结构(读取内置结构("craft"))
场景 = {
"装置形态": "场景手法",
"复用节奏": "同场景只用一次",
"异质化要求": "更换承载动作",
"单章上限": 1,
"读者收益": ["期待", "意外"],
}
assert 校验值(结构, 场景).值 == 场景
用例 = (
(
{"装置形态": "结构装置"},
{"缺少字段": ["回收点", "埋设手法", "记忆维持", "间隔纪律"], "禁止字段": []},
),
({**场景, "间隔纪律": "并不存在跨章间隔"}, {"缺少字段": [], "禁止字段": ["间隔纪律"]}),
({**场景, "读者收益": ["期待", "意外", "紧张"]}, {"路径": "$.读者收益"}),
)
for 非法, 预期上下文 in 用例:
with pytest.raises(元数据错误) as 错误:
校验值(结构, 非法)
assert 错误.value.错误码 == "FIELD_INVALID"
assert 错误.value.上下文 == 预期上下文
# ---------------------------------------------------------------------------
# 细纲读取与落库门禁(原 FineOutlineReader/FieldCoverage)
# ---------------------------------------------------------------------------
class 假连接:
"""按SQL特征返回合成行;只覆盖读取路径需要的最小查询。"""
def __init__(self, 行集: dict[str, object]):
self.行集 = 行集
self.请求记录 = []
def execute(self, sql: str, 参数=None):
self.请求记录.append((sql, 参数))
命中 = [k for k in self.行集 if k in sql]
if len(命中) != 1:
raise AssertionError(f"未预期或歧义SQL:{sql}")
return 假游标(self.行集[命中[0]])
def cursor(self, row_factory=None):
return self
class 假游标:
def __init__(self, 行):
self.行 = 行
def execute(self, sql: str, 参数=None):
return self
def fetchone(self):
return self.行
@pytest.mark.case_id(
"TC-4232c6fa3cfa",
environment="离线,固定结构与扩展样例",
when="合成结构并对具体实例字段及别名运行校验。",
contract="docs/系统架构/新版设计/数据模型/元数据与结构演进.md",
)
def test_文本投影JSON字符串解析为对象__4232c6() -> None:
"""历史文本投影的JSON载荷归一化为对象;解析失败关闭。"""
from muse.作品规划.存储 import 解析规划载荷
assert 解析规划载荷('{"targetChapter": 5, "chapterGoal": "y"}') == {
"targetChapter": 5,
"chapterGoal": "y",
}
assert 解析规划载荷({"targetChapter": 3}) == {"targetChapter": 3}
@pytest.mark.case_id(
"TC-58a5e287c8be",
environment="离线,固定结构与扩展样例",
when="合成结构并对具体实例字段及别名运行校验。",
contract="docs/系统架构/新版设计/数据模型/元数据与结构演进.md",
)
def test_非法规划载荷失败关闭__58a5e2() -> None:
from muse.作品规划.存储 import 解析规划载荷
from muse.作品规划.模型 import 作品错误
for 非法 in (None, "[]", '"str"', "not-json", 42):
with pytest.raises(作品错误) as 错:
解析规划载荷(非法)
assert 错.value.错误码 == "INVALID_OUTPUT"
@pytest.mark.case_id(
"TC-00a2432d3397",
environment="离线,固定结构与扩展样例",
when="合成结构并对具体实例字段及别名运行校验。",
contract="docs/系统架构/新版设计/数据模型/元数据与结构演进.md",
)
def test_缺已确认细纲失败关闭__00a243() -> None:
from muse.作品规划.近期细化 import 读取已确认细纲
连 = 假连接(
{
"muse_work_version": {"author_id": "author", "work_id": "w"},
"muse_directory_version": (1, [{"chapter_id": "c3", "title": "三", "position": 3}]),
"FROM muse_plan WHERE": None,
}
)
from muse.作品规划.接口 import 作品错误
with pytest.raises(作品错误) as 错:
读取已确认细纲(连, "author", "w", "c3")
assert getattr(错.value, "错误码", None) == "PRECONDITION_PENDING"
assert "已确认细纲" in str(错.value)
# 目标章不属于本书时同样失败关闭,不退回默认章。
with pytest.raises(作品错误) as 越界:
读取已确认细纲(连, "author", "w", "其他章")
assert getattr(越界.value, "错误码", None) == "SCOPE_DENIED"
@pytest.mark.case_id(
"TC-3c112c210a65",
environment="离线,固定结构与扩展样例",
when="合成结构并对具体实例字段及别名运行校验。",
contract="docs/系统架构/新版设计/数据模型/元数据与结构演进.md",
)
def test_细纲只能由章层规划承担__3c112c() -> None:
"""fine_outline实例粒度为chapter:作品级、卷或场景节点都拒绝。"""
from muse.作品规划.分层规划 import 规划目标
from muse.作品规划.模型 import 作品错误, 档案结构, 规划保存
from muse.元数据.接口 import 读取内置种子
类型 = next(s.类型 for s in 读取内置种子() if s.类型.type_id == "fine_outline")
assert 类型.实例粒度 == "chapter"
目录 = {"chapters": [{"chapter_id": "c1", "title": "一", "position": 1}]}
作品级 = 规划保存("w", 0, 档案结构("fine_outline", 1, "", ""), {})
with pytest.raises(作品错误) as 错:
规划目标(None, 作品级, 类型, 目录)
assert 错.value.错误码 == "PLAN_SCOPE_INVALID"
章级 = 规划保存("w", 0, 档案结构("fine_outline", 1, "", ""), {}, chapter_id="c1")
assert 规划目标(None, 章级, 类型, 目录)["chapter_id"] == "c1"
节点 = 假连接(
{
"muse_directory_version": (
1,
[
{"node_id": "v1", "kind": "volume", "title": "卷", "parent_id": None},
{"node_id": "s1", "kind": "scene", "title": "景", "parent_id": "c1"},
],
[{"chapter_id": "c1", "title": "一", "position": 1}],
)
}
)
for 节点ID in ("v1", "s1"):
请求 = 规划保存("w", 0, 档案结构("fine_outline", 1, "", ""), {}, node_id=节点ID)
with pytest.raises(作品错误) as 错:
规划目标(节点, 请求, 类型, 目录)
assert 错.value.错误码 == "PLAN_SCOPE_INVALID"
@pytest.mark.case_id(
"TC-4b15f1809a64",
environment="离线,固定结构与扩展样例",
when="合成结构并对具体实例字段及别名运行校验。",
contract="docs/系统架构/新版设计/数据模型/元数据与结构演进.md",
)
def test_缺必填细纲在纯校验阶段拒绝__4b15f1() -> None:
"""仅验证字段覆盖纯函数:缺必填字段拒绝;持久化由集成用例验证。"""
from muse.元数据.接口 import 读取内置结构
结构 = 合成结构(读取内置结构("fine_outline"))
完整 = {
"targetChapter": 3,
"sourceRef": {"sourceId": "outline:v1", "sourceVersion": 1},
"chapterGoal": "目标",
"hardConstraints": ["衔接"],
"keyEvents": [
{
"id": "e1",
"order": 1,
"event": "事件",
"participants": ["林深"],
"trigger": "触发",
"resultDirection": "结果",
}
],
"adjustableBeats": ["可调"],
"declaredNewFacts": [
{
"factId": "f1",
"text": "事实",
"sourceRef": {"sourceId": "outline:v1", "sourceVersion": 1},
}
],
"mustAppearEntities": ["林深"],
"chapterEndHook": "钩子",
}
assert 校验值(结构, 完整).值["targetChapter"] == 3
缺失 = {k: v for k, v in 完整.items() if k != "hardConstraints"}
with pytest.raises(元数据错误) as 错:
校验值(结构, 缺失)
呈现 = 错.value.呈现()
assert 呈现["code"] == "FIELD_INVALID"
assert "hardConstraints" in 呈现["details"]["字段"]