- src/muse 新版全模块(装配/共享/上下文/任务运行/作品规划/故事世界/正文写作/审校修订/知识方法/作者经验/效果评测/交付连载/资料研究/正式变更/元数据/接入/基础设施/编排)+ 测试树(单元/契约/集成/架构/迁移/端到端/夹具) - 129 项功能全部实现与自动验证(功能覆盖.json/矩阵),含 W31 补齐的规则与代价/节奏安排/伏笔与承诺 - 旧实现按处置清单退出(702 条中 324 删,保护合同与未迁移条目留存有据);web/app.py 旧工作台退役,新工作台为唯一写入口 - 数据库/旧库迁移:真实旧库内容批次迁移链(端点守卫/PG作品正文映射/质量资产缺省投影) - 运行手册 docs/运行手册.md;W30 本机服务阶段一已运行(infra PG 为正式内容权威) - R2 执行证据与私有运行材料在 .agents.local/改造/R2-20260909/(不入库)
2373 lines
104 KiB
Python
2373 lines
104 KiB
Python
"""隔离目标身份和数据库约束;全部在已授权的临时PG实例与随机新库中验证。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import subprocess
|
||
import sys
|
||
from dataclasses import asdict
|
||
from pathlib import Path
|
||
|
||
import psycopg
|
||
import pytest
|
||
from psycopg import sql
|
||
from pydantic import TypeAdapter
|
||
|
||
from muse.作品规划.接口 import 档案保存
|
||
from muse.元数据.接口 import 元数据服务, 导入内置结构
|
||
from muse.共享.调用身份 import 内容用途, 用途, 调用身份
|
||
from muse.共享.错误 import Muse错误
|
||
from muse.启动 import 构建
|
||
from muse.基础设施.数据库.连接 import 数据库工厂
|
||
from muse.故事世界.接口 import 定位字面引文, 正文依据
|
||
from muse.正文写作.接口 import 文本节点, 正文草稿, 段落
|
||
from muse.资料研究.接口 import 导入请求, 资料服务
|
||
from muse.配置 import 应用配置, 数据库引用
|
||
from 切换检查 import 检查切换条件
|
||
from 导入新库 import 初始化目标, 导入快照, 核对隔离目标
|
||
from 建立映射 import 旧快照, 映射配置, 迁移错误
|
||
from 映射台账 import 迁移命令ID, 迁移台账
|
||
from 核对结果 import 核对迁移结果
|
||
|
||
pytestmark = pytest.mark.数据库
|
||
|
||
|
||
@pytest.fixture
|
||
def 目标方法环境(迁移目标):
|
||
库, 参数, _ = 迁移目标
|
||
维护引用 = 数据库引用("受控存储", str(Path(库.引用.位置).with_name("维护连接.txt")))
|
||
with 数据库工厂(维护引用, 用途.维护).连接() as 连, 连.transaction():
|
||
导入内置结构(元数据服务(连))
|
||
装配 = 构建(应用配置(库.引用, "synthetic"))
|
||
身份 = 调用身份("migration-author", None, 用途.生产, 内容用途.规划)
|
||
with 库.连接() as 连:
|
||
来源 = 资料服务().导入(
|
||
连,
|
||
身份.作者,
|
||
导入请求(
|
||
kind="reference",
|
||
title="合成来源",
|
||
origin="seed://legacy-import",
|
||
content="第一章铺垫,第二章回收。",
|
||
authorized_uses=("analysis",),
|
||
),
|
||
)
|
||
字段 = {
|
||
"装置形态": "结构装置",
|
||
"原理": "在当前场景明确回收前文条件,保持承诺与结果可核对",
|
||
"埋设手法": "放在日常动线",
|
||
"回收点": "选择转折处",
|
||
"记忆维持": "中途轻提一次",
|
||
"间隔纪律": "隔两章",
|
||
"滥用反例": "反复铺垫却无回收",
|
||
"例证出处": "合成来源第一、二章",
|
||
}
|
||
快照 = 旧快照.从载荷(
|
||
{
|
||
"source": {
|
||
"system": "pg",
|
||
"database": "synthetic-legacy",
|
||
"table": "muse_knowledge_draft",
|
||
"id": "1",
|
||
"revision": "1",
|
||
},
|
||
"record": {
|
||
"work_id": 0,
|
||
"draft_type": "entity",
|
||
"source_type": "parse_book",
|
||
"source_id": 8,
|
||
"source_status": "active",
|
||
"source_action_policy": "allowed",
|
||
"status": "pending",
|
||
"draft_payload": {"型": "craft", "名称": "合成回收法", "字段": 字段},
|
||
},
|
||
}
|
||
)
|
||
原值 = {
|
||
"works": [
|
||
{
|
||
"system": "pg",
|
||
"database": "synthetic-legacy",
|
||
"legacy_id": 0,
|
||
"use": "method",
|
||
"target_ref": "library:migration-author",
|
||
"author_id": 身份.作者,
|
||
}
|
||
],
|
||
"sources": [
|
||
{
|
||
"system": "pg",
|
||
"database": "synthetic-legacy",
|
||
"source_type": "parse_book",
|
||
"legacy_id": 8,
|
||
"work_id": 8,
|
||
"target_ref": 来源.source_id,
|
||
"revision": "1",
|
||
}
|
||
],
|
||
"schemas": {
|
||
"craft": {
|
||
"type": "object",
|
||
"properties": {k: {"type": "string"} for k in 字段},
|
||
"additionalProperties": False,
|
||
}
|
||
},
|
||
"target_schemas": {
|
||
"B04:craft": {
|
||
"schema_id": "craft",
|
||
"base_version": 1,
|
||
"fields": {k: "fields." + k for k in 字段},
|
||
}
|
||
},
|
||
}
|
||
return 装配, 身份, 快照, 原值, 参数
|
||
|
||
|
||
@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")), 用途.生产)
|
||
try:
|
||
yield (
|
||
工厂,
|
||
{"允许实例": 允许文件, "管理引用": 管理引用, "目标回执": 输出 / "目标回执.json"},
|
||
回执,
|
||
)
|
||
finally:
|
||
with psycopg.connect(隔离数据库URL, autocommit=True) as 连:
|
||
连.execute(
|
||
sql.SQL("DROP DATABASE {} WITH (FORCE)").format(sql.Identifier(回执["database"]))
|
||
)
|
||
|
||
|
||
def test_随机隔离库回执与实际标记一致且应用不能伪造__21c001(迁移目标):
|
||
库, 参数, 回执 = 迁移目标
|
||
assert 核对隔离目标(库, **参数) == 回执
|
||
assert 回执["database"].startswith("muse_migration_")
|
||
assert 回执["role"] == "muse_app" and 回执["run_purpose"] == "production"
|
||
assert Path(库.引用.位置).stat().st_mode & 0o777 == 0o600
|
||
with 库.连接() as 连:
|
||
with pytest.raises(psycopg.errors.InsufficientPrivilege):
|
||
连.execute("UPDATE public.muse_legacy_target SET target_id=target_id")
|
||
|
||
|
||
def test_错误回执端点实例和用途拒绝而非自报隔离__21c002(迁移目标, tmp_path):
|
||
库, 参数, 回执 = 迁移目标
|
||
for 键, 值 in (("database_oid", 0), ("system_identifier", "0"), ("role", "muse_eval")):
|
||
坏 = tmp_path / (键 + ".json")
|
||
坏.write_text(json.dumps({**回执, 键: 值}), encoding="utf-8")
|
||
with pytest.raises(迁移错误, match="target_denied"):
|
||
核对隔离目标(库, **{**参数, "目标回执": 坏})
|
||
with pytest.raises(迁移错误, match="target_denied"):
|
||
核对隔离目标(数据库工厂(库.引用, 用途.评测), **参数)
|
||
允许 = json.loads(参数["允许实例"].read_text())
|
||
参数["允许实例"].write_text(json.dumps({**允许, "system_identifier": "0"}), encoding="utf-8")
|
||
with pytest.raises(迁移错误, match="target_denied"):
|
||
核对隔离目标(库, **参数)
|
||
|
||
|
||
def test_迁移原值请求和终态不能被原地改写__21c003(迁移目标):
|
||
库, _, _ = 迁移目标
|
||
with 库.连接() as 连:
|
||
连.execute(
|
||
"""INSERT INTO public.muse_legacy_import_record
|
||
(author_id,run_purpose,source_key,source_hash,mapping_hash,original,command_id,route)
|
||
VALUES ('合成作者','production','源键',%s,%s,'{}','命令','{}')""",
|
||
("a" * 64, "b" * 64),
|
||
)
|
||
for 语句 in (
|
||
"UPDATE public.muse_legacy_import_record SET source_key='另一源'",
|
||
"UPDATE public.muse_legacy_import_record SET source_hash=repeat('c',64)",
|
||
"UPDATE public.muse_legacy_import_record SET state='mapped'",
|
||
"DELETE FROM public.muse_legacy_import_record",
|
||
):
|
||
with pytest.raises(psycopg.Error), 库.连接() as 连:
|
||
连.execute(语句)
|
||
with 库.连接() as 连:
|
||
连.execute(
|
||
"UPDATE public.muse_legacy_import_record "
|
||
"SET frozen_request='{}',state='quarantined',reason_code='合成隔离'"
|
||
)
|
||
with pytest.raises(psycopg.Error), 库.连接() as 连:
|
||
连.execute("UPDATE public.muse_legacy_import_record SET state='reserved'")
|
||
with 库.连接(只读=True) as 连:
|
||
assert 连.execute(
|
||
"SELECT state,source_key FROM public.muse_legacy_import_record"
|
||
).fetchone() == ("quarantined", "源键")
|
||
|
||
|
||
def test_真实方法候选导入幂等且源漂移不生成新命令__21c004(目标方法环境):
|
||
装配, 身份, 快照, 原值, 参数 = 目标方法环境
|
||
for _ in range(2):
|
||
结果 = 导入快照(装配, 身份, [快照], 映射配置(原值), **参数)
|
||
assert 结果["states"] == {"mapped": 1} and 结果["production_switch_authorized"] is False
|
||
服务 = 装配.要求知识方法()
|
||
回执 = 服务.正式.读取回执(身份, 迁移命令ID(身份.作者, 快照.source.记录键))
|
||
提案 = 服务.读取方法提案(身份, 回执["target_ref"])
|
||
assert 提案["revision"] == 1 and 提案["content"]["原理"].startswith("在当前场景")
|
||
assert 提案["state"] != "enabled" and 提案["current_version"] == 0
|
||
assert 装配.要求作品().列出作品(身份) == []
|
||
数据 = 快照.导出()
|
||
数据.pop("source_hash")
|
||
数据["record"]["draft_payload"]["字段"]["原理"] = "不同源内容"
|
||
with pytest.raises(迁移错误, match="drift"):
|
||
导入快照(装配, 身份, [旧快照.从载荷(数据)], 映射配置(原值), **参数)
|
||
assert 服务.读取方法提案(身份, 回执["target_ref"])["revision"] == 1
|
||
|
||
|
||
def test_S01已提交而台账未完成时按原命令补齐__21c005(目标方法环境, monkeypatch):
|
||
装配, 身份, 快照, 原值, 参数 = 目标方法环境
|
||
原保存 = 迁移台账.保存终态
|
||
|
||
def 中断(self, key, 状态, **字段):
|
||
if 状态 == "mapped":
|
||
raise RuntimeError("合成中断")
|
||
return 原保存(self, key, 状态, **字段)
|
||
|
||
monkeypatch.setattr(迁移台账, "保存终态", 中断)
|
||
with pytest.raises(RuntimeError, match="合成中断"):
|
||
导入快照(装配, 身份, [快照], 映射配置(原值), **参数)
|
||
服务 = 装配.要求知识方法()
|
||
台账 = 迁移台账(服务.数据库, 身份, 服务.正式)
|
||
assert 台账.读取(快照.source.记录键)["state"] == "reserved"
|
||
原回执 = 服务.正式.读取回执(身份, 迁移命令ID(身份.作者, 快照.source.记录键))
|
||
monkeypatch.setattr(迁移台账, "保存终态", 原保存)
|
||
结果 = 导入快照(装配, 身份, [快照], 映射配置(原值), **参数)
|
||
assert 结果["states"] == {"mapped": 1}
|
||
assert 台账.读取(快照.source.记录键)["receipt"] == 原回执
|
||
assert 服务.读取方法提案(身份, 原回执["target_ref"])["revision"] == 1
|
||
|
||
|
||
def test_无效真实来源拒绝候选并保原值__21c006(目标方法环境):
|
||
装配, 身份, 快照, 原值, 参数 = 目标方法环境
|
||
原值["sources"][0]["revision"] = "99"
|
||
结果 = 导入快照(装配, 身份, [快照], 映射配置(原值), **参数)
|
||
assert 结果["states"] == {"quarantined": 1}
|
||
服务 = 装配.要求知识方法()
|
||
assert 服务.列出方法(身份) == []
|
||
行 = 迁移台账(服务.数据库, 身份).读取(快照.source.记录键)
|
||
assert 行["original"] == 快照.导出() and 行["receipt"] is None
|
||
assert 行["targets"] == []
|
||
|
||
|
||
def _本书输入(装配, 身份):
|
||
作品 = 装配.要求作品()
|
||
结构 = 作品.新档案结构(身份, "migration-work", "work_core", 1)
|
||
作品.保存档案(
|
||
身份, "create-migration-work", 档案保存("migration-work", 0, {"名称": "合成迁移作品"}, 结构)
|
||
)
|
||
作品.添加章节(
|
||
身份,
|
||
"create-migration-chapter",
|
||
"migration-work",
|
||
"migration-chapter",
|
||
"第一章",
|
||
预期目录版本=0,
|
||
)
|
||
正文 = 装配.要求正文()
|
||
正文.保存人工(
|
||
身份,
|
||
"save-migration-body",
|
||
"migration-chapter",
|
||
0,
|
||
正文草稿((段落("p1", (文本节点("甲经过城门。"),)),)),
|
||
)
|
||
文 = 正文.读取正文(身份, "migration-chapter")
|
||
依据 = 正文依据(
|
||
"migration-chapter",
|
||
"main",
|
||
文["revision"],
|
||
文["document_hash"],
|
||
(定位字面引文(TypeAdapter(正文草稿).validate_python(文["document"]), "甲经过城门。"),),
|
||
)
|
||
源 = 旧快照.从载荷(
|
||
{
|
||
"source": {
|
||
"system": "pg",
|
||
"database": "synthetic-legacy",
|
||
"table": "muse_knowledge_draft",
|
||
"id": "11",
|
||
"revision": "1",
|
||
},
|
||
"record": {
|
||
"work_id": 7,
|
||
"source_type": "chapter_extract",
|
||
"source_id": 70,
|
||
"status": "pending",
|
||
"source_status": "active",
|
||
"source_action_policy": "allowed",
|
||
"draft_type": "entity",
|
||
"draft_payload": {"type": "character", "name": "甲", "fields": {"身份": "驾驶员"}},
|
||
},
|
||
}
|
||
)
|
||
映射 = {
|
||
"works": [
|
||
{
|
||
"system": "pg",
|
||
"database": "synthetic-legacy",
|
||
"legacy_id": 7,
|
||
"use": "author",
|
||
"target_ref": "migration-work",
|
||
"author_id": 身份.作者,
|
||
}
|
||
],
|
||
"sources": [
|
||
{
|
||
"system": "pg",
|
||
"database": "synthetic-legacy",
|
||
"source_type": "chapter_extract",
|
||
"legacy_id": 70,
|
||
"work_id": 7,
|
||
"target_ref": "migration-chapter",
|
||
"revision": "1",
|
||
}
|
||
],
|
||
"schemas": {
|
||
"character": {
|
||
"type": "object",
|
||
"properties": {"身份": {"type": "string"}},
|
||
"additionalProperties": False,
|
||
}
|
||
},
|
||
"target_schemas": {
|
||
"B02:character": {
|
||
"schema_id": "character",
|
||
"base_version": 1,
|
||
"fields": {"名称": "name", "身份定位": "fields.身份"},
|
||
}
|
||
},
|
||
"body_bindings": [
|
||
{
|
||
"source_key": 源.source.记录键,
|
||
"source_hash": 源.源哈希,
|
||
"sources": [asdict(依据)],
|
||
"effective_from": "migration-chapter",
|
||
}
|
||
],
|
||
}
|
||
return 源, 映射
|
||
|
||
|
||
def test_本书事实只迁未决候选且保真实正文依据__21c007(目标方法环境):
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
源, 原值 = _本书输入(装配, 身份)
|
||
结果 = 导入快照(装配, 身份, [源], 映射配置(原值), **参数)
|
||
assert 结果["states"] == {"mapped": 1}
|
||
回执 = 装配.要求知识方法().正式.读取回执(身份, 迁移命令ID(身份.作者, 源.source.记录键))
|
||
当前 = 装配.要求故事世界().读取提案(身份, 回执["target_ref"])
|
||
assert 当前["content"] == {"名称": "甲", "身份定位": "驾驶员"}
|
||
assert 当前["independent"] is False and 当前["sources"][0]["revision"] == 1
|
||
assert 当前["sources"][0]["evidence"][0]["quote"] == "甲经过城门。"
|
||
assert 装配.要求故事世界().列出世界实体(身份, "migration-work") == []
|
||
|
||
|
||
def test_迁移命令使用配置作者且报告可重放__21c009(目标方法环境, tmp_path):
|
||
装配, 身份, 源, 原值, 参数 = 目标方法环境
|
||
配置 = tmp_path / "应用.toml"
|
||
配置.write_text(
|
||
'["数据库"]\n"取值方式"="受控存储"\n"位置"='
|
||
+ json.dumps(装配.配置.数据库.位置, ensure_ascii=False)
|
||
+ '\n["资源"]\n"发布身份"="synthetic"\n["运行"]\n"用途"="production"'
|
||
'\n[HTTP]\n"作者ID"="migration-author"\n"口令文件"="口令.txt"\n',
|
||
encoding="utf-8",
|
||
)
|
||
(tmp_path / "口令.txt").write_text("synthetic-only-password", encoding="utf-8")
|
||
输入, 映射文件, 报告 = [tmp_path / n for n in ("输入.json", "映射.json", "报告.json")]
|
||
输入.write_text(json.dumps([源.导出()], ensure_ascii=False), encoding="utf-8")
|
||
映射文件.write_text(json.dumps(原值, ensure_ascii=False), encoding="utf-8")
|
||
入口 = Path(__file__).resolve().parents[2] / "数据库/旧库迁移/入口.py"
|
||
命令 = [
|
||
sys.executable,
|
||
str(入口),
|
||
"导入目标",
|
||
"--快照",
|
||
str(输入),
|
||
"--映射",
|
||
str(映射文件),
|
||
"--配置",
|
||
str(配置),
|
||
"--允许实例",
|
||
str(参数["允许实例"]),
|
||
"--管理引用",
|
||
参数["管理引用"].位置,
|
||
"--目标回执",
|
||
str(参数["目标回执"]),
|
||
"--输出",
|
||
str(报告),
|
||
]
|
||
for _ in range(2):
|
||
完成 = subprocess.run(命令, capture_output=True, text=True, timeout=30)
|
||
assert 完成.returncode == 0, 完成.stderr
|
||
assert "在当前场景" not in 完成.stdout
|
||
assert json.loads(报告.read_text())["states"] == {"mapped": 1}
|
||
assert 报告.stat().st_mode & 0o777 == 0o600
|
||
|
||
|
||
def test_只读对账核实际产物并阻止源增量冒充完成__21c00a(目标方法环境):
|
||
装配, 身份, 源, 原值, 参数 = 目标方法环境
|
||
映射 = 映射配置(原值)
|
||
缺 = 核对迁移结果(装配, 身份, [源], 映射, **参数)
|
||
assert 缺["issues"][0]["reason_code"] == "source_unaccounted"
|
||
导入快照(装配, 身份, [源], 映射, **参数)
|
||
核对 = 核对迁移结果(装配, 身份, [源], 映射, **参数)
|
||
assert 核对["data_checks_passed"] is True and 核对["package_acceptance_claimed"] is False
|
||
数据 = 源.导出()
|
||
数据.pop("source_hash")
|
||
数据["source"]["revision"] = "2"
|
||
当前 = 旧快照.从载荷(数据)
|
||
检查 = 检查切换条件(装配, 身份, [源], [当前], 映射, **参数)
|
||
assert 检查["data_checks_passed"] is False and 检查["source_delta"]["changed"]
|
||
assert 检查["production_switch_authorized"] is False
|
||
|
||
|
||
def test_并发同源只产生同一候选与映射__21c00b(目标方法环境):
|
||
from concurrent.futures import ThreadPoolExecutor
|
||
|
||
装配, 身份, 源, 原值, 参数 = 目标方法环境
|
||
映射 = 映射配置(原值)
|
||
with ThreadPoolExecutor(max_workers=2) as 池:
|
||
一 = 池.submit(导入快照, 装配, 身份, [源], 映射, **参数)
|
||
二 = 池.submit(导入快照, 装配, 身份, [源], 映射, **参数)
|
||
assert 一.result()["states"] == 二.result()["states"] == {"mapped": 1}
|
||
服务 = 装配.要求知识方法()
|
||
assert len(服务.列出方法(身份)) == 1
|
||
记录 = 迁移台账(服务.数据库, 身份, 服务.正式).列出([源.source.记录键])
|
||
assert len(记录) == 1 and 记录[0]["state"] == "mapped"
|
||
|
||
|
||
@pytest.mark.parametrize("缺目标", [False, True])
|
||
def test_实体形关系迁为关系候选且核实目标端点__21c00c(目标方法环境, 缺目标):
|
||
from muse.故事世界.接口 import 事实定位
|
||
from muse.正式变更.接口 import 作者动作
|
||
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
初, 原值 = _本书输入(装配, 身份)
|
||
世界 = 装配.要求故事世界()
|
||
# 合成环境中由作者事先明确确认两个目标实体;迁移器自身不调用确认。
|
||
for id_, 名 in (("existing-a", "甲"), ("existing-b", "乙")):
|
||
回执 = 世界.保存实体提案(
|
||
身份,
|
||
"seed-" + id_,
|
||
work_id="migration-work",
|
||
object_id=id_,
|
||
expected_revision=0,
|
||
schema_id="character",
|
||
schema_version=1,
|
||
content={"名称": 名},
|
||
independent=True,
|
||
)
|
||
提案 = 世界.读取提案(身份, 回执["target_ref"])
|
||
定位 = 事实定位(
|
||
回执["target_ref"], 提案["revision"], 提案["proposal_hash"], "world:" + id_, 0
|
||
)
|
||
审阅 = 世界.打开事实审阅(身份, 定位)
|
||
世界.决定事实(
|
||
身份,
|
||
"confirm-" + id_,
|
||
定位,
|
||
作者动作.采纳,
|
||
审阅["review_id"],
|
||
审阅["review_hash"],
|
||
("object",),
|
||
)
|
||
数据 = 初.导出()
|
||
数据.pop("source_hash")
|
||
数据["record"]["draft_payload"] = {
|
||
"type": "character_relation",
|
||
"name": "甲乙同船",
|
||
"fields": {"甲方": "甲", "乙方": "乙", "关系类型": "同船"},
|
||
}
|
||
源 = 旧快照.从载荷(数据)
|
||
原值["schemas"] = {
|
||
"character_relation": {
|
||
"type": "object",
|
||
"properties": {k: {"type": "string"} for k in ("甲方", "乙方", "关系类型")},
|
||
"additionalProperties": False,
|
||
}
|
||
}
|
||
原值["target_schemas"] = {
|
||
"B02:character_relation": {
|
||
"schema_id": "character_relation",
|
||
"base_version": 1,
|
||
"fields": {"甲方": "source", "乙方": "target", "关系类型": "relation_type"},
|
||
}
|
||
}
|
||
原值["objects"] = [
|
||
{
|
||
"system": "pg",
|
||
"database": "synthetic-legacy",
|
||
"work_id": 7,
|
||
"use": "author",
|
||
"scope": "migration-work",
|
||
"refs": ["name:" + 名],
|
||
"target_ref": id_,
|
||
}
|
||
for 名, id_ in (("甲", "existing-a"), ("乙", "nonexistent" if 缺目标 else "existing-b"))
|
||
]
|
||
原值["body_bindings"][0]["source_hash"] = 源.源哈希
|
||
结果 = 导入快照(装配, 身份, [源], 映射配置(原值), **参数)
|
||
assert 结果["states"] == ({"quarantined": 1} if 缺目标 else {"mapped": 1})
|
||
assert len(世界.列出世界实体(身份, "migration-work")) == 2
|
||
if not 缺目标:
|
||
回执 = 世界.正式.读取回执(身份, 迁移命令ID(身份.作者, 源.source.记录键))
|
||
assert 世界.读取提案(身份, 回执["target_ref"])["content"] == {
|
||
"甲方": "existing-a",
|
||
"乙方": "existing-b",
|
||
"关系类型": "同船",
|
||
}
|
||
|
||
|
||
def test_目标结构不能把来源类型暗改为另一类型__21c00d(目标方法环境):
|
||
装配, 身份, 源, 原值, 参数 = 目标方法环境
|
||
原值["target_schemas"]["B04:craft"]["schema_id"] = "character"
|
||
结果 = 导入快照(装配, 身份, [源], 映射配置(原值), **参数)
|
||
assert 结果["states"] == {"quarantined": 1}
|
||
assert 结果["errors"][0]["reason_code"] == "target_type_mismatch"
|
||
assert 装配.要求知识方法().列出方法(身份) == []
|
||
|
||
|
||
def test_缺正文依据不降级独立设定__21c008(目标方法环境):
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
源, 原值 = _本书输入(装配, 身份)
|
||
原值["body_bindings"] = []
|
||
结果 = 导入快照(装配, 身份, [源], 映射配置(原值), **参数)
|
||
assert 结果["states"] == {"quarantined": 1}
|
||
assert 结果["errors"][0]["reason_code"] == "target_source_missing"
|
||
assert 装配.要求故事世界().列出世界实体(身份, "migration-work") == []
|
||
|
||
|
||
def _核对保全且未发业务命令(环境, 源, 原值, 状态, 原因):
|
||
from muse.正式变更.接口 import 变更错误
|
||
|
||
装配, 身份, _, _, 参数 = 环境
|
||
映射 = 映射配置(原值)
|
||
一 = 导入快照(装配, 身份, [源], 映射, **参数)
|
||
assert 一 == 导入快照(装配, 身份, [源], 映射, **参数)
|
||
assert 一["states"] == {状态: 1} and 一["all_accounted"] is True
|
||
正式 = 装配.要求知识方法().正式
|
||
行 = 迁移台账(正式.数据库, 身份, 正式).读取(源.source.记录键)
|
||
assert 行 is not None and 行["original"] == 源.导出()
|
||
assert 行["source_hash"] == 源.源哈希 and 行["reason_code"] == 原因
|
||
assert 行["targets"] == [] and 行["receipt"] is None and 行["frozen_request"] is None
|
||
with pytest.raises(变更错误) as 拒绝:
|
||
正式.读取回执(身份, 迁移命令ID(身份.作者, 源.source.记录键))
|
||
assert 拒绝.value.错误码 == "RECEIPT_NOT_FOUND"
|
||
assert 装配.要求知识方法().列出方法(身份) == []
|
||
核对 = 核对迁移结果(装配, 身份, [源], 映射, **参数)
|
||
assert 核对["data_checks_passed"] is (状态 == "historical")
|
||
assert 核对["package_acceptance_claimed"] is False
|
||
return 行
|
||
|
||
|
||
@pytest.mark.parametrize("多义", [False, True])
|
||
def test_显式关系缺失或多义端点真实隔离__21d001(目标方法环境, 多义):
|
||
装配, 身份, _, _, _ = 目标方法环境
|
||
初, 原值 = _本书输入(装配, 身份)
|
||
数据 = 初.导出()
|
||
数据.pop("source_hash")
|
||
数据["record"].update(
|
||
draft_type="relation",
|
||
draft_payload={"甲方": "甲", "乙方": "乙", "type": "同船", "fields": {}},
|
||
)
|
||
源 = 旧快照.从载荷(数据)
|
||
基础 = {
|
||
"system": "pg",
|
||
"database": "synthetic-legacy",
|
||
"work_id": 7,
|
||
"use": "author",
|
||
"scope": "migration-work",
|
||
}
|
||
原值["objects"] = [{**基础, "refs": ["name:甲"], "target_ref": "existing-a"}]
|
||
if 多义:
|
||
原值["objects"] += [
|
||
{**基础, "refs": ["name:乙"], "target_ref": id_} for id_ in ("existing-b", "existing-c")
|
||
]
|
||
原值["schemas"]["character_relation"] = {
|
||
"type": "object",
|
||
"properties": {},
|
||
"additionalProperties": False,
|
||
}
|
||
_核对保全且未发业务命令(
|
||
目标方法环境, 源, 原值, "quarantined", "ambiguous_endpoint" if 多义 else "missing_endpoint"
|
||
)
|
||
assert 装配.要求故事世界().列出世界实体(身份, "migration-work") == []
|
||
|
||
|
||
def test_冲突类型双值留台账且不写候选__21d002(目标方法环境):
|
||
_, _, 初, 原值, _ = 目标方法环境
|
||
数据 = 初.导出()
|
||
数据.pop("source_hash")
|
||
数据["record"]["draft_payload"]["type"] = "character"
|
||
源 = 旧快照.从载荷(数据)
|
||
行 = _核对保全且未发业务命令(目标方法环境, 源, 原值, "quarantined", "type_conflict")
|
||
载荷 = 行["original"]["record"]["draft_payload"]
|
||
assert 载荷["型"] == "craft" and 载荷["type"] == "character"
|
||
|
||
|
||
def test_零作品普通地点不能成为公共方法__21d003(目标方法环境):
|
||
装配, 身份, 初, 原值, _ = 目标方法环境
|
||
数据 = 初.导出()
|
||
数据.pop("source_hash")
|
||
数据["record"]["draft_payload"] = {"type": "location", "name": "旧城", "fields": {}}
|
||
原值["schemas"]["location"] = {
|
||
"type": "object",
|
||
"properties": {},
|
||
"additionalProperties": False,
|
||
}
|
||
源 = 旧快照.从载荷(数据)
|
||
_核对保全且未发业务命令(目标方法环境, 源, 原值, "quarantined", "zero_work_not_pattern")
|
||
assert 装配.要求作品().列出作品(身份) == []
|
||
|
||
|
||
def test_跨书同章号分别进入真实方法证据__21d004(目标方法环境):
|
||
装配, 身份, 初, 原值, 参数 = 目标方法环境
|
||
with 装配.要求知识方法().数据库.连接() as 连:
|
||
乙 = 资料服务().导入(
|
||
连,
|
||
身份.作者,
|
||
导入请求(
|
||
kind="reference",
|
||
title="合成乙书",
|
||
origin="seed://legacy-second-book",
|
||
content="第十二章,合成乙举例。",
|
||
authorized_uses=("analysis",),
|
||
),
|
||
)
|
||
甲引用 = 原值["sources"][0]
|
||
甲引用.update(aliases=["甲书"], chapters={"12": "甲书:第12章"})
|
||
原值["sources"].append(
|
||
{
|
||
**甲引用,
|
||
"legacy_id": 10,
|
||
"work_id": 10,
|
||
"target_ref": 乙.source_id,
|
||
"aliases": ["乙书"],
|
||
"chapters": {"12": "乙书:第12章"},
|
||
}
|
||
)
|
||
数据 = 初.导出()
|
||
数据.pop("source_hash")
|
||
数据["record"]["draft_payload"]["实例"] = [
|
||
{"书": "甲书", "章": 12, "引文": "合成甲"},
|
||
{"书": "乙书", "章": 12, "引文": "合成乙"},
|
||
]
|
||
源 = 旧快照.从载荷(数据)
|
||
映射 = 映射配置(原值)
|
||
一 = 导入快照(装配, 身份, [源], 映射, **参数)
|
||
assert 一 == 导入快照(装配, 身份, [源], 映射, **参数)
|
||
assert 一["states"] == {"mapped": 1}
|
||
服务 = 装配.要求知识方法()
|
||
回执 = 服务.正式.读取回执(身份, 迁移命令ID(身份.作者, 源.source.记录键))
|
||
提案 = 服务.读取方法提案(身份, 回执["target_ref"])
|
||
assert [(e.source_id, e.revision, e.定位) for e in 提案["证据"]][-2:] == [
|
||
(甲引用["target_ref"], 1, "甲书:第12章"),
|
||
(乙.source_id, 1, "乙书:第12章"),
|
||
]
|
||
assert 提案["current_version"] == 0 and 提案["state"] != "enabled"
|
||
行 = 迁移台账(服务.数据库, 身份, 服务.正式).读取(源.source.记录键)
|
||
assert 行 is not None and 行["original"] == 源.导出()
|
||
assert len(服务.列出方法(身份)) == 1
|
||
assert 核对迁移结果(装配, 身份, [源], 映射, **参数)["data_checks_passed"] is True
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"表,原行,原因",
|
||
[
|
||
(
|
||
"muse_knowledge_entity",
|
||
{"work_id": 7, "entity_type": "character", "name": "旧角色", "deleted": True},
|
||
"inactive_history",
|
||
),
|
||
(
|
||
"example_upgrade_alias",
|
||
{"work_id": 7, "canonical_name": "甲", "alias": "旧别名", "deleted": True},
|
||
"inactive_history",
|
||
),
|
||
(
|
||
"example_knowledge_embedding",
|
||
{"work_id": 7, "entity_id": 11, "embedding": [0.2, 0.3], "deleted": True},
|
||
"inactive_history",
|
||
),
|
||
(
|
||
"muse_knowledge_draft",
|
||
{"work_id": 0, "status": "rejected", "draft_payload": {"型": "craft"}},
|
||
"inactive_history",
|
||
),
|
||
(
|
||
"muse_knowledge_draft",
|
||
{"work_id": 0, "source_status": "withdrawn", "draft_payload": {"型": "craft"}},
|
||
"source_revoked",
|
||
),
|
||
],
|
||
)
|
||
def test_删除别名向量拒绝与撤权仅保历史__21d005(目标方法环境, 表, 原行, 原因):
|
||
_, _, 初, 原值, _ = 目标方法环境
|
||
数据 = 初.导出()
|
||
数据.pop("source_hash")
|
||
数据["source"]["table"], 数据["record"] = 表, 原行
|
||
源 = 旧快照.从载荷(数据)
|
||
行 = _核对保全且未发业务命令(目标方法环境, 源, 原值, "historical", 原因)
|
||
assert 行["route"]["content"] == {} and not 行["route"]["target_ref"]
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"方式,原因",
|
||
[("未知", "unknown_fields"), ("类型错", "field_type_mismatch"), ("缺必填", "missing_required")],
|
||
)
|
||
def test_不可解释动态值原样留存且不启用__21d006(目标方法环境, 方式, 原因):
|
||
_, _, 初, 原值, _ = 目标方法环境
|
||
数据 = 初.导出()
|
||
数据.pop("source_hash")
|
||
字段 = 数据["record"]["draft_payload"]["字段"]
|
||
原值["schemas"]["craft"]["required"] = ["原理"]
|
||
if 方式 == "未知":
|
||
字段["陌生字段"] = {"原值": [1, 2]}
|
||
elif 方式 == "类型错":
|
||
字段["原理"] = 3
|
||
else:
|
||
del 字段["原理"]
|
||
源 = 旧快照.从载荷(数据)
|
||
行 = _核对保全且未发业务命令(目标方法环境, 源, 原值, "quarantined", 原因)
|
||
assert 行["original"]["record"]["draft_payload"]["字段"] == 字段
|
||
|
||
|
||
def test_PG与SQLite同号卡不合并或复制活跃对象__21d007(目标方法环境):
|
||
装配, 身份, PG源, 原值, 参数 = 目标方法环境
|
||
SQLite源 = 旧快照.从载荷(
|
||
{
|
||
"source": {**PG源.导出()["source"], "system": "sqlite", "table": "cards"},
|
||
"record": {
|
||
"id": PG源.source.id,
|
||
"kind": "craft",
|
||
"work_id": 0,
|
||
"payload_json": json.dumps(PG源.原行["draft_payload"], ensure_ascii=False),
|
||
},
|
||
}
|
||
)
|
||
原值["works"] += [{**r, "system": "sqlite"} for r in 原值["works"]]
|
||
映射 = 映射配置(原值)
|
||
快照 = [PG源, SQLite源]
|
||
结果 = 导入快照(装配, 身份, 快照, 映射, **参数)
|
||
assert 结果 == 导入快照(装配, 身份, 快照, 映射, **参数)
|
||
assert 结果["states"] == {"mapped": 1, "quarantined": 1}
|
||
assert 结果["distinct_source_count"] == 2
|
||
服务 = 装配.要求知识方法()
|
||
行 = 迁移台账(服务.数据库, 身份, 服务.正式).列出([s.source.记录键 for s in 快照])
|
||
assert len(行) == 2 and len({r["command_id"] for r in 行}) == 2
|
||
assert {r["source_key"]: r["original"] for r in 行} == {s.source.记录键: s.导出() for s in 快照}
|
||
assert len(服务.列出方法(身份)) == 1
|
||
assert next(r for r in 行 if r["source_key"] == SQLite源.source.记录键)["targets"] == []
|
||
|
||
|
||
def test_旧账本WAL与五表按历史迁移__f6f4e4(目标方法环境, tmp_path):
|
||
import hashlib
|
||
import sqlite3
|
||
|
||
from 读取旧SQLite import 读取旧库
|
||
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
副本 = tmp_path / "旧账本副本.sqlite"
|
||
# 最小合成旧表保留原主键、关联键与待验证原值;不执行保全DDL建设新版库。
|
||
with sqlite3.connect(副本) as 连:
|
||
assert 连.execute("PRAGMA journal_mode=WAL").fetchone()[0].lower() == "wal"
|
||
连.executescript("""
|
||
CREATE TABLE runs(id TEXT PRIMARY KEY, input_json TEXT, output_text TEXT);
|
||
CREATE TABLE events(run_id TEXT, seq INTEGER, payload_json TEXT,
|
||
PRIMARY KEY(run_id,seq));
|
||
CREATE TABLE reviews(id INTEGER PRIMARY KEY, run_id TEXT, action TEXT, reviewer TEXT);
|
||
CREATE TABLE revisions(id INTEGER PRIMARY KEY, review_id INTEGER,
|
||
before_text TEXT, after_text TEXT);
|
||
CREATE TABLE cards(id TEXT PRIMARY KEY, kind TEXT, work_id INTEGER, payload_json TEXT);
|
||
INSERT INTO runs VALUES('old-run','{}','旧合成产物');
|
||
INSERT INTO events VALUES('old-run',1,'{"quote":"合成旧事件"}');
|
||
INSERT INTO reviews VALUES(1,'old-run','adopt','旧作者');
|
||
INSERT INTO revisions VALUES(1,1,'旧句','改句');
|
||
INSERT INTO cards VALUES('1','character',7,'{"type":"character","name":"合成旧卡"}');
|
||
""")
|
||
连.close()
|
||
哈希 = hashlib.sha256(副本.read_bytes()).hexdigest()
|
||
必表 = ("runs", "events", "reviews", "revisions", "cards")
|
||
快照 = 读取旧库(副本, 数据集="synthetic-wal-ledger", 表=必表, 快照版本="export-1")
|
||
assert {s.source.table for s in 快照} == set(必表)
|
||
assert hashlib.sha256(副本.read_bytes()).hexdigest() == 哈希
|
||
映射 = 映射配置({"works": []})
|
||
结果 = 导入快照(装配, 身份, 快照, 映射, **参数)
|
||
assert 结果 == 导入快照(装配, 身份, 快照, 映射, **参数)
|
||
assert 结果["states"] == {"historical": 4, "quarantined": 1}
|
||
服务 = 装配.要求知识方法()
|
||
行 = 迁移台账(服务.数据库, 身份, 服务.正式).列出([s.source.记录键 for s in 快照])
|
||
assert {r["source_key"]: r["original"] for r in 行} == {s.source.记录键: s.导出() for s in 快照}
|
||
assert all(r["receipt"] is None and r["targets"] == [] for r in 行)
|
||
assert 装配.任务运行 is not None and 装配.任务运行.列出任务(身份.作者) == []
|
||
assert 服务.列出方法(身份) == []
|
||
with sqlite3.connect(副本.as_uri() + "?mode=ro", uri=True) as 连:
|
||
assert 连.execute("PRAGMA journal_mode").fetchone()[0].lower() == "wal"
|
||
名称 = {r[0] for r in 连.execute("SELECT name FROM sqlite_master WHERE type='table'")}
|
||
assert set(必表) <= 名称
|
||
连.close()
|
||
|
||
|
||
def _正文文件输入(装配, 身份, tmp_path, *, archive=False):
|
||
import hashlib
|
||
|
||
from 读取旧文件 import 读取文件集
|
||
|
||
if not archive:
|
||
_本书输入(装配, 身份)
|
||
源根 = tmp_path / "文件源"
|
||
源根.mkdir()
|
||
内容 = b"\xff\x00archive" if archive else "\ufeff迁移候选\r\n😀e\u0301\n".encode()
|
||
(源根 / "章.txt").write_bytes(内容)
|
||
保全 = tmp_path / "文件保全"
|
||
源 = 读取文件集(
|
||
源根,
|
||
[{"relpath": "章.txt", "revision": "1", "sha256": hashlib.sha256(内容).hexdigest()}],
|
||
数据集="synthetic-target-file",
|
||
输出目录=保全,
|
||
)[0]
|
||
路由 = {
|
||
"source_key": 源.source.记录键,
|
||
"source_hash": 源.源哈希,
|
||
"route": "archive" if archive else "body_candidate",
|
||
}
|
||
值 = {"works": [], "file_routes": [路由]}
|
||
if not archive:
|
||
当前 = 装配.要求正文().读取正文(身份, "migration-chapter")
|
||
值["works"] = [
|
||
{
|
||
"system": "file",
|
||
"database": 源.source.database,
|
||
"legacy_id": 7,
|
||
"use": "author",
|
||
"target_ref": "migration-work",
|
||
"author_id": 身份.作者,
|
||
}
|
||
]
|
||
路由.update(
|
||
format="text_utf8",
|
||
legacy_work_id=7,
|
||
target={
|
||
"chapter_id": "migration-chapter",
|
||
"branch_id": "main",
|
||
"base_revision": 当前["revision"],
|
||
},
|
||
)
|
||
return 源, 值, 保全
|
||
|
||
|
||
def test_正文文件只产作者候选且重放不覆盖后续编辑__21f001(目标方法环境, tmp_path):
|
||
from 读取旧文件 import 解码文件
|
||
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
源, 值, 保全 = _正文文件输入(装配, 身份, tmp_path)
|
||
参数 = {**参数, "文件回执": 保全 / "文件回执.json"}
|
||
映射 = 映射配置(值)
|
||
正文 = 装配.要求正文()
|
||
前 = 正文.读取正文(身份, "migration-chapter")
|
||
结果 = 导入快照(装配, 身份, [源], 映射, **参数)
|
||
assert 结果["states"] == {"mapped": 1} and 结果["file_checks"]["passed"] is True
|
||
assert 正文.读取正文(身份, "migration-chapter") == 前
|
||
回执 = 正文.正式.读取回执(身份, 迁移命令ID(身份.作者, 源.source.记录键))
|
||
候选 = 正文.读取候选(身份, 回执["target_ref"], 版本=1)
|
||
assert 候选["origin"] == "author" and 候选["visible_text"] == 解码文件(源).decode()
|
||
assert 候选["revision"] == 1 and 候选["decision"] == "undecided"
|
||
改稿 = 正文草稿((段落("author-paragraph", (文本节点("作者后续修改"),)),))
|
||
正文.编辑候选(身份, "file-author-edit", 候选["candidate_id"], 1, 候选["candidate_hash"], 改稿)
|
||
assert 导入快照(装配, 身份, [源], 映射, **参数) == 结果
|
||
assert 正文.读取候选(身份, 候选["candidate_id"])["visible_text"] == "作者后续修改"
|
||
assert 正文.读取候选(身份, 候选["candidate_id"])["revision"] == 2
|
||
行 = 迁移台账(正文.数据库, 身份, 正文.正式).读取(源.source.记录键)
|
||
assert (
|
||
行 is not None
|
||
and 行["targets"][0]["kind"] == "candidate"
|
||
and 行["targets"][0]["revision"] == 1
|
||
)
|
||
assert 核对迁移结果(装配, 身份, [源], 映射, **参数)["data_checks_passed"] is True
|
||
(保全 / "原始文件/章.txt").unlink()
|
||
核对 = 核对迁移结果(装配, 身份, [源], 映射, **参数)
|
||
assert 核对["pg_checks"]["passed"] is True and 核对["file_checks"]["passed"] is False
|
||
assert 核对["data_checks_passed"] is False
|
||
assert 检查切换条件(装配, 身份, [源], [源], 映射, **参数)["data_checks_passed"] is False
|
||
assert 正文.读取候选(身份, 候选["candidate_id"])["revision"] == 2
|
||
|
||
|
||
def test_文件副本未核实即零目标写入__21f002(目标方法环境, tmp_path):
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
源, 值, _ = _正文文件输入(装配, 身份, tmp_path)
|
||
with pytest.raises(迁移错误, match="file_copy_unverified"):
|
||
导入快照(装配, 身份, [源], 映射配置(值), **参数)
|
||
正文 = 装配.要求正文()
|
||
assert 正文.列出候选(身份, "migration-chapter") == []
|
||
assert 迁移台账(正文.数据库, 身份, 正文.正式).读取(源.source.记录键) is None
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"方式,原因",
|
||
[
|
||
("陈旧基线", "target_base_stale"),
|
||
("同作者错书", "target_scope_mismatch"),
|
||
("跨作者", "SCOPE_DENIED"),
|
||
("坏格式", "file_document_invalid"),
|
||
],
|
||
)
|
||
def test_正文文件不绕基线作用域或格式__21f003(目标方法环境, tmp_path, 方式, 原因):
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
源, 值, 保全 = _正文文件输入(装配, 身份, tmp_path)
|
||
路由 = 值["file_routes"][0]
|
||
if 方式 == "陈旧基线":
|
||
路由["target"]["base_revision"] = 0
|
||
elif 方式 == "坏格式":
|
||
路由["format"] = "document_v1"
|
||
else:
|
||
另一身份 = (
|
||
身份
|
||
if 方式 == "同作者错书"
|
||
else 调用身份("another-author", None, 用途.生产, 内容用途.规划)
|
||
)
|
||
作品 = 装配.要求作品()
|
||
结构 = 作品.新档案结构(另一身份, "another-work", "work_core", 1)
|
||
作品.保存档案(
|
||
另一身份, "file-another-work", 档案保存("another-work", 0, {"名称": "另一作品"}, 结构)
|
||
)
|
||
作品.添加章节(
|
||
另一身份,
|
||
"file-another-chapter",
|
||
"another-work",
|
||
"another-chapter",
|
||
"另一章",
|
||
预期目录版本=0,
|
||
)
|
||
路由["target"].update(chapter_id="another-chapter", base_revision=0)
|
||
结果 = 导入快照(装配, 身份, [源], 映射配置(值), **参数, 文件回执=保全 / "文件回执.json")
|
||
assert 结果["states"] == {"quarantined": 1}
|
||
assert 结果["errors"][0]["reason_code"] == 原因
|
||
assert 装配.要求正文().列出候选(身份, "migration-chapter") == []
|
||
|
||
|
||
def test_正文候选提交后中断取原回执且拒错种类__21f004(目标方法环境, tmp_path, monkeypatch):
|
||
from 转换业务对象 import 核对目标产物
|
||
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
源, 值, 保全 = _正文文件输入(装配, 身份, tmp_path)
|
||
参数 = {**参数, "文件回执": 保全 / "文件回执.json"}
|
||
映射 = 映射配置(值)
|
||
原保存 = 迁移台账.保存终态
|
||
|
||
def 中断(self, *args, **kwargs):
|
||
raise RuntimeError("模拟领域已提交后台账中断")
|
||
|
||
with monkeypatch.context() as m:
|
||
m.setattr(迁移台账, "保存终态", 中断)
|
||
with pytest.raises(RuntimeError, match="模拟领域"):
|
||
导入快照(装配, 身份, [源], 映射, **参数)
|
||
正文 = 装配.要求正文()
|
||
行 = 迁移台账(正文.数据库, 身份, 正文.正式).读取(源.source.记录键)
|
||
assert 行 is not None and 行["state"] == "reserved"
|
||
assert len(正文.列出候选(身份, "migration-chapter")) == 1
|
||
assert 导入快照(装配, 身份, [源], 映射, **参数)["states"] == {"mapped": 1}
|
||
assert len(正文.列出候选(身份, "migration-chapter")) == 1
|
||
回执 = 正文.正式.读取回执(身份, 行["command_id"])
|
||
错 = json.loads(json.dumps(回执))
|
||
错["results"][0]["proposal_id"] = 错["results"][0].pop("candidate_id")
|
||
with pytest.raises(迁移错误, match="target_receipt_invalid"):
|
||
核对目标产物(装配, 身份, 行["frozen_request"], 错)
|
||
错 = json.loads(json.dumps(回执))
|
||
错["results"][0]["candidate_hash"] = "0" * 64
|
||
with pytest.raises(迁移错误, match="target_mismatch"):
|
||
核对目标产物(装配, 身份, 行["frozen_request"], 错)
|
||
目标 = 核对目标产物(装配, 身份, 行["frozen_request"], 回执)
|
||
目标[0]["kind"] = "proposal"
|
||
with pytest.raises(迁移错误, match="target_evidence_mismatch"):
|
||
迁移台账(正文.数据库, 身份, 正文.正式).保存终态(
|
||
源.source.记录键, "mapped", 目标=目标, 回执=回执
|
||
)
|
||
assert 迁移台账.保存终态 is 原保存
|
||
|
||
|
||
def test_文件归档保全原字节而不造正文候选__21f005(目标方法环境, tmp_path):
|
||
from 读取旧文件 import 解码文件
|
||
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
源, 值, 保全 = _正文文件输入(装配, 身份, tmp_path, archive=True)
|
||
参数 = {**参数, "文件回执": 保全 / "文件回执.json"}
|
||
结果 = 导入快照(装配, 身份, [源], 映射配置(值), **参数)
|
||
assert 结果["states"] == {"historical": 1}
|
||
正式 = 装配.要求知识方法().正式
|
||
行 = 迁移台账(正式.数据库, 身份, 正式).读取(源.source.记录键)
|
||
assert 行 is not None and 行["original"] == 源.导出() and 行["targets"] == []
|
||
assert 解码文件(旧快照.从载荷(行["original"])) == b"\xff\x00archive"
|
||
assert 核对迁移结果(装配, 身份, [源], 映射配置(值), **参数)["data_checks_passed"] is True
|
||
|
||
|
||
def test_正文文件并发只建一候选且源或映射漂移拒绝__21f006(目标方法环境, tmp_path):
|
||
import hashlib
|
||
from concurrent.futures import ThreadPoolExecutor
|
||
|
||
from 读取旧文件 import 读取文件集
|
||
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
源, 值, 保全 = _正文文件输入(装配, 身份, tmp_path)
|
||
参数 = {**参数, "文件回执": 保全 / "文件回执.json"}
|
||
映射 = 映射配置(值)
|
||
with ThreadPoolExecutor(max_workers=2) as 池:
|
||
任务 = [池.submit(导入快照, 装配, 身份, [源], 映射, **参数) for _ in range(2)]
|
||
assert all(t.result()["states"] == {"mapped": 1} for t in 任务)
|
||
正文 = 装配.要求正文()
|
||
assert len(正文.列出候选(身份, "migration-chapter")) == 1
|
||
错映射 = json.loads(json.dumps(值))
|
||
错映射["file_routes"][0]["target"]["branch_id"] = "other"
|
||
with pytest.raises(迁移错误, match="drift"):
|
||
导入快照(装配, 身份, [源], 映射配置(错映射), **参数)
|
||
(tmp_path / "文件源/章.txt").write_bytes(b"changed")
|
||
新 = 读取文件集(
|
||
tmp_path / "文件源",
|
||
[{"relpath": "章.txt", "revision": "1", "sha256": hashlib.sha256(b"changed").hexdigest()}],
|
||
数据集="synthetic-target-file",
|
||
输出目录=tmp_path / "新保全",
|
||
)[0]
|
||
值["file_routes"][0]["source_hash"] = 新.源哈希
|
||
参数["文件回执"] = tmp_path / "新保全/文件回执.json"
|
||
with pytest.raises(迁移错误, match="drift"):
|
||
导入快照(装配, 身份, [新], 映射配置(值), **参数)
|
||
assert len(正文.列出候选(身份, "migration-chapter")) == 1
|
||
行 = 迁移台账(正文.数据库, 身份, 正文.正式).读取(源.source.记录键)
|
||
assert 行 is not None and 行["original"] == 源.导出()
|
||
|
||
|
||
def test_文件CLI向空章导入真实候选且要求文件面__21f007(目标方法环境, tmp_path, capsys):
|
||
from 入口 import main
|
||
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
_, 值, 保全 = _正文文件输入(装配, 身份, tmp_path)
|
||
装配.要求作品().添加章节(
|
||
身份, "file-empty-chapter", "migration-work", "empty-chapter", "空章", 预期目录版本=1
|
||
)
|
||
值["file_routes"][0]["target"].update(chapter_id="empty-chapter", base_revision=0)
|
||
配置 = tmp_path / "应用.toml"
|
||
配置.write_text(
|
||
'["数据库"]\n"取值方式"="受控存储"\n"位置"='
|
||
+ json.dumps(装配.配置.数据库.位置, ensure_ascii=False)
|
||
+ '\n["资源"]\n"发布身份"="synthetic"\n["运行"]\n"用途"="production"'
|
||
'\n[HTTP]\n"作者ID"="migration-author"\n"口令文件"="口令.txt"\n',
|
||
encoding="utf-8",
|
||
)
|
||
(tmp_path / "口令.txt").write_text("synthetic-only", encoding="utf-8")
|
||
映射文件 = tmp_path / "映射.json"
|
||
映射文件.write_text(json.dumps(值, ensure_ascii=False), encoding="utf-8")
|
||
命令 = [
|
||
"导入目标",
|
||
"--快照",
|
||
str(保全 / "快照.json"),
|
||
"--映射",
|
||
str(映射文件),
|
||
"--配置",
|
||
str(配置),
|
||
"--允许实例",
|
||
str(参数["允许实例"]),
|
||
"--管理引用",
|
||
参数["管理引用"].位置,
|
||
"--目标回执",
|
||
str(参数["目标回执"]),
|
||
"--输出",
|
||
str(tmp_path / "导入报告.json"),
|
||
]
|
||
assert main(命令) == 1
|
||
正文 = 装配.要求正文()
|
||
assert 正文.列出候选(身份, "empty-chapter") == []
|
||
命令 += ["--文件回执", str(保全 / "文件回执.json")]
|
||
assert main(命令) == 0
|
||
候选 = 正文.列出候选(身份, "empty-chapter")
|
||
assert len(候选) == 1 and 候选[0]["base_revision"] == 0 and 候选[0]["origin"] == "author"
|
||
assert 正文.读取正文(身份, "empty-chapter")["revision"] == 0
|
||
assert "迁移候选" not in capsys.readouterr().out
|
||
assert json.loads((tmp_path / "导入报告.json").read_text())["states"] == {"mapped": 1}
|
||
|
||
|
||
def _旧载荷输入(装配, 身份, 载荷=None, *, 关系=False):
|
||
初, 映射 = _本书输入(装配, 身份)
|
||
正文 = 装配.要求正文()
|
||
正文.保存人工(
|
||
身份,
|
||
"save-old-shape-body",
|
||
"migration-chapter",
|
||
1,
|
||
正文草稿((段落("p1", (文本节点("林深坐在二号观测位。"),)),)),
|
||
)
|
||
文 = 正文.读取正文(身份, "migration-chapter")
|
||
数据 = 初.导出()
|
||
数据.pop("source_hash")
|
||
数据["record"].update(
|
||
work_id=12,
|
||
source_id=17473,
|
||
draft_type="relation" if 关系 else "entity",
|
||
draft_payload=载荷
|
||
if 载荷 is not None
|
||
else {
|
||
"type": "character",
|
||
"name": "林深",
|
||
"brief": "机师",
|
||
"fields": {"身份": "驾驶员"},
|
||
"source": {"workId": 12, "chapter": 1, "chapterId": 17473},
|
||
"evidence": "林深坐在二号观测位。",
|
||
},
|
||
)
|
||
源 = 旧快照.从载荷(数据)
|
||
映射["works"][0]["legacy_id"] = 12
|
||
映射["sources"][0].update(
|
||
legacy_id=17473,
|
||
work_id=12,
|
||
revision=str(文["revision"]),
|
||
chapters={"1": "chapter:migration-chapter"},
|
||
)
|
||
依据 = 正文依据(
|
||
"migration-chapter",
|
||
"main",
|
||
文["revision"],
|
||
文["document_hash"],
|
||
(
|
||
定位字面引文(
|
||
TypeAdapter(正文草稿).validate_python(文["document"]), "林深坐在二号观测位。"
|
||
),
|
||
),
|
||
)
|
||
映射["body_bindings"][0].update(source_hash=源.源哈希, sources=[asdict(依据)])
|
||
if 关系:
|
||
映射["schemas"] = {
|
||
"character_relation": {
|
||
"type": "object",
|
||
"properties": {},
|
||
"additionalProperties": False,
|
||
}
|
||
}
|
||
映射["target_schemas"] = {
|
||
"B02:character_relation": {
|
||
"schema_id": "character_relation",
|
||
"base_version": 1,
|
||
"fields": {"甲方": "source", "乙方": "target", "关系类型": "relation_type"},
|
||
}
|
||
}
|
||
return 源, 映射
|
||
|
||
|
||
@pytest.mark.parametrize("版本类型", [int, str])
|
||
def test_旧英文实体卡保字段与章来源而只建未决提案__a01de7(目标方法环境, 版本类型):
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
源, 值 = _旧载荷输入(装配, 身份)
|
||
值["body_bindings"][0]["sources"][0]["revision"] = 版本类型(2)
|
||
映射 = 映射配置(值)
|
||
一 = 导入快照(装配, 身份, [源], 映射, **参数)
|
||
assert 一["states"] == {"mapped": 1}
|
||
assert 导入快照(装配, 身份, [源], 映射, **参数) == 一
|
||
世界 = 装配.要求故事世界()
|
||
回执 = 世界.正式.读取回执(身份, 迁移命令ID(身份.作者, 源.source.记录键))
|
||
提案 = 世界.读取提案(身份, 回执["target_ref"])
|
||
assert 提案["content"] == {"名称": "林深", "身份定位": "驾驶员"}
|
||
assert (
|
||
提案["sources"][0]["chapter_id"] == "migration-chapter"
|
||
and 提案["sources"][0]["revision"] == 2
|
||
)
|
||
assert 提案["sources"][0]["evidence"][0]["quote"] == "林深坐在二号观测位。"
|
||
行 = 迁移台账(世界.数据库, 身份, 世界.正式).读取(源.source.记录键)
|
||
assert 行 is not None and 行["original"] == 源.导出()
|
||
assert 行["route"]["type_id"] == "character" and 行["route"]["content"]["brief"] == "机师"
|
||
assert 行["route"]["content"]["source_hint"] == {
|
||
"workId": 12,
|
||
"chapter": 1,
|
||
"chapterId": 17473,
|
||
"evidence": "林深坐在二号观测位。",
|
||
}
|
||
assert 世界.列出世界实体(身份, "migration-work") == []
|
||
assert 核对迁移结果(装配, 身份, [源], 映射, **参数)["data_checks_passed"] is True
|
||
|
||
|
||
@pytest.mark.parametrize("乙方类型", ["character", "item"])
|
||
@pytest.mark.parametrize("形状", ["对象", "名称", "编号"])
|
||
def test_旧关系名称按明确身份承接并守实际类型__a423fe(目标方法环境, 乙方类型, 形状):
|
||
from muse.故事世界.接口 import 事实定位
|
||
from muse.正式变更.接口 import 作者动作, 变更错误
|
||
from 知识记录分流 import 分流知识记录
|
||
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
载荷 = {
|
||
"type": "驾驶",
|
||
"source": {"name": "林深"},
|
||
"target": {"name": "茧"},
|
||
"description": "首次实战驾驶",
|
||
}
|
||
if 形状 == "名称":
|
||
载荷.update(source="林深", target="茧")
|
||
if 形状 == "编号":
|
||
载荷.update(source=11, target=12)
|
||
源, 值 = _旧载荷输入(装配, 身份, 载荷, 关系=True)
|
||
世界 = 装配.要求故事世界()
|
||
for id_, 名, 类型 in (("existing-a", "林深", "character"), ("existing-b", "茧", 乙方类型)):
|
||
回 = 世界.保存实体提案(
|
||
身份,
|
||
"seed-" + id_,
|
||
work_id="migration-work",
|
||
object_id=id_,
|
||
expected_revision=0,
|
||
schema_id=类型,
|
||
schema_version=1,
|
||
content={"名称": 名},
|
||
independent=True,
|
||
)
|
||
提 = 世界.读取提案(身份, 回["target_ref"])
|
||
定位 = 事实定位(回["target_ref"], 提["revision"], 提["proposal_hash"], "world:" + id_, 0)
|
||
审 = 世界.打开事实审阅(身份, 定位)
|
||
世界.决定事实(
|
||
身份,
|
||
"confirm-" + id_,
|
||
定位,
|
||
作者动作.采纳,
|
||
审["review_id"],
|
||
审["review_hash"],
|
||
("object",),
|
||
)
|
||
# 类型来自明确合成前置,不从名字或“驾驶”一词推断,也不由迁移器改类型。
|
||
值["objects"] = [
|
||
{
|
||
"system": "pg",
|
||
"database": "synthetic-legacy",
|
||
"work_id": 12,
|
||
"use": "author",
|
||
"scope": "migration-work",
|
||
"refs": ["name:" + 名, "entity:" + str(号)],
|
||
"target_ref": id_,
|
||
}
|
||
for 名, 号, id_ in (("林深", 11, "existing-a"), ("茧", 12, "existing-b"))
|
||
]
|
||
映射 = 映射配置(值)
|
||
分流 = 分流知识记录(源, 映射)
|
||
assert 分流.disposition == "待转换"
|
||
assert (
|
||
分流.content["source"],
|
||
分流.content["target"],
|
||
分流.content["relation_type"],
|
||
分流.content["brief"],
|
||
) == ("existing-a", "existing-b", "驾驶", "首次实战驾驶")
|
||
一 = 导入快照(装配, 身份, [源], 映射, **参数)
|
||
assert 一 == 导入快照(装配, 身份, [源], 映射, **参数)
|
||
命令 = 迁移命令ID(身份.作者, 源.source.记录键)
|
||
if 乙方类型 == "character":
|
||
assert 一["states"] == {"mapped": 1}
|
||
回执 = 世界.正式.读取回执(身份, 命令)
|
||
assert 世界.读取提案(身份, 回执["target_ref"])["content"] == {
|
||
"甲方": "existing-a",
|
||
"乙方": "existing-b",
|
||
"关系类型": "驾驶",
|
||
}
|
||
assert 核对迁移结果(装配, 身份, [源], 映射, **参数)["data_checks_passed"] is True
|
||
else:
|
||
assert (
|
||
一["states"] == {"quarantined": 1}
|
||
and 一["errors"][0]["reason_code"] == "FACT_REFERENCE_INVALID"
|
||
)
|
||
with pytest.raises(变更错误) as 错:
|
||
世界.正式.读取回执(身份, 命令)
|
||
assert 错.value.错误码 == "RECEIPT_NOT_FOUND"
|
||
端点 = {
|
||
e["object_id"]: (e["type_id"], e["content"]["名称"])
|
||
for e in 世界.列出世界实体(身份, "migration-work")
|
||
}
|
||
assert 端点 == {"existing-a": ("character", "林深"), "existing-b": (乙方类型, "茧")}
|
||
assert 世界.人物关系视图(身份, "migration-work")["关系"] == []
|
||
行 = 迁移台账(世界.数据库, 身份, 世界.正式).读取(源.source.记录键)
|
||
assert 行 is not None and 行["original"] == 源.导出()
|
||
|
||
|
||
def test_旧缺端样例不被描述键遮蔽且精确失败关闭__aaeff3(目标方法环境):
|
||
from 知识记录分流 import 分流知识记录
|
||
|
||
装配, 身份, _, _, _ = 目标方法环境
|
||
源, 值 = _旧载荷输入(
|
||
装配, 身份, {"type": "驾驶", "source": "林深", "description": "x"}, 关系=True
|
||
)
|
||
值["objects"] = [
|
||
{
|
||
"system": "pg",
|
||
"database": "synthetic-legacy",
|
||
"work_id": 12,
|
||
"use": "author",
|
||
"scope": "migration-work",
|
||
"refs": ["name:林深"],
|
||
"target_ref": "existing-a",
|
||
}
|
||
]
|
||
判定 = 分流知识记录(源, 映射配置(值))
|
||
assert 判定.reason_code == "missing_endpoint" and 判定.detail == "relation.target"
|
||
行 = _核对保全且未发业务命令(目标方法环境, 源, 值, "quarantined", "missing_endpoint")
|
||
assert 行["route"]["detail"] == "relation.target"
|
||
assert 装配.要求故事世界().列出世界实体(身份, "migration-work") == []
|
||
assert 装配.要求故事世界().人物关系视图(身份, "migration-work")["关系"] == []
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"改动,原因",
|
||
[
|
||
("作品声明", "source_scope_conflict"),
|
||
("旧章声明", "source_scope_conflict"),
|
||
("错误引文", "target_source_mismatch"),
|
||
("跨章同句", "target_source_mismatch"),
|
||
("布尔版本", "target_source_mismatch"),
|
||
("坏依据集合", "target_source_mismatch"),
|
||
],
|
||
)
|
||
def test_旧章声明和真实依据不能各自正确却互相矛盾__211001(目标方法环境, 改动, 原因):
|
||
装配, 身份, _, _, _ = 目标方法环境
|
||
源, 值 = _旧载荷输入(装配, 身份)
|
||
数据 = 源.导出()
|
||
数据.pop("source_hash")
|
||
if 改动 == "作品声明":
|
||
数据["record"]["draft_payload"]["source"]["workId"] = 99
|
||
elif 改动 == "旧章声明":
|
||
数据["record"]["draft_payload"]["source"]["chapterId"] = 99
|
||
elif 改动 == "错误引文":
|
||
数据["record"]["draft_payload"]["evidence"] = "字面不同的引文"
|
||
elif 改动 == "布尔版本":
|
||
值["body_bindings"][0]["sources"][0]["revision"] = True
|
||
elif 改动 == "坏依据集合":
|
||
值["body_bindings"][0]["sources"] = True
|
||
else:
|
||
装配.要求作品().添加章节(
|
||
身份, "other-chapter", "migration-work", "other-chapter", "第二章", 预期目录版本=1
|
||
)
|
||
正文 = 装配.要求正文()
|
||
草稿 = 正文草稿((段落("p1", (文本节点("林深坐在二号观测位。"),)),))
|
||
for 版 in (0, 1):
|
||
正文.保存人工(身份, "other-body-" + str(版), "other-chapter", 版, 草稿)
|
||
文 = 正文.读取正文(身份, "other-chapter")
|
||
assert 文["revision"] == 2
|
||
值["body_bindings"][0]["sources"][0] = asdict(
|
||
正文依据(
|
||
"other-chapter",
|
||
"main",
|
||
文["revision"],
|
||
文["document_hash"],
|
||
(定位字面引文(草稿, "林深坐在二号观测位。"),),
|
||
)
|
||
)
|
||
源 = 旧快照.从载荷(数据)
|
||
值["body_bindings"][0]["source_hash"] = 源.源哈希
|
||
行 = _核对保全且未发业务命令(目标方法环境, 源, 值, "quarantined", 原因)
|
||
assert (
|
||
行["reason_code"] == 原因 and 装配.要求故事世界().列出世界实体(身份, "migration-work") == []
|
||
)
|
||
|
||
|
||
def _预先确认世界(装配, 身份, id_, 类型, 内容, 依据, *, 家族="entity", 版本=0, 动作=None):
|
||
from muse.故事世界.接口 import 事实定位, 事实提案请求
|
||
from muse.正式变更.接口 import 作者动作
|
||
|
||
世界 = 装配.要求故事世界()
|
||
结构 = 世界.新事实结构(身份, "migration-work", id_, 类型, 1, 家族=家族)
|
||
回 = 世界.提出事实(
|
||
身份,
|
||
"seed-proposal-" + id_ + str(版本),
|
||
事实提案请求(
|
||
work_id="migration-work",
|
||
object_id=id_,
|
||
expected_revision=版本,
|
||
schema=结构,
|
||
content=内容,
|
||
sources=tuple(TypeAdapter(正文依据).validate_python(x) for x in 依据),
|
||
effective_from="migration-chapter",
|
||
instance_family=家族,
|
||
independent=False,
|
||
),
|
||
)
|
||
if 动作 is 作者动作.人工保存:
|
||
return 回
|
||
提 = 世界.读取提案(身份, 回["target_ref"])
|
||
定位 = 事实定位(提["proposal_id"], 提["revision"], 提["proposal_hash"], "world:" + id_, 版本)
|
||
审 = 世界.打开事实审阅(身份, 定位)
|
||
return 世界.决定事实(
|
||
身份,
|
||
"seed-adopt-" + id_ + str(版本),
|
||
定位,
|
||
作者动作.采纳 if 动作 is None else 动作,
|
||
审["review_id"],
|
||
审["review_hash"],
|
||
("object",),
|
||
)
|
||
|
||
|
||
def _已确认历史输入(装配, 身份, *, 关系=False):
|
||
初, 值 = _旧载荷输入(装配, 身份)
|
||
依据 = 值["body_bindings"][0]["sources"]
|
||
if 关系:
|
||
for id_, 名 in (("old-end-a", "林深"), ("old-end-b", "茧")):
|
||
_预先确认世界(装配, 身份, id_, "character", {"名称": 名}, 依据)
|
||
新ID = "linked-relation" if 关系 else "linked-entity"
|
||
类型 = "character_relation" if 关系 else "character"
|
||
内容 = (
|
||
{"甲方": "old-end-a", "乙方": "old-end-b", "关系类型": "驾驶"}
|
||
if 关系
|
||
else {"名称": "林深", "身份定位": "驾驶员"}
|
||
)
|
||
回执 = _预先确认世界(装配, 身份, 新ID, 类型, 内容, 依据, 家族="relation" if 关系 else "entity")
|
||
正式行 = {
|
||
"id": 91,
|
||
"work_id": 12,
|
||
"revision": 1,
|
||
"deleted": False,
|
||
"source_status": "active",
|
||
"source_action_policy": "allowed",
|
||
"description": "首次实战驾驶" if 关系 else "机师",
|
||
"attributes": None if 关系 else {"身份": "驾驶员"},
|
||
}
|
||
if 关系:
|
||
正式行.update(source_entity_id=11, target_entity_id=12, relation_type="驾驶")
|
||
else:
|
||
正式行.update(
|
||
entity_type="character",
|
||
normalized_name="林深",
|
||
status="active",
|
||
source_type=None,
|
||
source_id=None,
|
||
lineage_payload={"sourceRefs": [{"chapter": 1}]},
|
||
)
|
||
原目标 = 旧快照.从载荷(
|
||
{
|
||
"source": {
|
||
**初.导出()["source"],
|
||
"table": "muse_knowledge_relation" if 关系 else "muse_knowledge_entity",
|
||
"id": "91",
|
||
},
|
||
"record": 正式行,
|
||
}
|
||
)
|
||
草稿行 = {
|
||
**初.原行,
|
||
"id": 7,
|
||
"revision": 2,
|
||
"draft_type": "relation" if 关系 else "entity",
|
||
"draft_payload": {},
|
||
"status": "confirmed",
|
||
"confidence": 0.9,
|
||
"entity_id": None if 关系 else 91,
|
||
"target_object_id": 91 if 关系 else None,
|
||
}
|
||
草稿 = 旧快照.从载荷(
|
||
{"source": {**初.导出()["source"], "id": "7", "revision": "2"}, "record": 草稿行}
|
||
)
|
||
新确认 = {"command_id": 回执["command_id"], "receipt_id": 回执["receipt_id"], "revision": 1}
|
||
值["confirmations"] = [
|
||
{
|
||
"system": "pg",
|
||
"database": "synthetic-legacy",
|
||
"source_key": s.source.记录键,
|
||
"source_hash": s.源哈希,
|
||
"target_ref": 新ID,
|
||
"decision_ref": "old-decision:" + s.source.id,
|
||
"existing_confirmation": dict(新确认),
|
||
}
|
||
for s in (原目标, 草稿)
|
||
]
|
||
值["confirmations"][1]["target_source"] = {
|
||
"source_key": 原目标.source.记录键,
|
||
"source_hash": 原目标.源哈希,
|
||
}
|
||
域 = {
|
||
"system": "pg",
|
||
"database": "synthetic-legacy",
|
||
"work_id": 12,
|
||
"use": "author",
|
||
"scope": "migration-work",
|
||
}
|
||
值["objects"] = [
|
||
{**域, "refs": [("relation:" if 关系 else "entity:") + "91"], "target_ref": 新ID}
|
||
]
|
||
if 关系:
|
||
值["objects"] += [
|
||
{**域, "refs": ["entity:" + str(i)], "target_ref": id_}
|
||
for i, id_ in ((11, "old-end-a"), (12, "old-end-b"))
|
||
]
|
||
值["schemas"] = {类型: {"type": "object", "properties": {}, "additionalProperties": False}}
|
||
值["target_schemas"] = {
|
||
"B02:" + 类型: {
|
||
"schema_id": 类型,
|
||
"base_version": 1,
|
||
"fields": {"甲方": "source", "乙方": "target", "关系类型": "relation_type"},
|
||
}
|
||
}
|
||
return 原目标, 草稿, 值, 回执
|
||
|
||
|
||
def _业务与索引指纹(装配):
|
||
# 只在隔离测试库聚合指纹,不把SQL放入迁移产品代码,不读取旧embedding表。
|
||
表 = (
|
||
"muse_world_object",
|
||
"muse_world_version",
|
||
"muse_fact_proposal",
|
||
"muse_fact_proposal_version",
|
||
"muse_author_review",
|
||
"muse_candidate_decision",
|
||
"muse_change_command",
|
||
"muse_change_impact",
|
||
"muse_document_index",
|
||
"muse_method_index",
|
||
"muse_method_index_version",
|
||
)
|
||
with 装配.要求故事世界().数据库.连接(只读=True) as 连:
|
||
return {
|
||
名: 连.execute(
|
||
sql.SQL(
|
||
"SELECT count(*),md5(coalesce(string_agg(j,',' ORDER BY j),'')) "
|
||
"FROM (SELECT row_to_json(t)::text AS j FROM {} t) s"
|
||
).format(sql.Identifier(名))
|
||
).fetchone()
|
||
for 名 in 表
|
||
}
|
||
|
||
|
||
@pytest.mark.parametrize("关系", [False, True])
|
||
@pytest.mark.parametrize("草稿在前", [False, True])
|
||
def test_已确认双源只关联既存版本和真实决定不复制__212000(目标方法环境, 关系, 草稿在前):
|
||
from muse.正式变更.接口 import 变更错误
|
||
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
正式源, 草稿, 值, 原回 = _已确认历史输入(装配, 身份, 关系=关系)
|
||
输入 = [草稿, 正式源] if 草稿在前 else [正式源, 草稿]
|
||
前 = _业务与索引指纹(装配)
|
||
映射 = 映射配置(值)
|
||
一 = 导入快照(装配, 身份, 输入, 映射, **参数)
|
||
assert 一["states"] == {"mapped": 2}
|
||
assert 一 == 导入快照(装配, 身份, 输入, 映射, **参数)
|
||
assert len(一["history_links"]) == 2
|
||
assert {r["target_ref"] for r in 一["history_links"]} == {值["confirmations"][0]["target_ref"]}
|
||
assert {r["receipt_id"] for r in 一["history_links"]} == {原回["receipt_id"]}
|
||
正式 = 装配.要求故事世界().正式
|
||
账 = 迁移台账(正式.数据库, 身份, 正式)
|
||
for s in 输入:
|
||
行 = 账.读取(s.source.记录键)
|
||
assert 行 is not None and 行["original"] == s.导出() and 行["receipt"] == 原回
|
||
assert (
|
||
行["frozen_request"]["mode"] == "link_confirmed"
|
||
and 行["targets"][0]["kind"] == "world_object"
|
||
)
|
||
assert 行["frozen_request"]["payload"]["decision_ref"] == "old-decision:" + s.source.id
|
||
with pytest.raises(变更错误) as 错:
|
||
正式.读取回执(身份, 行["command_id"])
|
||
assert 错.value.错误码 == "RECEIPT_NOT_FOUND"
|
||
assert 核对迁移结果(装配, 身份, 输入, 映射, **参数)["data_checks_passed"] is True
|
||
assert _业务与索引指纹(装配) == 前
|
||
|
||
|
||
def test_已确认空关系草稿点名重放保旧7到91__ef5133(目标方法环境):
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
正式源, 草稿, 值, 回 = _已确认历史输入(装配, 身份, 关系=True)
|
||
assert 草稿.原行["draft_payload"] == {} and 草稿.原行["target_object_id"] == 91
|
||
assert not {"source_type", "source_id"} & set(正式源.原行)
|
||
映射 = 映射配置(值)
|
||
导入快照(装配, 身份, [正式源, 草稿], 映射, **参数)
|
||
前 = _业务与索引指纹(装配)
|
||
一 = 导入快照(装配, 身份, [草稿], 映射, **参数)
|
||
assert 一["states"] == {"mapped": 1} and 一 == 导入快照(装配, 身份, [草稿], 映射, **参数)
|
||
h = 一["history_links"][0]
|
||
assert (
|
||
h["legacy_draft_id"],
|
||
h["legacy_target_id"],
|
||
h["target_ref"],
|
||
h["revision"],
|
||
h["receipt_id"],
|
||
) == ("7", "91", "linked-relation", 1, 回["receipt_id"])
|
||
assert h["kind"] == "linked_existing_confirmation"
|
||
assert 核对迁移结果(装配, 身份, [草稿], 映射, **参数)["data_checks_passed"] is True
|
||
assert _业务与索引指纹(装配) == 前
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"错误", ["假命令", "假回执", "错版本", "缺正式源", "错正式哈希", "错旧主键", "pending伪装"]
|
||
)
|
||
def test_历史关联不能只凭旧声明或伪指针转正__212001(目标方法环境, 错误):
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
正式源, 草稿, 值, _ = _已确认历史输入(装配, 身份, 关系=True)
|
||
if 错误 == "假命令":
|
||
for r in 值["confirmations"]:
|
||
r["existing_confirmation"]["command_id"] = "no-such-command"
|
||
elif 错误 == "假回执":
|
||
for r in 值["confirmations"]:
|
||
r["existing_confirmation"]["receipt_id"] = "fake-receipt"
|
||
elif 错误 == "错版本":
|
||
for r in 值["confirmations"]:
|
||
r["existing_confirmation"]["revision"] = 2
|
||
elif 错误 == "错正式哈希":
|
||
值["confirmations"][1]["target_source"]["source_hash"] = "0" * 64
|
||
elif 错误 in ("错旧主键", "pending伪装"):
|
||
d = 草稿.导出()
|
||
d.pop("source_hash")
|
||
if 错误 == "错旧主键":
|
||
d["record"]["target_object_id"] = 92
|
||
else:
|
||
d["record"]["status"] = "pending"
|
||
草稿 = 旧快照.从载荷(d)
|
||
值["confirmations"][1]["source_hash"] = 草稿.源哈希
|
||
输入 = [草稿] if 错误 == "缺正式源" else [草稿, 正式源]
|
||
前 = _业务与索引指纹(装配)
|
||
一 = 导入快照(装配, 身份, 输入, 映射配置(值), **参数)
|
||
assert 一["states"].get("quarantined", 0) >= 1
|
||
行 = 迁移台账(装配.要求故事世界().数据库, 身份, 装配.要求故事世界().正式).读取(
|
||
草稿.source.记录键
|
||
)
|
||
assert (
|
||
行 is not None
|
||
and 行["state"] == "quarantined"
|
||
and 行["targets"] == []
|
||
and 行["receipt"] is None
|
||
)
|
||
assert 行["original"] == 草稿.导出() and _业务与索引指纹(装配) == 前
|
||
assert 行["reason_code"] == (
|
||
"legacy_target_source_missing"
|
||
if 错误 in ("假命令", "假回执", "错版本", "缺正式源")
|
||
else "legacy_link_conflict"
|
||
)
|
||
if 错误 in ("假命令", "假回执", "错版本"):
|
||
账 = 迁移台账(装配.要求故事世界().数据库, 身份, 装配.要求故事世界().正式)
|
||
父 = 账.读取(正式源.source.记录键)
|
||
assert 父 is not None and 父["reason_code"] == "existing_confirmation_invalid"
|
||
|
||
|
||
@pytest.mark.parametrize("断点", ["正式", "草稿"])
|
||
def test_关联中断和父源处理中保持可重试且不写业务__212002(目标方法环境, monkeypatch, 断点):
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
正式源, 草稿, 值, _ = _已确认历史输入(装配, 身份, 关系=True)
|
||
原保存 = 迁移台账._保存关联
|
||
指定 = (正式源 if 断点 == "正式" else 草稿).source.记录键
|
||
|
||
def 中断(self, key, 目标, 回执):
|
||
if key == 指定:
|
||
raise RuntimeError("合成中断")
|
||
return 原保存(self, key, 目标, 回执)
|
||
|
||
monkeypatch.setattr(迁移台账, "_保存关联", 中断)
|
||
前 = _业务与索引指纹(装配)
|
||
映射 = 映射配置(值)
|
||
with pytest.raises(RuntimeError, match="合成中断"):
|
||
导入快照(装配, 身份, [草稿, 正式源], 映射, **参数)
|
||
账 = 迁移台账(装配.要求故事世界().数据库, 身份, 装配.要求故事世界().正式)
|
||
行 = 账.读取(指定)
|
||
assert (
|
||
行 is not None
|
||
and 行["state"] == "reserved"
|
||
and 行["frozen_request"]["mode"] == "link_confirmed"
|
||
)
|
||
if 断点 == "正式":
|
||
with pytest.raises(迁移错误) as 错:
|
||
导入快照(装配, 身份, [草稿], 映射, **参数)
|
||
assert 错.value.code == "legacy_dependency_pending"
|
||
子 = 账.读取(草稿.source.记录键)
|
||
assert 子 is not None and 子["state"] == "reserved"
|
||
monkeypatch.setattr(迁移台账, "_保存关联", 原保存)
|
||
assert 导入快照(装配, 身份, [草稿, 正式源], 映射, **参数)["states"] == {"mapped": 2}
|
||
assert _业务与索引指纹(装配) == 前
|
||
|
||
|
||
def test_历史双源并发按统一锁序仅关联一次__212003(目标方法环境):
|
||
from concurrent.futures import ThreadPoolExecutor
|
||
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
正式源, 草稿, 值, _ = _已确认历史输入(装配, 身份, 关系=True)
|
||
前 = _业务与索引指纹(装配)
|
||
|
||
def 执行(反序):
|
||
return 导入快照(
|
||
装配, 身份, [草稿, 正式源] if 反序 else [正式源, 草稿], 映射配置(值), **参数
|
||
)
|
||
|
||
with ThreadPoolExecutor(max_workers=2) as 池:
|
||
结果 = list(池.map(执行, [False, True]))
|
||
assert 结果[0] == 结果[1] and 结果[0]["states"] == {"mapped": 2}
|
||
assert _业务与索引指纹(装配) == 前
|
||
|
||
|
||
@pytest.mark.parametrize("修改", ["世界", "正文"])
|
||
def test_后续编辑与新源撤回不倒改已关联历史__212004(目标方法环境, 修改):
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
正式源, 草稿, 值, 原回 = _已确认历史输入(装配, 身份)
|
||
映射 = 映射配置(值)
|
||
一 = 导入快照(装配, 身份, [正式源, 草稿], 映射, **参数)
|
||
if 修改 == "世界":
|
||
_预先确认世界(
|
||
装配,
|
||
身份,
|
||
"linked-entity",
|
||
"character",
|
||
{"名称": "林深", "身份定位": "新的身份"},
|
||
值["body_bindings"][0]["sources"],
|
||
版本=1,
|
||
)
|
||
else:
|
||
装配.要求正文().保存人工(
|
||
身份,
|
||
"change-history-source",
|
||
"migration-chapter",
|
||
2,
|
||
正文草稿((段落("p1", (文本节点("正文内容已经改变。"),)),)),
|
||
)
|
||
前 = _业务与索引指纹(装配)
|
||
assert 导入快照(装配, 身份, [正式源, 草稿], 映射, **参数) == 一
|
||
assert {r["revision"] for r in 一["history_links"]} == {1}
|
||
d = 草稿.导出()
|
||
d.pop("source_hash")
|
||
d["record"]["source_status"] = "withdrawn"
|
||
同版 = 旧快照.从载荷(d)
|
||
with pytest.raises(迁移错误) as 错:
|
||
导入快照(装配, 身份, [同版], 映射, **参数)
|
||
assert 错.value.code == "drift"
|
||
d["source"]["revision"] = "3"
|
||
d["record"]["revision"] = 3
|
||
新版 = 旧快照.从载荷(d)
|
||
assert 导入快照(装配, 身份, [新版], 映射, **参数)["states"] == {"historical": 1}
|
||
assert (
|
||
导入快照(装配, 身份, [草稿], 映射, **参数)["history_links"][0]["receipt_id"]
|
||
== 原回["receipt_id"]
|
||
)
|
||
assert _业务与索引指纹(装配) == 前
|
||
|
||
|
||
def test_冻结模式原行与世界读取能力不能靠自报绕过__212005(目标方法环境):
|
||
from copy import deepcopy
|
||
|
||
from 知识记录分流 import 分流结果
|
||
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
正式源, 草稿, 值, _ = _已确认历史输入(装配, 身份)
|
||
导入快照(装配, 身份, [正式源, 草稿], 映射配置(值), **参数)
|
||
世界 = 装配.要求故事世界()
|
||
账 = 迁移台账(世界.数据库, 身份, 世界.正式, 世界)
|
||
行 = 账.读取(草稿.source.记录键)
|
||
assert 行 is not None
|
||
前 = _业务与索引指纹(装配)
|
||
for 模式 in ("create", "unknown", [], {"mode": "link_confirmed"}):
|
||
冻结 = deepcopy(行["frozen_request"])
|
||
冻结["mode"] = 模式
|
||
with pytest.raises(迁移错误) as 错:
|
||
账.冻结请求(草稿.source.记录键, 冻结)
|
||
assert 错.value.code == "target_request_invalid"
|
||
缺世界 = 迁移台账(世界.数据库, 身份, 世界.正式)
|
||
with pytest.raises(迁移错误) as 错:
|
||
缺世界.保存终态(草稿.source.记录键, "mapped", 目标=行["targets"], 回执=行["receipt"])
|
||
assert 错.value.code == "target_evidence_missing"
|
||
坏目标 = deepcopy(行["targets"])
|
||
坏目标[0]["kind"] = "proposal"
|
||
with pytest.raises(迁移错误) as 错:
|
||
账.保存终态(草稿.source.记录键, "mapped", 目标=坏目标, 回执=行["receipt"])
|
||
assert 错.value.code == "target_evidence_mismatch"
|
||
d = 草稿.导出()
|
||
d.pop("source_hash")
|
||
d["source"]["id"] = "88"
|
||
d["record"].update(id=88, status="pending")
|
||
待审 = 旧快照.从载荷(d)
|
||
伪路由 = {
|
||
**deepcopy(行["route"]),
|
||
"source_key": 待审.source.记录键,
|
||
"source_hash": 待审.源哈希,
|
||
"original": 待审.导出(),
|
||
}
|
||
账.占用(待审, 行["mapping_hash"], 分流结果(**伪路由))
|
||
冻结 = deepcopy(行["frozen_request"])
|
||
冻结["payload"].update(source_key=待审.source.记录键, source_hash=待审.源哈希)
|
||
with pytest.raises(迁移错误) as 错:
|
||
账.冻结请求(待审.source.记录键, 冻结)
|
||
assert 错.value.code == "legacy_link_conflict"
|
||
assert _业务与索引指纹(装配) == 前
|
||
|
||
|
||
def test_同事务只读入口不另开连接且保持事务归调用方__212006(目标方法环境, monkeypatch):
|
||
装配, 身份, _, _, _ = 目标方法环境
|
||
_, _, _, 回 = _已确认历史输入(装配, 身份)
|
||
世界 = 装配.要求故事世界()
|
||
with 世界.数据库.连接(只读=True) as 连, 连.transaction():
|
||
|
||
def 禁新连接(*args, **kwargs):
|
||
raise AssertionError("只读于入口不能另开连接")
|
||
|
||
with monkeypatch.context() as m:
|
||
m.setattr(数据库工厂, "连接", 禁新连接)
|
||
assert 世界.正式.读取回执于(连, 身份, 回["command_id"]) == 回
|
||
历史 = 世界.读取实体轨迹于(连, 身份, "migration-work", "linked-entity")
|
||
assert (
|
||
历史["current_revision"] == 1
|
||
and 历史["版本"][0]["content_hash"] == 回["results"][0]["content_hash"]
|
||
)
|
||
assert not 连.closed and 连.info.transaction_status.name == "INTRANS"
|
||
|
||
|
||
@pytest.mark.parametrize("差异", ["内容", "结构", "作用域"])
|
||
def test_旧正式值与真实目标不符必须精确拒绝__212007(目标方法环境, 差异):
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
正式源, 草稿, 值, _ = _已确认历史输入(装配, 身份)
|
||
if 差异 == "内容":
|
||
d = 正式源.导出()
|
||
d.pop("source_hash")
|
||
d["record"]["attributes"] = {"身份": "与已确认值不同"}
|
||
正式源 = 旧快照.从载荷(d)
|
||
值["confirmations"][0]["source_hash"] = 正式源.源哈希
|
||
值["confirmations"][1]["target_source"]["source_hash"] = 正式源.源哈希
|
||
elif 差异 == "结构":
|
||
值["target_schemas"]["B02:character"]["schema_id"] = "item"
|
||
else:
|
||
作品 = 装配.要求作品()
|
||
结构 = 作品.新档案结构(身份, "other-work", "work_core", 1)
|
||
作品.保存档案(身份, "other-work", 档案保存("other-work", 0, {"名称": "明确另一作品"}, 结构))
|
||
值["works"][0]["target_ref"] = "other-work"
|
||
for o in 值["objects"]:
|
||
o["scope"] = "other-work"
|
||
前 = _业务与索引指纹(装配)
|
||
一 = 导入快照(装配, 身份, [草稿, 正式源], 映射配置(值), **参数)
|
||
assert 一["states"] == {"quarantined": 2}
|
||
账 = 迁移台账(装配.要求故事世界().数据库, 身份, 装配.要求故事世界().正式)
|
||
行 = 账.读取(正式源.source.记录键)
|
||
assert 行 is not None
|
||
assert 行["reason_code"] == (
|
||
"target_type_mismatch" if 差异 == "结构" else "existing_target_mismatch"
|
||
)
|
||
assert _业务与索引指纹(装配) == 前
|
||
|
||
|
||
@pytest.mark.parametrize("动作名", ["人工保存", "拒绝", "暂缓"])
|
||
def test_真实非采纳回执不能冒充已确认版本__212008(目标方法环境, 动作名):
|
||
from muse.正式变更.接口 import 作者动作
|
||
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
正式源, 草稿, 值, _ = _已确认历史输入(装配, 身份)
|
||
回 = _预先确认世界(
|
||
装配,
|
||
身份,
|
||
"linked-entity",
|
||
"character",
|
||
{"名称": "林深", "身份定位": "驾驶员"},
|
||
值["body_bindings"][0]["sources"],
|
||
版本=1,
|
||
动作=getattr(作者动作, 动作名),
|
||
)
|
||
for r in 值["confirmations"]:
|
||
r["existing_confirmation"] = {
|
||
"command_id": 回["command_id"],
|
||
"receipt_id": 回["receipt_id"],
|
||
"revision": 1,
|
||
}
|
||
前 = _业务与索引指纹(装配)
|
||
assert 导入快照(装配, 身份, [草稿, 正式源], 映射配置(值), **参数)["states"] == {
|
||
"quarantined": 2
|
||
}
|
||
账 = 迁移台账(装配.要求故事世界().数据库, 身份, 装配.要求故事世界().正式)
|
||
行 = 账.读取(正式源.source.记录键)
|
||
assert 行 is not None and 行["reason_code"] == "existing_confirmation_invalid"
|
||
assert _业务与索引指纹(装配) == 前
|
||
|
||
|
||
@pytest.mark.parametrize("差异", ["原主键", "别名坏类型"])
|
||
def test_原主键与类型映射不一致不能关联__212009(目标方法环境, 差异):
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
正式源, 草稿, 值, _ = _已确认历史输入(装配, 身份)
|
||
前 = _业务与索引指纹(装配)
|
||
if 差异 == "原主键":
|
||
d = 正式源.导出()
|
||
d.pop("source_hash")
|
||
d["record"]["id"] = 92
|
||
with pytest.raises(迁移错误) as 错:
|
||
旧快照.从载荷(d)
|
||
assert 错.value.code == "source_invalid"
|
||
else:
|
||
值["type_aliases"] = {"character": ["character"]}
|
||
结果 = 导入快照(装配, 身份, [正式源, 草稿], 映射配置(值), **参数)
|
||
assert 结果["states"] == {"quarantined": 2}
|
||
账 = 迁移台账(装配.要求故事世界().数据库, 身份, 装配.要求故事世界().正式)
|
||
父 = 账.读取(正式源.source.记录键)
|
||
assert 父 is not None and 父["reason_code"] == "unknown_type"
|
||
assert _业务与索引指纹(装配) == 前
|
||
|
||
|
||
def _来源回放输入(装配, 身份, *, 坏来源=False):
|
||
from copy import deepcopy
|
||
|
||
正式源, 草稿, 值, _ = _已确认历史输入(装配, 身份)
|
||
d = 正式源.导出()
|
||
d.pop("source_hash")
|
||
d["record"]["lineage_payload"] = {"sourceRefs": [{"chapter": 1}], "lastDraftId": 10}
|
||
正式源 = 旧快照.从载荷(d)
|
||
根指针 = {"source_key": 正式源.source.记录键, "source_hash": 正式源.源哈希}
|
||
值["confirmations"][0]["source_hash"] = 正式源.源哈希
|
||
原子声明 = deepcopy(值["confirmations"][1])
|
||
值["confirmations"] = 值["confirmations"][:1]
|
||
子 = []
|
||
for id_, 章 in ((11, 1), (12, 2)):
|
||
d = 草稿.导出()
|
||
d.pop("source_hash")
|
||
d["source"]["id"] = str(id_)
|
||
d["record"].update(
|
||
id=id_,
|
||
source_id=17472 + 章,
|
||
draft_payload={"source": "非结构化提示" if 坏来源 and 章 == 2 else {"chapter": 章}},
|
||
)
|
||
s = 旧快照.从载荷(d)
|
||
子.append(s)
|
||
值["confirmations"].append(
|
||
{
|
||
**deepcopy(原子声明),
|
||
"source_key": s.source.记录键,
|
||
"source_hash": s.源哈希,
|
||
"target_source": dict(根指针),
|
||
}
|
||
)
|
||
请求 = {
|
||
"base": 根指针,
|
||
"steps": [{"source_key": s.source.记录键, "source_hash": s.源哈希} for s in 子],
|
||
}
|
||
return [正式源, *子], 映射配置(值), 请求
|
||
|
||
|
||
def test_旧来源链按明确请求追加去重且不改正式数据__b671f4(目标方法环境, tmp_path, capsys):
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
输入, 映射, 请求 = _来源回放输入(装配, 身份)
|
||
assert 导入快照(装配, 身份, 输入, 映射, **参数)["states"] == {"mapped": 3}
|
||
前 = _业务与索引指纹(装配)
|
||
结果 = 核对迁移结果(装配, 身份, 输入, 映射, 来源回放=请求, **参数)
|
||
回放 = 结果["lineage_replay"]
|
||
assert 结果["data_checks_passed"] is True and 回放["passed"] is True
|
||
assert 回放["initial"] == {"sourceRefs": [{"chapter": 1}], "lastDraftId": 10}
|
||
assert 回放["steps"][0]["lineage"]["sourceRefs"] == [{"chapter": 1}]
|
||
assert 回放["steps"][0]["lineage"]["lastDraftId"] == 11
|
||
assert 回放["final"]["sourceRefs"] == [{"chapter": 1}, {"chapter": 2}]
|
||
assert (
|
||
回放["final"]["lastDraftId"] == 12 and 回放["final"]["lastSourceType"] == "chapter_extract"
|
||
)
|
||
assert (
|
||
回放["order_basis"] == "caller_selected_order"
|
||
and 回放["historical_order_verified"] is False
|
||
and 回放["canonical_written"] is False
|
||
)
|
||
assert 结果 == 核对迁移结果(装配, 身份, 输入, 映射, 来源回放=请求, **参数)
|
||
账 = 迁移台账(装配.要求故事世界().数据库, 身份, 装配.要求故事世界().正式)
|
||
for s in 输入:
|
||
行 = 账.读取(s.source.记录键)
|
||
assert 行 is not None and 行["original"] == s.导出()
|
||
from 入口 import main
|
||
|
||
配置 = tmp_path / "应用.toml"
|
||
配置.write_text(
|
||
'["数据库"]\n"取值方式"="受控存储"\n"位置"='
|
||
+ json.dumps(装配.配置.数据库.位置, ensure_ascii=False)
|
||
+ '\n["资源"]\n"发布身份"="synthetic"\n["运行"]\n"用途"="production"'
|
||
'\n[HTTP]\n"作者ID"="migration-author"\n"口令文件"="口令.txt"\n',
|
||
encoding="utf-8",
|
||
)
|
||
(tmp_path / "口令.txt").write_text("synthetic-only", encoding="utf-8")
|
||
for 名, 值 in (("快照", [s.导出() for s in 输入]), ("映射", 映射.值), ("回放", 请求)):
|
||
(tmp_path / (名 + ".json")).write_text(json.dumps(值, ensure_ascii=False), encoding="utf-8")
|
||
命令 = [
|
||
"核对目标",
|
||
"--快照",
|
||
str(tmp_path / "快照.json"),
|
||
"--映射",
|
||
str(tmp_path / "映射.json"),
|
||
"--配置",
|
||
str(配置),
|
||
"--允许实例",
|
||
str(参数["允许实例"]),
|
||
"--管理引用",
|
||
参数["管理引用"].位置,
|
||
"--目标回执",
|
||
str(参数["目标回执"]),
|
||
"--来源回放",
|
||
str(tmp_path / "回放.json"),
|
||
"--输出",
|
||
str(tmp_path / "报告.json"),
|
||
]
|
||
assert main(命令 + ["--输出", str(tmp_path / "回放.json")]) == 1
|
||
assert json.loads((tmp_path / "回放.json").read_text()) == 请求
|
||
assert main(命令) == 0
|
||
assert json.loads((tmp_path / "报告.json").read_text())["lineage_replay"] == 回放
|
||
assert "林深" not in capsys.readouterr().out
|
||
assert _业务与索引指纹(装配) == 前
|
||
|
||
|
||
@pytest.mark.parametrize("错误", ["错hash", "错根", "非对象引用", "缺步骤源"])
|
||
def test_回放必须核真实留存来源且错误不清空原值__213001(目标方法环境, 错误):
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
输入, 映射, 请求 = _来源回放输入(装配, 身份, 坏来源=错误 == "非对象引用")
|
||
导入快照(装配, 身份, 输入, 映射, **参数)
|
||
if 错误 == "错hash":
|
||
请求["steps"][0]["source_hash"] = "0" * 64
|
||
elif 错误 == "错根":
|
||
请求["base"] = dict(请求["steps"][0])
|
||
elif 错误 == "缺步骤源":
|
||
请求["steps"][1]["source_key"] = "not-stored"
|
||
前 = _业务与索引指纹(装配)
|
||
结果 = 核对迁移结果(装配, 身份, 输入, 映射, 来源回放=请求, **参数)
|
||
assert 结果["data_checks_passed"] is False and 结果["lineage_replay"]["passed"] is False
|
||
assert 结果["lineage_replay"]["issues"][0]["reason_code"] == "legacy_lineage_invalid"
|
||
assert 结果["lineage_replay"]["final"] is None and _业务与索引指纹(装配) == 前
|
||
|
||
|
||
def _参考分析输入(快照, 配置, 身份):
|
||
配置 = json.loads(json.dumps(配置))
|
||
数据 = 快照.导出()
|
||
数据.pop("source_hash")
|
||
数据["record"].update(
|
||
work_id=8,
|
||
draft_payload={
|
||
"type": "character",
|
||
"name": "林深",
|
||
"brief": "机师",
|
||
"fields": {"身份": "驾驶员"},
|
||
},
|
||
)
|
||
源 = 旧快照.从载荷(数据)
|
||
配置["works"][0].update(
|
||
legacy_id=8, use="reference", target_ref=配置["sources"][0]["target_ref"]
|
||
)
|
||
配置["schemas"] = {
|
||
"character": {
|
||
"type": "object",
|
||
"properties": {"身份": {"type": "string"}},
|
||
"additionalProperties": False,
|
||
}
|
||
}
|
||
配置["target_schemas"] = {
|
||
"B03:character": {
|
||
"schema_id": "character",
|
||
"base_version": 1,
|
||
"fields": {"名称": "name", "身份定位": "fields.身份"},
|
||
}
|
||
}
|
||
return 源, 映射配置(配置)
|
||
|
||
|
||
def test_参考人物真实进入B03分析且不造任务或世界事实__214000(目标方法环境):
|
||
装配, 身份, 快照, 配置, 参数 = 目标方法环境
|
||
源, 映射 = _参考分析输入(快照, 配置, 身份)
|
||
前 = _业务与索引指纹(装配)
|
||
assert 装配.任务运行.列出任务(身份.作者) == []
|
||
结果 = 导入快照(装配, 身份, [源], 映射, **参数)
|
||
assert 结果["states"] == {"mapped": 1}
|
||
账 = 迁移台账(装配.要求故事世界().数据库, 身份, 装配.要求故事世界().正式)
|
||
行 = 账.读取(源.source.记录键)
|
||
目标 = 行["targets"][0]
|
||
assert 目标["kind"] == "analysis" and 目标["owner"] == "B03" and 目标["revision"] == 1
|
||
with 装配.要求数据库().连接(只读=True) as 连:
|
||
分析 = 装配.要求资料().读取分析(连, 身份.作者, 目标["id"])
|
||
assert (
|
||
分析["origin"] == "legacy_import"
|
||
and 分析["source_id"] == 映射.值["works"][0]["target_ref"]
|
||
)
|
||
assert 分析["payload"]["request"]["content"] == {"名称": "林深", "身份定位": "驾驶员"}
|
||
assert 分析["payload"]["request"]["brief"] == "机师"
|
||
assert (
|
||
分析["payload"]["status"] == "proposed" and 分析["payload"]["model_verified"] is False
|
||
)
|
||
assert 分析["payload"]["canonical_written"] is False
|
||
assert 行["receipt"]["action"] == "manual_save" and 行["receipt"]["author_review_id"] is None
|
||
assert 行["original"] == 源.导出()
|
||
assert 导入快照(装配, 身份, [源], 映射, **参数) == 结果
|
||
assert 核对迁移结果(装配, 身份, [源], 映射, **参数)["data_checks_passed"] is True
|
||
后 = _业务与索引指纹(装配)
|
||
assert 后.pop("muse_change_command")[0] == 前.pop("muse_change_command")[0] + 1
|
||
assert 后 == 前 and 装配.任务运行.列出任务(身份.作者) == []
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"坏处", ["错版本", "错哈希", "错作者", "错类型族", "未知字段", "坏来源身份"]
|
||
)
|
||
def test_参考分析拒绝坏依据且零领域和任务副作用__214001(目标方法环境, 坏处):
|
||
from dataclasses import replace
|
||
|
||
from muse.资料研究.接口 import 参考分析请求
|
||
from 知识记录分流 import 分流知识记录
|
||
from 转换业务对象 import 准备目标请求
|
||
|
||
装配, 身份, 快照, 配置, _ = 目标方法环境
|
||
源, 映射 = _参考分析输入(快照, 配置, 身份)
|
||
冻结 = 准备目标请求(装配, 身份, 源, 分流知识记录(源, 映射), 映射)
|
||
请求 = TypeAdapter(参考分析请求).validate_python(冻结["payload"])
|
||
前 = _业务与索引指纹(装配)
|
||
with pytest.raises(Muse错误):
|
||
if 坏处 == "错版本":
|
||
请求 = replace(请求, source_revision=99)
|
||
elif 坏处 == "错哈希":
|
||
请求 = replace(请求, source_content_hash="a" * 64)
|
||
elif 坏处 == "错作者":
|
||
身份 = replace(身份, 作者="foreign-author")
|
||
elif 坏处 == "错类型族":
|
||
请求 = replace(请求, type_id="craft", schema_id="craft")
|
||
elif 坏处 == "未知字段":
|
||
请求 = replace(请求, content={"名称": "甲", "未定义": "不能藏值"})
|
||
else:
|
||
请求 = replace(请求, source_id="not-a-source-id")
|
||
装配.要求资料().提出参考分析(身份, "bad-analysis", 请求)
|
||
with 装配.要求数据库().连接(只读=True) as 连:
|
||
assert 装配.要求资料().列出分析(连, "migration-author", 冻结["scope"]) == []
|
||
assert _业务与索引指纹(装配) == 前
|
||
assert 装配.任务运行.列出任务("migration-author") == []
|
||
|
||
|
||
def test_参考分析并发幂等及异命令不覆盖__214002(目标方法环境):
|
||
from concurrent.futures import ThreadPoolExecutor
|
||
from dataclasses import replace
|
||
|
||
from muse.资料研究.接口 import 参考分析请求
|
||
|
||
装配, 身份, 快照, 配置, 参数 = 目标方法环境
|
||
源, 映射 = _参考分析输入(快照, 配置, 身份)
|
||
with ThreadPoolExecutor(max_workers=2) as 池:
|
||
结果 = list(池.map(lambda _: 导入快照(装配, 身份, [源], 映射, **参数), range(2)))
|
||
assert all(r["states"] == {"mapped": 1} for r in 结果)
|
||
账 = 迁移台账(装配.要求故事世界().数据库, 身份, 装配.要求故事世界().正式)
|
||
行 = 账.读取(源.source.记录键)
|
||
请求 = TypeAdapter(参考分析请求).validate_python(行["frozen_request"]["payload"])
|
||
前 = _业务与索引指纹(装配)
|
||
with pytest.raises(Muse错误):
|
||
装配.要求资料().提出参考分析(
|
||
身份, "new-command-for-same-id", replace(请求, title="不能覆盖")
|
||
)
|
||
assert _业务与索引指纹(装配) == 前
|
||
with 装配.要求数据库().连接(只读=True) as 连:
|
||
assert len(装配.要求资料().列出分析(连, 身份.作者, 请求.source_id)) == 1
|
||
|
||
|
||
def test_参考引用不能跨类型或把未决卡当确认对象__214003(目标方法环境):
|
||
from dataclasses import replace
|
||
|
||
from muse.资料研究.接口 import 参考分析请求
|
||
from 知识记录分流 import 分流知识记录
|
||
from 转换业务对象 import 准备目标请求
|
||
|
||
装配, 身份, 快照, 配置, _ = 目标方法环境
|
||
源, 映射 = _参考分析输入(快照, 配置, 身份)
|
||
冻结 = 准备目标请求(装配, 身份, 源, 分流知识记录(源, 映射), 映射)
|
||
原 = TypeAdapter(参考分析请求).validate_python(冻结["payload"])
|
||
for 型 in ("character", "faction"):
|
||
请求 = replace(
|
||
原,
|
||
analysis_id="reference-" + 型,
|
||
type_id=型,
|
||
schema_id=型,
|
||
title="引用目标",
|
||
content={"名称": "引用目标"},
|
||
)
|
||
装配.要求资料().提出参考分析(身份, "create-reference-" + 型, 请求)
|
||
前 = _业务与索引指纹(装配)
|
||
for 型 in ("character", "faction"):
|
||
with pytest.raises(Muse错误, match="引用类型、状态或来源范围不一致"):
|
||
请求 = replace(原, content={"名称": "甲", "阵营": "reference-" + 型})
|
||
装配.要求资料().提出参考分析(身份, "bad-reference-" + 型, 请求)
|
||
assert _业务与索引指纹(装配) == 前
|
||
|
||
|
||
@pytest.mark.parametrize("断点", ["回执事务", "迁移终态"])
|
||
def test_参考分析中断按原命令恢复且事务失败不留孤儿__214004(目标方法环境, monkeypatch, 断点):
|
||
from muse.正式变更.存储 import 变更存储
|
||
|
||
装配, 身份, 快照, 配置, 参数 = 目标方法环境
|
||
源, 映射 = _参考分析输入(快照, 配置, 身份)
|
||
|
||
def 中断(*args, **kwargs):
|
||
raise RuntimeError("合成中断")
|
||
|
||
with monkeypatch.context() as m:
|
||
if 断点 == "回执事务":
|
||
m.setattr(变更存储, "保存回执", 中断)
|
||
else:
|
||
m.setattr(迁移台账, "保存终态", 中断)
|
||
with pytest.raises(RuntimeError, match="合成中断"):
|
||
导入快照(装配, 身份, [源], 映射, **参数)
|
||
with 装配.要求数据库().连接(只读=True) as 连:
|
||
初 = 装配.要求资料().列出分析(连, 身份.作者, 映射.值["works"][0]["target_ref"])
|
||
assert len(初) == (0 if 断点 == "回执事务" else 1)
|
||
assert 导入快照(装配, 身份, [源], 映射, **参数)["states"] == {"mapped": 1}
|
||
with 装配.要求数据库().连接(只读=True) as 连:
|
||
后 = 装配.要求资料().列出分析(连, 身份.作者, 映射.值["works"][0]["target_ref"])
|
||
assert len(后) == 1
|
||
if 初:
|
||
assert 后 == 初
|
||
assert 核对迁移结果(装配, 身份, [源], 映射, **参数)["data_checks_passed"] is True
|
||
assert 装配.任务运行.列出任务(身份.作者) == []
|
||
|
||
|
||
def test_参考分析按用途投影且不改留存哈希__214005(目标方法环境):
|
||
from muse.元数据.接口 import 启用命令, 字段限制, 定义哈希, 策略快照
|
||
from muse.编排.研究拆书 import 查询拆书状态
|
||
|
||
装配, 身份, 快照, 配置, 参数 = 目标方法环境
|
||
源, 映射 = _参考分析输入(快照, 配置, 身份)
|
||
导入快照(装配, 身份, [源], 映射, **参数)
|
||
with 装配.要求数据库().连接(只读=True) as 连:
|
||
初 = 装配.要求资料().列出分析(连, 身份.作者, 映射.值["works"][0]["target_ref"])[0]
|
||
维护 = 数据库引用("受控存储", str(Path(装配.配置.数据库.位置).with_name("维护连接.txt")))
|
||
with 数据库工厂(维护, 用途.维护).连接() as 连, 连.transaction():
|
||
元数据 = 元数据服务(连)
|
||
版 = 元数据.当前策略("character").version
|
||
新 = 策略快照(
|
||
"character", 版 + 1, (字段限制("character.身份定位", ("uiVisible", "exportable")),)
|
||
)
|
||
元数据.更新策略(
|
||
新,
|
||
启用命令(
|
||
"hide-analysis-field",
|
||
定义哈希(asdict(新)),
|
||
版,
|
||
"synthetic-review",
|
||
"type:character",
|
||
),
|
||
)
|
||
视图 = 查询拆书状态(装配, 身份.作者, 初["source_id"])
|
||
assert 视图["台账"] == [] and 视图["对照"] is None
|
||
assert 视图["分析"][0]["payload"]["request"]["content"] == {"名称": "林深"}
|
||
with 装配.要求数据库().连接(只读=True) as 连:
|
||
assert 装配.要求资料().读取分析(连, 身份.作者, 初["analysis_id"]) == 初
|
||
with pytest.raises(Muse错误, match="不授予AI消费"):
|
||
装配.要求资料().读取分析(连, 身份.作者, 初["analysis_id"], 字段用途="aiContext")
|
||
assert 核对迁移结果(装配, 身份, [源], 映射, **参数)["data_checks_passed"] is True
|
||
|
||
|
||
@pytest.mark.parametrize("坏形状", ["空步骤", "超限", "未知键"])
|
||
def test_回放请求有界且非法形状不读取额外来源__213002(目标方法环境, 坏形状):
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
输入, 映射, 请求 = _来源回放输入(装配, 身份)
|
||
导入快照(装配, 身份, 输入, 映射, **参数)
|
||
if 坏形状 == "空步骤":
|
||
请求["steps"] = []
|
||
elif 坏形状 == "超限":
|
||
请求["steps"] = [请求["steps"][0]] * 501
|
||
else:
|
||
请求["unknown"] = True
|
||
前 = _业务与索引指纹(装配)
|
||
结果 = 核对迁移结果(装配, 身份, 输入, 映射, 来源回放=请求, **参数)
|
||
assert 结果["data_checks_passed"] is False
|
||
assert 结果["lineage_replay"]["checked_sources"] == 0
|
||
assert 结果["lineage_replay"]["issues"] == [{"reason_code": "legacy_lineage_invalid"}]
|
||
assert _业务与索引指纹(装配) == 前
|
||
|
||
|
||
@pytest.mark.parametrize("use", ["author", "reference"])
|
||
@pytest.mark.parametrize("分片", [False, True])
|
||
def test_PG作品章block真实公开导入与幂等重放_零自动确认__8f17e3(
|
||
目标方法环境, monkeypatch, use, 分片
|
||
):
|
||
"""真实PG用例;合成文本不是数据库替身,需要父代理显式外部环境运行。"""
|
||
from test_PG作品正文迁移 import 合成PG组
|
||
|
||
from muse.正文写作.接口 import 可见文本
|
||
from PG作品映射 import PG目标ID
|
||
|
||
装配, 身份, _, _, 参数 = 目标方法环境
|
||
rows, value = 合成PG组(use=use)
|
||
mapping = 映射配置(value)
|
||
calls = []
|
||
正式 = 装配.要求作品().正式
|
||
submit = 正式.提交变更
|
||
|
||
def 记录提交(身份, 命令):
|
||
calls.append((命令.入口, 命令.动作.value))
|
||
assert 命令.动作.value == "manual_save"
|
||
assert 命令.审阅ID is None
|
||
return submit(身份, 命令)
|
||
|
||
monkeypatch.setattr(正式, "提交变更", 记录提交)
|
||
if 分片:
|
||
first = 导入快照(装配, 身份, list(reversed(rows[:4])), mapping, **参数)
|
||
assert first["states"] == ({"mapped": 4} if use == "author" else {"reserved": 4})
|
||
partial = 导入快照(装配, 身份, [rows[4]], mapping, **参数)
|
||
assert partial["states"] == {"reserved": 1}
|
||
导入快照(装配, 身份, rows[5:], mapping, **参数)
|
||
result = 导入快照(装配, 身份, list(reversed(rows)), mapping, **参数)
|
||
else:
|
||
result = 导入快照(装配, 身份, list(reversed(rows)), mapping, **参数)
|
||
assert result["states"] == {"mapped": len(rows)}
|
||
ledger = 迁移台账(装配.要求数据库(), 身份, 正式, 装配=装配)
|
||
records = [ledger.读取(s.source.记录键) for s in rows]
|
||
assert all(r["original"] == s.导出() for r, s in zip(records, rows, strict=True))
|
||
assert records[1]["original"]["record"]["status"] == "confirmed"
|
||
assert (
|
||
records[1]["original"]["source"]["database"] != records[0]["original"]["source"]["database"]
|
||
)
|
||
if use == "author":
|
||
assert [c[0] for c in calls] == [
|
||
"B01.save_profile",
|
||
*("B01.change_directory" for _ in range(3)),
|
||
"B05.save_candidate",
|
||
"B05.save_candidate",
|
||
]
|
||
work_id = PG目标ID(rows[0], 身份.作者, "work")
|
||
directory = 装配.要求作品().读取目录(身份, work_id)
|
||
assert [c["chapter_id"] for c in directory["chapters"]] == [
|
||
PG目标ID(c, 身份.作者, "chapter") for c in rows[1:4]
|
||
]
|
||
assert records[4]["receipt"] == records[5]["receipt"]
|
||
assert records[4]["targets"] == records[5]["targets"]
|
||
target = records[4]["targets"][0]
|
||
candidate = 装配.要求正文().读取候选(身份, target["id"], 版本=1)
|
||
assert (
|
||
可见文本(TypeAdapter(正文草稿).validate_python(candidate["document"]))
|
||
== rows[4].原行["content_text"] + "\n" + rows[5].原行["content_text"]
|
||
)
|
||
assert len(records[4]["frozen_request"]["payload"]["draft"]["block_map"]) == 2
|
||
for chapter in rows[1:4]:
|
||
assert (
|
||
装配.要求正文().读取正文(身份, PG目标ID(chapter, 身份.作者, "chapter"))["revision"]
|
||
== 0
|
||
)
|
||
# 修改当前档案后仍核原历史版本,而不是拿当前值冒充导入值。
|
||
profile = 装配.要求作品().读取表单(身份, work_id)
|
||
schema = 装配.要求作品().新档案结构(身份, work_id, "work_core", 1)
|
||
装配.要求作品().保存档案(
|
||
身份,
|
||
"pg-later-author-edit",
|
||
档案保存(work_id, profile["revision"], {"名称": "后续编辑"}, schema),
|
||
)
|
||
assert 装配.要求作品().读取档案版本依据(身份, work_id, 1)["revision"] == 1
|
||
else:
|
||
assert calls == []
|
||
assert all(r["receipt"] == records[0]["receipt"] for r in records)
|
||
assert all(
|
||
r["frozen_request"]["payload"] == {"member_of": value["pg_works"][0]["work"]}
|
||
for r in records[1:]
|
||
)
|
||
assert 装配.要求作品().列出作品(身份) == []
|
||
with 装配.要求数据库().连接(只读=True) as 连:
|
||
sources = 装配.要求资料().列出来源(连)
|
||
source = next(
|
||
s for s in sources if str(s["source_id"]) == records[0]["receipt"]["source_id"]
|
||
)
|
||
assert source["authorized_uses"] == []
|
||
version = 装配.要求资料().读取版本(连, source["source_id"], 1)
|
||
assert (
|
||
version["content"]
|
||
== rows[4].原行["content_text"] + "\n" + rows[5].原行["content_text"] + "\n\n"
|
||
)
|
||
before_calls = len(calls)
|
||
assert 导入快照(装配, 身份, rows, mapping, **参数)["states"] == result["states"]
|
||
assert len(calls) == before_calls
|
||
assert [ledger.读取(s.source.记录键)["receipt"] for s in rows] == [
|
||
r["receipt"] for r in records
|
||
]
|
||
assert 核对迁移结果(装配, 身份, rows, mapping, **参数)["data_checks_passed"]
|