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

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

351 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.

"""真实连接、权限、事务与迁移验证;仅使用显式隔离 PostgreSQL。"""
from __future__ import annotations
import hashlib
import json
from collections.abc import Iterator
from pathlib import Path
import psycopg
import pytest
from psycopg import sql
from muse.__main__ import main
from muse.共享.调用身份 import 用途
from muse.共享.错误 import 环境缺失错误
from muse.启动 import 构建
from muse.基础设施.数据库.事务 import 事务, 事务失败, 保存点
from muse.基础设施.数据库.用途隔离 import 用途越权
from muse.基础设施.数据库.迁移 import 已应用版本, 执行迁移, 迁移错误
from muse.基础设施.数据库.连接 import 解析连接串, 连接
from muse.配置 import 应用配置, 数据库引用
迁移目录 = Path(__file__).resolve().parents[2] / "数据库" / "迁移"
角色表 = {用途.生产: "muse_app", 用途.评测: "muse_eval", 用途.维护: "muse_maint"}
@pytest.fixture
def 临时库(空测试库) -> Iterator[dict[用途, 数据库引用]]:
"""空库没有预执行被测 DDL;真实提交、并发和迁移动作由测试执行。"""
yield {用途值: 工厂.引用 for 用途值, 工厂 in 空测试库.items()}
def 维护连接(临时库: dict[用途, 数据库引用], **参数: object) -> psycopg.Connection:
return 连接(临时库[用途.维护], 用途标记=用途.维护, **参数)
@pytest.mark.case_id(
"NC-db-no-fallback",
environment="离线",
given="数据库凭据引用未提供",
when="从真实装配取得数据库连接",
then=["报环境缺失并拒绝任何默认库回退"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
def test_缺配置拒绝回退默认库__d10001(monkeypatch: pytest.MonkeyPatch) -> None:
"""given 缺少连接引用;when 装配连接;then 拒绝默认库回退。"""
monkeypatch.delenv("MUSE_MISSING_DATABASE_URL", raising=False)
配置 = 应用配置(数据库引用("环境变量", "MUSE_MISSING_DATABASE_URL"), "test")
装配 = 构建(配置)
工厂 = 装配.要求数据库()
with 装配.生命周期():
# 打开空池不读凭据;首次借用才解析,缺环境变量时拒绝默认库回退。
with pytest.raises(环境缺失错误, match="拒绝回退"):
with 工厂.连接():
pass
@pytest.mark.case_id(
"NC-db-readonly-write",
environment="隔离 PostgreSQL",
given="维护角色连接且附加普通 options",
when="请求只读并写入",
then=["两种 options 下 PostgreSQL 都拒绝写入"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
@pytest.mark.数据库
@pytest.mark.parametrize(
"options", ["", "-c application_name=readonly-probe"], ids=["default", "options"]
)
def test_只读连接拒绝写__d50005(临时库: dict[用途, 数据库引用], options: str) -> None:
"""given 普通连接选项;when 请求只读;then 数据库拒绝写入。"""
with 维护连接(临时库, 只读=True, options=options) as 连:
with pytest.raises(psycopg.errors.ReadOnlySqlTransaction):
连.execute("CREATE TABLE readonly_probe (id int)")
连.rollback()
@pytest.mark.case_id(
"NC-db-txn-rollback",
environment="隔离 PostgreSQL",
given="默认或自动提交连接",
when="事务第二参与者失败",
then=["全部写入回滚且错误不包含参与者原文"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
@pytest.mark.数据库
@pytest.mark.parametrize("autocommit", [False, True], ids=["manual", "auto"])
def test_事务回滚不留半成品__d60006(临时库: dict[用途, 数据库引用], autocommit: bool) -> None:
"""given 两种提交模式;when 第二参与者失败;then 全部写入回滚。"""
with 维护连接(临时库, autocommit=autocommit) as 连:
with pytest.raises(事务失败) as 记录:
with 事务(连):
连.execute("CREATE TABLE participant_one (id int)")
连.execute("CREATE TABLE participant_two (id int)")
raise RuntimeError("第二参与者失败,正文与凭据不得被拼入错误")
assert "正文与凭据" not in str(记录.value.呈现())
assert (
连.execute(
"SELECT count(*) FROM information_schema.tables "
"WHERE table_name IN ('participant_one', 'participant_two')"
).fetchone()[0]
== 0
)
@pytest.mark.case_id(
"NC-db-purpose-role",
environment="隔离 PostgreSQL",
given="三个固定普通登录角色",
when="正式连接入口声明其他用途",
then=["六种不匹配全部拒绝,匹配组合可用"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
@pytest.mark.数据库
@pytest.mark.parametrize(
("登录用途", "声明用途"),
[(角色, 声明) for 角色 in 用途 for 声明 in 用途 if 角色 != 声明],
ids=[f"{角色.value}-as-{声明.value}" for 角色 in 用途 for 声明 in 用途 if 角色 != 声明],
)
def test_用途角色不匹配被拒__d70007(
临时库: dict[用途, 数据库引用], 登录用途: 用途, 声明用途: 用途
) -> None:
"""given 固定普通角色;when 正式连接入口声明其他用途;then 连接被拒。"""
with pytest.raises(用途越权):
连接(临时库[登录用途], 用途标记=声明用途)
with 连接(临时库[登录用途], 用途标记=登录用途) as 连:
assert 连.execute("SELECT current_user").fetchone()[0] == 角色表[登录用途]
@pytest.mark.case_id(
"NC-db-eval-oracle",
environment="隔离 PostgreSQL",
given="oracle schema 中的合成答案和匿名映射",
when="生产角色与评测角色实际 SELECT",
then=["生产被 PG 拒权,评测可读"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
@pytest.mark.数据库
@pytest.mark.parametrize("目标表", ["answer", "blind_assignment"], ids=["oracle", "blind-map"])
def test_生产用途拒绝oracle及匿名映射读取__d80008(
临时库: dict[用途, 数据库引用], 目标表: str
) -> None:
"""given 维护角色建立答案或匿名映射;when 生产连接查询;then PG 拒权。"""
目标 = sql.Identifier("oracle", 目标表)
with 维护连接(临时库) as 连:
执行迁移(连, 迁移目录, 目标版本=1)
连.execute(sql.SQL("CREATE TABLE {} (id int)").format(目标))
连.execute(sql.SQL("INSERT INTO {} VALUES (1)").format(目标))
with 连接(临时库[用途.生产], 用途标记=用途.生产) as 连:
with pytest.raises(psycopg.errors.InsufficientPrivilege):
连.execute(sql.SQL("SELECT * FROM {}").format(目标))
连.rollback()
with 连接(临时库[用途.评测], 用途标记=用途.评测) as 连:
assert 连.execute(sql.SQL("SELECT id FROM {}").format(目标)).fetchone()[0] == 1
@pytest.mark.case_id(
"NC-db-savepoint",
environment="隔离 PostgreSQL",
given="两种提交模式下已写入的外层事务",
when="保存点内写入后失败",
then=["局部回滚,外层提交且新连接可见"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
@pytest.mark.数据库
@pytest.mark.parametrize("autocommit", [False, True], ids=["manual", "auto"])
def test_保存点局部失败保留外层事务__d90009(
临时库: dict[用途, 数据库引用], autocommit: bool
) -> None:
"""given 外层事务已有写入;when 保存点失败;then 局部回滚且外层可提交。"""
with 维护连接(临时库, autocommit=autocommit) as 连:
with 事务(连):
连.execute("CREATE TABLE savepoint_probe (id int)")
连.execute("INSERT INTO savepoint_probe VALUES (1)")
with pytest.raises(RuntimeError):
with 保存点(连, '中文保存点"'):
连.execute("INSERT INTO savepoint_probe VALUES (2)")
raise RuntimeError("局部失败")
连.execute("INSERT INTO savepoint_probe VALUES (3)")
assert 连.execute("SELECT id FROM savepoint_probe ORDER BY id").fetchall() == [(1,), (3,)]
with 维护连接(临时库) as 再读:
assert 再读.execute("SELECT count(*) FROM savepoint_probe").fetchone()[0] == 2
@pytest.mark.case_id(
"NC-db-eval-production-write",
environment="隔离 PostgreSQL",
given="由维护迁移授权的生产和评测表",
when="两个角色分别写入",
then=["生产角色可写生产表,评测只能写评测表"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
@pytest.mark.数据库
def test_评测角色仅可写评测对象__da000a(临时库: dict[用途, 数据库引用]) -> None:
"""given 生产与评测对象;when 评测连接写入;then 仅评测对象可写。"""
with 维护连接(临时库) as 连:
执行迁移(连, 迁移目录, 目标版本=1)
连.execute("CREATE TABLE public.production_probe (id int)")
连.execute("CREATE TABLE evaluation.sample_probe (id int)")
with 连接(临时库[用途.生产], 用途标记=用途.生产) as 连:
连.execute("INSERT INTO public.production_probe VALUES (1)")
with 连接(临时库[用途.评测], 用途标记=用途.评测) as 连:
with pytest.raises(psycopg.errors.InsufficientPrivilege):
连.execute("INSERT INTO public.production_probe VALUES (2)")
连.rollback()
连.execute("INSERT INTO evaluation.sample_probe VALUES (3)")
assert 连.execute("SELECT id FROM evaluation.sample_probe").fetchone()[0] == 3
with 维护连接(临时库) as 连:
assert 连.execute("SELECT id FROM public.production_probe").fetchall() == [(1,)]
@pytest.mark.case_id(
"NC-db-migration-idempotent",
environment="隔离 PostgreSQL",
given="隔离测试库与受控连接引用",
when="执行连接、只读、事务、用途或迁移操作",
then=["拒绝与失败如实呈现;成功路径可复验"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
@pytest.mark.数据库
def test_迁移幂等与篡改拦截__d20002(临时库: dict[用途, 数据库引用], tmp_path: Path) -> None:
"""given 已登记迁移;when 重跑或篡改同版本;then 幂等跳过或拒绝。"""
with 维护连接(临时库) as 连:
assert [项.版本 for 项 in 执行迁移(连, 迁移目录, 目标版本=1)] == [1]
assert len(已应用版本(连)[1]) == 64
assert 执行迁移(连, 迁移目录, 目标版本=1) == []
(tmp_path / "V0001__共享标识与版本.sql").write_text("-- 改动\n", encoding="utf-8")
with pytest.raises(迁移错误, match="校验和"):
执行迁移(连, tmp_path)
@pytest.mark.case_id(
"NC-db-migration-failure",
environment="隔离 PostgreSQL",
given="隔离测试库与受控连接引用",
when="执行连接、只读、事务、用途或迁移操作",
then=["拒绝与失败如实呈现;成功路径可复验"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
@pytest.mark.数据库
def test_迁移失败不登记且可重跑__d30003(临时库: dict[用途, 数据库引用], tmp_path: Path) -> None:
"""given 两个版本;when 第二版本失败;then 仅第一版入账且修复可重跑。"""
(tmp_path / "V0001__共享标识与版本.sql").write_bytes(
(迁移目录 / "V0001__共享标识与版本.sql").read_bytes()
)
V2 = tmp_path / "V0002__迁移.sql"
V2.write_text("CREATE TABLE migration_probe (id int);\n这不是合法 SQL;\n", encoding="utf-8")
with 维护连接(临时库) as 连:
with pytest.raises(迁移错误, match="未登记成功"):
执行迁移(连, tmp_path)
assert set(已应用版本(连)) == {1}
with 连.transaction():
assert 连.execute("SELECT to_regclass('public.migration_probe')").fetchone()[0] is None
V2.write_text("CREATE TABLE migration_probe (id int);\n", encoding="utf-8")
assert [项.版本 for 项 in 执行迁移(连, tmp_path)] == [2]
assert set(已应用版本(连)) == {1, 2}
@pytest.mark.case_id(
"NC-db-migration-order",
environment="隔离 PostgreSQL",
given="隔离测试库与受控连接引用",
when="执行连接、只读、事务、用途或迁移操作",
then=["拒绝与失败如实呈现;成功路径可复验"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
@pytest.mark.数据库
def test_低版本补录被拒绝__d40004(临时库: dict[用途, 数据库引用], tmp_path: Path) -> None:
"""given 已登记较高版本;when 提供未应用低版本;then 拒绝补录。"""
with 维护连接(临时库) as 连:
执行迁移(连, 迁移目录, 目标版本=1)
with 连.transaction():
连.execute(
"INSERT INTO public.muse_migration (version, name, checksum) VALUES (%s, %s, %s)",
(3, "V0003__既往.sql", hashlib.sha256(b"past").hexdigest()),
)
(tmp_path / "V0002__补录.sql").write_text(
"CREATE TABLE backfill (id int);", encoding="utf-8"
)
with pytest.raises(迁移错误, match="低版本"):
执行迁移(连, tmp_path)
@pytest.mark.case_id(
"NC-db-migration-lock",
environment="隔离 PostgreSQL",
given="另一连接持有迁移锁且账本已最新",
when="迁移入口重跑",
then=["首先拒绝锁冲突,释放锁后可幂等重跑"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
@pytest.mark.数据库
def test_迁移先取锁再判定账本__db000b(临时库: dict[用途, 数据库引用]) -> None:
"""given 一个进程持有迁移锁;when 另一连接重跑;then 不读取未保护账本。"""
with 维护连接(临时库, autocommit=True) as 持有者:
执行迁移(持有者, 迁移目录, 目标版本=1)
持有者.execute("SELECT pg_advisory_lock(%s)", (0x6D757365,))
with 维护连接(临时库) as 竞争者:
with pytest.raises(迁移错误, match="持有锁"):
执行迁移(竞争者, 迁移目录, 目标版本=1)
持有者.execute("SELECT pg_advisory_unlock(%s)", (0x6D757365,))
assert 执行迁移(竞争者, 迁移目录, 目标版本=1) == []
@pytest.mark.case_id(
"NC-db-cli-assembly",
environment="隔离 PostgreSQL",
given="生产和维护用途 TOML 配置",
when="运行真实 CLI 迁移入口",
then=["生产配置被拒,维护从包资源执行并登记 V0001"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
@pytest.mark.数据库
def test_迁移命令使用维护装配与包资源__dc000c(
临时库: dict[用途, 数据库引用], tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""given 用途配置;when CLI 迁移;then 生产被拒且维护从安装资源执行。"""
配置文件 = tmp_path / "连接.toml"
for 声明用途, 预期码 in [(用途.生产, 1), (用途.维护, 0)]:
引用 = 临时库[声明用途]
配置文件.write_text(
'["数据库"]\n"取值方式" = ' + json.dumps(引用.取值方式) + "\n"
'"位置" = ' + json.dumps(引用.位置) + "\n"
'["资源"]\n"发布身份" = "test"\n'
f'["运行"]\n"用途" = "{声明用途.value}"\n',
encoding="utf-8",
)
# 取值方式与位置都取自夹具真实引用:夹具用受控存储文件,不假设环境变量。
assert main(["迁移", str(配置文件), "--目标版本", "1"]) == 预期码
assert "已应用 V0001" in capsys.readouterr().out
with 维护连接(临时库) as 连:
assert set(已应用版本(连)) == {1}
@pytest.mark.case_id(
"NC-db-controlled-reference",
environment="离线",
given="受控存储中的合成连接引用",
when="连接 owner 解析引用",
then=["通过凭据 owner 读取值且无默认回退"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
def test_数据库引用通过唯一凭据入口读取受控文件__dd000d(tmp_path: Path) -> None:
"""given 受控文件引用;when 解析连接;then 与环境引用共用读取入口。"""
文件 = tmp_path / "合成连接.txt"
文件.write_text("dbname=isolated_example user=muse_app\n", encoding="utf-8")
assert 解析连接串(数据库引用("受控存储", str(文件))) == (
"dbname=isolated_example user=muse_app"
)