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

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

847 lines
36 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.

"""合成快照的纯分流证据;不代替清单中 TC-knowledge-* 的隔离 PG 导入验收。"""
from __future__ import annotations
import copy
import json
import re
import subprocess
import sys
from pathlib import Path
import pytest
from 建立映射 import 稳定JSON, 旧快照, 映射配置, 身份映射, 迁移错误
from 知识记录分流 import 分流批次, 分流知识记录
@pytest.fixture
def 映射原值():
作品 = [
{"legacy_id": 0, "use": "method", "target_ref": "author-library:a"},
{"legacy_id": 7, "use": "author", "target_ref": "author-work:7"},
{"legacy_id": 8, "use": "reference", "target_ref": "reference:8"},
{"legacy_id": 9, "use": "method", "target_ref": "author-library:a"},
]
来源 = [
{
"source_type": "parse_book",
"legacy_id": 8,
"work_id": 8,
"target_ref": "source:8",
"revision": "source-v1",
"aliases": ["甲书"],
"chapters": {"12": "source:8:chapter12"},
},
{
"source_type": "parse_book",
"legacy_id": 10,
"work_id": 10,
"target_ref": "source:10",
"revision": "source-v2",
"aliases": ["乙书"],
"chapters": {"12": "source:10:chapter12"},
},
{
"source_type": "chapter_extract",
"legacy_id": 70,
"work_id": 7,
"target_ref": "chapter:70",
"revision": "body-v1",
},
]
对象 = [
{"work_id": 7, "refs": ["entity:11", "draft:101", "name:甲"], "target_ref": "world:甲"},
{"work_id": 7, "refs": ["entity:12", "draft:102", "name:乙"], "target_ref": "world:乙"},
]
通用 = {"system": "pg", "database": "synthetic-legacy"}
def 模式(属性):
return {"type": "object", "properties": 属性, "additionalProperties": False}
return {
"works": [{**通用, "author_id": "author:a", **r} for r in 作品],
"sources": [{**通用, **r} for r in 来源],
"objects": [{**通用, "use": "author", "scope": "author-work:7", **r} for r in 对象],
"schemas": {
"character": 模式({"性格": {"type": "string"}}),
"location": 模式({}),
"craft": 模式({"原理": {"type": "string"}}),
"character_relation": 模式(
{k: {"type": "string"} for k in ("甲方", "乙方", "关系类型")}
),
"style": 模式({}),
"pacing": 模式({}),
},
}
def _快照(载荷=None, *, 源=None, **覆盖):
原行 = {
"work_id": 7,
"status": "pending",
"draft_type": "entity",
"source_type": "chapter_extract",
"source_id": 70,
"source_status": "active",
"source_action_policy": "allowed",
"draft_payload": 载荷
if 载荷 is not None
else {"type": "character", "name": "甲", "fields": {"性格": "果断"}},
**覆盖,
}
return 旧快照.从载荷(
{
"source": {
"system": "pg",
"database": "synthetic-legacy",
"table": "muse_knowledge_draft",
"id": "1",
"revision": "1",
**(源 or {}),
},
"record": 原行,
}
)
def _判定(原值, 快照):
结果 = 分流知识记录(快照, 映射配置(原值))
assert 结果.original == 快照.导出()
assert 结果.source_hash == 快照.源哈希
return 结果
def _公共(**覆盖):
return _快照(
{"型": "craft", "名称": "合成铺垫法", "字段": {"原理": "先建立可核对的条件"}, **覆盖},
work_id=0,
source_type="parse_book",
source_id=8,
)
@pytest.mark.case_id(
"NC-w21-21a001",
environment="离线合成JSON/SQLite与CLI;不含目标PG导入",
given="公共范围五型合成范式",
when="对明确合成源执行只读加载、纯分流、身份核对或独立CLI",
then=["B04候选保留类型、原书引用,不生成作品0"],
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
def test_公共五型保持候选与明确作者库__21a001(映射原值):
结果 = _判定(映射原值, _公共())
assert (结果.rule_id, 结果.owner, 结果.state) == ("K01", "B04", "pending")
assert 结果.scope == "author-library:a" and 结果.type_id == "craft"
assert 结果.references == ({"target_ref": "source:8", "revision": "source-v1"},)
@pytest.mark.case_id(
"NC-w21-21a002",
environment="离线合成JSON/SQLite与CLI;不含目标PG导入",
given="作者作品实体及双用途映射",
when="对明确合成源执行只读加载、纯分流、身份核对或独立CLI",
then=["B02未决提案保留字段;多用途须按源哈希显式选择"],
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
def test_作者实体字段与来源不丢失__21a002(映射原值):
结果 = _判定(映射原值, _快照())
assert (结果.rule_id, 结果.owner, 结果.state) == ("K02", "B02", "pending")
assert 结果.content["fields"] == {"性格": "果断"}
assert 结果.references[0]["target_ref"] == "chapter:70"
重复用途 = {**映射原值["works"][1], "use": "reference", "target_ref": "reference:7"}
映射原值["works"].append(重复用途)
assert _判定(映射原值, _快照()).reason_code == "unknown_use"
快照 = _快照()
映射原值["routes"] = [
{
"system": "pg",
"database": "synthetic-legacy",
"source_key": 快照.source.记录键,
"source_hash": 快照.源哈希,
"use": "author",
}
]
assert _判定(映射原值, 快照).owner == "B02"
@pytest.mark.case_id(
"NC-w21-21a003",
environment="离线合成JSON/SQLite与CLI;不含目标PG导入",
given="参考作品的同型实体",
when="对明确合成源执行只读加载、纯分流、身份核对或独立CLI",
then=["仅建议B03分析,来源作品不一致时隔离"],
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
def test_同模具参考实体不进入作者事实__21a003(映射原值):
结果 = _判定(映射原值, _快照(work_id=8, source_type="parse_book", source_id=8))
assert (结果.rule_id, 结果.owner, 结果.scope) == ("K03", "B03", "reference:8")
assert _判定(映射原值, _快照(work_id=8)).reason_code == "source_scope_conflict"
@pytest.mark.case_id(
"NC-w21-21a004",
environment="离线合成JSON/SQLite与CLI;不含目标PG导入",
given="同一旧对象两个版本的entity形关系与relation形关系",
when="对明确合成源执行只读加载、纯分流、身份核对或独立CLI",
then=["端点及关系类型一致,来源对象身份不因行形状变化"],
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
def test_实体形关系和显式关系共享身份与端点__21a004(映射原值):
a = _快照(
{
"type": "character_relation",
"name": "甲乙师徒",
"fields": {"甲方": "甲", "乙方": "乙", "关系类型": "师徒"},
}
)
b = _快照(
{"type": "师徒", "source": 11, "target": 12}, draft_type="relation", 源={"revision": "2"}
)
x, y = _判定(映射原值, a), _判定(映射原值, b)
assert (x.rule_id, y.rule_id) == ("K04", "K05")
assert x.target_ref == y.target_ref
for r in (x, y):
assert r.owner == "B02" and r.type_id == "character_relation"
assert (r.content["source"], r.content["target"], r.content["relation_type"]) == (
"world:甲",
"world:乙",
"师徒",
)
@pytest.mark.case_id(
"NC-w21-21a005",
environment="离线合成JSON/SQLite与CLI;不含目标PG导入",
given="缺端点、同名或名称编号矛盾",
when="对明确合成源执行只读加载、纯分流、身份核对或独立CLI",
then=["分别隔离并保留原载荷,不建立不完整关系"],
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
@pytest.mark.parametrize(
"问题,原因",
[
("缺失", "missing_endpoint"),
("同名", "ambiguous_endpoint"),
("编号矛盾", "endpoint_conflict"),
],
)
def test_关系端点缺失或多义不得猜测__21a005(映射原值, 问题, 原因):
载荷 = {"type": "师徒", "甲方": "甲", "乙方": "乙"}
if 问题 == "缺失":
映射原值["objects"].pop()
elif 问题 == "同名":
映射原值["objects"].append({**映射原值["objects"][0], "target_ref": "world:重名甲"})
else:
载荷["source"] = 12
结果 = _判定(映射原值, _快照(载荷, draft_type="relation"))
assert 结果.disposition == "隔离" and 结果.reason_code == 原因
assert 结果.target_ref == ""
@pytest.mark.case_id(
"NC-w21-21a006",
environment="离线合成JSON/SQLite与CLI;不含目标PG导入",
given="有确认声明和目标映射的历史草稿",
when="对明确合成源执行只读加载、纯分流、身份核对或独立CLI",
then=["目标旧编号与声明一致才承接历史;同源登记幂等"],
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
def test_确认历史核对目标并按源身份幂等__21a006(映射原值):
快照 = _快照(status="confirmed", entity_id=11)
确认 = {
"system": "pg",
"database": "synthetic-legacy",
"source_key": 快照.source.记录键,
"source_hash": 快照.源哈希,
"target_ref": "world:甲",
"decision_ref": "legacy-decision:1",
}
映射原值["confirmations"] = [确认]
结果 = _判定(映射原值, 快照)
assert 结果.state == "confirmed_history" and 结果.target_ref == "world:甲"
台账 = 身份映射()
assert 台账.登记(快照, 结果.target_ref, 结果.rule_id, scope=结果.scope) is True
assert 台账.登记(快照, 结果.target_ref, 结果.rule_id, scope=结果.scope) is False
assert len(台账.记录) == 1
坏 = _快照(status="confirmed", entity_id=99)
确认.update(source_hash=坏.源哈希)
assert _判定(映射原值, 坏).state == "migration_review_required"
assert _判定(映射原值, 坏).target_ref == ""
@pytest.mark.case_id(
"NC-w21-21a007",
environment="离线合成JSON/SQLite与CLI;不含目标PG导入",
given="type与型指向不同类别",
when="对明确合成源执行只读加载、纯分流、身份核对或独立CLI",
then=["隔离且保留两值,不按优先级覆盖"],
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
def test_双键冲突不按优先级吞值__21a007(映射原值):
快照 = _公共(type="character")
结果 = _判定(映射原值, 快照)
assert 结果.disposition == "隔离" and 结果.reason_code == "type_conflict"
assert 结果.original["record"]["draft_payload"]["型"] == "craft"
assert 结果.original["record"]["draft_payload"]["type"] == "character"
@pytest.mark.case_id(
"NC-w21-21a008",
environment="离线合成JSON/SQLite与CLI;不含目标PG导入",
given="work_id=0的非范式",
when="对明确合成源执行只读加载、纯分流、身份核对或独立CLI",
then=["不猜成方法库或作者作品"],
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
def test_零哨兵不能成为作品或普通公共实体__21a008(映射原值):
结果 = _判定(
映射原值,
_快照(
{"type": "location", "name": "旧城", "fields": {}},
work_id=0,
source_type="parse_book",
source_id=8,
),
)
assert 结果.reason_code == "zero_work_not_pattern" and not 结果.target_ref
@pytest.mark.case_id(
"NC-w21-21a009",
environment="离线合成JSON/SQLite与CLI;不含目标PG导入",
given="实例引用两本书相同章号",
when="对明确合成源执行只读加载、纯分流、身份核对或独立CLI",
then=["分别保留各自来源版本与稳定章定位,缺定位隔离"],
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
def test_跨书实例分别绑定来源和章定位__21a009(映射原值):
快照 = _公共(
实例=[
{"书": "甲书", "章": 12, "引文": "合成甲"},
{"书": "乙书", "章": 12, "引文": "合成乙"},
]
)
结果 = _判定(映射原值, 快照)
assert [r["locator"] for r in 结果.references[1:]] == [
"source:8:chapter12",
"source:10:chapter12",
]
del 映射原值["sources"][1]["chapters"]["12"]
assert _判定(映射原值, 快照).reason_code == "missing_location"
@pytest.mark.case_id(
"NC-w21-21a00a",
environment="离线合成JSON/SQLite与CLI;不含目标PG导入",
given="软删、拒绝、替代、撤权的旧记录",
when="对明确合成源执行只读加载、纯分流、身份核对或独立CLI",
then=["只留历史,不产生活跃对象或索引"],
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
@pytest.mark.parametrize(
"覆盖",
[
{"deleted": True},
{"status": "rejected"},
{"status": "discarded"},
{"status": "superseded"},
{"source_status": "withdrawn"},
{"source_action_policy": "denied"},
],
)
def test_删除拒绝和撤权不复活__21a00a(映射原值, 覆盖):
结果 = _判定(映射原值, _快照(**覆盖))
assert 结果.disposition == "历史保留" and not 结果.target_ref and not 结果.content
@pytest.mark.case_id(
"NC-w21-21a00b",
environment="离线合成JSON/SQLite与CLI;不含目标PG导入",
given="未知动态字段、类型错误或缺必填",
when="对明确合成源执行只读加载、纯分流、身份核对或独立CLI",
then=["原值完整隔离,不补造或忽略"],
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
@pytest.mark.parametrize(
"字段,原因",
[
({"陌生字段": {"原值": [1, 2]}}, "unknown_fields"),
({"性格": 3}, "field_type_mismatch"),
({}, "missing_required"),
],
)
def test_未知或不合法动态字段原样隔离__21a00b(映射原值, 字段, 原因):
映射原值["schemas"]["character"]["required"] = ["性格"]
快照 = _快照({"type": "character", "name": "甲", "fields": 字段})
结果 = _判定(映射原值, 快照)
assert 结果.reason_code == 原因 and 结果.disposition == "隔离"
assert 结果.original["record"]["draft_payload"]["fields"] == 字段
@pytest.mark.case_id(
"NC-w21-21a00c",
environment="离线合成JSON/SQLite与CLI;不含目标PG导入",
given="PG与SQLite同号相同内容",
when="对明确合成源执行只读加载、纯分流、身份核对或独立CLI",
then=["来源身份不合并,陌生数据库不能借用映射"],
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
def test_来源命名空间区分同号对象__21a00c(映射原值):
for 类别 in ("works", "sources", "objects"):
映射原值[类别] += [{**r, "system": "sqlite"} for r in 映射原值[类别]]
x = _判定(映射原值, _快照())
y = _判定(映射原值, _快照(源={"system": "sqlite"}))
assert x.source_key != y.source_key and x.target_ref != y.target_ref
assert x.source_hash == y.source_hash
未知 = _判定(映射原值, _快照(源={"database": "another-database"}))
assert 未知.reason_code == "unknown_use"
@pytest.mark.case_id(
"NC-w21-21a00d",
environment="离线合成JSON/SQLite与CLI;不含目标PG导入",
given="本书风格、节奏和明确方法用途写法",
when="对明确合成源执行只读加载、纯分流、身份核对或独立CLI",
then=["按实际用途进入B06、B01、B03或B04建议,不凭类型转正"],
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
@pytest.mark.parametrize(
"类型,作品号,owner",
[
("style", 7, "B06"),
("pacing", 7, "B01"),
("style", 8, "B03"),
("pacing", 8, "B03"),
("character", 9, "B04"),
],
)
def test_通用写法和本书风格按用途分流__21a00d(映射原值, 类型, 作品号, owner):
来源号, 来源型 = (70, "chapter_extract") if 作品号 == 7 else (8, "parse_book")
结果 = _判定(
映射原值,
_快照(
{"type": 类型, "name": "合成写法", "fields": {}},
work_id=作品号,
source_id=来源号,
source_type=来源型,
),
)
assert 结果.rule_id == "K07" and 结果.owner == owner and 结果.state == "pending"
@pytest.mark.case_id(
"NC-w21-21a00e",
environment="离线合成JSON/SQLite与CLI;不含目标PG导入",
given="向量指针和内容镜像",
when="对明确合成源执行只读加载、纯分流、身份核对或独立CLI",
then=["仅承接来源身份;无确认来源不新造候选或正式对象"],
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
def test_向量只承接指针且内容镜像不复制候选__21a00e(映射原值):
向量 = _快照(
源={"table": "example_knowledge_embedding"},
entity_id=11,
embedding=[0.2, 0.3],
model="synthetic-v1",
dimensions=2,
)
结果 = _判定(映射原值, 向量)
assert (结果.rule_id, 结果.owner, 结果.state) == ("K08", "B09", "historical")
assert 结果.content == {} and 结果.references == ({"target_ref": "world:甲"},)
@pytest.mark.case_id(
"NC-w21-21a00f",
environment="离线合成JSON/SQLite与CLI;不含目标PG导入",
given="同源同版本内容改变及新版本",
when="对明确合成源执行只读加载、纯分流、身份核对或独立CLI",
then=["原快照不可变;哈希、目标漂移拒绝;版本变化不复制身份"],
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
def test_不可变快照和相同身份漂移__21a00f(映射原值):
原 = _快照()
值 = 原.原行
值["draft_payload"]["name"] = "不可污染冻结原文"
assert 原.原行["draft_payload"]["name"] == "甲"
坏 = _快照({"type": "character", "name": "乙", "fields": {"性格": "果断"}})
台账 = 身份映射()
台账.登记(原, "world:甲", "K02", scope="author-work:7")
with pytest.raises(迁移错误, match="drift"):
台账.登记(坏, "world:甲", "K02", scope="author-work:7")
assert all(
r.disposition == "隔离" and r.reason_code == "drift"
for r in 分流批次([原, 坏], 映射配置(映射原值))
)
with pytest.raises(迁移错误, match="snapshot_hash_mismatch"):
旧快照.从载荷({**原.导出(), "source_hash": "0" * 64})
新版本 = _快照(源={"revision": "2"})
assert 原.建议身份(
"B02", "character", scope="author-work:7", author_id="author:a"
) == 新版本.建议身份("B02", "character", scope="author-work:7", author_id="author:a")
with pytest.raises(迁移错误, match="identity_conflict"):
台账.登记(新版本, "world:另一个", "K02", scope="author-work:7")
@pytest.mark.case_id(
"NC-w21-21a010",
environment="离线合成JSON/SQLite与CLI;不含目标PG导入",
given="坏JSON、重复键、不可解释或远程结构引用",
when="对明确合成源执行只读加载、纯分流、身份核对或独立CLI",
then=["隔离、不联网、不崩溃或静默补字段"],
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
def test_坏载荷与不可解释结构不崩溃不联网__21a010(映射原值):
for 载荷 in ('{"type":"character","type":"craft"}', "null", '{"type": NaN}'):
assert _判定(映射原值, _快照(载荷)).disposition == "隔离"
for 引用 in ("https://example.invalid/schema", "#/$defs/missing"):
映射原值["schemas"]["character"]["properties"]["性格"] = {"$ref": 引用}
assert _判定(映射原值, _快照()).reason_code == "schema_invalid"
assert _判定(映射原值, _快照(source_status=[])).reason_code == "permission_conflict"
for 坏标记 in (None, [], 2, "未知"):
assert _判定(映射原值, _快照(deleted=坏标记)).reason_code == "state_conflict"
assert _判定(映射原值, _快照(draft_type="unknown")).reason_code == "unknown_draft_type"
@pytest.mark.case_id(
"NC-w21-21a011",
environment="离线合成JSON/SQLite与CLI;不含目标PG导入",
given="显式合成快照和映射文件",
when="对明确合成源执行只读加载、纯分流、身份核对或独立CLI",
then=["独立CLI只生成私有计划;可重放、不覆盖输入或不同输出"],
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
def test_命令只生成计划并保护输入和已有输出__21a011(映射原值, tmp_path):
输入, 映射, 输出 = [tmp_path / n for n in ("快照.json", "映射.json", "计划.json")]
输入.write_text(稳定JSON([_公共().导出()]), encoding="utf-8")
映射.write_text(稳定JSON(映射原值), encoding="utf-8")
原文 = 输入.read_bytes()
脚本 = Path(__file__).resolve().parents[2] / "数据库/旧库迁移/入口.py"
命令 = [
sys.executable,
str(脚本),
"分流",
"--快照",
str(输入),
"--映射",
str(映射),
"--输出",
str(输出),
]
for _ in range(2):
进程 = subprocess.run(命令, capture_output=True, text=True, timeout=15)
assert 进程.returncode == 0, 进程.stderr
assert "合成铺垫法" not in 进程.stdout
计划 = json.loads(输出.read_text())
assert 计划["stage"] == "classification_only" and 计划["dispositions"] == {"待转换": 1}
assert 计划["results"][0]["original"] == _公共().导出()
assert (输出.stat().st_mode & 0o777) == 0o600
assert subprocess.run([*命令[:-1], str(输入)], capture_output=True).returncode == 1
输出.write_text("已有产物", encoding="utf-8")
assert subprocess.run(命令, capture_output=True).returncode == 1
assert 输出.read_text() == "已有产物" and 输入.read_bytes() == 原文
assert not list(tmp_path.glob(".迁移分流-*"))
@pytest.mark.case_id(
"NC-w21-21a012",
environment="离线合成JSON/SQLite与CLI;不含目标PG导入",
given="旧运行骨架、经验和缺用途映射",
when="对明确合成源执行只读加载、纯分流、身份核对或独立CLI",
then=["历史不是新任务;后续语义明确隔离;缺映射不猜"],
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
def test_运行历史不成为新任务且延后语义隔离__21a012(映射原值):
运行 = _判定(映射原值, _快照(源={"table": "example_run"}, status="completed"))
assert 运行.disposition == "历史保留" and 运行.owner == "S02" and not 运行.target_ref
经验 = _判定(映射原值, _快照(源={"table": "example_lesson"}))
assert 经验.disposition == "隔离" and 经验.reason_code == "deferred_W26_27"
无映射 = copy.deepcopy(映射原值)
无映射["works"] = []
assert _判定(无映射, _快照()).reason_code == "unknown_use"
@pytest.mark.case_id(
"NC-w21-21a013",
environment="离线合成JSON/SQLite与CLI;不含目标PG导入",
given="同书author与reference两个目标作用域",
when="对明确合成源执行只读加载、纯分流、身份核对或独立CLI",
then=["端点和确认不串用作者对象;用途选择必须匹配原快照"],
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
def test_双用途端点和确认不能串入作者作用域__21a013(映射原值):
快照 = _快照({"type": "师徒", "source": 11, "target": 12}, draft_type="relation")
映射原值["works"].append(
{**映射原值["works"][1], "use": "reference", "target_ref": "reference:7"}
)
映射原值["routes"] = [
{
"system": "pg",
"database": "synthetic-legacy",
"source_key": 快照.source.记录键,
"source_hash": 快照.源哈希,
"use": "reference",
}
]
assert _判定(映射原值, 快照).reason_code == "missing_endpoint"
映射原值["objects"] += [
{
**r,
"use": "reference",
"scope": "reference:7",
"target_ref": "reference:" + r["target_ref"],
}
for r in 映射原值["objects"]
]
关系 = _判定(映射原值, 快照)
assert 关系.owner == "B03" and 关系.content["source"] == "reference:world:甲"
作者映射 = copy.deepcopy(映射原值)
作者映射["routes"][0]["use"] = "author"
assert _判定(作者映射, 快照).target_ref != 关系.target_ref
映射原值["routes"][0]["source_hash"] = "0" * 64
assert _判定(映射原值, 快照).reason_code == "unknown_use"
已确认 = _快照(status="confirmed", entity_id=11)
映射原值["routes"][0]["source_hash"] = 已确认.源哈希
映射原值["confirmations"] = [
{
"system": "pg",
"database": "synthetic-legacy",
"source_key": 已确认.source.记录键,
"source_hash": 已确认.源哈希,
"target_ref": "world:甲",
"decision_ref": "legacy-decision:1",
}
]
assert _判定(映射原值, 已确认).state == "migration_review_required"
映射原值["confirmations"][0]["target_ref"] = "reference:world:甲"
assert _判定(映射原值, 已确认).state == "confirmed_history"
@pytest.mark.case_id(
"NC-w21-21a014",
environment="离线合成JSON/SQLite与CLI;不含目标PG导入",
given="缺来源编号、错误原行身份及目标归属变更",
when="对明确合成源执行只读加载、纯分流、身份核对或独立CLI",
then=["不能靠缺值匹配补来源;原身份或作用域变化明确拒绝"],
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
def test_缺来源编号和错误映射不能自洽补造__21a014(映射原值):
del 映射原值["sources"][2]["legacy_id"]
assert _判定(映射原值, _快照(source_id=None)).reason_code == "missing_source"
assert _判定(映射原值, _快照(source_type=[])).reason_code == "unknown_source"
with pytest.raises(迁移错误, match="source_invalid"):
_快照(id=2)
台账 = 身份映射()
台账.登记(_快照(), "world:甲", "K02", scope="author-work:7")
with pytest.raises(迁移错误, match="drift"):
台账.登记(_快照(), "world:甲", "K02", scope="author-work:另一个")
@pytest.mark.case_id(
"NC-w21-21b001",
environment="离线合成JSON/SQLite与CLI;不含目标PG导入",
given="正式实体与关系表含description及其他原列",
when="对明确合成源执行只读加载、纯分流、身份核对或独立CLI",
then=["描述完整归一,NULL为空;非法类型隔离;其余原列不丢失"],
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
@pytest.mark.parametrize("表", ["muse_knowledge_entity", "muse_knowledge_relation"])
@pytest.mark.parametrize("描述", [" 保留原描述\n第二行。", None, 0])
def test_正式表描述进入归一结果且保全其他原列__21b001(映射原值, 表, 描述):
数据 = _快照(源={"table": 表}).导出()
数据.pop("source_hash")
行 = 数据["record"]
行.pop("draft_payload")
行.pop("draft_type")
行.update(
description=描述, confidence=0.91, scope="旧作用域", lineage_payload={"来源轨迹": [8, 10]}
)
if 表 == "muse_knowledge_entity":
行.update(entity_type="character", normalized_name="甲", attributes={"性格": "果断"})
else:
行.update(source_entity_id=11, target_entity_id=12, relation_type="师徒", attributes={})
结果 = _判定(映射原值, 旧快照.从载荷(数据))
if 描述 == 0:
assert 结果.disposition == "隔离" and 结果.reason_code == "field_type_mismatch"
else:
assert 结果.content["brief"] == ("" if 描述 is None else 描述)
assert 结果.state == "migration_review_required"
assert 结果.original["record"]["lineage_payload"] == {"来源轨迹": [8, 10]}
assert 结果.original["record"]["confidence"] == 0.91
@pytest.mark.case_id(
"NC-w21-21b002",
environment="离线合成JSON/SQLite与CLI;不含目标PG导入",
given="同一旧对象跨版本的确认声明指向不同目标",
when="对明确合成源执行只读加载、纯分流、身份核对或独立CLI",
then=["整批对象在写库前隔离并保留冲突目标;同目标版本仍可承接"],
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
def test_同旧对象跨版本异目标在分流阶段全部隔离__21b002(映射原值):
一 = _快照(status="confirmed", entity_id=11)
二 = _快照(源={"revision": "2"}, status="confirmed", entity_id=12)
映射原值["confirmations"] = [
{
"system": "pg",
"database": "synthetic-legacy",
"source_key": x.source.记录键,
"source_hash": x.源哈希,
"target_ref": t,
"decision_ref": "legacy-decision:" + x.source.revision,
}
for x, t in ((一, "world:甲"), (二, "world:乙"))
]
配置 = 映射配置(映射原值)
# 两条单独输入均能关联声明;合批必须先隔离,不能等导入顺序抢占。
assert all(分流知识记录(x, 配置).state == "confirmed_history" for x in (一, 二))
结果 = 分流批次([一, 二], 配置)
assert len(结果) == 2
for r in 结果:
assert r.disposition == "隔离" and r.reason_code == "identity_conflict"
assert r.target_ref == "" and r.content == {} and r.references == ()
assert {x["target_ref"] for x in r.conflicts} == {"world:甲", "world:乙"}
assert {r.source_key for r in 结果} == {一.source.记录键, 二.source.记录键}
映射原值["confirmations"][1]["target_ref"] = "world:甲"
二同 = _快照(源={"revision": "2"}, status="confirmed", entity_id=11)
映射原值["confirmations"][1]["source_hash"] = 二同.源哈希
重复 = 分流批次([一, 二同], 映射配置(映射原值))
assert all(r.disposition == "待转换" and r.target_ref == "world:甲" for r in 重复)
def _SQLite镜像(表, *, 载荷=None, **覆盖):
目录 = Path(__file__).resolve().parents[2] / "数据库/旧库迁移/旧结构/SQLite"
文件 = "0001_init.sql" if 表 == "cards" else "0003_snapshots.sql"
定义 = re.search(
rf"CREATE TABLE IF NOT EXISTS {表} \(\s*(.*?)\n\);",
(目录 / 文件).read_text(encoding="utf-8"),
re.S,
)
assert 定义 is not None
列 = {行.strip().split()[0] for 行 in 定义[1].splitlines() if 行.strip()}
原行 = {
"id": "1",
"kind": "character",
"payload_json": json.dumps(
{"type": "character", "name": "合成人物", "fields": {}}, ensure_ascii=False
)
if 载荷 is None
else 载荷,
"created_at": "2026-09-17T00:00:00Z",
}
if 表 == "cards":
原行 |= {
"title": "合成卡",
"embedding": None,
"work_id": 7,
"source_path": None,
"content_hash": None,
}
原行 |= 覆盖
assert set(原行) == 列
return 旧快照.从载荷(
{
"source": {
"system": "sqlite",
"database": "synthetic-legacy",
"table": 表,
"id": "1",
"revision": "export-v1",
},
"record": 原行,
}
)
@pytest.mark.case_id(
"TC-b0aa6de304b1",
environment="离线合成快照与连接替身",
when="旧库分流或迁移目标检查",
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
@pytest.mark.parametrize(
"表,预期", [("cards", "permission_conflict"), ("snapshots", "unknown_use")]
)
def test_真实SQLite镜像列缺身份授权保留隔离__b0aa6d(映射原值, 表, 预期):
for 集合 in ("works", "sources", "objects"):
for 行 in 映射原值[集合]:
行["system"] = "sqlite"
原 = _SQLite镜像(表)
结果 = _判定(映射原值, 原)
assert 结果.disposition == "隔离" and 结果.reason_code == 预期
assert not 结果.target_ref and not 结果.content
assert "source_status" not in 原.原行 and "source_action_policy" not in 原.原行
@pytest.mark.case_id(
"TC-ba2a5493ca6c",
environment="离线合成快照与连接替身",
when="旧库分流或迁移目标检查",
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
@pytest.mark.parametrize("表", ["cards", "snapshots"])
def test_真实SQLite坏载荷先报告载荷错误__ba2a54(映射原值, 表):
结果 = _判定(映射原值, _SQLite镜像(表, 载荷="[]"))
assert 结果.disposition == "隔离" and 结果.reason_code == "payload_invalid"
@pytest.mark.case_id(
"TC-19b1d5944236",
environment="离线合成快照与连接替身",
when="旧库分流或迁移目标检查",
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
@pytest.mark.parametrize("表", ["cards", "snapshots"])
def test_镜像不能注入旧DDL不存在的授权列__19b1d5(映射原值, 表):
原 = _SQLite镜像(表).导出()
原.pop("source_hash")
原["record"] |= {"source_status": "active", "source_action_policy": "allowed", "work_id": 7}
结果 = _判定(映射原值, 旧快照.从载荷(原))
assert 结果.disposition == "隔离" and 结果.reason_code == "unknown_fields"
assert not 结果.target_ref
@pytest.mark.case_id(
"TC-860d6194955c",
environment="离线合成快照与连接替身",
when="旧库分流或迁移目标检查",
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
def test_镜像不能伪称PG表__860d61(映射原值):
原 = _SQLite镜像("cards").导出()
原["source"]["system"] = "pg"
assert _判定(映射原值, 旧快照.从载荷(原)).reason_code == "unknown_table"
@pytest.mark.case_id(
"TC-0b85026a6ff7",
environment="离线合成快照与连接替身",
when="旧库分流或迁移目标检查",
contract="docs/系统架构/新版设计/数据模型/旧知识记录分流.md",
)
def test_冻结白名单未承接表不能凭历史状态绕过隔离__0b8502(映射原值):
from 建立映射 import 旧表白名单
from 知识记录分流 import 已登记表
PG白名单 = 旧表白名单("pg")
白名单 = PG白名单 | 旧表白名单("sqlite")
未承接 = 白名单 - 已登记表
assert len(白名单) == 66 and len(已登记表) == 26 and len(未承接) == 40
for 表 in sorted(未承接):
系统 = "pg" if 表 in PG白名单 else "sqlite"
for 状态 in ("pending", "deleted"):
原 = 旧快照.从载荷(
{
"source": {
"system": 系统,
"database": "synthetic-legacy",
"table": 表,
"id": "1",
"revision": "export-v1",
},
"record": {"status": 状态},
}
)
结果 = _判定(映射原值, 原)
assert 结果.disposition == "隔离" and 结果.reason_code == "unknown_table"