muse-agent-example/tests/契约/test_动态结构投影与版本.py
zizi 50a5522014 W01 落实合同与逐用例工作清单:合同与逐用例工作清单落实(接口契约、文件设计与用例页)。
按 R2 串行阶段整理提交;包内文件为该阶段交付(含后续小增量),状态以工作包清单为准。
2026-09-10 19:25:40 +08:00

331 lines
13 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.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"
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"
)
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/系统架构/新版设计/研究依据/原结构快照"
总字段 = 0
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 新.说明
总字段 += len(按键)
assert 总字段 == 216
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).字段)
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 投影值(投影, 值) == {"资料": [{"公开": "公开信息"}]}
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"]) == {"公开"}
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")
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(元数据错误):
校验值(新, {"情绪": "焦虑"})
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
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", {"公开": "乙"}),))