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