| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- migrate_state_to_sqlite tests
- """
- import json
- import pytest
- import data_modules.migrate_state_to_sqlite as migrate_module
- from data_modules.migrate_state_to_sqlite import (
- migrate_state_to_sqlite,
- _slim_world_settings,
- _slim_relationships,
- )
- from data_modules.config import DataModulesConfig
- from data_modules.index_manager import IndexManager
- @pytest.fixture
- def temp_project(tmp_path):
- cfg = DataModulesConfig.from_project_root(tmp_path)
- cfg.ensure_dirs()
- return cfg
- def test_migrate_state_missing_file(tmp_path):
- cfg = DataModulesConfig.from_project_root(tmp_path)
- stats = migrate_state_to_sqlite(cfg, dry_run=True, backup=False, verbose=False)
- assert stats["entities"] == 0
- def test_migrate_state_to_sqlite_flow(temp_project):
- state = {
- "entities_v3": {
- "角色": {
- "xiaoyan": {
- "canonical_name": "萧炎",
- "tier": "核心",
- "desc": "主角",
- "current": {"realm": "斗者"},
- "first_appearance": 1,
- "last_appearance": 2,
- "is_protagonist": True,
- }
- }
- },
- "alias_index": {
- "萧炎": [{"type": "角色", "id": "xiaoyan"}]
- },
- "state_changes": [
- {"entity_id": "xiaoyan", "field": "realm", "old": "斗者", "new": "斗师", "reason": "突破", "chapter": 2}
- ],
- "structured_relationships": [
- {"from_entity": "xiaoyan", "to_entity": "yaolao", "type": "师徒", "description": "收徒", "chapter": 1}
- ],
- "world_settings": {
- "power_system": [{"name": "斗者"}, {"name": "斗师"}],
- "factions": [{"name": "天云宗", "type": "宗门"}],
- "locations": [{"name": "天云宗"}],
- },
- "plot_threads": {"active_threads": [], "foreshadowing": []},
- "relationships": {},
- "review_checkpoints": [],
- "project_info": {"title": "测试书名"},
- }
- temp_project.state_file.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
- stats = migrate_state_to_sqlite(temp_project, dry_run=True, backup=False, verbose=False)
- assert stats["entities"] == 1
- assert stats["aliases"] == 1
- stats = migrate_state_to_sqlite(temp_project, dry_run=False, backup=False, verbose=False)
- assert stats["entities"] == 1
- # state.json 被精简
- saved = json.loads(temp_project.state_file.read_text(encoding="utf-8"))
- assert saved.get("_migrated_to_sqlite") is True
- assert "entities_v3" not in saved
- # SQLite 中可查询实体
- idx = IndexManager(temp_project)
- entity = idx.get_entity("xiaoyan")
- assert entity is not None
- def test_slim_helpers():
- world = {
- "power_system": [{"name": "斗者"}],
- "factions": [{"name": "天云宗", "type": "宗门"}],
- "locations": [{"name": "天云宗"}],
- }
- slim = _slim_world_settings(world)
- assert slim["power_system"][0] == "斗者"
- rels = _slim_relationships({"a": 1})
- assert rels["a"] == 1
- def test_slim_helpers_non_dict():
- assert _slim_world_settings("bad") == {}
- assert _slim_relationships("bad") == {}
- def test_migrate_state_verbose_and_dry_run(temp_project, capsys):
- state = {
- "entities_v3": {},
- "alias_index": {},
- "state_changes": [],
- "structured_relationships": [],
- "world_settings": {},
- "plot_threads": {},
- "relationships": {},
- "review_checkpoints": [],
- "project_info": {},
- }
- temp_project.state_file.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
- stats = migrate_state_to_sqlite(temp_project, dry_run=True, backup=False, verbose=True)
- output = capsys.readouterr().out
- assert stats["errors"] == 0
- assert "dry-run" in output or "dry run" in output
- def test_migrate_state_cli_main(tmp_path, monkeypatch, capsys):
- project_root = tmp_path
- args = [
- "migrate_state_to_sqlite",
- "--project-root",
- str(project_root),
- "--dry-run",
- "--no-backup",
- ]
- monkeypatch.setattr("sys.argv", args)
- migrate_module.main()
- output = json.loads(capsys.readouterr().out or "{}")
- assert output.get("status") == "success"
- def test_migrate_state_backup_and_skips(temp_project):
- state = {
- "entities_v3": {
- "角色": {
- "good": {"canonical_name": "好人"},
- "bad": "not-dict",
- }
- },
- "alias_index": {
- "好人": [{"type": "角色", "id": "good"}],
- "坏条目": ["oops", {"type": "角色"}],
- },
- "state_changes": ["bad", {"field": "realm"}],
- "structured_relationships": ["bad", {"from_entity": "", "to_entity": ""}],
- "relationships": {},
- "world_settings": {},
- "plot_threads": {},
- "review_checkpoints": [],
- "project_info": {},
- }
- temp_project.state_file.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
- stats = migrate_state_to_sqlite(temp_project, dry_run=False, backup=True, verbose=False)
- assert stats["entities"] == 1
- assert stats["skipped"] >= 3
- backups = list(temp_project.state_file.parent.glob("state.json.backup-*"))
- assert backups
- def test_migrate_state_error_branches(tmp_path, monkeypatch):
- cfg = DataModulesConfig.from_project_root(tmp_path)
- cfg.ensure_dirs()
- state = {
- "entities_v3": {"角色": {"boom": {"canonical_name": "爆"}}},
- "alias_index": {"爆": [{"type": "角色", "id": "boom"}]},
- "state_changes": [
- {"entity_id": "boom", "field": "realm", "old": "", "new": "斗者", "reason": "测试", "chapter": 1}
- ],
- "structured_relationships": [
- {"from_entity": "boom", "to_entity": "yao", "type": "相识", "description": "测试", "chapter": 1}
- ],
- "relationships": {},
- "world_settings": {},
- "plot_threads": {},
- "review_checkpoints": [],
- "project_info": {},
- }
- cfg.state_file.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
- class BoomSQL:
- def __init__(self, *args, **kwargs):
- pass
- def upsert_entity(self, *args, **kwargs):
- raise RuntimeError("boom")
- def register_alias(self, *args, **kwargs):
- raise RuntimeError("boom")
- def record_state_change(self, *args, **kwargs):
- raise RuntimeError("boom")
- def upsert_relationship(self, *args, **kwargs):
- raise RuntimeError("boom")
- monkeypatch.setattr(migrate_module, "SQLStateManager", BoomSQL)
- stats = migrate_state_to_sqlite(cfg, dry_run=False, backup=False, verbose=False)
- assert stats["errors"] >= 4
|