test_run_ledger.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. from __future__ import annotations
  4. import json
  5. import sys
  6. from pathlib import Path
  7. def _ensure_scripts_on_path() -> None:
  8. scripts_dir = Path(__file__).resolve().parents[2]
  9. if str(scripts_dir) not in sys.path:
  10. sys.path.insert(0, str(scripts_dir))
  11. _ensure_scripts_on_path()
  12. from data_modules.run_ledger import build_write_resume_plan, record_write_step # noqa: E402
  13. def _write_json(path: Path, payload: dict) -> None:
  14. path.parent.mkdir(parents=True, exist_ok=True)
  15. path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
  16. def _make_project(project_root: Path) -> None:
  17. (project_root / ".webnovel" / "tmp").mkdir(parents=True, exist_ok=True)
  18. (project_root / ".story-system" / "commits").mkdir(parents=True, exist_ok=True)
  19. (project_root / "正文").mkdir(parents=True, exist_ok=True)
  20. _write_json(project_root / ".webnovel" / "state.json", {"project_info": {"title": "测试书"}, "progress": {}})
  21. def _commit_payload(status: str = "accepted") -> dict:
  22. return {
  23. "meta": {"chapter": 1, "status": status},
  24. "projection_status": {
  25. "state": "done",
  26. "index": "skipped",
  27. "summary": "skipped",
  28. "memory": "skipped",
  29. "vector": "skipped",
  30. },
  31. }
  32. def test_run_ledger_records_write_step_status(tmp_path: Path) -> None:
  33. _make_project(tmp_path)
  34. chapter_file = tmp_path / "正文" / "第0001章.md"
  35. chapter_file.write_text("正文\n", encoding="utf-8")
  36. entry = record_write_step(
  37. tmp_path,
  38. chapter=1,
  39. step="draft",
  40. status="completed",
  41. outputs={"chapter_file": chapter_file},
  42. )
  43. assert entry["status"] == "completed"
  44. assert entry["outputs"]["chapter_file"]["exists"] is True
  45. assert (tmp_path / ".webnovel" / "run_ledger.json").is_file()
  46. def test_write_resume_skips_completed_draft_and_review(tmp_path: Path) -> None:
  47. _make_project(tmp_path)
  48. chapter_file = tmp_path / "正文" / "第0001章.md"
  49. chapter_file.write_text("正文\n", encoding="utf-8")
  50. review_path = tmp_path / ".webnovel" / "tmp" / "review_results.json"
  51. _write_json(review_path, {"blocking_count": 0})
  52. record_write_step(tmp_path, chapter=1, step="draft", status="completed", outputs={"chapter_file": chapter_file})
  53. record_write_step(
  54. tmp_path,
  55. chapter=1,
  56. step="review",
  57. status="completed",
  58. inputs={"chapter_file": chapter_file},
  59. outputs={"review_result": review_path},
  60. )
  61. plan = build_write_resume_plan(tmp_path, chapter=1)
  62. actions = {item["step"]: item["action"] for item in plan["steps"]}
  63. assert actions["draft"] == "skip"
  64. assert actions["review"] == "skip"
  65. assert actions["data"] == "run"
  66. def test_write_resume_rechecks_review_when_chapter_file_changed(tmp_path: Path) -> None:
  67. _make_project(tmp_path)
  68. chapter_file = tmp_path / "正文" / "第0001章.md"
  69. chapter_file.write_text("正文 v1\n", encoding="utf-8")
  70. record_write_step(tmp_path, chapter=1, step="draft", status="completed", outputs={"chapter_file": chapter_file})
  71. chapter_file.write_text("正文 v2\n", encoding="utf-8")
  72. plan = build_write_resume_plan(tmp_path, chapter=1)
  73. actions = {item["step"]: item["action"] for item in plan["steps"]}
  74. assert actions["draft"] == "run"
  75. assert actions["review"] == "run"
  76. assert any(item["code"] == "chapter_file_changed" for item in plan["needs_user_confirmation"])
  77. def test_write_resume_retries_backup_after_commit_done(tmp_path: Path) -> None:
  78. _make_project(tmp_path)
  79. chapter_file = tmp_path / "正文" / "第0001章.md"
  80. chapter_file.write_text("正文\n", encoding="utf-8")
  81. record_write_step(tmp_path, chapter=1, step="draft", status="completed", outputs={"chapter_file": chapter_file})
  82. _write_json(tmp_path / ".story-system" / "commits" / "chapter_001.commit.json", _commit_payload("accepted"))
  83. plan = build_write_resume_plan(tmp_path, chapter=1)
  84. actions = {item["step"]: item["action"] for item in plan["steps"]}
  85. assert actions["draft"] == "skip"
  86. assert actions["review"] == "skip"
  87. assert actions["data"] == "skip"
  88. assert actions["commit"] == "skip"
  89. assert actions["projection"] == "skip"
  90. assert actions["backup"] == "retry"
  91. assert plan["resume_from"] == "backup"
  92. assert any(item["code"] == "chapter_already_accepted" for item in plan["needs_user_confirmation"])
  93. def test_write_resume_reruns_commit_after_rejected_commit(tmp_path: Path) -> None:
  94. _make_project(tmp_path)
  95. chapter_file = tmp_path / "正文" / "第0001章.md"
  96. chapter_file.write_text("正文\n", encoding="utf-8")
  97. review_path = tmp_path / ".webnovel" / "tmp" / "review_results.json"
  98. _write_json(review_path, {"blocking_count": 1})
  99. fulfillment_path = tmp_path / ".webnovel" / "tmp" / "fulfillment_result.json"
  100. disambiguation_path = tmp_path / ".webnovel" / "tmp" / "disambiguation_result.json"
  101. extraction_path = tmp_path / ".webnovel" / "tmp" / "extraction_result.json"
  102. _write_json(fulfillment_path, {"planned_nodes": [], "covered_nodes": [], "missed_nodes": [], "extra_nodes": []})
  103. _write_json(disambiguation_path, {"pending": []})
  104. _write_json(extraction_path, {"accepted_events": [], "state_deltas": [], "entity_deltas": []})
  105. record_write_step(
  106. tmp_path,
  107. chapter=1,
  108. step="draft",
  109. status="completed",
  110. outputs={"chapter_file": chapter_file},
  111. )
  112. record_write_step(
  113. tmp_path,
  114. chapter=1,
  115. step="review",
  116. status="completed",
  117. inputs={"chapter_file": chapter_file},
  118. outputs={"review_result": review_path},
  119. )
  120. record_write_step(
  121. tmp_path,
  122. chapter=1,
  123. step="data",
  124. status="completed",
  125. inputs={"chapter_file": chapter_file},
  126. outputs={
  127. "fulfillment_result": fulfillment_path,
  128. "disambiguation_result": disambiguation_path,
  129. "extraction_result": extraction_path,
  130. },
  131. )
  132. _write_json(tmp_path / ".story-system" / "commits" / "chapter_001.commit.json", _commit_payload("rejected"))
  133. plan = build_write_resume_plan(tmp_path, chapter=1)
  134. actions = {item["step"]: item["action"] for item in plan["steps"]}
  135. assert actions["commit"] == "run"
  136. assert actions["projection"] == "run"
  137. assert plan["resume_from"] == "commit"
  138. assert any(item["code"] == "chapter_commit_rejected" for item in plan["needs_user_confirmation"])