test_production_validation.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. from __future__ import annotations
  2. import unittest
  3. from production_build_agents.contracts.production_validation_evaluation import (
  4. evaluate_final_production_delivery,
  5. evaluate_production_readiness_report,
  6. evaluate_production_validation_report,
  7. evaluate_production_validator_candidate,
  8. compute_production_readiness_checks,
  9. )
  10. from production_build_agents.contracts.production_validation_models import (
  11. ProductionMediaInspection,
  12. ProductionValidationReport,
  13. )
  14. from tests.support.production_fixtures import (
  15. ProductionScenario,
  16. final_delivery,
  17. immutable_ref,
  18. readiness_records,
  19. text_sha256,
  20. validation_records,
  21. )
  22. class ProductionValidationContractTest(unittest.TestCase):
  23. def setUp(self) -> None:
  24. self.scenario = ProductionScenario()
  25. self.__dict__.update(vars(self.scenario))
  26. def test_readiness_and_validation_verdicts_are_code_owned(self) -> None:
  27. inspection, readiness = readiness_records(
  28. self.assembly_delivery,
  29. self.assembly,
  30. )
  31. self.assertEqual(
  32. evaluate_production_readiness_report(
  33. self.assembly_delivery,
  34. self.assembly,
  35. inspection,
  36. readiness,
  37. assembly_delivery_ref=immutable_ref("assembly-delivery"),
  38. media_inspection_ref=immutable_ref("media-inspection"),
  39. ),
  40. [],
  41. )
  42. failed_check = readiness.check_results[0].model_copy(
  43. update={"verdict": "FAIL", "reason": "失败"}
  44. )
  45. forged_readiness = readiness.model_copy(
  46. update={
  47. "check_results": [
  48. failed_check,
  49. *readiness.check_results[1:],
  50. ],
  51. "verdict": "PASS",
  52. }
  53. )
  54. codes = {
  55. item.code
  56. for item in evaluate_production_readiness_report(
  57. self.assembly_delivery,
  58. self.assembly,
  59. inspection,
  60. forged_readiness,
  61. assembly_delivery_ref=immutable_ref("assembly-delivery"),
  62. media_inspection_ref=immutable_ref("media-inspection"),
  63. )
  64. }
  65. self.assertIn("production_preflight_verdict_mismatch", codes)
  66. forged_inspection = inspection.model_copy(
  67. update={"audio_stream_count": 0}
  68. )
  69. codes = {
  70. item.code
  71. for item in evaluate_production_readiness_report(
  72. self.assembly_delivery,
  73. self.assembly,
  74. forged_inspection,
  75. readiness,
  76. assembly_delivery_ref=immutable_ref("assembly-delivery"),
  77. media_inspection_ref=immutable_ref("media-inspection"),
  78. )
  79. }
  80. self.assertIn("production_preflight_check_mismatch", codes)
  81. candidate, report = validation_records(self.assembly_delivery)
  82. self.assertEqual(
  83. evaluate_production_validation_report(
  84. self.assembly_delivery,
  85. self.assembly,
  86. inspection,
  87. readiness,
  88. candidate,
  89. report,
  90. assembly_delivery_ref=immutable_ref("assembly-delivery"),
  91. preflight_report_ref=immutable_ref("readiness-report"),
  92. media_inspection_ref=immutable_ref("media-inspection"),
  93. validator_candidate_ref=immutable_ref(
  94. "validator-candidate"
  95. ),
  96. ),
  97. [],
  98. )
  99. no_boundaries = candidate.model_copy(
  100. update={"boundary_results": []}
  101. )
  102. forged_report = ProductionValidationReport(
  103. run_id=no_boundaries.run_id,
  104. plan_id=no_boundaries.plan_id,
  105. plan_version=no_boundaries.plan_version,
  106. assembly_delivery_ref=no_boundaries.assembly_delivery_ref,
  107. preflight_report_ref=no_boundaries.preflight_report_ref,
  108. validator_candidate_ref=report.validator_candidate_ref,
  109. validator_run_id=report.validator_run_id,
  110. verdict="PASS",
  111. summary=no_boundaries.summary,
  112. )
  113. codes = {
  114. item.code
  115. for item in evaluate_production_validation_report(
  116. self.assembly_delivery,
  117. self.assembly,
  118. inspection,
  119. readiness,
  120. no_boundaries,
  121. forged_report,
  122. assembly_delivery_ref=immutable_ref("assembly-delivery"),
  123. preflight_report_ref=immutable_ref("readiness-report"),
  124. media_inspection_ref=immutable_ref("media-inspection"),
  125. validator_candidate_ref=immutable_ref(
  126. "validator-candidate"
  127. ),
  128. )
  129. }
  130. self.assertIn(
  131. "production_validation_boundaries_mismatch",
  132. codes,
  133. )
  134. self.assertIn("production_validation_verdict_mismatch", codes)
  135. def test_python_renames_preserve_current_json_fields(self) -> None:
  136. _, readiness = readiness_records(
  137. self.assembly_delivery,
  138. self.assembly,
  139. )
  140. delivery = final_delivery(self.assembly_delivery)
  141. readiness_payload = readiness.model_dump(mode="json")
  142. delivery_payload = delivery.model_dump(mode="json")
  143. self.assertEqual(readiness_payload["schema_version"], "0.7")
  144. self.assertIn("coverage_evaluation", readiness_payload)
  145. self.assertNotIn("production_coverage", readiness_payload)
  146. self.assertEqual(delivery_payload["schema_version"], "0.7")
  147. self.assertIn("preflight_report_ref", delivery_payload)
  148. self.assertNotIn("readiness_report_ref", delivery_payload)
  149. def test_candidate_rules_are_the_formal_report_rules(self) -> None:
  150. inspection, readiness = readiness_records(
  151. self.assembly_delivery,
  152. self.assembly,
  153. )
  154. candidate, report = validation_records(self.assembly_delivery)
  155. invalid = candidate.model_copy(
  156. update={
  157. "criterion_results": candidate.criterion_results[:-1],
  158. "boundary_results": [],
  159. }
  160. )
  161. candidate_codes = {
  162. item.code
  163. for item in evaluate_production_validator_candidate(
  164. self.assembly_delivery,
  165. invalid,
  166. assembly_delivery_ref=immutable_ref("assembly-delivery"),
  167. preflight_report_ref=immutable_ref("readiness-report"),
  168. )
  169. }
  170. report_codes = {
  171. item.code
  172. for item in evaluate_production_validation_report(
  173. self.assembly_delivery,
  174. self.assembly,
  175. inspection,
  176. readiness,
  177. invalid,
  178. report,
  179. assembly_delivery_ref=immutable_ref("assembly-delivery"),
  180. preflight_report_ref=immutable_ref("readiness-report"),
  181. media_inspection_ref=immutable_ref("media-inspection"),
  182. validator_candidate_ref=immutable_ref(
  183. "validator-candidate"
  184. ),
  185. )
  186. }
  187. self.assertEqual(
  188. candidate_codes,
  189. {
  190. "production_validation_criteria_mismatch",
  191. "production_validation_boundaries_mismatch",
  192. },
  193. )
  194. self.assertTrue(candidate_codes.issubset(report_codes))
  195. def test_media_inspection_can_record_bad_media_as_fail(self) -> None:
  196. inspection, _ = readiness_records(
  197. self.assembly_delivery,
  198. self.assembly,
  199. )
  200. wrong_container = ProductionMediaInspection.model_validate(
  201. {
  202. **inspection.model_dump(mode="python"),
  203. "actual_output_profile": {
  204. **inspection.actual_output_profile.model_dump(
  205. mode="python"
  206. ),
  207. "container": "matroska",
  208. },
  209. }
  210. )
  211. self.assertFalse(
  212. compute_production_readiness_checks(
  213. self.assembly_delivery,
  214. self.assembly,
  215. wrong_container,
  216. )["output_profile"]
  217. )
  218. no_subtitles = ProductionMediaInspection.model_validate(
  219. {
  220. **inspection.model_dump(mode="python"),
  221. "subtitle_cue_count": 0,
  222. "subtitle_first_start_ms": None,
  223. "subtitle_last_end_ms": None,
  224. }
  225. )
  226. self.assertFalse(
  227. compute_production_readiness_checks(
  228. self.assembly_delivery,
  229. self.assembly,
  230. no_subtitles,
  231. )["subtitle_integrity"]
  232. )
  233. zero_frame_rate = inspection.model_copy(
  234. update={
  235. "actual_output_profile": (
  236. inspection.actual_output_profile.model_copy(
  237. update={
  238. "frame_rate_numerator": 0,
  239. "frame_rate_denominator": 0,
  240. }
  241. )
  242. )
  243. }
  244. )
  245. self.assertFalse(
  246. compute_production_readiness_checks(
  247. self.assembly_delivery,
  248. self.assembly,
  249. zero_frame_rate,
  250. )["output_profile"]
  251. )
  252. def test_media_inspection_binds_audio_master_content(self) -> None:
  253. inspection, _ = readiness_records(
  254. self.assembly_delivery,
  255. self.assembly,
  256. )
  257. artifacts = [
  258. (
  259. item.model_copy(
  260. update={"content_sha256": text_sha256("tampered-audio")}
  261. )
  262. if item.artifact_id
  263. == self.assembly_delivery.audio_master_artifact_id
  264. else item
  265. )
  266. for item in self.assembly_delivery.artifacts
  267. ]
  268. tampered = self.assembly_delivery.model_copy(
  269. update={"artifacts": artifacts}
  270. )
  271. self.assertFalse(
  272. compute_production_readiness_checks(
  273. tampered,
  274. self.assembly,
  275. inspection,
  276. )["artifact_integrity"]
  277. )
  278. def test_final_delivery_requires_both_validation_layers_to_pass(
  279. self,
  280. ) -> None:
  281. inspection, readiness = readiness_records(
  282. self.assembly_delivery,
  283. self.assembly,
  284. )
  285. candidate, report = validation_records(self.assembly_delivery)
  286. final = final_delivery(self.assembly_delivery)
  287. self.assertEqual(self.scenario.final_issues(), [])
  288. failed_report = report.model_copy(update={"verdict": "FAIL"})
  289. codes = {
  290. item.code
  291. for item in evaluate_final_production_delivery(
  292. self.assembly_delivery,
  293. self.assembly,
  294. inspection,
  295. readiness,
  296. candidate,
  297. failed_report,
  298. final,
  299. production_package_ref=(
  300. self.assembly_delivery.production_package_ref
  301. ),
  302. assembly_delivery_ref=immutable_ref("assembly-delivery"),
  303. preflight_report_ref=immutable_ref("readiness-report"),
  304. media_inspection_ref=immutable_ref("media-inspection"),
  305. validator_candidate_ref=immutable_ref(
  306. "validator-candidate"
  307. ),
  308. validation_report_ref=immutable_ref("validation-report"),
  309. )
  310. }
  311. self.assertIn("final_production_not_passed", codes)
  312. forged_refs = final.model_copy(
  313. update={
  314. "assembly_delivery_ref": immutable_ref("forged-assembly"),
  315. "validation_report_ref": immutable_ref("forged-validation"),
  316. }
  317. )
  318. codes = {
  319. item.code
  320. for item in evaluate_final_production_delivery(
  321. self.assembly_delivery,
  322. self.assembly,
  323. inspection,
  324. readiness,
  325. candidate,
  326. report,
  327. forged_refs,
  328. production_package_ref=(
  329. self.assembly_delivery.production_package_ref
  330. ),
  331. assembly_delivery_ref=immutable_ref("assembly-delivery"),
  332. preflight_report_ref=immutable_ref("readiness-report"),
  333. media_inspection_ref=immutable_ref("media-inspection"),
  334. validator_candidate_ref=immutable_ref(
  335. "validator-candidate"
  336. ),
  337. validation_report_ref=immutable_ref("validation-report"),
  338. )
  339. }
  340. self.assertIn("final_production_document_ref_mismatch", codes)
  341. def test_validation_report_references_candidate_without_copying_it(
  342. self,
  343. ) -> None:
  344. _, report = validation_records(self.assembly_delivery)
  345. self.assertNotIn(
  346. "criterion_results",
  347. ProductionValidationReport.model_fields,
  348. )
  349. self.assertNotIn(
  350. "boundary_results",
  351. ProductionValidationReport.model_fields,
  352. )
  353. self.assertEqual(
  354. report.validator_candidate_ref,
  355. immutable_ref("validator-candidate"),
  356. )
  357. if __name__ == "__main__":
  358. import unittest
  359. unittest.main()