test_segment_acceptance.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  1. from __future__ import annotations
  2. import tempfile
  3. import unittest
  4. from pathlib import Path
  5. from unittest.mock import patch
  6. from production_build_agents.contracts.models import (
  7. Artifact,
  8. CandidateArtifact,
  9. GlobalDataStageDelivery,
  10. )
  11. from production_build_agents.contracts.document_identity import (
  12. exact_document_sha256,
  13. )
  14. from production_build_agents.contracts.production_execution_models import (
  15. AcceptedSegmentRef,
  16. ImmutableJsonRef,
  17. SegmentDependencyRef,
  18. )
  19. from production_build_agents.contracts.production_planning_models import (
  20. ProductionPlanningPackage,
  21. )
  22. from production_build_agents.run.artifacts import (
  23. ArtifactIntegrityError,
  24. content_identity_for_path,
  25. file_sha256,
  26. seal_shared_visual_anchor,
  27. )
  28. from production_build_agents.run.layout import (
  29. segment_delivery_path,
  30. segment_executor_candidate_path,
  31. segment_package_path,
  32. segment_validation_report_path,
  33. segment_validator_candidate_path,
  34. )
  35. from production_build_agents.run.records import write_once_json
  36. from production_build_agents.run.segment_inputs import (
  37. reusable_accepted_segment_snapshot,
  38. )
  39. from production_build_agents.production.segment_acceptance import (
  40. build_accepted_segment_ref,
  41. segment_reuse_inputs_compatible,
  42. )
  43. from tests.support.production_fixtures import (
  44. production_plan,
  45. segment_bundle,
  46. )
  47. from tests.support.production_planner_fixtures import (
  48. build_production_planner_scenario,
  49. )
  50. def _persist_pass_bundle(
  51. root: Path,
  52. *,
  53. include_unselected_actual_input: bool = False,
  54. accepted_dependency: AcceptedSegmentRef | None = None,
  55. segment_index: int = 1,
  56. run_id: str = "source-run",
  57. ):
  58. root.mkdir(parents=True, exist_ok=True)
  59. (
  60. package,
  61. candidate,
  62. delivery,
  63. validator_candidate,
  64. report,
  65. _,
  66. ) = segment_bundle(segment_index, run_id=run_id)
  67. run_dir = root / run_id
  68. source_path = root / "source.png"
  69. source_path.write_bytes(b"source")
  70. old_source_uri = package.artifacts[0].uri
  71. source_artifact = package.artifacts[0].model_copy(
  72. update={
  73. "uri": str(source_path.resolve()),
  74. **content_identity_for_path(source_path),
  75. }
  76. )
  77. old_anchor = package.shared_visual_anchor
  78. anchor_path = run_dir / "production_inputs" / "shared_visual_anchor.png"
  79. anchor_path.parent.mkdir(parents=True, exist_ok=True)
  80. anchor_path.write_bytes(b"shared anchor")
  81. shared_anchor = seal_shared_visual_anchor(anchor_path)
  82. package = package.model_copy(
  83. update={
  84. "artifacts": [source_artifact],
  85. "shared_visual_anchor": shared_anchor,
  86. }
  87. )
  88. global_artifacts = [source_artifact]
  89. optional_artifact = None
  90. if include_unselected_actual_input:
  91. optional_path = root / "optional-audio.wav"
  92. optional_path.write_bytes(b"optional-audio")
  93. optional_artifact = Artifact(
  94. artifact_id="Task2-v1-artifact-1",
  95. artifact_type="audio",
  96. uri=str(optional_path.resolve()),
  97. source_uri=None,
  98. description="Planner 未选择但 Executor 实际使用的素材",
  99. **content_identity_for_path(optional_path),
  100. )
  101. global_artifacts.append(optional_artifact)
  102. global_delivery = build_production_planner_scenario(
  103. run_id="source-global"
  104. ).global_data_delivery.model_copy(
  105. update={"active_artifacts": global_artifacts}
  106. )
  107. global_delivery_path = root / "global_data_stage_delivery.json"
  108. global_delivery_path.write_text(
  109. global_delivery.model_dump_json(),
  110. encoding="utf-8",
  111. )
  112. package = package.model_copy(
  113. update={
  114. "global_data_delivery_ref": ImmutableJsonRef(
  115. uri=str(global_delivery_path.resolve()),
  116. document_sha256=exact_document_sha256(
  117. global_delivery_path.read_bytes()
  118. ),
  119. )
  120. }
  121. )
  122. if accepted_dependency is not None:
  123. package = package.model_copy(
  124. update={
  125. "dependencies": [
  126. SegmentDependencyRef(
  127. segment_id=accepted_dependency.segment_id,
  128. segment_delivery_ref=(
  129. accepted_dependency.segment_delivery_ref
  130. ),
  131. validation_report_ref=(
  132. accepted_dependency.validation_report_ref
  133. ),
  134. )
  135. ]
  136. }
  137. )
  138. old_to_new = {
  139. old_source_uri: str(source_path.resolve()),
  140. old_anchor.uri: shared_anchor.uri,
  141. }
  142. formal_artifacts = []
  143. for artifact in delivery.artifacts:
  144. suffix = Path(artifact.uri).suffix
  145. path = run_dir / "formal_artifacts" / f"{artifact.artifact_id}{suffix}"
  146. path.parent.mkdir(parents=True, exist_ok=True)
  147. if suffix == ".ass":
  148. path.write_text("subtitle", encoding="utf-8")
  149. else:
  150. path.write_bytes(f"artifact:{artifact.artifact_id}".encode())
  151. old_to_new[artifact.uri] = str(path.resolve())
  152. formal_artifacts.append(
  153. artifact.model_copy(
  154. update={
  155. "uri": str(path.resolve()),
  156. **content_identity_for_path(path),
  157. }
  158. )
  159. )
  160. candidate = candidate.model_copy(
  161. update={
  162. "artifacts": [
  163. CandidateArtifact(
  164. artifact_type=item.artifact_type,
  165. uri=old_to_new[item.uri],
  166. source_uri=item.source_uri,
  167. description=item.description,
  168. )
  169. for item in candidate.artifacts
  170. ],
  171. "lineage_claims": [
  172. claim.model_copy(
  173. update={
  174. "artifact_uri": old_to_new[claim.artifact_uri],
  175. "input_artifact_uris": [
  176. old_to_new.get(uri, uri)
  177. for uri in claim.input_artifact_uris
  178. ]
  179. + (
  180. [optional_artifact.uri]
  181. if (
  182. optional_artifact is not None
  183. and index == 0
  184. )
  185. else []
  186. )
  187. + (
  188. [accepted_dependency.primary_artifact.uri]
  189. if accepted_dependency is not None and index == 0
  190. else []
  191. ),
  192. }
  193. )
  194. for index, claim in enumerate(candidate.lineage_claims)
  195. ],
  196. "shot_outputs": [
  197. claim.model_copy(
  198. update={
  199. "artifact_uri": old_to_new[claim.artifact_uri]
  200. }
  201. )
  202. for claim in candidate.shot_outputs
  203. ],
  204. "narration_artifact_uri": old_to_new[
  205. candidate.narration_artifact_uri
  206. ],
  207. "subtitle_artifact_uri": old_to_new[
  208. candidate.subtitle_artifact_uri
  209. ],
  210. "primary_artifact_uri": old_to_new[
  211. candidate.primary_artifact_uri
  212. ],
  213. }
  214. )
  215. package_path = write_once_json(
  216. segment_package_path(
  217. run_dir,
  218. package.segment_id,
  219. package.plan_version,
  220. ),
  221. package.model_dump(mode="json"),
  222. )
  223. candidate_path = write_once_json(
  224. segment_executor_candidate_path(
  225. run_dir,
  226. package.segment_id,
  227. package.plan_version,
  228. ),
  229. candidate.model_dump(mode="json"),
  230. )
  231. delivery = delivery.model_copy(
  232. update={
  233. "segment_package_uri": str(package_path.resolve()),
  234. "segment_package_sha256": file_sha256(package_path),
  235. "candidate_uri": str(candidate_path.resolve()),
  236. "artifacts": formal_artifacts,
  237. "tool_calls": [
  238. call.model_copy(
  239. update={
  240. "arguments": (
  241. {
  242. **call.arguments,
  243. "dynamic_segment_input": (
  244. accepted_dependency.primary_artifact.uri
  245. ),
  246. }
  247. if accepted_dependency is not None and index == 0
  248. else call.arguments
  249. ),
  250. "output_refs": [
  251. old_to_new.get(uri, uri)
  252. for uri in call.output_refs
  253. ]
  254. }
  255. )
  256. for index, call in enumerate(delivery.tool_calls)
  257. ],
  258. "artifact_graph": delivery.artifact_graph.model_copy(
  259. update={
  260. "lineages": [
  261. lineage.model_copy(
  262. update={
  263. "input_artifact_ids": [
  264. (
  265. shared_anchor.anchor_id
  266. if artifact_id
  267. == old_anchor.anchor_id
  268. else artifact_id
  269. )
  270. for artifact_id
  271. in lineage.input_artifact_ids
  272. ]
  273. + (
  274. [optional_artifact.artifact_id]
  275. if (
  276. optional_artifact is not None
  277. and index == 0
  278. )
  279. else []
  280. )
  281. + (
  282. [
  283. accepted_dependency.primary_artifact.artifact_id
  284. ]
  285. if (
  286. accepted_dependency is not None
  287. and index == 0
  288. )
  289. else []
  290. )
  291. }
  292. )
  293. for index, lineage in enumerate(
  294. delivery.artifact_graph.lineages
  295. )
  296. ]
  297. }
  298. ),
  299. }
  300. )
  301. write_once_json(
  302. segment_delivery_path(
  303. run_dir,
  304. package.segment_id,
  305. package.plan_version,
  306. ),
  307. delivery.model_dump(mode="json"),
  308. )
  309. write_once_json(
  310. segment_validator_candidate_path(
  311. run_dir,
  312. package.segment_id,
  313. package.plan_version,
  314. ),
  315. validator_candidate.model_dump(mode="json"),
  316. )
  317. write_once_json(
  318. segment_validation_report_path(
  319. run_dir,
  320. package.segment_id,
  321. package.plan_version,
  322. ),
  323. report.model_dump(mode="json"),
  324. )
  325. return package, run_dir
  326. class SegmentAcceptanceTest(unittest.TestCase):
  327. def _accepted(self, package, run_dir):
  328. return build_accepted_segment_ref(
  329. package,
  330. run_dir=run_dir,
  331. package_record_path=segment_package_path(
  332. run_dir,
  333. package.segment_id,
  334. package.plan_version,
  335. ),
  336. )
  337. def test_complete_current_pass_bundle_can_be_accepted(self) -> None:
  338. with tempfile.TemporaryDirectory() as temp_dir:
  339. package, run_dir = _persist_pass_bundle(Path(temp_dir))
  340. accepted = build_accepted_segment_ref(
  341. package,
  342. run_dir=run_dir,
  343. package_record_path=segment_package_path(
  344. run_dir,
  345. package.segment_id,
  346. package.plan_version,
  347. ),
  348. )
  349. self.assertEqual(accepted.segment_id, package.segment_id)
  350. self.assertEqual(
  351. accepted.primary_artifact.segment_id,
  352. package.segment_id,
  353. )
  354. self.assertEqual(
  355. accepted.subtitle_artifact.segment_id,
  356. package.segment_id,
  357. )
  358. def test_tampered_subtitle_is_rejected_before_acceptance(self) -> None:
  359. with tempfile.TemporaryDirectory() as temp_dir:
  360. package, run_dir = _persist_pass_bundle(Path(temp_dir))
  361. delivery_path = segment_delivery_path(
  362. run_dir,
  363. package.segment_id,
  364. package.plan_version,
  365. )
  366. from production_build_agents.contracts.production_models import (
  367. SegmentDelivery,
  368. )
  369. delivery = SegmentDelivery.model_validate_json(
  370. delivery_path.read_text(encoding="utf-8")
  371. )
  372. subtitle = next(
  373. item
  374. for item in delivery.artifacts
  375. if item.artifact_id == delivery.subtitle_artifact_id
  376. )
  377. Path(subtitle.uri).write_text("tampered", encoding="utf-8")
  378. with self.assertRaises(ArtifactIntegrityError):
  379. build_accepted_segment_ref(
  380. package,
  381. run_dir=run_dir,
  382. package_record_path=segment_package_path(
  383. run_dir,
  384. package.segment_id,
  385. package.plan_version,
  386. ),
  387. )
  388. def test_optional_global_pool_growth_does_not_break_reuse(self) -> None:
  389. with tempfile.TemporaryDirectory() as temp_dir:
  390. root = Path(temp_dir)
  391. package, run_dir = _persist_pass_bundle(root)
  392. accepted = self._accepted(package, run_dir)
  393. delivery_path = Path(package.global_data_delivery_ref.uri)
  394. global_delivery = GlobalDataStageDelivery.model_validate_json(
  395. delivery_path.read_bytes()
  396. )
  397. optional_path = root / "new-optional.wav"
  398. optional_path.write_bytes(b"new optional")
  399. optional = Artifact(
  400. artifact_id="Task2-v1-artifact-1",
  401. artifact_type="audio",
  402. uri=str(optional_path.resolve()),
  403. source_uri=None,
  404. description="新增但未使用的可选素材",
  405. **content_identity_for_path(optional_path),
  406. )
  407. current_delivery = global_delivery.model_copy(
  408. update={
  409. "active_artifacts": [
  410. *global_delivery.active_artifacts,
  411. optional,
  412. ]
  413. }
  414. )
  415. current_delivery_path = root / "current-global-delivery.json"
  416. current_delivery_path.write_text(
  417. current_delivery.model_dump_json(),
  418. encoding="utf-8",
  419. )
  420. current = package.model_copy(
  421. update={
  422. "plan_version": 2,
  423. "global_data_delivery_ref": ImmutableJsonRef(
  424. uri=str(current_delivery_path.resolve()),
  425. document_sha256=exact_document_sha256(
  426. current_delivery_path.read_bytes()
  427. ),
  428. ),
  429. }
  430. )
  431. self.assertTrue(
  432. segment_reuse_inputs_compatible(current, accepted)
  433. )
  434. def test_authorized_business_input_change_forces_execute(self) -> None:
  435. with tempfile.TemporaryDirectory() as temp_dir:
  436. root = Path(temp_dir)
  437. package, run_dir = _persist_pass_bundle(root)
  438. accepted = self._accepted(package, run_dir)
  439. current = package.model_copy(
  440. update={
  441. "plan_version": 2,
  442. "shots": [
  443. shot.model_copy(
  444. update={"objective": "授权修订后的镜头目标"}
  445. )
  446. for shot in package.shots
  447. ],
  448. }
  449. )
  450. self.assertFalse(
  451. segment_reuse_inputs_compatible(current, accepted)
  452. )
  453. def test_unused_accepted_segment_pool_growth_does_not_break_reuse(
  454. self,
  455. ) -> None:
  456. with tempfile.TemporaryDirectory() as temp_dir:
  457. root = Path(temp_dir)
  458. package, run_dir = _persist_pass_bundle(root / "current")
  459. accepted = self._accepted(package, run_dir)
  460. dependency_package, dependency_run_dir = _persist_pass_bundle(
  461. root / "dependency",
  462. segment_index=2,
  463. run_id="dependency-run",
  464. )
  465. dependency_accepted = self._accepted(
  466. dependency_package,
  467. dependency_run_dir,
  468. )
  469. current = package.model_copy(
  470. update={
  471. "plan_version": 2,
  472. "dependencies": [
  473. SegmentDependencyRef(
  474. segment_id=dependency_accepted.segment_id,
  475. segment_delivery_ref=(
  476. dependency_accepted.segment_delivery_ref
  477. ),
  478. validation_report_ref=(
  479. dependency_accepted.validation_report_ref
  480. ),
  481. )
  482. ],
  483. }
  484. )
  485. self.assertTrue(
  486. segment_reuse_inputs_compatible(current, accepted)
  487. )
  488. def test_independent_dag_replan_invalidates_dynamic_cross_segment_input(
  489. self,
  490. ) -> None:
  491. """无显式 DAG 边时,也不能把已变化的旧上游泄漏给消费方。"""
  492. with tempfile.TemporaryDirectory() as temp_dir:
  493. root = Path(temp_dir)
  494. producer_package, producer_run_dir = _persist_pass_bundle(
  495. root / "producer",
  496. segment_index=1,
  497. run_id="dynamic-run",
  498. )
  499. producer_accepted = self._accepted(
  500. producer_package,
  501. producer_run_dir,
  502. )
  503. consumer_package, consumer_run_dir = _persist_pass_bundle(
  504. root / "consumer",
  505. accepted_dependency=producer_accepted,
  506. segment_index=2,
  507. run_id="dynamic-run",
  508. )
  509. consumer_accepted = self._accepted(
  510. consumer_package,
  511. consumer_run_dir,
  512. )
  513. previous = production_plan(
  514. [producer_package, consumer_package]
  515. )
  516. independent_segments = [
  517. previous.segments[0].model_copy(
  518. update={"depends_on": [], "priority": 20}
  519. ),
  520. previous.segments[1].model_copy(
  521. update={"depends_on": [], "priority": 10}
  522. ),
  523. ]
  524. previous = previous.model_copy(
  525. update={"segments": independent_segments}
  526. )
  527. changed_producer = independent_segments[0].model_copy(
  528. update={
  529. "shots": [
  530. independent_segments[0].shots[0].model_copy(
  531. update={"objective": "Replan 后的新镜头目标"}
  532. )
  533. ]
  534. }
  535. )
  536. current = previous.model_copy(
  537. update={
  538. "plan_version": 2,
  539. "segments": [
  540. changed_producer,
  541. independent_segments[1],
  542. ],
  543. }
  544. )
  545. planning = build_production_planner_scenario(
  546. run_id="dynamic-run"
  547. ).planning_package.model_copy(
  548. update={
  549. "mode": "REPLAN",
  550. "target_plan_version": 2,
  551. "previous_plan_ref": (
  552. producer_accepted.segment_package_ref
  553. ),
  554. "failure_report_ref": (
  555. producer_accepted.validation_report_ref
  556. ),
  557. "accepted_segments": [
  558. producer_accepted,
  559. consumer_accepted,
  560. ],
  561. "authorized_revision_segment_ids": ["Segment1"],
  562. }
  563. )
  564. current_producer = producer_package.model_copy(
  565. update={
  566. "plan_version": 2,
  567. "shots": changed_producer.shots,
  568. }
  569. )
  570. current_consumer = consumer_package.model_copy(
  571. update={
  572. "plan_version": 2,
  573. "dependencies": [],
  574. }
  575. )
  576. scenario = build_production_planner_scenario(
  577. run_id="dynamic-run"
  578. )
  579. packages = {
  580. "Segment1": current_producer,
  581. "Segment2": current_consumer,
  582. }
  583. with patch(
  584. "production_build_agents.run.segment_inputs."
  585. "materialize_segment_package",
  586. side_effect=lambda *_args, segment_id, **_kwargs: (
  587. packages[segment_id]
  588. ),
  589. ):
  590. snapshot = reusable_accepted_segment_snapshot(
  591. scenario.brief,
  592. scenario.global_data_delivery,
  593. scenario.production_input_catalog,
  594. planning,
  595. current,
  596. {},
  597. planning_package_ref=planning.production_brief_ref,
  598. )
  599. self.assertEqual(list(snapshot), ["Segment2"])
  600. self.assertFalse(
  601. segment_reuse_inputs_compatible(
  602. current_consumer,
  603. consumer_accepted,
  604. )
  605. )
  606. def test_missing_actually_used_optional_artifact_forces_execute(
  607. self,
  608. ) -> None:
  609. with tempfile.TemporaryDirectory() as temp_dir:
  610. root = Path(temp_dir)
  611. package, run_dir = _persist_pass_bundle(
  612. root,
  613. include_unselected_actual_input=True,
  614. )
  615. accepted = self._accepted(package, run_dir)
  616. source_delivery = GlobalDataStageDelivery.model_validate_json(
  617. Path(package.global_data_delivery_ref.uri).read_bytes()
  618. )
  619. reduced = source_delivery.model_copy(
  620. update={
  621. "active_artifacts": [
  622. source_delivery.active_artifacts[0]
  623. ]
  624. }
  625. )
  626. reduced_path = root / "reduced-global-delivery.json"
  627. reduced_path.write_text(
  628. reduced.model_dump_json(),
  629. encoding="utf-8",
  630. )
  631. current = package.model_copy(
  632. update={
  633. "plan_version": 2,
  634. "global_data_delivery_ref": ImmutableJsonRef(
  635. uri=str(reduced_path.resolve()),
  636. document_sha256=exact_document_sha256(
  637. reduced_path.read_bytes()
  638. ),
  639. ),
  640. }
  641. )
  642. self.assertFalse(
  643. segment_reuse_inputs_compatible(current, accepted)
  644. )
  645. changed_path = root / "changed-optional-audio.wav"
  646. changed_path.write_bytes(b"changed optional audio")
  647. changed_optional = source_delivery.active_artifacts[1].model_copy(
  648. update={
  649. "uri": str(changed_path.resolve()),
  650. **content_identity_for_path(changed_path),
  651. }
  652. )
  653. changed_delivery = source_delivery.model_copy(
  654. update={
  655. "active_artifacts": [
  656. source_delivery.active_artifacts[0],
  657. changed_optional,
  658. ]
  659. }
  660. )
  661. changed_delivery_path = root / "changed-global-delivery.json"
  662. changed_delivery_path.write_text(
  663. changed_delivery.model_dump_json(),
  664. encoding="utf-8",
  665. )
  666. changed_current = package.model_copy(
  667. update={
  668. "plan_version": 2,
  669. "global_data_delivery_ref": ImmutableJsonRef(
  670. uri=str(changed_delivery_path.resolve()),
  671. document_sha256=exact_document_sha256(
  672. changed_delivery_path.read_bytes()
  673. ),
  674. ),
  675. }
  676. )
  677. self.assertFalse(
  678. segment_reuse_inputs_compatible(
  679. changed_current,
  680. accepted,
  681. )
  682. )
  683. def test_reuse_compatibility_fails_closed_on_source_bundle_tamper(
  684. self,
  685. ) -> None:
  686. for target in ("artifact", "report"):
  687. with self.subTest(target=target):
  688. with tempfile.TemporaryDirectory() as temp_dir:
  689. root = Path(temp_dir)
  690. package, run_dir = _persist_pass_bundle(root)
  691. accepted = self._accepted(package, run_dir)
  692. current = package.model_copy(
  693. update={"plan_version": 2}
  694. )
  695. if target == "artifact":
  696. from production_build_agents.contracts.production_models import (
  697. SegmentDelivery,
  698. )
  699. delivery = SegmentDelivery.model_validate_json(
  700. Path(
  701. accepted.segment_delivery_ref.uri
  702. ).read_bytes()
  703. )
  704. artifact = next(
  705. item
  706. for item in delivery.artifacts
  707. if item.artifact_id
  708. == delivery.subtitle_artifact_id
  709. )
  710. Path(artifact.uri).write_text(
  711. "tampered",
  712. encoding="utf-8",
  713. )
  714. else:
  715. Path(accepted.validation_report_ref.uri).write_text(
  716. "{}",
  717. encoding="utf-8",
  718. )
  719. with self.assertRaises(ValueError):
  720. segment_reuse_inputs_compatible(
  721. current,
  722. accepted,
  723. )
  724. if __name__ == "__main__":
  725. unittest.main()