test_migrate_state_to_sqlite.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. migrate_state_to_sqlite tests
  5. """
  6. import json
  7. import pytest
  8. import data_modules.migrate_state_to_sqlite as migrate_module
  9. from data_modules.migrate_state_to_sqlite import (
  10. migrate_state_to_sqlite,
  11. _slim_world_settings,
  12. _slim_relationships,
  13. )
  14. from data_modules.config import DataModulesConfig
  15. from data_modules.index_manager import IndexManager
  16. @pytest.fixture
  17. def temp_project(tmp_path):
  18. cfg = DataModulesConfig.from_project_root(tmp_path)
  19. cfg.ensure_dirs()
  20. return cfg
  21. def test_migrate_state_missing_file(tmp_path):
  22. cfg = DataModulesConfig.from_project_root(tmp_path)
  23. stats = migrate_state_to_sqlite(cfg, dry_run=True, backup=False, verbose=False)
  24. assert stats["entities"] == 0
  25. def test_migrate_state_to_sqlite_flow(temp_project):
  26. state = {
  27. "entities_v3": {
  28. "角色": {
  29. "xiaoyan": {
  30. "canonical_name": "萧炎",
  31. "tier": "核心",
  32. "desc": "主角",
  33. "current": {"realm": "斗者"},
  34. "first_appearance": 1,
  35. "last_appearance": 2,
  36. "is_protagonist": True,
  37. }
  38. }
  39. },
  40. "alias_index": {
  41. "萧炎": [{"type": "角色", "id": "xiaoyan"}]
  42. },
  43. "state_changes": [
  44. {"entity_id": "xiaoyan", "field": "realm", "old": "斗者", "new": "斗师", "reason": "突破", "chapter": 2}
  45. ],
  46. "structured_relationships": [
  47. {"from_entity": "xiaoyan", "to_entity": "yaolao", "type": "师徒", "description": "收徒", "chapter": 1}
  48. ],
  49. "world_settings": {
  50. "power_system": [{"name": "斗者"}, {"name": "斗师"}],
  51. "factions": [{"name": "天云宗", "type": "宗门"}],
  52. "locations": [{"name": "天云宗"}],
  53. },
  54. "plot_threads": {"active_threads": [], "foreshadowing": []},
  55. "relationships": {},
  56. "review_checkpoints": [],
  57. "project_info": {"title": "测试书名"},
  58. }
  59. temp_project.state_file.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
  60. stats = migrate_state_to_sqlite(temp_project, dry_run=True, backup=False, verbose=False)
  61. assert stats["entities"] == 1
  62. assert stats["aliases"] == 1
  63. stats = migrate_state_to_sqlite(temp_project, dry_run=False, backup=False, verbose=False)
  64. assert stats["entities"] == 1
  65. # state.json 被精简
  66. saved = json.loads(temp_project.state_file.read_text(encoding="utf-8"))
  67. assert saved.get("_migrated_to_sqlite") is True
  68. assert "entities_v3" not in saved
  69. # SQLite 中可查询实体
  70. idx = IndexManager(temp_project)
  71. entity = idx.get_entity("xiaoyan")
  72. assert entity is not None
  73. def test_slim_helpers():
  74. world = {
  75. "power_system": [{"name": "斗者"}],
  76. "factions": [{"name": "天云宗", "type": "宗门"}],
  77. "locations": [{"name": "天云宗"}],
  78. }
  79. slim = _slim_world_settings(world)
  80. assert slim["power_system"][0] == "斗者"
  81. rels = _slim_relationships({"a": 1})
  82. assert rels["a"] == 1
  83. def test_slim_helpers_non_dict():
  84. assert _slim_world_settings("bad") == {}
  85. assert _slim_relationships("bad") == {}
  86. def test_migrate_state_verbose_and_dry_run(temp_project, capsys):
  87. state = {
  88. "entities_v3": {},
  89. "alias_index": {},
  90. "state_changes": [],
  91. "structured_relationships": [],
  92. "world_settings": {},
  93. "plot_threads": {},
  94. "relationships": {},
  95. "review_checkpoints": [],
  96. "project_info": {},
  97. }
  98. temp_project.state_file.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
  99. stats = migrate_state_to_sqlite(temp_project, dry_run=True, backup=False, verbose=True)
  100. output = capsys.readouterr().out
  101. assert stats["errors"] == 0
  102. assert "dry-run" in output or "dry run" in output
  103. def test_migrate_state_cli_main(tmp_path, monkeypatch, capsys):
  104. project_root = tmp_path
  105. args = [
  106. "migrate_state_to_sqlite",
  107. "--project-root",
  108. str(project_root),
  109. "--dry-run",
  110. "--no-backup",
  111. ]
  112. monkeypatch.setattr("sys.argv", args)
  113. migrate_module.main()
  114. output = json.loads(capsys.readouterr().out or "{}")
  115. assert output.get("status") == "success"
  116. def test_migrate_state_backup_and_skips(temp_project):
  117. state = {
  118. "entities_v3": {
  119. "角色": {
  120. "good": {"canonical_name": "好人"},
  121. "bad": "not-dict",
  122. }
  123. },
  124. "alias_index": {
  125. "好人": [{"type": "角色", "id": "good"}],
  126. "坏条目": ["oops", {"type": "角色"}],
  127. },
  128. "state_changes": ["bad", {"field": "realm"}],
  129. "structured_relationships": ["bad", {"from_entity": "", "to_entity": ""}],
  130. "relationships": {},
  131. "world_settings": {},
  132. "plot_threads": {},
  133. "review_checkpoints": [],
  134. "project_info": {},
  135. }
  136. temp_project.state_file.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
  137. stats = migrate_state_to_sqlite(temp_project, dry_run=False, backup=True, verbose=False)
  138. assert stats["entities"] == 1
  139. assert stats["skipped"] >= 3
  140. backups = list(temp_project.state_file.parent.glob("state.json.backup-*"))
  141. assert backups
  142. def test_migrate_state_error_branches(tmp_path, monkeypatch):
  143. cfg = DataModulesConfig.from_project_root(tmp_path)
  144. cfg.ensure_dirs()
  145. state = {
  146. "entities_v3": {"角色": {"boom": {"canonical_name": "爆"}}},
  147. "alias_index": {"爆": [{"type": "角色", "id": "boom"}]},
  148. "state_changes": [
  149. {"entity_id": "boom", "field": "realm", "old": "", "new": "斗者", "reason": "测试", "chapter": 1}
  150. ],
  151. "structured_relationships": [
  152. {"from_entity": "boom", "to_entity": "yao", "type": "相识", "description": "测试", "chapter": 1}
  153. ],
  154. "relationships": {},
  155. "world_settings": {},
  156. "plot_threads": {},
  157. "review_checkpoints": [],
  158. "project_info": {},
  159. }
  160. cfg.state_file.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
  161. class BoomSQL:
  162. def __init__(self, *args, **kwargs):
  163. pass
  164. def upsert_entity(self, *args, **kwargs):
  165. raise RuntimeError("boom")
  166. def register_alias(self, *args, **kwargs):
  167. raise RuntimeError("boom")
  168. def record_state_change(self, *args, **kwargs):
  169. raise RuntimeError("boom")
  170. def upsert_relationship(self, *args, **kwargs):
  171. raise RuntimeError("boom")
  172. monkeypatch.setattr(migrate_module, "SQLStateManager", BoomSQL)
  173. stats = migrate_state_to_sqlite(cfg, dry_run=False, backup=False, verbose=False)
  174. assert stats["errors"] >= 4