test_runtime_boundary.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. from __future__ import annotations
  2. import hashlib
  3. import json
  4. import tempfile
  5. import unittest
  6. from pathlib import Path
  7. from unittest.mock import patch
  8. from production_build_agents.production.profile import default_output_profile
  9. from production_build_agents.production.runtime import (
  10. production_request_sha256,
  11. validate_production_run_directory_boundary,
  12. )
  13. from run_production import _build_parser, main
  14. class ProductionRunDirectoryBoundaryTest(unittest.TestCase):
  15. def test_request_digest_uses_current_protocol_domain(self) -> None:
  16. with tempfile.TemporaryDirectory() as temp_dir:
  17. root = Path(temp_dir)
  18. delivery = root / "delivery.json"
  19. anchor = root / "anchor.png"
  20. delivery.write_bytes(b"delivery")
  21. anchor.write_bytes(b"anchor")
  22. old_digest = hashlib.sha256()
  23. old_digest.update(b"production-protocol-0.6\0")
  24. old_digest.update(delivery.read_bytes())
  25. old_digest.update(b"\0shared-visual-anchor\0")
  26. old_digest.update(anchor.read_bytes())
  27. old_digest.update(b"\0output-profile\0")
  28. old_digest.update(
  29. json.dumps(
  30. default_output_profile().model_dump(mode="json"),
  31. ensure_ascii=False,
  32. sort_keys=True,
  33. separators=(",", ":"),
  34. ).encode("utf-8")
  35. )
  36. actual = production_request_sha256(
  37. delivery,
  38. shared_visual_anchor_path=anchor,
  39. )
  40. self.assertNotEqual(actual, old_digest.hexdigest())
  41. def test_cli_exposes_only_shared_plan_revision_budget(self) -> None:
  42. option_strings = {
  43. option
  44. for action in _build_parser()._actions
  45. for option in action.option_strings
  46. }
  47. self.assertIn("--max-plan-revisions", option_strings)
  48. self.assertNotIn("--max-replans", option_strings)
  49. def test_cli_exposes_progress_review_pause(self) -> None:
  50. parser = _build_parser()
  51. option_strings = {
  52. option
  53. for action in parser._actions
  54. for option in action.option_strings
  55. }
  56. self.assertIn("--pause-after-progress-review", option_strings)
  57. args = parser.parse_args(
  58. [
  59. "delivery.json",
  60. "--shared-visual-anchor",
  61. "anchor.png",
  62. "--pause-after-progress-review",
  63. ]
  64. )
  65. self.assertTrue(args.pause_after_progress_review)
  66. def test_cli_treats_requested_running_pause_as_success(self) -> None:
  67. argv = [
  68. "run_production.py",
  69. "delivery.json",
  70. "--shared-visual-anchor",
  71. "anchor.png",
  72. "--pause-after-progress-review",
  73. ]
  74. with (
  75. patch("sys.argv", argv),
  76. patch(
  77. "run_production.run_full_production",
  78. return_value={
  79. "status": "RUNNING",
  80. "phase": "PREPARE_SEGMENT",
  81. },
  82. ) as run,
  83. ):
  84. main()
  85. self.assertTrue(
  86. run.call_args.kwargs["pause_after_progress_review"]
  87. )
  88. def test_cli_still_fails_for_unrequested_running_result(self) -> None:
  89. argv = [
  90. "run_production.py",
  91. "delivery.json",
  92. "--shared-visual-anchor",
  93. "anchor.png",
  94. ]
  95. with (
  96. patch("sys.argv", argv),
  97. patch(
  98. "run_production.run_full_production",
  99. return_value={"status": "RUNNING"},
  100. ),
  101. self.assertRaises(SystemExit) as raised,
  102. ):
  103. main()
  104. self.assertEqual(raised.exception.code, 1)
  105. def test_new_and_empty_directories_are_valid(self) -> None:
  106. with tempfile.TemporaryDirectory() as temp_dir:
  107. root = Path(temp_dir)
  108. validate_production_run_directory_boundary(root / "missing")
  109. empty = root / "empty"
  110. empty.mkdir()
  111. validate_production_run_directory_boundary(empty)
  112. def test_non_production_evidence_is_outside_scan_scope(self) -> None:
  113. with tempfile.TemporaryDirectory() as temp_dir:
  114. root = Path(temp_dir)
  115. (root / "global_data_stage_delivery.json").write_text(
  116. '{"schema_version":"0.3"}',
  117. encoding="utf-8",
  118. )
  119. (root / "production_brief.json").write_text(
  120. '{"brief":true}',
  121. encoding="utf-8",
  122. )
  123. for directory in ("agent_runs", "tool_operations"):
  124. target = root / directory
  125. target.mkdir()
  126. (target / "evidence.json").write_text(
  127. '{"schema_version":"0.4"}',
  128. encoding="utf-8",
  129. )
  130. validate_production_run_directory_boundary(root)
  131. def test_formal_records_are_objects_with_current_schema(self) -> None:
  132. invalid_payloads = (
  133. [],
  134. {"plan_id": "missing-version"},
  135. {"schema_version": "0.4"},
  136. {"schema_version": "0.5"},
  137. )
  138. for index, payload in enumerate(invalid_payloads):
  139. with self.subTest(payload=payload), tempfile.TemporaryDirectory() as temp_dir:
  140. root = Path(temp_dir)
  141. plans = root / "production_plans"
  142. plans.mkdir()
  143. path = plans / f"production_dag.v{index + 1}.json"
  144. path.write_text(
  145. json.dumps(payload),
  146. encoding="utf-8",
  147. )
  148. before = path.read_bytes()
  149. with self.assertRaisesRegex(ValueError, "Production"):
  150. validate_production_run_directory_boundary(root)
  151. self.assertEqual(path.read_bytes(), before)
  152. self.assertEqual(
  153. [item.relative_to(root) for item in root.rglob("*")],
  154. [
  155. Path("production_plans"),
  156. path.relative_to(root),
  157. ],
  158. )
  159. with tempfile.TemporaryDirectory() as temp_dir:
  160. root = Path(temp_dir)
  161. plans = root / "production_plans"
  162. plans.mkdir()
  163. (plans / "production_dag.v1.json").write_text(
  164. '{"schema_version":"0.7"}',
  165. encoding="utf-8",
  166. )
  167. validate_production_run_directory_boundary(root)
  168. def test_mixed_run_is_rejected_without_writes(self) -> None:
  169. with tempfile.TemporaryDirectory() as temp_dir:
  170. root = Path(temp_dir)
  171. plans = root / "production_plans"
  172. segments = root / "segment_packages"
  173. plans.mkdir()
  174. segments.mkdir()
  175. (plans / "production_dag.v1.json").write_text(
  176. '{"schema_version":"0.7"}',
  177. encoding="utf-8",
  178. )
  179. old_segment = segments / "Segment1.v1.json"
  180. old_segment.write_text(
  181. '{"schema_version":"0.6"}',
  182. encoding="utf-8",
  183. )
  184. before = {
  185. path.relative_to(root): path.read_bytes()
  186. for path in root.rglob("*")
  187. if path.is_file()
  188. }
  189. with self.assertRaisesRegex(ValueError, "非当前协议"):
  190. validate_production_run_directory_boundary(root)
  191. after = {
  192. path.relative_to(root): path.read_bytes()
  193. for path in root.rglob("*")
  194. if path.is_file()
  195. }
  196. self.assertEqual(after, before)
  197. def test_legacy_run_is_rejected_without_writes(self) -> None:
  198. with tempfile.TemporaryDirectory() as temp_dir:
  199. root = Path(temp_dir)
  200. plans = root / "production_plans"
  201. plans.mkdir()
  202. legacy = plans / "production_dag.v1.json"
  203. legacy.write_text(
  204. '{"schema_version":"0.6"}',
  205. encoding="utf-8",
  206. )
  207. before = legacy.read_bytes()
  208. with self.assertRaisesRegex(ValueError, "非当前协议"):
  209. validate_production_run_directory_boundary(root)
  210. self.assertEqual(legacy.read_bytes(), before)
  211. self.assertEqual(
  212. [path.relative_to(root) for path in root.rglob("*")],
  213. [
  214. Path("production_plans"),
  215. Path("production_plans/production_dag.v1.json"),
  216. ],
  217. )
  218. def test_progress_records_use_current_schema_and_segment_version_name(
  219. self,
  220. ) -> None:
  221. with tempfile.TemporaryDirectory() as temp_dir:
  222. root = Path(temp_dir)
  223. for directory in (
  224. "production_progress_packages",
  225. "production_progress_decisions",
  226. ):
  227. target = root / directory
  228. target.mkdir()
  229. (target / "Segment1.v2.json").write_text(
  230. '{"schema_version":"0.7"}',
  231. encoding="utf-8",
  232. )
  233. validate_production_run_directory_boundary(root)
  234. def test_obsolete_unversioned_path_is_rejected(self) -> None:
  235. with tempfile.TemporaryDirectory() as temp_dir:
  236. root = Path(temp_dir)
  237. old_package = root / "production_package.json"
  238. old_package.write_text(
  239. '{"schema_version":"0.5"}',
  240. encoding="utf-8",
  241. )
  242. with self.assertRaisesRegex(ValueError, "旧版正式路径"):
  243. validate_production_run_directory_boundary(root)
  244. if __name__ == "__main__":
  245. unittest.main()