muse-agent-example/tests/skills/备份作品抽取结果/test_backup_upgrade_work_offline.py

651 lines
27 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.

#!/usr/bin/env python3
"""升格作品备份脚本的纯离线回归测试:不连接数据库、不调用模型。"""
import datetime as dt
import json
import pathlib
import shutil
import sys
import uuid
from contextlib import contextmanager
from decimal import Decimal
from unittest import mock
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "entity" / "skills" / "ingest" / "备份作品抽取结果" / "scripts"
sys.path.insert(0, str(SCRIPT_DIR))
import backup_upgrade_work as backup # noqa: E402
class _Result:
"""模拟 psycopg 查询结果,仅暴露脚本使用的读取接口。"""
def __init__(self, rows):
self.rows = rows
def fetchone(self):
return self.rows[0] if self.rows else None
def fetchall(self):
return list(self.rows)
class _OfflineConnection:
"""按表名返回固定数据,并记录完整 SQL 与连接关闭时机。"""
def __init__(self):
self.closed = False
self.executions = []
self.rows = {
"drafts": [
{
"id": 2,
"work_id": 8,
"source_type": "upgrade_book",
"confidence": Decimal("0.8200"),
"draft_payload": {"名称": "乙"},
"create_time": dt.datetime(2026, 7, 21, 9, 1, 2, 3000),
"deleted": True,
"tenant_id": 1,
},
{
"id": 1,
"work_id": 8,
"source_type": "upgrade_book",
"confidence": Decimal("0.90"),
"draft_payload": {"名称": "甲"},
"create_time": dt.datetime(2026, 7, 21, 9, 1, 1),
"deleted": False,
"tenant_id": 1,
},
],
"windows": [
{"id": 12, "work_id": 8, "window_no": 2, "from_chapter": 2,
"to_chapter": 2, "deleted": False, "tenant_id": 1},
{"id": 11, "work_id": 8, "window_no": 1, "from_chapter": 1,
"to_chapter": 1, "deleted": False, "tenant_id": 1},
],
"aliases": [{"id": 21, "work_id": 8, "alias": "阿甲", "tenant_id": 1}],
"presence": [{"id": 31, "work_id": 8, "name": "路人", "tenant_id": 1}],
"card_state": [{"draft_id": 2, "work_id": 8, "watermark_window": 2,
"tenant_id": 1}],
"audits": [{"id": 41, "draft_id": 2, "window_no": 2, "tenant_id": 1}],
"embeddings": [{"id": 51, "draft_id": 2, "embedding": "[0.1,0.20]",
"content_hash": "a" * 64, "tenant_id": 1}],
}
def __enter__(self):
return self
def __exit__(self, exc_type, exc, traceback):
self.closed = True
def execute(self, sql, params=None):
normalized = " ".join(sql.split()).lower()
self.executions.append((normalized, params))
if normalized.startswith("begin") or normalized in {"commit", "rollback"}:
return _Result([])
if "current_database()" in normalized:
return _Result([{"database": "muse-example", "server_addr": "100.64.0.8",
"server_port": 5433, "server_version": "160000"}])
if "from muse_content_chapter c" in normalized:
return _Result([
{"chapter_id": 1, "chapter_order": 1, "chapter_title": "一",
"chapter_revision": 1, "block_id": 101, "block_order": 1,
"block_type": "scene", "block_title": None, "content_doc": None,
"content_text": "正文一", "block_revision": 1},
{"chapter_id": 2, "chapter_order": 2, "chapter_title": "二",
"chapter_revision": 1, "block_id": 102, "block_order": 1,
"block_type": "scene", "block_title": None, "content_doc": None,
"content_text": "正文二", "block_revision": 1},
])
if "from muse_meta_schema s" in normalized:
return _Result([
{"schema_key": key, "active_version_id": index,
"field_contract_snapshot": {"key": key}}
for index, key in enumerate(sorted(backup.UPGRADE_SCHEMA_KEYS), start=1)
])
if "from muse_content_work" in normalized:
return _Result([{"title": "离线测试书"}])
table_to_domain = {
"from muse_knowledge_draft d": "drafts",
"from example_upgrade_window w": "windows",
"from example_upgrade_alias a": "aliases",
"from example_upgrade_presence p": "presence",
"from example_upgrade_card_state s": "card_state",
"from example_upgrade_audit a": "audits",
"from example_knowledge_embedding e": "embeddings",
}
for marker, domain in table_to_domain.items():
if marker in normalized:
return _Result(self.rows[domain])
raise AssertionError(f"未覆盖的离线 SQL:{normalized}")
class _RestoreConnection:
"""记录恢复事务 SQL;查询默认返回空集合,便于只验证恢复边界。"""
def __init__(self, sequence_last=100, table_max=50, extra_window=False):
self.closed = False
self.executions = []
self.sequence_last = sequence_last
self.table_max = table_max
self.extra_window = extra_window
def __enter__(self):
return self
def __exit__(self, exc_type, exc, traceback):
self.closed = True
def execute(self, sql, params=None):
normalized = " ".join(sql.split()).lower()
self.executions.append((normalized, params))
if "from pg_sequences" in normalized:
return _Result([{"last_value": self.sequence_last}])
if "select max(id) as max_id" in normalized:
return _Result([{"max_id": self.table_max}])
if "pg_get_serial_sequence" in normalized:
table = params[0] if params else "unknown"
return _Result([{"sequence_name": f"public.{table}_id_seq"}])
if "from example_upgrade_window w" in normalized and "not (w.id = any" in normalized:
return _Result([{"id": 999}] if self.extra_window else [])
return _Result([])
def _new_backup_dir():
"""返回 /private/tmp 下尚不存在的测试目录。"""
return pathlib.Path("/private/tmp") / f"backup-upgrade-offline-{uuid.uuid4()}"
def _code_identity():
"""离线测试固定代码身份,避免读取工作区状态影响摘要。"""
return {
"gitCommit": "0123456789abcdef",
"gitHeadCoversCodeFiles": False,
"codeFiles": {
"backup_upgrade_work.py": "b" * 64,
"parse_upgrade.py": "u" * 64,
"parse_llm.py": "p" * 64,
"embed_drafts.py": "e" * 64,
"upgrade_work_lock.py": "k" * 64,
"llm.py": "l" * 64,
"parse-book/SKILL.md": "s" * 64,
},
"headMatches": {},
}
@contextmanager
def _no_lock(*_args, **_kwargs):
"""离线测试锁替身,不连接 PostgreSQL。"""
yield (1, 2)
def _check(name, condition):
"""沿用 parse-book 离线测试的简明输出格式。"""
if not condition:
raise AssertionError(name)
print(f"[PASS] {name}")
def test_stable_sort_and_json_types():
"""相同主键集合不受查询返回顺序影响,特殊数据库类型可稳定 JSON 化。"""
rows = [
{"id": 10, "at": dt.datetime(2026, 7, 21, 8, 0), "score": Decimal("1.2300")},
{"id": 2, "at": dt.date(2026, 7, 20), "score": Decimal("0.10")},
]
ordered = backup.sort_rows("drafts", rows)
_check("排序稳定-按域主键升序", [row["id"] for row in ordered] == [2, 10])
encoded = json.loads(backup.canonical_json(ordered[1]))
_check("JSON类型-datetime转ISO字符串", encoded["at"] == "2026-07-21T08:00:00")
_check("JSON类型-Decimal保留精度字符串", encoded["score"] == "1.2300")
vector = json.loads(backup.canonical_json({"embedding": [Decimal("0.10"), Decimal("-2.0")]}))
_check("JSON类型-vector元素稳定序列化", vector["embedding"] == ["0.10", "-2.0"])
def test_path_guards():
"""输出只能是 /private/tmp 下的新目录。"""
for invalid in ("/tmp/not-explicit", "/private/tmp", "/private/tmp/../etc/backup"):
try:
backup.validate_new_output_dir(pathlib.Path(invalid))
except ValueError:
pass
else:
raise AssertionError(f"路径越界未拒绝:{invalid}")
_check("路径越界-全部拒绝", True)
existing = _new_backup_dir()
existing.mkdir(mode=0o700)
try:
try:
backup.validate_new_output_dir(existing)
except FileExistsError:
_check("重复目录-拒绝覆盖", True)
else:
raise AssertionError("已存在目录未拒绝")
finally:
existing.rmdir()
def test_backup_transaction_queries_and_round_trip():
"""七域在单个只读一致性事务中读取,关库后再从磁盘复验。"""
output = _new_backup_dir()
conn = _OfflineConnection()
original_verify = backup.verify_backup
def verify_after_close(path):
if not conn.closed:
raise AssertionError("数据库连接尚未关闭就开始磁盘复验")
return original_verify(path)
try:
with mock.patch.object(backup, "verify_backup", side_effect=verify_after_close):
manifest = backup.backup_work(
work_id=8,
output_dir=output,
tenant=1,
dsn="postgresql://user:secret@example.invalid/db",
connect=lambda *_args, **_kwargs: conn,
git_commit="0123456789abcdef",
code_identity=_code_identity(),
expected_chapters=2,
expected_windows=2,
lock_context=_no_lock,
)
_check("事务-连接关闭后复验", conn.closed)
statements = [sql for sql, _params in conn.executions]
_check("事务-仅一个REPEATABLE READ READ ONLY", statements.count(
"begin isolation level repeatable read read only") == 1)
_check("事务-正常提交一次", statements.count("commit") == 1)
_check("事务-无数据库写语句", not any(
sql.startswith(("insert ", "update ", "delete ", "alter ", "drop "))
for sql in statements
))
_check("七域-文件与清单齐全", set(manifest["domains"]) == set(backup.DOMAIN_SPECS))
# 作品标题一条 SELECT,加七域各一条 SELECT;draft 子查询不应被误算成独立域查询。
_check("七域与input-各查询只执行一次",
sum(sql.startswith("select") for sql in statements) == 11)
sql_by_domain = {
name: next(sql for sql in statements if f"from {spec.table} " in sql)
for name, spec in backup.DOMAIN_SPECS.items()
}
_check("查询边界-drafts含软删", "d.deleted" not in sql_by_domain["drafts"])
_check("查询边界-windows仅active", "w.deleted = false" in sql_by_domain["windows"])
_check("查询边界-audits指向目标draft", "a.draft_id in" in sql_by_domain["audits"]
and "source_type" in sql_by_domain["audits"])
_check("查询边界-embeddings指向目标draft", "e.draft_id in" in sql_by_domain["embeddings"]
and "source_type" in sql_by_domain["embeddings"])
_check("清单-身份与边界摘要齐全", manifest["work"] == 8
and manifest["tenant"] == 1
and manifest["title"] == "离线测试书"
and len(manifest["windowBoundarySha"]) == 64
and len(manifest["inputSha"]) == 64)
_check("input-同快照绑定Canonical窗合同代码与计划",
manifest["input"]["canonicalContent"]["chapterCount"] == 2
and manifest["input"]["windowBoundary"]["count"] == 2
and manifest["input"]["activeFieldContracts"]["count"] == 7
and manifest["input"]["codeFiles"]["llm.py"] == "l" * 64
and manifest["input"]["plan"] == {
"model": "MiniMax-M3", "semanticDedup": True})
_check("清单-不泄露数据库密码", "secret" not in json.dumps(manifest, ensure_ascii=False))
_check("排序稳定-JSONL按主键", [
json.loads(line)["id"]
for line in (output / "drafts.jsonl").read_text(encoding="utf-8").splitlines()
] == [1, 2])
_check("原子写-无临时文件残留", not list(output.glob("*.tmp")))
_check("离线复验-七域校验通过", original_verify(output)["backup_id"] == manifest["backup_id"])
finally:
shutil.rmtree(output, ignore_errors=True)
def test_accidental_corruption_detection():
"""任一 JSONL 意外损坏后离线 verify 必须失败;不模拟恶意重写摘要。"""
output = _new_backup_dir()
conn = _OfflineConnection()
try:
backup.backup_work(
work_id=8,
output_dir=output,
tenant=1,
dsn="unused",
connect=lambda *_args, **_kwargs: conn,
git_commit="fedcba9876543210",
code_identity=_code_identity(),
expected_chapters=2,
expected_windows=2,
lock_context=_no_lock,
)
with (output / "aliases.jsonl").open("ab") as stream:
stream.write(b'{"id":999}\n')
try:
backup.verify_backup(output)
except backup.VerificationError:
_check("意外损坏检测-文件变化被拒绝", True)
else:
raise AssertionError("意外损坏后的备份仍通过 verify")
finally:
shutil.rmtree(output, ignore_errors=True)
def test_dict_row_and_tuple_rejection():
"""真实连接必须请求 dict_row,tuple 行不得被误转成 dict。"""
with mock.patch("psycopg.connect") as connect:
backup._default_connect("postgresql://example.invalid/db", autocommit=True)
from psycopg.rows import dict_row
_check("连接-dict_row", connect.call_args.kwargs["row_factory"] is dict_row)
try:
backup.sort_rows("drafts", [(1, 8)])
except backup.BackupError:
_check("真实rows-tuple拒绝", True)
else:
raise AssertionError("tuple 数据行被错误接受")
def _restore_manifest():
"""构造恢复 SQL 测试所需的最小已验证 manifest。"""
input_summary = {
"databaseIdentity": {"database": "muse-example", "serverAddr": "100.64.0.8",
"serverPort": 5433, "serverVersion": "160000"},
"canonicalContent": {"chapterCount": 594, "blockCount": 594, "sha": "c" * 64},
"windowBoundary": {"count": 116, "sha": "w" * 64},
"activeFieldContracts": {"count": 7, "sha": "f" * 64},
"codeFiles": {
"backup_upgrade_work.py": "b" * 64,
"parse_upgrade.py": "u" * 64,
"parse_llm.py": "p" * 64,
"embed_drafts.py": "e" * 64,
"upgrade_work_lock.py": "k" * 64,
"llm.py": "l" * 64,
"parse-book/SKILL.md": "s" * 64,
},
"plan": {"model": "MiniMax-M3", "semanticDedup": True},
"expectedChapters": 594,
"expectedWindows": 116,
"sourceType": "upgrade_book",
"tenant": 1,
"work": 8,
}
manifest = {
"backup_id": "11111111-1111-4111-8111-111111111111",
"work": 8,
"tenant": 1,
"input": input_summary,
"inputSha": backup._sha256(backup.canonical_json(input_summary).encode("utf-8")),
}
confirmation = {
"backup_id": manifest["backup_id"],
"codeFiles": input_summary["codeFiles"],
"inputSha": manifest["inputSha"],
"tenant": manifest["tenant"],
"work": manifest["work"],
}
manifest["confirmationSha"] = backup._sha256(
backup.canonical_json(confirmation).encode("utf-8")
)
return manifest
def _restore_rows():
"""七域各给一行,驱动 upsert 与清理顺序测试。"""
return {
"drafts": [{"id": 1, "work_id": 8, "source_type": "upgrade_book",
"draft_payload": {"名称": "甲"}, "deleted": False, "tenant_id": 1}],
"windows": [{"id": 11, "work_id": 8, "window_no": 1, "from_chapter": 1,
"to_chapter": 5, "deleted": False, "tenant_id": 1}],
"aliases": [{"id": 21, "work_id": 8, "canonical_name": "甲", "alias": "阿甲",
"tenant_id": 1}],
"presence": [{"id": 31, "work_id": 8, "window_no": 1, "chapter_no": 1,
"entity_type": "character", "name": "甲", "tenant_id": 1}],
"card_state": [{"draft_id": 1, "work_id": 8, "watermark_window": 1,
"tenant_id": 1}],
"audits": [{"id": 41, "draft_id": 1, "window_no": 1, "field_name": "名称",
"tenant_id": 1}],
"embeddings": [{"id": 51, "draft_id": 1, "content_hash": "a" * 64,
"embed_text": "甲", "model": "Qwen", "dimensions": 2,
"embedding": "[0.1,0.2]", "tenant_id": 1}],
}
def test_input_identity_and_drift_guard():
"""input 必须绑定完整身份,任一 Canonical 输入漂移均拒绝恢复。"""
manifest = _restore_manifest()
expected = manifest["input"]
_check("input-模型与语义判重绑定", expected["plan"] == {
"model": "MiniMax-M3", "semanticDedup": True})
_check("input-594章与116窗绑定", expected["canonicalContent"]["chapterCount"] == 594
and expected["windowBoundary"]["count"] == 116)
changed = json.loads(json.dumps(expected))
changed["canonicalContent"]["sha"] = "d" * 64
try:
backup.assert_restore_input(manifest, changed, work_id=8, tenant=1)
except backup.BackupError:
_check("input漂移-拒绝恢复", True)
else:
raise AssertionError("Canonical input 漂移未拒绝")
wrong_database = json.loads(json.dumps(expected))
wrong_database["databaseIdentity"]["database"] = "other-db"
try:
backup.assert_restore_input(manifest, wrong_database, work_id=8, tenant=1)
except backup.BackupError:
_check("数据库identity漂移-拒绝恢复", True)
else:
raise AssertionError("数据库 identity 不符未拒绝")
for work_id, tenant in ((9, 1), (8, 2)):
try:
backup.assert_restore_input(manifest, expected, work_id=work_id, tenant=tenant)
except backup.BackupError:
pass
else:
raise AssertionError("work/tenant 不符未拒绝")
_check("恢复范围-work与tenant不符拒绝", True)
def test_restore_sql_boundaries_and_rehearse_rollback():
"""共享恢复内核按七域边界操作,演练无条件回滚。"""
manifest = _restore_manifest()
rows = _restore_rows()
conn = _RestoreConnection()
with mock.patch.object(backup, "verify_backup", return_value=manifest), \
mock.patch.object(backup, "read_backup_rows", return_value=rows), \
mock.patch.object(backup, "capture_input_snapshot", return_value=manifest["input"]), \
mock.patch.object(backup, "verify_restored_domains"):
backup.restore_backup(
pathlib.Path("/private/tmp/offline-restore"), work_id=8, tenant=1,
dsn="unused", connect=lambda *_args, **_kwargs: conn, execute=False,
confirmation_sha=manifest["confirmationSha"], lock_context=_no_lock,
)
statements = [sql for sql, _params in conn.executions]
_check("rehearse-强制ROLLBACK", statements[-1] == "rollback" and "commit" not in statements)
_check("exact恢复-事务局部关闭更新时间触发器",
"set local session_replication_role to replica" in statements)
_check("恢复边界-额外卡软删", any(
sql.startswith("update muse_knowledge_draft") and "not (d.id = any" in sql
for sql in statements
))
_check("恢复边界-额外向量软删", any(
sql.startswith("update example_knowledge_embedding") and "not (e.id = any" in sql
for sql in statements
))
for table in ("example_upgrade_alias", "example_upgrade_presence",
"example_upgrade_card_state", "example_upgrade_audit"):
if not any(sql.startswith(f"delete from {table}") for sql in statements):
raise AssertionError(f"恢复前未清理 {table}")
_check("恢复边界-四域先清再插", True)
_check("恢复边界-windows拒绝额外active", any(
"from example_upgrade_window" in sql and "not (w.id = any" in sql
for sql in statements
))
_check("恢复边界-七域均有upsert", sum("on conflict" in sql for sql in statements) == 7)
rejected = _RestoreConnection(extra_window=True)
try:
backup.apply_restore_kernel(rejected, manifest, rows)
except backup.BackupError:
_check("恢复边界-实际拒绝额外active窗且写前失败", not any(
sql.startswith(("insert ", "update ", "delete "))
for sql, _params in rejected.executions
))
else:
raise AssertionError("额外 active 窗未阻断恢复")
def test_restore_execute_confirmation_and_sequence_forward_only():
"""execute 必须双确认;sequence 当前值更大时不得 setval 回退。"""
manifest = _restore_manifest()
for supplied in (None, "22222222-2222-4222-8222-222222222222"):
try:
backup.validate_execute_confirmation(
True, supplied, manifest["backup_id"],
manifest["confirmationSha"], manifest["confirmationSha"],
)
except backup.BackupError:
pass
else:
raise AssertionError("execute 缺少匹配 backup_id 仍被允许")
backup.validate_execute_confirmation(
True, manifest["backup_id"], manifest["backup_id"],
manifest["confirmationSha"], manifest["confirmationSha"],
)
try:
backup.validate_execute_confirmation(
False, None, manifest["backup_id"], "wrong", manifest["confirmationSha"]
)
except backup.BackupError:
_check("confirmation确认-rehearse也必须精确匹配", True)
else:
raise AssertionError("rehearse confirmationSha 不匹配仍被允许")
_check("execute确认-必须显式flag与匹配backup_id", True)
conn = _RestoreConnection(sequence_last=100, table_max=50)
backup.advance_sequences_forward(conn)
_check("序列-不回退", not any("setval" in sql for sql, _params in conn.executions))
forward = _RestoreConnection(sequence_last=100, table_max=150)
backup.advance_sequences_forward(forward)
_check("序列-仅在需要时向前", sum(
sql.startswith("select setval") for sql, _params in forward.executions
) == len(backup.SEQUENCE_TABLES))
execute_conn = _RestoreConnection()
with mock.patch.object(backup, "verify_backup", return_value=manifest), \
mock.patch.object(backup, "read_backup_rows", return_value=_restore_rows()), \
mock.patch.object(backup, "capture_input_snapshot", return_value=manifest["input"]), \
mock.patch.object(backup, "verify_restored_domains"), \
mock.patch.object(backup, "advance_sequences_forward"):
backup.restore_backup(
pathlib.Path("/private/tmp/offline-restore"), work_id=8, tenant=1,
dsn="unused", connect=lambda *_args, **_kwargs: execute_conn, execute=True,
backup_id=manifest["backup_id"], code_identity=_code_identity(),
confirmation_sha=manifest["confirmationSha"], lock_context=_no_lock,
)
execute_statements = [sql for sql, _params in execute_conn.executions]
_check("execute确认-匹配后才COMMIT", execute_statements[-1] == "commit")
def test_expected_scope_code_dependencies_and_lock_fail_closed():
"""期望范围、依赖代码与同书锁均进入机械门。"""
chapters = [
{"chapter_id": 1, "chapter_order": 1, "block_id": 101, "content_text": "甲"},
{"chapter_id": 2, "chapter_order": 2, "block_id": 102, "content_text": "乙"},
]
windows = [
{"id": 11, "window_no": 1, "from_chapter": 1, "to_chapter": 1},
{"id": 12, "window_no": 2, "from_chapter": 2, "to_chapter": 2},
]
backup.validate_expected_scope(chapters, windows, expected_chapters=2, expected_windows=2)
_check("expected-章节窗口连续且每章非空", True)
for bad_chapters, bad_windows in (
(chapters[:1], windows),
([dict(chapters[0], content_text=""), chapters[1]], windows),
(chapters, [windows[0], dict(windows[1], window_no=3)]),
(chapters, [windows[0], dict(windows[1], from_chapter=3)]),
):
try:
backup.validate_expected_scope(
bad_chapters, bad_windows, expected_chapters=2, expected_windows=2
)
except backup.BackupError:
pass
else:
raise AssertionError("expected 范围异常未拒绝")
_check("expected-数量非空与连续性异常均拒绝", True)
identity = backup.capture_code_identity("HEAD")
_check("代码身份-补齐实际依赖SHA", {
"reset_upgrade_work.py", "parse_llm.py", "embed_drafts.py", "upgrade_work_lock.py"
}.issubset(identity["codeFiles"]))
business_calls = {"count": 0}
@contextmanager
def unavailable_lock(*_args, **_kwargs):
raise backup.UpgradeWorkLockUnavailable(1, 8)
yield
def forbidden_connect(*_args, **_kwargs):
business_calls["count"] += 1
raise AssertionError("锁失败后仍创建业务连接")
output = _new_backup_dir()
try:
backup.backup_work(
8, output, tenant=1, dsn="unused", connect=forbidden_connect,
code_identity=_code_identity(), expected_chapters=594, expected_windows=116,
lock_context=unavailable_lock,
)
except backup.UpgradeWorkLockUnavailable:
_check("advisory-lock-失败时零业务SQL", business_calls["count"] == 0)
else:
raise AssertionError("同书锁失败未阻断 backup")
with mock.patch.object(backup, "verify_backup") as offline_verify:
try:
backup.restore_backup(
pathlib.Path("/private/tmp/not-read"), work_id=8, tenant=1, dsn="unused",
connect=forbidden_connect, confirmation_sha="unused",
lock_context=unavailable_lock,
)
except backup.UpgradeWorkLockUnavailable:
_check("advisory-lock-恢复演练也在离线verify前阻断", not offline_verify.called)
else:
raise AssertionError("同书锁失败未阻断 restore/rehearse")
def main():
"""顺序运行全部离线测试,便于直接以 Python 执行。"""
test_stable_sort_and_json_types()
test_path_guards()
test_backup_transaction_queries_and_round_trip()
test_accidental_corruption_detection()
test_dict_row_and_tuple_rejection()
test_input_identity_and_drift_guard()
test_restore_sql_boundaries_and_rehearse_rollback()
test_restore_execute_confirmation_and_sequence_forward_only()
test_expected_scope_code_dependencies_and_lock_fail_closed()
print("\n全部离线自测通过:49 项(未连库、未发任何 LLM 调用)")
if __name__ == "__main__":
main()