"""真实 PostgreSQL 验证元数据版本与调用方提交点;每例独占可销毁数据库。""" from __future__ import annotations from collections.abc import Iterator from pathlib import Path import psycopg import pytest from muse.元数据.接口 import ( 元数据服务, 元数据错误, 导入内置结构, 读取内置种子, ) from muse.共享.调用身份 import 用途 from muse.基础设施.数据库.连接 import 连接 from muse.配置 import 数据库引用 pytestmark = pytest.mark.数据库 @pytest.fixture def 元数据库(数据库底座, tmp_path: Path) -> Iterator[数据库引用]: # 此模板不导入种子;导入幂等、结构发布仍由每个测试实际执行。 with 数据库底座.借库(tmp_path / "元数据引用", (1, 2)) as 工厂: yield 工厂[用途.维护].引用 @pytest.mark.case_id( "NC-meta-seed-import-pg", environment="隔离 PostgreSQL,每例独占新库", given="隔离空库", when="两次导入种子", then=["24 个固定版本可回读且没有隐式绑定。"], contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md", ) def test_内置发布可重复且不自动启用__a42001(元数据库: 数据库引用) -> None: """given 隔离空库;when 两次导入种子;then 24 个固定版本可回读且没有隐式绑定。""" with 连接(元数据库, 用途标记=用途.维护) as 连: 服务 = 元数据服务(连) with 连.transaction(): 初次 = 导入内置结构(服务) 再次 = 导入内置结构(服务) assert len(初次) == len(再次) == 24 assert sum(not f.field_id.startswith("common.") for 项 in 初次 for f in 项.字段) == 216 assert sum(f.field_id.startswith("common.") for 项 in 初次 for f in 项.字段) == 88 for 种子 in 读取内置种子(): assert 服务.读取结构(种子.结构.schema_id, 1) == 种子.结构 with pytest.raises(元数据错误) as 错误: 服务.读取绑定("work-a:character") assert 错误.value.错误码 == "SCHEMA_UNKNOWN" def 安装动态结构(连: psycopg.Connection): from muse.元数据.接口 import ( 值规则, 合成结构, 启用命令, 字段定义, 字段用途, 类型定义, 结构定义, ) 服务 = 元数据服务(连) 类型 = 类型定义("weather_spirit", "天气精灵", "world", "entity", ("entity",)) 基础 = 结构定义( "weather_spirit", "weather_spirit", 1, ( 字段定义( "weather.name", "名称", "名称", 值规则("text"), 必填=True, 用途=字段用途(aiContext=True), ), ), ) 服务.登记类型(类型) 服务.登记结构候选(基础) 服务.发布结构(基础.schema_id, 1, 基础.内容哈希) 有效 = 合成结构(基础) 命令 = 启用命令("activate-initial", 有效.effective_schema_hash, 0, "author-a", "work-a:spirit") 绑定 = 服务.启用绑定("work-a:spirit", "work-a", 有效, 命令) return 服务, 基础, 有效, 绑定, 命令 @pytest.mark.case_id( "NC-meta-binding-rollback-history", environment="隔离 PostgreSQL,每例独占新库", given="已启用结构", when="发布作品扩展、回滚与重复内置导入", then=["版本固定且旧值可回读。"], contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md", ) def test_扩展发布保留历史绑定且回滚不留半成品__a42002(元数据库: 数据库引用) -> None: """given 已启用结构;when 发布作品扩展、回滚与重复内置导入;then 版本固定且旧值可回读。""" from muse.元数据.接口 import 作品扩展, 值规则, 启用命令, 字段定义 with 连接(元数据库, 用途标记=用途.维护) as 连: with 连.transaction(): 服务, 基础, _, 旧绑定, 原命令 = 安装动态结构(连) 扩展 = 作品扩展( "work-a", 基础.schema_id, 1, 1, ( 字段定义( "work-a.affinity", "天气亲和", "天气亲和", 值规则("enum", 枚举=("雨", "雪")) ), ), ) 服务.发布扩展(扩展, 扩展.内容哈希) 服务.发布扩展(扩展, 扩展.内容哈希) 新结构 = 服务.读取有效结构(基础.schema_id, 1, work_id="work-a", extension_version=1) assert 服务.读取绑定("work-a:spirit") == 旧绑定 with pytest.raises(RuntimeError), 连.transaction(): 服务.启用绑定( "work-a:spirit", "work-a", 新结构, 启用命令( "activate-fail", 新结构.effective_schema_hash, 1, "author-a", "work-a:spirit" ), ) raise RuntimeError("第二参与者失败") with 连.transaction(): assert 服务.读取绑定("work-a:spirit") == 旧绑定 新绑定 = 服务.启用绑定( "work-a:spirit", "work-a", 新结构, 启用命令( "activate-ok", 新结构.effective_schema_hash, 1, "author-a", "work-a:spirit" ), ) assert 新绑定.extension_version == 1 导入内置结构(服务) assert 服务.读取绑定("work-a:spirit") == 新绑定 assert 服务.读取结构(基础.schema_id, 1) == 基础 # 原命令重放返回当时回执,不把当前绑定降回原版本。 assert ( 服务.启用绑定( "work-a:spirit", "work-a", 服务.读取有效结构(基础.schema_id, 1), 原命令 ) == 旧绑定 ) assert 服务.读取绑定("work-a:spirit") == 新绑定 @pytest.mark.case_id( "NC-meta-reviewed-version-binding", environment="隔离 PostgreSQL,每例独占新库", given="已启用结构", when="候选未发布或预期版本过期", then=["不改变当前绑定。"], contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md", ) def test_候选未发布与过期绑定均不可启用__a42003(元数据库: 数据库引用) -> None: """given 已启用结构;when 候选未发布或预期版本过期;then 不改变当前绑定。""" from dataclasses import replace from muse.元数据.接口 import 合成结构, 启用命令 with 连接(元数据库, 用途标记=用途.维护) as 连: with 连.transaction(): 服务, 基础, _, 绑定, _ = 安装动态结构(连) 新版 = replace(基础, schema_version=2, 父版本=1) 服务.登记结构候选(新版) 新 = 合成结构(新版) with pytest.raises(元数据错误) as 错误: 服务.启用绑定( 绑定.target_ref, 绑定.work_id, 新, 启用命令("draft", 新.effective_schema_hash, 1, "author-a", 绑定.target_ref), ) assert 错误.value.错误码 == "SCHEMA_UNKNOWN" 服务.发布结构(新版.schema_id, 2, 新版.内容哈希) with pytest.raises(元数据错误) as 错误: 服务.启用绑定( 绑定.target_ref, 绑定.work_id, 新, 启用命令("stale", 新.effective_schema_hash, 0, "author-a", 绑定.target_ref), ) assert 错误.value.错误码 == "SCHEMA_STALE" assert 服务.读取绑定(绑定.target_ref) == 绑定 @pytest.mark.case_id( "NC-meta-concurrent-submit-guard", environment="隔离 PostgreSQL,每例独占新库", given="已预览候选", when="提交持有结构锁且另连接切换", then=["切换等待且旧请求在切换后拒绝。"], contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md", ) @pytest.mark.parametrize( "变更类别", ["structure", "policy"], ids=["schema-switch", "policy-revoke"] ) def test_结构策略切换与业务提交共享锁__a42004(元数据库: 数据库引用, 变更类别: str) -> None: """given 已预览候选;when 提交持有结构锁且另连接切换;then 切换等待且旧请求在切换后拒绝。""" from concurrent.futures import ThreadPoolExecutor from dataclasses import asdict, replace from muse.元数据.接口 import ( 合成结构, 启用命令, 字段限制, 定义哈希, 投影字段, 授权摘要, 策略快照, ) 授权 = 授权摘要("author-v1", "source-v1") with 连接(元数据库, 用途标记=用途.维护) as 连: with 连.transaction(): 服务, 基础, 原结构, 绑定, _ = 安装动态结构(连) 新版 = replace(基础, schema_version=2, 父版本=1) 服务.登记结构候选(新版) 服务.发布结构(新版.schema_id, 2, 新版.内容哈希) 新结构 = 合成结构(新版) # 仅夹具业务 owner 拥有此表;元数据模块从不访问它。 连.execute("CREATE TABLE public.meta_owner_probe (id int PRIMARY KEY, body text)") 预览 = 投影字段( 原结构, 服务.当前策略(基础.type_id), 授权, 字段用途名="userEditable", 内容用途="planning", 运行用途="production", ) def 切换(限时: bool) -> None: with 连接(元数据库, 用途标记=用途.维护) as 第二连, 第二连.transaction(): if 限时: 第二连.execute("SET LOCAL lock_timeout='250ms'") 第二服务 = 元数据服务(第二连) if 变更类别 == "structure": 第二服务.启用绑定( 绑定.target_ref, 绑定.work_id, 新结构, 启用命令( "switch", 新结构.effective_schema_hash, 1, "author-a", 绑定.target_ref ), ) else: 新策略 = 策略快照( 基础.type_id, 1, (字段限制("weather.name", ("userEditable",)),) ) 第二服务.更新策略( 新策略, 启用命令( "revoke", 定义哈希(asdict(新策略)), 0, "author-a", f"type:{基础.type_id}", ), ) with 连.transaction(): 服务.保护提交( 绑定.target_ref, 预览.effective_schema_hash, 预览.projection_version, 授权, 字段用途名="userEditable", 内容用途="planning", 运行用途="production", ) with ThreadPoolExecutor(max_workers=1) as 池: with pytest.raises(psycopg.errors.LockNotAvailable): 池.submit(切换, True).result(timeout=5) 连.execute("INSERT INTO public.meta_owner_probe VALUES (1, '作者确认的天气精灵')") 切换(False) with 连.transaction(): with pytest.raises(元数据错误) as 错误: 服务.保护提交( 绑定.target_ref, 预览.effective_schema_hash, 预览.projection_version, 授权, 字段用途名="userEditable", 内容用途="planning", 运行用途="production", ) assert 错误.value.错误码 == ( "SCHEMA_STALE" if 变更类别 == "structure" else "PROJECTION_STALE" ) assert 连.execute("SELECT id FROM public.meta_owner_probe").fetchall() == [(1,)] @pytest.mark.case_id( "NC-meta-immutable-extensions", environment="隔离 PostgreSQL,每例独占新库", given="已发布结构与作品扩展", when="覆盖同版本或删除既有扩展", then=["历史定义保持不变。"], contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md", ) def test_已发布定义与扩展追加合同不可绕过__a42005(元数据库: 数据库引用) -> None: """given 已发布结构与作品扩展;when 覆盖同版本或删除既有扩展;then 历史定义保持不变。""" from dataclasses import replace from muse.元数据.接口 import 作品扩展, 值规则, 字段定义 with 连接(元数据库, 用途标记=用途.维护) as 连, 连.transaction(): 服务, 基础, _, _, _ = 安装动态结构(连) assert 服务.读取类型(基础.type_id).实例族 == ("entity",) with pytest.raises(元数据错误) as 错误: 服务.登记结构候选(replace(基础, 字段=())) assert 错误.value.错误码 == "SCHEMA_CONFLICT" 扩展 = 作品扩展( "work-a", 基础.schema_id, 1, 1, (字段定义("work-a.temp", "体温", "体温", 值规则("number")),), ) 服务.发布扩展(扩展, 扩展.内容哈希) 丢字段 = replace(扩展, extension_version=2, 字段=()) with pytest.raises(元数据错误) as 错误: 服务.发布扩展(丢字段, 丢字段.内容哈希) assert 错误.value.错误码 == "FIELD_NOT_ALLOWED" assert 服务.读取结构(基础.schema_id, 1) == 基础 assert ( 服务.读取有效结构(基础.schema_id, 1, work_id="work-a", extension_version=1).扩展 == 扩展 )