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

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

442 lines
17 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
from dataclasses import replace
import pytest
from muse.元数据.接口 import (
值规则,
元数据错误,
合成结构,
字段定义,
字段用途,
字段限制,
投影值,
投影字段,
授权摘要,
策略快照,
结构定义,
)
@pytest.mark.case_id(
"NC-meta-purpose-projection",
environment="离线,固定结构及合成候选值",
given="作者备忘仅可看可编辑",
when="选择五类用途",
then=["不扩大 AI、检索或导出权限。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
@pytest.mark.parametrize(
"用途",
["uiVisible", "userEditable", "userSearchable", "exportable", "aiContext"],
ids=["display", "edit", "search", "export", "generation"],
)
def test_五类用途分别投影并解释剔除__a41001(用途: str) -> None:
"""given 作者备忘仅可看可编辑;when 选择五类用途;then 不扩大 AI、检索或导出权限。"""
结构 = 合成结构(
结构定义(
"character",
"character",
1,
(
字段定义("name", "姓名", "姓名", 值规则("text"), 用途=字段用途(aiContext=True)),
字段定义(
"memo",
"创作备忘",
"创作备忘",
值规则("text"),
用途=字段用途(userSearchable=False, exportable=False),
),
),
)
)
投影 = 投影字段(
结构,
策略快照("character", 1),
授权摘要("a1", "s1"),
字段用途名=用途,
内容用途="generation",
运行用途="production",
)
assert 投影值(投影, {"姓名": "青棠", "创作备忘": "作者私记"}) == (
{"姓名": "青棠", "创作备忘": "作者私记"}
if 用途 in {"uiVisible", "userEditable"}
else {"姓名": "青棠"}
)
if 用途 not in {"uiVisible", "userEditable"}:
assert 投影.剔除原因["创作备忘"] == "FIELD_NOT_ALLOWED"
@pytest.mark.case_id(
"NC-meta-projection-fingerprint",
environment="离线,固定结构及合成候选值",
given="历史结构",
when="撤回用途或改变授权来源",
then=["投影版本变化且旧值不泄露。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_投影身份包含授权来源及当前策略__a41002() -> None:
"""given 历史结构;when 撤回用途或改变授权来源;then 投影版本变化且旧值不泄露。"""
结构 = 合成结构(
结构定义(
"character",
"character",
1,
(字段定义("secret", "秘密", "秘密", 值规则("text"), 用途=字段用途(aiContext=True)),),
)
)
策略 = 策略快照("character", 1)
授权 = 授权摘要("author-a1", "source-s1")
def 生成(策略=策略, 授权=授权):
return 投影字段(
结构, 策略, 授权, 字段用途名="aiContext", 内容用途="generation", 运行用途="production"
)
旧 = 生成()
assert (
旧.projection_version != 生成(授权=replace(授权, 授权指纹="author-a2")).projection_version
)
assert (
旧.projection_version != 生成(授权=replace(授权, 来源指纹="source-s2")).projection_version
)
新 = 生成(策略=策略快照("character", 2, (字段限制("secret", ("aiContext",)),)))
assert 新.projection_version != 旧.projection_version
assert 投影值(新, {"秘密": "暗门"}) == {}
with pytest.raises(元数据错误):
投影字段(
结构, 策略, 授权, 字段用途名="aiContext", 内容用途="unknown", 运行用途="production"
)
@pytest.mark.case_id(
"NC-meta-seed-semantics",
environment="离线,固定结构及合成候选值",
given="冻结原定义",
when="检查发布种子",
then=["逐型身份、别名和 AI 用途原样承接。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_原结构字段语义用途别名逐项承接__a41003() -> None:
"""given 冻结原定义;when 检查发布种子;then 逐型身份、别名和 AI 用途原样承接。"""
import hashlib
from pathlib import Path
import yaml
from muse.元数据.接口 import 读取内置种子
根 = Path(__file__).resolve().parents[2]
源 = 根 / "docs/系统架构/新版设计/研究依据/原结构快照"
import json
字段合同 = json.loads((根 / "tests/夹具/内置结构字段合同.json").read_text())["schemas"]
assert {v.结构.schema_id for v in 读取内置种子()} == set(字段合同)
for 种子 in 读取内置种子():
文件 = 源 / f"{种子.类型.type_id}.yaml"
原 = yaml.safe_load(文件.read_text(encoding="utf-8"))
assert 种子.原定义哈希 == hashlib.sha256(文件.read_bytes()).hexdigest()
按键 = {项.key: 项 for 项 in 种子.结构.字段 if not 项.field_id.startswith("common.")}
assert set(按键) == {项["key"] for 项 in 原.get("特有字段", [])}
for 项 in 原.get("特有字段", []):
新 = 按键[项["key"]]
ai = 项.get("aiContext", False)
assert 新.用途.aiContext == (tuple(ai) if isinstance(ai, list) else ai)
assert not 项.get("alias") or 项["alias"] in 新.别名
assert 项.get("说明", "") in 新.说明
_核对结构字段合同(种子.结构, 字段合同[种子.结构.schema_id])
@pytest.mark.case_id(
"NC-meta-common-fields",
environment="离线纯规则",
given="内置人物结构和公共名称标签",
when="合成、校验并按 AI 用途投影",
then=[
"公共字段完整往返;错误列表类型拒绝",
"来源和状态仍受业务骨架保护;运行外壳不合入公共字段",
],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_公共内容字段参加同源校验投影且不混入运行外壳__a41009() -> None:
"""名称等公共内容与逐型字段合成;来源和状态仍由业务骨架维护。"""
from muse.元数据.接口 import 校验值, 读取内置结构
结构 = 合成结构(读取内置结构("character"))
值 = {"名称": "青棠", "别名": ["阿棠"], "一句话摘要": "守住旧约的人", "标签": ["守信"]}
assert 校验值(结构, 值).值 == 值
投影 = 投影字段(
结构,
策略快照("character", 1),
授权摘要("a1", "s1"),
字段用途名="aiContext",
内容用途="generation",
运行用途="production",
)
assert 投影值(投影, 值) == 值
with pytest.raises(元数据错误):
校验值(结构, {"标签": "不能把列表写成字符串"})
with pytest.raises(元数据错误):
校验值(结构, {"状态": "已确认"})
for schema_id in ("generation_context", "fine_outline"):
assert not any(f.field_id.startswith("common.") for f in 读取内置结构(schema_id).字段)
@pytest.mark.case_id(
"NC-meta-union-projection",
environment="离线纯规则",
given="文本列表与对象列表联合结构,合法值含私记",
when="校验后投影至模型",
then=["按原结构真实分支裁剪,私记不进入输出"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_联合类型按已验证分支裁剪隐藏值__a41010() -> None:
"""两个分支都是列表时,不能因外形相同把私记按文本列表原样送出。"""
from muse.元数据.接口 import 校验值
分支 = 值规则(
"object",
字段=(
字段定义("public", "公开", "公开", 值规则("text"), 用途=字段用途(aiContext=True)),
字段定义("private", "私记", "私记", 值规则("text")),
),
)
规则 = 值规则(
"union",
分支=(
值规则("list", 元素=值规则("text")),
值规则("list", 元素=分支),
),
)
结构 = 合成结构(
结构定义(
"t", "s", 1, (字段定义("data", "资料", "资料", 规则, 用途=字段用途(aiContext=True)),)
)
)
值 = 校验值(结构, {"资料": [{"公开": "公开信息", "私记": "私人想法"}]}).值
投影 = 投影字段(
结构,
策略快照("t", 1),
授权摘要("a1", "s1"),
字段用途名="aiContext",
内容用途="generation",
运行用途="production",
)
assert 投影值(投影, 值) == {"资料": [{"公开": "公开信息"}]}
@pytest.mark.case_id(
"NC-meta-nested-projection",
environment="离线,固定结构及合成候选值",
given="嵌套动态对象含私人备注",
when="模型投影",
then=["输出合同和值均删除隐藏项。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_嵌套隐藏字段不会流入模型合同或值__a41004() -> None:
"""given 嵌套动态对象含私人备注;when 模型投影;then 输出合同和值均删除隐藏项。"""
from muse.元数据.接口 import 生成字段合同
对象 = 值规则(
"object",
字段=(
字段定义(
"nested.public", "公开", "公开", 值规则("text"), 用途=字段用途(aiContext=True)
),
字段定义("nested.private", "私记", "私记", 值规则("text")),
),
)
结构 = 合成结构(
结构定义(
"new_type",
"new_schema",
1,
(
字段定义(
"outer",
"观察",
"观察",
值规则("list", 元素=对象),
用途=字段用途(aiContext=True),
),
),
)
)
投影 = 投影字段(
结构,
策略快照("new_type", 1),
授权摘要("a1", "s1"),
字段用途名="aiContext",
内容用途="generation",
运行用途="production",
)
assert 投影值(投影, {"观察": [{"公开": "桥下有人", "私记": "后续身份计划"}]}) == {
"观察": [{"公开": "桥下有人"}]
}
合同 = 生成字段合同(投影.允许字段)
assert set(合同["properties"]["观察"]["items"]["properties"]) == {"公开"}
@pytest.mark.case_id(
"NC-meta-open-type-owner",
environment="离线,固定结构及合成候选值",
given="新题材类型",
when="选择实体生命周期",
then=["可归 B02,不能声明表名或脚本。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_开放类型只选择已登记实例维护者__a41005() -> None:
"""given 新题材类型;when 选择实体生命周期;then 可归 B02,不能声明表名或脚本。"""
from muse.元数据.接口 import 类型定义, 选择实例维护者
类型 = 类型定义("weather_spirit", "天气精灵", "world", "entity", ("entity",))
assert 选择实例维护者(类型, "entity") == "B02"
with pytest.raises(元数据错误):
选择实例维护者(replace(类型, 实例族=("public.character",)), "public.character")
@pytest.mark.case_id(
"NC-meta-change-impact",
environment="离线,固定结构及合成候选值",
given="原枚举与可选字段",
when="缩窄枚举新增必填",
then=["明确转换对象并保留旧解释。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_结构升级要求转换且旧结构可解释__a41006() -> None:
"""given 原枚举与可选字段;when 缩窄枚举新增必填;then 明确转换对象并保留旧解释。"""
from muse.元数据.接口 import 分析变更, 校验值
字段 = 字段定义("mood", "情绪", "情绪", 值规则("enum", 枚举=("平静", "焦虑")))
旧 = 合成结构(结构定义("character", "character", 1, (字段,)))
新 = 合成结构(
结构定义(
"character",
"character",
2,
(replace(字段, 值=值规则("enum", 枚举=("平静",)), 必填=True),),
父版本=1,
)
)
影响 = 分析变更(旧, 新, ("entity-a",))
assert 影响.分类 == "incompatible"
assert 影响.转换字段 == ("mood",)
assert 影响.影响对象 == ("entity-a",)
assert 校验值(旧, {"情绪": "焦虑"}).值 == {"情绪": "焦虑"}
with pytest.raises(元数据错误):
校验值(新, {"情绪": "焦虑"})
@pytest.mark.case_id(
"NC-meta-editable-contract",
environment="离线,固定结构及合成候选值",
given="作品动态表单",
when="重发计算字数",
then=["只允许编辑字段且条件来自同源合同。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_表单修改不能重发只读字段__a41007() -> None:
"""given 作品动态表单;when 重发计算字数;then 只允许编辑字段且条件来自同源合同。"""
from muse.元数据.接口 import 校验字段修改, 生成结构合同, 读取内置结构
作品 = 合成结构(读取内置结构("novel_work"))
投影 = 投影字段(
作品,
策略快照("novel_work", 1),
授权摘要("author-v1", "source-v1"),
字段用途名="userEditable",
内容用途="planning",
运行用途="production",
)
assert 校验字段修改(投影, {"书名": "候雨"}).值 == {"书名": "候雨"}
with pytest.raises(元数据错误):
校验字段修改(投影, {"总字数": 999999})
合同 = 生成结构合同(合成结构(读取内置结构("craft")))
条件 = 合同["allOf"]
assert 条件[0]["then"]["required"] == ["埋设手法", "回收点", "记忆维持", "间隔纪律"]
assert 条件[1]["then"]["properties"] == {"间隔纪律": False}
assert 合同["properties"]["读者收益"]["maxItems"] == 2
@pytest.mark.case_id(
"NC-meta-field-edit-operations",
environment="离线纯合同",
given="结构与投影限制不同,已有实例含不展示的子字段",
when="提交可编辑叶子的修改",
then=["只改指定叶子,隐藏子值保留,越界修改被拒绝"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_逐字段修改保留未展示子字段__aa1201():
from muse.元数据.接口 import 字段变更, 校验字段变更
结构 = 合成结构(
结构定义(
"profile",
"profile",
1,
(
字段定义(
"p.info",
"资料",
"资料",
值规则(
"object",
字段=(
字段定义("p.public", "公开", "公开", 值规则("text")),
字段定义(
"p.private",
"私记",
"私记",
值规则("text"),
用途=字段用途(userEditable=False),
),
),
),
),
),
)
)
投影 = 投影字段(
结构,
策略快照("profile", 0),
授权摘要("author", "source"),
字段用途名="userEditable",
内容用途="planning",
运行用途="production",
)
原 = {"资料": {"公开": "甲", "私记": "不改这里"}}
结果 = 校验字段变更(结构, 投影, 原, (字段变更(("资料", "公开"), "set", "乙"),))
assert 结果.值 == {"资料": {"公开": "乙", "私记": "不改这里"}}
assert 原["资料"]["公开"] == "甲"
with pytest.raises(元数据错误):
校验字段变更(结构, 投影, 原, (字段变更(("资料",), "set", {"公开": "乙"}),))
def _核对结构字段合同(结构, 预期):
assert 结构.schema_version == 预期["schema_version"]
实际 = {f.field_id: {"key": f.key, "type": f.值.类型, "required": f.必填} for f in 结构.字段}
assert 实际 == 预期["fields"], f"{结构.schema_id} 字段身份、类型或必填合同漂移"
@pytest.mark.case_id("NC-O09-FIELD-SWAP")
def test_字段总数相同不能掩盖字段替换():
import json
from pathlib import Path
from muse.元数据.接口 import 读取内置结构
结构 = 读取内置结构("character")
预期 = json.loads((Path(__file__).parents[1] / "夹具/内置结构字段合同.json").read_text())[
"schemas"
]["character"]
更换 = replace(
结构,
字段=(*结构.字段[:-1], replace(结构.字段[-1], field_id="unapproved", key="未批准字段")),
)
assert len(更换.字段) == len(结构.字段)
with pytest.raises(AssertionError, match="字段身份、类型或必填合同漂移"):
_核对结构字段合同(更换, 预期)