|
|
@@ -5,7 +5,16 @@ import unittest
|
|
|
from pathlib import Path
|
|
|
|
|
|
from production_build_agents.contracts.models import (
|
|
|
+ Artifact,
|
|
|
CandidateArtifact,
|
|
|
+ GlobalDataStageDelivery,
|
|
|
+)
|
|
|
+from production_build_agents.contracts.document_identity import (
|
|
|
+ exact_document_sha256,
|
|
|
+)
|
|
|
+from production_build_agents.contracts.production_execution_models import (
|
|
|
+ ImmutableJsonRef,
|
|
|
+ SegmentDependencyRef,
|
|
|
)
|
|
|
from production_build_agents.run.artifacts import (
|
|
|
ArtifactIntegrityError,
|
|
|
@@ -23,11 +32,22 @@ from production_build_agents.run.layout import (
|
|
|
from production_build_agents.run.records import write_once_json
|
|
|
from production_build_agents.production.segment_acceptance import (
|
|
|
build_accepted_segment_ref,
|
|
|
+ segment_reuse_inputs_compatible,
|
|
|
)
|
|
|
from tests.support.production_fixtures import segment_bundle
|
|
|
+from tests.support.production_planner_fixtures import (
|
|
|
+ build_production_planner_scenario,
|
|
|
+)
|
|
|
|
|
|
|
|
|
-def _persist_pass_bundle(root: Path):
|
|
|
+def _persist_pass_bundle(
|
|
|
+ root: Path,
|
|
|
+ *,
|
|
|
+ include_unselected_actual_input: bool = False,
|
|
|
+ segment_index: int = 1,
|
|
|
+ run_id: str = "source-run",
|
|
|
+):
|
|
|
+ root.mkdir(parents=True, exist_ok=True)
|
|
|
(
|
|
|
package,
|
|
|
candidate,
|
|
|
@@ -35,8 +55,8 @@ def _persist_pass_bundle(root: Path):
|
|
|
validator_candidate,
|
|
|
report,
|
|
|
_,
|
|
|
- ) = segment_bundle(1, run_id="source-run")
|
|
|
- run_dir = root / "source-run"
|
|
|
+ ) = segment_bundle(segment_index, run_id=run_id)
|
|
|
+ run_dir = root / run_id
|
|
|
source_path = root / "source.png"
|
|
|
source_path.write_bytes(b"source")
|
|
|
old_source_uri = package.artifacts[0].uri
|
|
|
@@ -57,6 +77,40 @@ def _persist_pass_bundle(root: Path):
|
|
|
"shared_visual_anchor": shared_anchor,
|
|
|
}
|
|
|
)
|
|
|
+ global_artifacts = [source_artifact]
|
|
|
+ optional_artifact = None
|
|
|
+ if include_unselected_actual_input:
|
|
|
+ optional_path = root / "optional-audio.wav"
|
|
|
+ optional_path.write_bytes(b"optional-audio")
|
|
|
+ optional_artifact = Artifact(
|
|
|
+ artifact_id="Task2-v1-artifact-1",
|
|
|
+ artifact_type="audio",
|
|
|
+ uri=str(optional_path.resolve()),
|
|
|
+ source_uri=None,
|
|
|
+ description="Planner 未选择但 Executor 实际使用的素材",
|
|
|
+ **content_identity_for_path(optional_path),
|
|
|
+ )
|
|
|
+ global_artifacts.append(optional_artifact)
|
|
|
+ global_delivery = build_production_planner_scenario(
|
|
|
+ run_id="source-global"
|
|
|
+ ).global_data_delivery.model_copy(
|
|
|
+ update={"active_artifacts": global_artifacts}
|
|
|
+ )
|
|
|
+ global_delivery_path = root / "global_data_stage_delivery.json"
|
|
|
+ global_delivery_path.write_text(
|
|
|
+ global_delivery.model_dump_json(),
|
|
|
+ encoding="utf-8",
|
|
|
+ )
|
|
|
+ package = package.model_copy(
|
|
|
+ update={
|
|
|
+ "global_data_delivery_ref": ImmutableJsonRef(
|
|
|
+ uri=str(global_delivery_path.resolve()),
|
|
|
+ document_sha256=exact_document_sha256(
|
|
|
+ global_delivery_path.read_bytes()
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ }
|
|
|
+ )
|
|
|
|
|
|
old_to_new = {
|
|
|
old_source_uri: str(source_path.resolve()),
|
|
|
@@ -99,10 +153,18 @@ def _persist_pass_bundle(root: Path):
|
|
|
"input_artifact_uris": [
|
|
|
old_to_new.get(uri, uri)
|
|
|
for uri in claim.input_artifact_uris
|
|
|
- ],
|
|
|
+ ]
|
|
|
+ + (
|
|
|
+ [optional_artifact.uri]
|
|
|
+ if (
|
|
|
+ optional_artifact is not None
|
|
|
+ and index == 0
|
|
|
+ )
|
|
|
+ else []
|
|
|
+ ),
|
|
|
}
|
|
|
)
|
|
|
- for claim in candidate.lineage_claims
|
|
|
+ for index, claim in enumerate(candidate.lineage_claims)
|
|
|
],
|
|
|
"shot_outputs": [
|
|
|
claim.model_copy(
|
|
|
@@ -171,9 +233,19 @@ def _persist_pass_bundle(root: Path):
|
|
|
for artifact_id
|
|
|
in lineage.input_artifact_ids
|
|
|
]
|
|
|
+ + (
|
|
|
+ [optional_artifact.artifact_id]
|
|
|
+ if (
|
|
|
+ optional_artifact is not None
|
|
|
+ and index == 0
|
|
|
+ )
|
|
|
+ else []
|
|
|
+ )
|
|
|
}
|
|
|
)
|
|
|
- for lineage in delivery.artifact_graph.lineages
|
|
|
+ for index, lineage in enumerate(
|
|
|
+ delivery.artifact_graph.lineages
|
|
|
+ )
|
|
|
]
|
|
|
}
|
|
|
),
|
|
|
@@ -207,6 +279,17 @@ def _persist_pass_bundle(root: Path):
|
|
|
|
|
|
|
|
|
class SegmentAcceptanceTest(unittest.TestCase):
|
|
|
+ def _accepted(self, package, run_dir):
|
|
|
+ return build_accepted_segment_ref(
|
|
|
+ package,
|
|
|
+ run_dir=run_dir,
|
|
|
+ package_record_path=segment_package_path(
|
|
|
+ run_dir,
|
|
|
+ package.segment_id,
|
|
|
+ package.plan_version,
|
|
|
+ ),
|
|
|
+ )
|
|
|
+
|
|
|
def test_complete_current_pass_bundle_can_be_accepted(self) -> None:
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
|
package, run_dir = _persist_pass_bundle(Path(temp_dir))
|
|
|
@@ -261,6 +344,231 @@ class SegmentAcceptanceTest(unittest.TestCase):
|
|
|
),
|
|
|
)
|
|
|
|
|
|
+ def test_optional_global_pool_growth_does_not_break_reuse(self) -> None:
|
|
|
+ with tempfile.TemporaryDirectory() as temp_dir:
|
|
|
+ root = Path(temp_dir)
|
|
|
+ package, run_dir = _persist_pass_bundle(root)
|
|
|
+ accepted = self._accepted(package, run_dir)
|
|
|
+ delivery_path = Path(package.global_data_delivery_ref.uri)
|
|
|
+ global_delivery = GlobalDataStageDelivery.model_validate_json(
|
|
|
+ delivery_path.read_bytes()
|
|
|
+ )
|
|
|
+ optional_path = root / "new-optional.wav"
|
|
|
+ optional_path.write_bytes(b"new optional")
|
|
|
+ optional = Artifact(
|
|
|
+ artifact_id="Task2-v1-artifact-1",
|
|
|
+ artifact_type="audio",
|
|
|
+ uri=str(optional_path.resolve()),
|
|
|
+ source_uri=None,
|
|
|
+ description="新增但未使用的可选素材",
|
|
|
+ **content_identity_for_path(optional_path),
|
|
|
+ )
|
|
|
+ current_delivery = global_delivery.model_copy(
|
|
|
+ update={
|
|
|
+ "active_artifacts": [
|
|
|
+ *global_delivery.active_artifacts,
|
|
|
+ optional,
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ )
|
|
|
+ current_delivery_path = root / "current-global-delivery.json"
|
|
|
+ current_delivery_path.write_text(
|
|
|
+ current_delivery.model_dump_json(),
|
|
|
+ encoding="utf-8",
|
|
|
+ )
|
|
|
+ current = package.model_copy(
|
|
|
+ update={
|
|
|
+ "plan_version": 2,
|
|
|
+ "global_data_delivery_ref": ImmutableJsonRef(
|
|
|
+ uri=str(current_delivery_path.resolve()),
|
|
|
+ document_sha256=exact_document_sha256(
|
|
|
+ current_delivery_path.read_bytes()
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ }
|
|
|
+ )
|
|
|
+ self.assertTrue(
|
|
|
+ segment_reuse_inputs_compatible(current, accepted)
|
|
|
+ )
|
|
|
+
|
|
|
+ def test_authorized_business_input_change_forces_execute(self) -> None:
|
|
|
+ with tempfile.TemporaryDirectory() as temp_dir:
|
|
|
+ root = Path(temp_dir)
|
|
|
+ package, run_dir = _persist_pass_bundle(root)
|
|
|
+ accepted = self._accepted(package, run_dir)
|
|
|
+ current = package.model_copy(
|
|
|
+ update={
|
|
|
+ "plan_version": 2,
|
|
|
+ "shots": [
|
|
|
+ shot.model_copy(
|
|
|
+ update={"objective": "授权修订后的镜头目标"}
|
|
|
+ )
|
|
|
+ for shot in package.shots
|
|
|
+ ],
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ self.assertFalse(
|
|
|
+ segment_reuse_inputs_compatible(current, accepted)
|
|
|
+ )
|
|
|
+
|
|
|
+ def test_unused_accepted_segment_pool_growth_does_not_break_reuse(
|
|
|
+ self,
|
|
|
+ ) -> None:
|
|
|
+ with tempfile.TemporaryDirectory() as temp_dir:
|
|
|
+ root = Path(temp_dir)
|
|
|
+ package, run_dir = _persist_pass_bundle(root / "current")
|
|
|
+ accepted = self._accepted(package, run_dir)
|
|
|
+ dependency_package, dependency_run_dir = _persist_pass_bundle(
|
|
|
+ root / "dependency",
|
|
|
+ segment_index=2,
|
|
|
+ run_id="dependency-run",
|
|
|
+ )
|
|
|
+ dependency_accepted = self._accepted(
|
|
|
+ dependency_package,
|
|
|
+ dependency_run_dir,
|
|
|
+ )
|
|
|
+ current = package.model_copy(
|
|
|
+ update={
|
|
|
+ "plan_version": 2,
|
|
|
+ "dependencies": [
|
|
|
+ SegmentDependencyRef(
|
|
|
+ segment_id=dependency_accepted.segment_id,
|
|
|
+ segment_delivery_ref=(
|
|
|
+ dependency_accepted.segment_delivery_ref
|
|
|
+ ),
|
|
|
+ validation_report_ref=(
|
|
|
+ dependency_accepted.validation_report_ref
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ }
|
|
|
+ )
|
|
|
+ self.assertTrue(
|
|
|
+ segment_reuse_inputs_compatible(current, accepted)
|
|
|
+ )
|
|
|
+
|
|
|
+ def test_missing_actually_used_optional_artifact_forces_execute(
|
|
|
+ self,
|
|
|
+ ) -> None:
|
|
|
+ with tempfile.TemporaryDirectory() as temp_dir:
|
|
|
+ root = Path(temp_dir)
|
|
|
+ package, run_dir = _persist_pass_bundle(
|
|
|
+ root,
|
|
|
+ include_unselected_actual_input=True,
|
|
|
+ )
|
|
|
+ accepted = self._accepted(package, run_dir)
|
|
|
+ source_delivery = GlobalDataStageDelivery.model_validate_json(
|
|
|
+ Path(package.global_data_delivery_ref.uri).read_bytes()
|
|
|
+ )
|
|
|
+ reduced = source_delivery.model_copy(
|
|
|
+ update={
|
|
|
+ "active_artifacts": [
|
|
|
+ source_delivery.active_artifacts[0]
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ )
|
|
|
+ reduced_path = root / "reduced-global-delivery.json"
|
|
|
+ reduced_path.write_text(
|
|
|
+ reduced.model_dump_json(),
|
|
|
+ encoding="utf-8",
|
|
|
+ )
|
|
|
+ current = package.model_copy(
|
|
|
+ update={
|
|
|
+ "plan_version": 2,
|
|
|
+ "global_data_delivery_ref": ImmutableJsonRef(
|
|
|
+ uri=str(reduced_path.resolve()),
|
|
|
+ document_sha256=exact_document_sha256(
|
|
|
+ reduced_path.read_bytes()
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ }
|
|
|
+ )
|
|
|
+ self.assertFalse(
|
|
|
+ segment_reuse_inputs_compatible(current, accepted)
|
|
|
+ )
|
|
|
+ changed_path = root / "changed-optional-audio.wav"
|
|
|
+ changed_path.write_bytes(b"changed optional audio")
|
|
|
+ changed_optional = source_delivery.active_artifacts[1].model_copy(
|
|
|
+ update={
|
|
|
+ "uri": str(changed_path.resolve()),
|
|
|
+ **content_identity_for_path(changed_path),
|
|
|
+ }
|
|
|
+ )
|
|
|
+ changed_delivery = source_delivery.model_copy(
|
|
|
+ update={
|
|
|
+ "active_artifacts": [
|
|
|
+ source_delivery.active_artifacts[0],
|
|
|
+ changed_optional,
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ )
|
|
|
+ changed_delivery_path = root / "changed-global-delivery.json"
|
|
|
+ changed_delivery_path.write_text(
|
|
|
+ changed_delivery.model_dump_json(),
|
|
|
+ encoding="utf-8",
|
|
|
+ )
|
|
|
+ changed_current = package.model_copy(
|
|
|
+ update={
|
|
|
+ "plan_version": 2,
|
|
|
+ "global_data_delivery_ref": ImmutableJsonRef(
|
|
|
+ uri=str(changed_delivery_path.resolve()),
|
|
|
+ document_sha256=exact_document_sha256(
|
|
|
+ changed_delivery_path.read_bytes()
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ }
|
|
|
+ )
|
|
|
+ self.assertFalse(
|
|
|
+ segment_reuse_inputs_compatible(
|
|
|
+ changed_current,
|
|
|
+ accepted,
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ def test_reuse_compatibility_fails_closed_on_source_bundle_tamper(
|
|
|
+ self,
|
|
|
+ ) -> None:
|
|
|
+ for target in ("artifact", "report"):
|
|
|
+ with self.subTest(target=target):
|
|
|
+ with tempfile.TemporaryDirectory() as temp_dir:
|
|
|
+ root = Path(temp_dir)
|
|
|
+ package, run_dir = _persist_pass_bundle(root)
|
|
|
+ accepted = self._accepted(package, run_dir)
|
|
|
+ current = package.model_copy(
|
|
|
+ update={"plan_version": 2}
|
|
|
+ )
|
|
|
+ if target == "artifact":
|
|
|
+ from production_build_agents.contracts.production_models import (
|
|
|
+ SegmentDelivery,
|
|
|
+ )
|
|
|
+
|
|
|
+ delivery = SegmentDelivery.model_validate_json(
|
|
|
+ Path(
|
|
|
+ accepted.segment_delivery_ref.uri
|
|
|
+ ).read_bytes()
|
|
|
+ )
|
|
|
+ artifact = next(
|
|
|
+ item
|
|
|
+ for item in delivery.artifacts
|
|
|
+ if item.artifact_id
|
|
|
+ == delivery.subtitle_artifact_id
|
|
|
+ )
|
|
|
+ Path(artifact.uri).write_text(
|
|
|
+ "tampered",
|
|
|
+ encoding="utf-8",
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ Path(accepted.validation_report_ref.uri).write_text(
|
|
|
+ "{}",
|
|
|
+ encoding="utf-8",
|
|
|
+ )
|
|
|
+ with self.assertRaises(ValueError):
|
|
|
+ segment_reuse_inputs_compatible(
|
|
|
+ current,
|
|
|
+ accepted,
|
|
|
+ )
|
|
|
+
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
unittest.main()
|