test_phase_three_contracts.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. from __future__ import annotations
  2. from dataclasses import replace
  3. import pytest
  4. from script_build_host.domain.errors import (
  5. LegacyCanonicalIdentityConflict,
  6. ProtocolViolation,
  7. )
  8. from script_build_host.domain.phase_three_artifacts import (
  9. RootDeliveryManifestV1,
  10. canonicalize_legacy_projection,
  11. )
  12. from script_build_host.domain.phase_two_artifacts import (
  13. ScriptElementV1,
  14. ScriptParagraphElementLinkV1,
  15. ScriptParagraphV1,
  16. StructuredScriptArtifactV1,
  17. )
  18. _DIGEST = "sha256:" + "a" * 64
  19. def _script(
  20. *,
  21. root_id: int = 10,
  22. child_id: int = 20,
  23. element_id: int = 30,
  24. child_description: str = "child body",
  25. ) -> StructuredScriptArtifactV1:
  26. return StructuredScriptArtifactV1(
  27. direction_ref="script-build://artifact-versions/1",
  28. input_closure_digest=_DIGEST,
  29. paragraphs=(
  30. ScriptParagraphV1(
  31. paragraph_id=root_id,
  32. paragraph_index=1,
  33. level=1,
  34. parent_id=None,
  35. name="root",
  36. content_range={"start": 0, "end": 10},
  37. description="root body",
  38. ),
  39. ScriptParagraphV1(
  40. paragraph_id=child_id,
  41. paragraph_index=2,
  42. level=2,
  43. parent_id=root_id,
  44. name="child",
  45. content_range={"start": 10, "end": 20},
  46. description=child_description,
  47. ),
  48. ),
  49. elements=(
  50. ScriptElementV1(
  51. element_id=element_id,
  52. name="hook",
  53. dimension_primary="形式",
  54. dimension_secondary="opening",
  55. topic_support={"score": 1},
  56. ),
  57. ),
  58. paragraph_element_links=(
  59. ScriptParagraphElementLinkV1(
  60. paragraph_id=child_id,
  61. element_id=element_id,
  62. ),
  63. ),
  64. source_artifact_refs=(),
  65. evidence_refs=(),
  66. acceptance_notes=(),
  67. )
  68. def test_legacy_canonical_digest_ignores_every_physical_identity() -> None:
  69. first = canonicalize_legacy_projection(_script(), direction="# D", summary="summary")
  70. second = canonicalize_legacy_projection(
  71. _script(root_id=101, child_id=202, element_id=303),
  72. direction="# D",
  73. summary="summary",
  74. )
  75. assert first.content_payload() == second.content_payload()
  76. assert first.canonical_sha256 == second.canonical_sha256
  77. def test_legacy_canonical_digest_changes_with_business_content() -> None:
  78. first = canonicalize_legacy_projection(_script(), direction="# D", summary="summary")
  79. changed_body = canonicalize_legacy_projection(
  80. _script(child_description="changed"), direction="# D", summary="summary"
  81. )
  82. changed_direction = canonicalize_legacy_projection(
  83. _script(), direction="# other", summary="summary"
  84. )
  85. assert first.canonical_sha256 != changed_body.canonical_sha256
  86. assert first.canonical_sha256 != changed_direction.canonical_sha256
  87. def test_legacy_canonical_rejects_parent_cycles_and_duplicate_natural_elements() -> None:
  88. cycle = replace(
  89. _script(),
  90. paragraphs=(
  91. ScriptParagraphV1(10, 1, 2, 20, "a", {}),
  92. ScriptParagraphV1(20, 2, 2, 10, "b", {}),
  93. ),
  94. )
  95. with pytest.raises(LegacyCanonicalIdentityConflict, match="cycle"):
  96. canonicalize_legacy_projection(cycle)
  97. duplicate_elements = replace(
  98. _script(),
  99. elements=(
  100. ScriptElementV1(30, "hook", "形式", "opening"),
  101. ScriptElementV1(31, "hook", "形式", "opening"),
  102. ),
  103. paragraph_element_links=(),
  104. )
  105. with pytest.raises(LegacyCanonicalIdentityConflict, match="duplicated"):
  106. canonicalize_legacy_projection(duplicate_elements)
  107. def test_root_delivery_manifest_is_strict_bounded_and_publishable() -> None:
  108. manifest = RootDeliveryManifestV1.from_payload(
  109. {
  110. "schema_version": "root-delivery-manifest/v1",
  111. "direction_ref": "script-build://artifact-versions/1",
  112. "candidate_portfolio_ref": "script-build://artifact-versions/2",
  113. "structured_script_ref": "script-build://artifact-versions/3",
  114. "input_closure_digest": _DIGEST,
  115. "legacy_projection_digest": _DIGEST,
  116. "build_summary": "ok",
  117. "blocking_defects": [],
  118. }
  119. ).with_digest()
  120. manifest.require_publishable()
  121. assert manifest.canonical_sha256.startswith("sha256:")
  122. with pytest.raises(ProtocolViolation, match="unknown fields"):
  123. RootDeliveryManifestV1.from_payload({**manifest.content_payload(), "unknown": True})
  124. with pytest.raises(ProtocolViolation, match="blocking defects"):
  125. replace(
  126. manifest, blocking_defects=("not ready",), canonical_sha256=""
  127. ).require_publishable()
  128. with pytest.raises(ProtocolViolation, match="2,000"):
  129. replace(manifest, build_summary="x" * 2_001, canonical_sha256="")