muse-agent-example/tests/迁移/test_质量资产迁移.py
zizi 9e6f1c4481 R2 改造交付:新版模块化单体全量成果
- src/muse 新版全模块(装配/共享/上下文/任务运行/作品规划/故事世界/正文写作/审校修订/知识方法/作者经验/效果评测/交付连载/资料研究/正式变更/元数据/接入/基础设施/编排)+ 测试树(单元/契约/集成/架构/迁移/端到端/夹具)
- 129 项功能全部实现与自动验证(功能覆盖.json/矩阵),含 W31 补齐的规则与代价/节奏安排/伏笔与承诺
- 旧实现按处置清单退出(702 条中 324 删,保护合同与未迁移条目留存有据);web/app.py 旧工作台退役,新工作台为唯一写入口
- 数据库/旧库迁移:真实旧库内容批次迁移链(端点守卫/PG作品正文映射/质量资产缺省投影)
- 运行手册 docs/运行手册.md;W30 本机服务阶段一已运行(infra PG 为正式内容权威)
- R2 执行证据与私有运行材料在 .agents.local/改造/R2-20260909/(不入库)
2026-09-15 12:47:42 +08:00

707 lines
29 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.

"""旧规则/质量案例只经B06公开用例迁移,原状态不升格。"""
from __future__ import annotations
import base64
import hashlib
import json
from copy import deepcopy
from types import SimpleNamespace
import psycopg
import pytest
import yaml
from psycopg import sql
from muse.作品规划.接口 import 档案保存
from muse.元数据.接口 import 元数据服务, 导入内置结构
from muse.共享.调用身份 import 内容用途, 用途, 调用身份
from muse.启动 import 构建
from muse.基础设施.数据库.连接 import 数据库工厂
from muse.正文写作.接口 import 文本节点, 正文结构哈希, 正文草稿, 段落
from muse.配置 import 应用配置, 数据库引用
from 导入新库 import 初始化目标, 导入快照
from 建立映射 import 稳定JSON, 旧快照, 映射配置, 迁移错误
from 映射台账 import 迁移台账
from 核对结果 import 核对迁移结果
from 知识记录分流 import 分流批次
from 读取旧文件 import 读取文件集
from 转换业务对象 import 准备目标请求, 提交目标请求
from 转换质量资产 import 准备质量请求, 转换规则库
def _文件快照(路径: str, 值: dict) -> 旧快照:
字节 = yaml.safe_dump(值, allow_unicode=True, sort_keys=False).encode()
哈希 = hashlib.sha256(字节).hexdigest()
return 旧快照.从载荷(
{
"source": {
"system": "file",
"database": '{"dataset":"synthetic-quality-files","scope":"files"}',
"table": "legacy_file",
"id": 路径,
"revision": "snapshot-v1",
},
"record": {
"id": 路径,
"revision": "snapshot-v1",
"relpath": 路径,
"size": len(字节),
"sha256": 哈希,
"content_base64": base64.b64encode(字节).decode(),
},
}
)
def _例证(kind: str) -> dict:
前缀 = {"sf": "sf", "snf": "snf", "boundary": "b", "regression": "reg"}[kind]
return {
"id": f"{前缀}-l002-01",
"type": kind,
"rules": ["l002"],
"carrier": "narration",
"source": "synthetic",
"text": f"{kind}的合成句子。",
"note": f"{kind}的合成说明。",
}
def _规则文件批次():
规则 = {
"id": "l002",
"name": "空洞元话语",
"layer": "lexical",
"carrier_scope": "narration",
"trigger": {"type": "regex", "pattern": "值得注意的是"},
"carve_out": ["评书腔"],
"default_disposition": "candidate",
"function_check": ["是否承担声线功能"],
"fix_hint": "无功能时删除",
"samples": {kind: [_例证(kind)["id"]] for kind in ("sf", "snf", "boundary", "regression")},
"version": 1,
"status": "active",
"evidence": "旧库人工已激活历史",
"case_card_ids": [],
}
主 = _文件快照("rules/l002.yaml", 规则)
例证 = [
_文件快照(f"samples/{kind}.yaml", {"samples": [_例证(kind)]})
for kind in ("sf", "snf", "boundary", "regression")
]
绑定 = [
{
"source_key": 主.source.记录键,
"source_hash": 主.源哈希,
"route": "rule_version",
"author_id": "migration-author",
"sources": [
{
"source_key": s.source.记录键,
"source_hash": s.源哈希,
"sample_ids": [
next(
iter(
yaml.safe_load(base64.b64decode(s.原行["content_base64"]))[
"samples"
]
)
)["id"]
],
"license": "synthetic",
"source_ref": "legacy-file:" + s.source.id,
}
for s in 例证
],
"parameters": {"rule_id": "l002", "version": 1},
},
*[
{
"source_key": s.source.记录键,
"source_hash": s.源哈希,
"route": "rule_example",
"author_id": "migration-author",
"sources": [],
"parameters": {"sample_ids": [例["id"]]},
}
for s, 例 in zip(
例证, [_例证(k) for k in ("sf", "snf", "boundary", "regression")], strict=True
)
],
]
return [主, *例证], 映射配置({"works": [], "quality_bindings": 绑定})
def _PG规则批次():
快照, 映射 = _规则文件批次()
规则 = yaml.safe_load(base64.b64decode(快照[0].原行["content_base64"]))
行 = deepcopy(规则)
行["id"], 行["rule_id"] = 1, 行.pop("id")
行["trigger_json"] = 行.pop("trigger")
行["sample_refs"] = 行.pop("samples")
del 规则["case_card_ids"]
行["payload"] = 规则
行["content_sha256"] = hashlib.sha256(稳定JSON(规则).encode()).hexdigest()
主 = 旧快照.从载荷(
{
"source": {
"system": "pg",
"database": "synthetic-quality-pg",
"table": "example_ai_flavor_rule",
"id": "1",
"revision": "export-v1",
},
"record": 行,
}
)
绑定 = {**映射.质量绑定(快照[0]), "source_key": 主.source.记录键, "source_hash": 主.源哈希}
可用源 = {}
for i, 源绑定 in enumerate(绑定["sources"], start=1):
样例 = yaml.safe_load(base64.b64decode(快照[i].原行["content_base64"]))["samples"][0]
行 = {
**样例,
"id": i,
"sample_id": 样例["id"],
"sample_type": 样例["type"],
"source_license": "synthetic",
"source_ref": "",
"payload": 样例,
"content_sha256": hashlib.sha256(稳定JSON(样例).encode()).hexdigest(),
}
del 行["type"]
源 = 旧快照.从载荷(
{
"source": {
"system": "pg",
"database": "synthetic-quality-pg",
"table": "example_ai_flavor_sample",
"id": str(i),
"revision": "export-v1",
},
"record": 行,
}
)
源绑定.update(
source_key=源.source.记录键,
source_hash=源.源哈希,
source_ref="migration-source:" + 源.源哈希,
)
可用源[源.source.记录键] = 源
return 主, 绑定, 可用源
def test_PG规则及样例缺省字段仅兼容DDL默认且保留原哈希__2b8da9():
主, 绑定, 可用源 = _PG规则批次()
原始 = deepcopy(主.导出())
原例证 = {键: deepcopy(源.导出()) for 键, 源 in 可用源.items()}
库, 旧源 = 转换规则库(主, 绑定, 可用源)
assert 库["rules"]["l002"]["case_card_ids"] == []
assert 库["rules"]["l002"]["status"] == "candidate"
assert set(库["rules"]["l002"]["samples"]) == {"sf", "snf", "boundary", "regression"}
assert 旧源 == {
"status": "active",
"source_key": 主.source.记录键,
"source_hash": 原始["source_hash"],
}
assert 主.导出() == 原始
assert "case_card_ids" not in 主.原行["payload"]
assert 主.原行["content_sha256"] == 原始["record"]["content_sha256"]
assert {键: 源.导出() for 键, 源 in 可用源.items()} == 原例证
assert {样例["source_license"] for 样例 in 库["samples"].values()} == {"synthetic"}
for 源绑定 in 绑定["sources"]:
源 = 可用源[源绑定["source_key"]]
assert "source_license" not in 源.原行["payload"]
assert "source_ref" not in 源.原行["payload"]
样例 = 库["samples"][源绑定["sample_ids"][0]]
assert 样例["source_ref"] == "migration-source:" + 源.源哈希
@pytest.mark.parametrize("字段", ["case_card_ids", "source_license", "source_ref"])
@pytest.mark.parametrize(
"反例",
["显式null", "非默认投影", "投影错型", "载荷错型", "投影缺失", "其他字段缺省", "内容哈希不符"],
)
def test_PG质量默认投影兼容不放过异常__f2e3f7(字段, 反例):
主, 绑定, 可用源 = _PG规则批次()
源 = 主 if 字段 == "case_card_ids" else next(iter(可用源.values()))
导出 = 源.导出()
del 导出["source_hash"]
行 = 导出["record"]
错误码 = "quality_projection_mismatch"
不符字段 = 字段
if 反例 == "显式null":
行["payload"][字段] = None
elif 反例 == "非默认投影":
行[字段] = ["case-other"] if 字段 == "case_card_ids" else "other"
elif 反例 == "投影错型":
行[字段] = {}
if 字段 == "case_card_ids":
错误码 = "quality_source_invalid"
elif 反例 == "载荷错型":
行["payload"][字段] = {}
elif 反例 == "投影缺失":
del 行[字段]
if 字段 == "case_card_ids":
错误码 = "quality_source_invalid"
elif 反例 == "其他字段缺省":
不符字段 = "carve_out" if 字段 == "case_card_ids" else "note"
del 行["payload"][不符字段]
行[不符字段] = [] if 字段 == "case_card_ids" else ""
行["content_sha256"] = hashlib.sha256(稳定JSON(行["payload"]).encode()).hexdigest()
if 反例 == "内容哈希不符":
行["content_sha256"] = "0" * 64
错误码 = "quality_content_hash_mismatch"
异常源 = 旧快照.从载荷(导出)
if 字段 == "case_card_ids":
主 = 异常源
绑定["source_hash"] = 主.源哈希
else:
可用源[源.source.记录键] = 异常源
绑定["sources"][0]["source_hash"] = 异常源.源哈希
原始 = 异常源.导出()
匹配 = 错误码
if 错误码 == "quality_projection_mismatch":
匹配 += ".*:" + 不符字段
with pytest.raises(迁移错误, match=匹配):
转换规则库(主, 绑定, 可用源)
assert 异常源.导出() == 原始
@pytest.mark.parametrize(
("字段", "值", "错误码"),
[
("source_ref", "", "quality_binding_invalid"),
("source_ref", None, "quality_binding_invalid"),
("source_hash", "0" * 64, "quality_source_drift"),
("license", "unknown", "quality_binding_invalid"),
],
ids=["出处为空", "出处null", "源哈希漂移", "许可未知"],
)
def test_PG样例默认投影不替代冻结来源绑定__ba3dc2(字段, 值, 错误码):
主, 绑定, 可用源 = _PG规则批次()
绑定["sources"][0][字段] = 值
with pytest.raises(迁移错误, match=错误码):
转换规则库(主, 绑定, 可用源)
def _保全规则文件(snapshots, tmp_path):
source = tmp_path / "source-quality"
source.mkdir()
files = []
for snapshot in snapshots:
path = source / snapshot.source.id
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(base64.b64decode(snapshot.原行["content_base64"]))
files.append(
{
"relpath": snapshot.source.id,
"revision": snapshot.source.revision,
"sha256": snapshot.原行["sha256"],
}
)
output = tmp_path / "preserved-quality"
actual = 读取文件集(source, files, 数据集="synthetic-quality-files", 输出目录=output)
assert [s.导出() for s in actual] == [s.导出() for s in snapshots]
return output / "文件回执.json"
def test_文件规则及四类例证冻结且旧active只留历史__29e201():
快照, 映射 = _规则文件批次()
分流 = 分流批次(快照, 映射)
assert 分流[0].reason_code == "quality_rule_version"
assert all(r.reason_code == "rule_example_dependency" for r in 分流[1:])
绑定 = 映射.质量绑定(快照[0])
库, 旧源 = 转换规则库(快照[0], 绑定, {s.source.记录键: s for s in 快照})
assert 旧源["status"] == "active"
assert 库["rules"]["l002"]["status"] == "candidate"
assert set(库["rules"]["l002"]["samples"]) == {"sf", "snf", "boundary", "regression"}
assert {s["source_license"] for s in 库["samples"].values()} == {"synthetic"}
def test_缺例证和经验表不会被猜成质量案例__29e202():
快照, 映射 = _规则文件批次()
with pytest.raises(迁移错误, match="quality_dependency_missing"):
转换规则库(快照[0], 映射.质量绑定(快照[0]), {s.source.记录键: s for s in 快照[:-1]})
经验 = 旧快照.从载荷(
{
"source": {
"system": "sqlite",
"database": "synthetic-lessons",
"table": "lessons",
"id": "lesson-1",
"revision": "v1",
},
"record": {
"id": "lesson-1",
"status": "promoted",
"kind": "lesson",
"title": "不是案例",
},
}
)
判定 = 分流批次([经验], 映射配置({"works": []}))[0]
assert (判定.disposition, 判定.reason_code) == ("隔离", "deferred_W26_27")
def test_旧案例准备固定为未决backfill并核真实片段__29e207():
文本 = "她把伞递过去。值得注意的是,门外下雨了。"
片段 = "值得注意的是,门外下雨了。"
开始, 结束 = 文本.index(片段), len(文本)
草稿 = 正文草稿((段落("p1", (文本节点(文本),)),))
文档哈希 = 正文结构哈希(草稿)
片段哈希 = hashlib.sha256(片段.encode()).hexdigest()
行 = {
"id": 9,
"card_id": "case-legacy-9",
"state": "canonical",
"label": "snf",
"capture_mode": "live_feedback",
"source_license": "owned",
"source_sha256": "9" * 64,
"excerpt_sha256": 片段哈希,
"source_ref": "legacy:chapter:9",
"work_ref": "legacy-work-9",
"source_location": {"chapter": 9, "start": 开始, "end": 结束},
"excerpt": 片段,
"context": 文本,
"deleted": False,
}
快照 = 旧快照.从载荷(
{
"source": {
"system": "pg",
"database": "synthetic-case",
"table": "example_ai_flavor_case",
"id": "9",
"revision": "export-v1",
},
"record": 行,
}
)
绑定 = {
"source_key": 快照.source.记录键,
"source_hash": 快照.源哈希,
"route": "quality_case",
"author_id": "migration-author",
"sources": [
{
"role": "before",
"license": "owned",
"license_ref": "legacy-decision:9",
"legacy": {
"source_ref": 行["source_ref"],
"work_ref": 行["work_ref"],
"source_sha256": 行["source_sha256"],
"excerpt_sha256": 片段哈希,
"location": 行["source_location"],
},
"target": {
"kind": "document",
"ref_id": "chapter-9",
"revision": 3,
"source_hash": 文档哈希,
"start": 开始,
"end": 结束,
"excerpt_hash": 片段哈希,
"branch_id": "main",
"work_ref": "work:new-work-9",
},
}
],
"parameters": {"card_id": 行["card_id"], "preserve_text": True},
}
class 正文替身:
def 读取正文(self, 身份, chapter_id, *, 分支, 版本):
assert (chapter_id, 分支, 版本) == ("chapter-9", "main", 3)
return {"work_id": "new-work-9", "document": 草稿}
class 装配替身:
def 要求正文(self):
return 正文替身()
身份 = SimpleNamespace(作者="migration-author")
冻结 = 准备质量请求(
装配替身(),
身份,
快照,
映射配置({"works": [], "quality_bindings": [绑定]}),
{快照.source.记录键: 快照},
)
assert 冻结["payload"]["mode"] == "backfill"
assert 冻结["payload"]["feedback_command_id"] is None
assert 冻结["payload"]["label"] == "snf"
assert 冻结["payload"]["disposition"] == "ask"
assert 冻结["legacy_source"]["state"] == "canonical"
@pytest.fixture
def 质量迁移目标(应用测试库, 隔离数据库URL, tmp_path):
管理文件 = tmp_path / "管理连接.txt"
管理文件.write_text(隔离数据库URL, encoding="utf-8")
管理文件.chmod(0o600)
with psycopg.connect(隔离数据库URL) as 连:
允许 = {
"endpoint_type": "unix" if 连.info.host.startswith("/") else "tcp",
"host": 连.info.host,
"port": 连.info.port,
"system_identifier": str(
连.execute(
"SELECT system_identifier FROM pg_catalog.pg_control_system()"
).fetchone()[0]
),
"data_directory": 连.execute("SHOW data_directory").fetchone()[0],
}
允许文件 = tmp_path / "允许实例.json"
允许文件.write_text(json.dumps(允许), encoding="utf-8")
输出 = tmp_path / "目标"
管理引用 = 数据库引用("受控存储", str(管理文件))
回执 = 初始化目标(
允许实例=允许文件,
管理引用=管理引用,
维护模板=应用测试库[用途.维护].引用,
应用模板=应用测试库[用途.生产].引用,
输出目录=输出,
)
库 = 数据库工厂(数据库引用("受控存储", str(输出 / "应用连接.txt")), 用途.生产)
维护 = 数据库工厂(数据库引用("受控存储", str(输出 / "维护连接.txt")), 用途.维护)
with 维护.连接() as 连, 连.transaction():
导入内置结构(元数据服务(连))
装配 = 构建(应用配置(库.引用, "synthetic"))
身份 = 调用身份("migration-author", None, 用途.生产, 内容用途.规划)
作品 = 装配.要求作品()
结构 = 作品.新档案结构(身份, "quality-work", "work_core", 1)
作品.保存档案(
身份, "quality-work", 档案保存("quality-work", 0, {"名称": "质量迁移合成作品"}, 结构)
)
作品.添加章节(身份, "quality-ch", "quality-work", "quality-ch", "合成章", 预期目录版本=0)
文本 = "她把伞递过去。值得注意的是,门外已经下起了雨。两人谁也没说话。"
装配.要求正文().保存人工(
身份, "quality-body", "quality-ch", 0, 正文草稿((段落("p1", (文本节点(文本),)),))
)
参数 = {"允许实例": 允许文件, "管理引用": 管理引用, "目标回执": 输出 / "目标回执.json"}
try:
yield 装配, 身份, 库, 参数, 文本
finally:
with psycopg.connect(隔离数据库URL, autocommit=True) as 连:
连.execute(
sql.SQL("DROP DATABASE {} WITH (FORCE)").format(sql.Identifier(回执["database"]))
)
def _案例快照与映射(装配, 身份, 文本, *, 错作品=False):
正文 = 装配.要求正文().读取正文(身份, "quality-ch")
片段 = "值得注意的是,门外已经下起了雨。"
开始, 结束 = 文本.index(片段), 文本.index(片段) + len(片段)
片段哈希 = hashlib.sha256(片段.encode()).hexdigest()
行 = {
"id": 1,
"card_id": "case-legacy-quality-001",
"schema_version": "ai-flavor-case-v1",
"card_type": "ai_flavor_case",
"state": "canonical",
"label": "sf",
"layer": "lexical",
"carrier": "narration",
"capture_mode": "live_feedback",
"source_kind": "existing_work",
"source_license": "owned",
"source_sha256": "1" * 64,
"excerpt_sha256": 片段哈希,
"source_ref": "legacy:quality-ch:1",
"work_ref": "legacy-work-7",
"source_location": {"chapter": 1, "start": 开始, "end": 结束},
"excerpt": 片段,
"context": 文本,
"deleted": False,
}
快照 = 旧快照.从载荷(
{
"source": {
"system": "pg",
"database": "synthetic-quality-pg",
"table": "example_ai_flavor_case",
"id": "1",
"revision": "export-v1",
},
"record": 行,
}
)
绑定 = {
"source_key": 快照.source.记录键,
"source_hash": 快照.源哈希,
"route": "quality_case",
"author_id": 身份.作者,
"sources": [
{
"role": "before",
"license": "owned",
"license_ref": "legacy-decision:owned-1",
"legacy": {
"source_ref": 行["source_ref"],
"work_ref": 行["work_ref"],
"source_sha256": 行["source_sha256"],
"excerpt_sha256": 行["excerpt_sha256"],
"location": 行["source_location"],
},
"target": {
"kind": "document",
"ref_id": "quality-ch",
"revision": 正文["revision"],
"source_hash": 正文["document_hash"],
"start": 开始,
"end": 结束,
"excerpt_hash": 片段哈希,
"branch_id": "main",
"work_ref": "work:wrong" if 错作品 else "work:quality-work",
},
}
],
"parameters": {"card_id": 行["card_id"], "preserve_text": True},
}
return 快照, 映射配置({"works": [], "quality_bindings": [绑定]})
@pytest.mark.数据库
def test_案例经B06回填为未决且重放不重复创建__29e203(质量迁移目标):
装配, 身份, 库, 参数, 文本 = 质量迁移目标
快照, 映射 = _案例快照与映射(装配, 身份, 文本)
首次 = 导入快照(装配, 身份, [快照], 映射, **参数)
重放 = 导入快照(装配, 身份, [快照], 映射, **参数)
assert 首次["states"] == 重放["states"] == {"mapped": 1}
记录 = 迁移台账(库, 身份, 装配.要求知识方法().正式, 装配.要求故事世界(), 装配.要求审校()).读取(
快照.source.记录键
)
案例 = 装配.要求审校().读取案例(身份, 记录["targets"][0]["id"], revision=1)
assert 案例["state"] == "shadow" and not 案例["sampleable"]
assert 案例["payload"]["request"]["mode"] == "backfill"
assert 案例["payload"]["request"]["label"] == "sf"
assert 记录["frozen_request"]["legacy_source"]["state"] == "canonical"
with 库.连接(只读=True) as 连:
assert 连.execute("SELECT count(*) FROM muse_quality_case").fetchone()[0] == 1
assert 核对迁移结果(装配, 身份, [快照], 映射, **参数)["data_checks_passed"]
@pytest.mark.数据库
def test_S01已写案例但台账中断时只补原回执__29e204(质量迁移目标):
装配, 身份, 库, 参数, 文本 = 质量迁移目标
快照, 映射 = _案例快照与映射(装配, 身份, 文本)
判定 = 分流批次([快照], 映射)[0]
台账 = 迁移台账(库, 身份, 装配.要求知识方法().正式, 装配.要求故事世界(), 装配.要求审校())
映射哈希 = hashlib.sha256(稳定JSON(映射.值).encode()).hexdigest()
行 = 台账.占用(快照, 映射哈希, 判定)
冻结 = 准备目标请求(装配, 身份, 快照, 判定, 映射, 可用源={快照.source.记录键: 快照})
台账.冻结请求(快照.source.记录键, 冻结)
原回执 = 提交目标请求(装配, 身份, 行["command_id"], 冻结)
assert 台账.读取(快照.source.记录键)["state"] == "reserved"
导入快照(装配, 身份, [快照], 映射, **参数)
恢复 = 台账.读取(快照.source.记录键)
assert 恢复["state"] == "mapped" and 恢复["receipt"] == 原回执
with 库.连接(只读=True) as 连:
assert 连.execute("SELECT count(*) FROM muse_quality_case").fetchone()[0] == 1
@pytest.mark.数据库
def test_错作品绑定在B06业务前隔离且零案例写入__29e205(质量迁移目标):
装配, 身份, 库, 参数, 文本 = 质量迁移目标
with 库.连接(只读=True) as 连:
原命令数 = 连.execute("SELECT count(*) FROM muse_change_command").fetchone()[0]
快照, 映射 = _案例快照与映射(装配, 身份, 文本, 错作品=True)
结果 = 导入快照(装配, 身份, [快照], 映射, **参数)
assert 结果["states"] == {"quarantined": 1}
assert 结果["errors"][0]["reason_code"] == "quality_source_mismatch"
with 库.连接(只读=True) as 连:
assert 连.execute("SELECT count(*) FROM muse_quality_case").fetchone()[0] == 0
assert 连.execute("SELECT count(*) FROM muse_change_command").fetchone()[0] == 原命令数
@pytest.mark.数据库
def test_规则缺精确单版读回时保留reserved且不调B06导入__29e206(质量迁移目标, monkeypatch):
装配, 身份, 库, _, _ = 质量迁移目标
monkeypatch.setattr(装配.要求审校(), "读取规则版本", None)
快照, 映射 = _规则文件批次()
判定 = 分流批次(快照, 映射)[0]
台账 = 迁移台账(库, 身份, 装配.要求知识方法().正式, 装配.要求故事世界(), 装配.要求审校())
台账.占用(快照[0], hashlib.sha256(稳定JSON(映射.值).encode()).hexdigest(), 判定)
with pytest.raises(迁移错误, match="target_owner_read_unavailable"):
准备目标请求(
装配,
身份,
快照[0],
判定,
映射,
可用源={s.source.记录键: s for s in 快照},
)
with 库.连接(只读=True) as 连:
assert 连.execute("SELECT count(*) FROM muse_rule_version").fetchone()[0] == 0
assert (
连.execute(
"SELECT count(*) FROM muse_legacy_import_record WHERE state='reserved'"
).fetchone()[0]
== 1
)
@pytest.mark.数据库
def test_真实规则导入未启用且新版本存在时仍核对原版本__29e208(质量迁移目标, tmp_path):
app, author, db, options, _ = 质量迁移目标
snapshots, mapping = _规则文件批次()
options = {**options, "文件回执": _保全规则文件(snapshots, tmp_path)}
imported = 导入快照(app, author, snapshots, mapping, **options)
assert imported["states"].get("mapped") == 1, imported
assert imported["states"].get("quarantined", 0) == 0, imported
assert sum(imported["states"].values()) == 5
ledger = 迁移台账(db, author, app.要求知识方法().正式, app.要求故事世界(), app.要求审校())
entry = ledger.读取(snapshots[0].source.记录键)
original = app.要求审校().读取规则版本(author, "l002", 1)
assert original["status"] == "candidate" and not original["activation_recorded"]
assert original["version_id"] == entry["targets"][0]["id"]
assert entry["frozen_request"]["legacy_source"]["status"] == "active"
later = deepcopy(entry["frozen_request"]["payload"])
later["rules"]["l002"]["version"] = 2
later["rules"]["l002"]["name"] = "后续候选版本"
app.要求审校().导入规则候选(author, 规则库=later)
assert app.要求审校().读取规则版本(author, "l002", 2)["version_id"] != original["version_id"]
assert app.要求审校().读取规则版本(author, "l002", 1) == original
assert 导入快照(app, author, snapshots, mapping, **options)["states"].get("mapped") == 1
assert 核对迁移结果(app, author, snapshots, mapping, **options)["data_checks_passed"]
with db.连接(只读=True) as conn:
assert conn.execute("SELECT count(*) FROM muse_rule_activation").fetchone()[0] == 0
@pytest.mark.数据库
def test_规则已保存但台账中断时补同一版本且不伪造S01回执__29e209(质量迁移目标, tmp_path):
app, author, db, options, _ = 质量迁移目标
snapshots, mapping = _规则文件批次()
options = {**options, "文件回执": _保全规则文件(snapshots, tmp_path)}
route = 分流批次(snapshots, mapping)[0]
ledger = 迁移台账(db, author, app.要求知识方法().正式, app.要求故事世界(), app.要求审校())
row = ledger.占用(
snapshots[0],
hashlib.sha256(稳定JSON(mapping.值).encode()).hexdigest(),
route,
)
frozen = 准备目标请求(
app,
author,
snapshots[0],
route,
mapping,
可用源={s.source.记录键: s for s in snapshots},
)
ledger.冻结请求(snapshots[0].source.记录键, frozen)
receipt = 提交目标请求(app, author, row["command_id"], frozen)
assert ledger.读取(snapshots[0].source.记录键)["state"] == "reserved"
result = 导入快照(app, author, snapshots, mapping, **options)
assert result["states"].get("mapped") == 1, result
current = ledger.读取(snapshots[0].source.记录键)
assert current["targets"][0]["id"] == receipt["results"][0]["version_id"]
with db.连接(只读=True) as conn:
assert conn.execute("SELECT count(*) FROM muse_rule_version").fetchone()[0] == 1
assert (
conn.execute(
"SELECT count(*) FROM muse_change_command WHERE command_id=%s", (row["command_id"],)
).fetchone()[0]
== 0
)