test_direction_artifact.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. from __future__ import annotations
  2. import pytest
  3. from script_build_host.domain.artifacts import (
  4. ArtifactKind,
  5. DirectionArtifact,
  6. DirectionConstraint,
  7. DirectionGoal,
  8. DirectionPreference,
  9. )
  10. from script_build_host.domain.errors import ProtocolViolation
  11. from script_build_host.repositories.sqlalchemy import _hydrate_artifact
  12. from script_build_host.tools.gateway import _direction_goals
  13. from script_build_host.tools.registry import _save_direction_candidate_schema
  14. EVIDENCE_REF = "script-build://artifact-versions/1"
  15. def _goal(index: int, *, parent: str | None = None) -> DirectionGoal:
  16. return DirectionGoal(
  17. goal_id=f"goal-{index}",
  18. parent_goal_id=parent,
  19. statement=f"goal statement {index}",
  20. rationale=f"goal rationale {index}",
  21. success_criteria=(f"goal criterion {index}",),
  22. )
  23. def test_direction_supports_hierarchy_rules_and_deterministic_projection() -> None:
  24. direction = DirectionArtifact(
  25. goals=(_goal(1), _goal(2, parent="goal-1"), _goal(3)),
  26. constraints=(
  27. DirectionConstraint("constraint-1", "facts remain traceable", "hard boundary"),
  28. ),
  29. preferences=(DirectionPreference("preference-1", "prefer a concise opening", priority=1),),
  30. evidence_refs=(EVIDENCE_REF,),
  31. )
  32. payload = direction.content_payload()
  33. assert payload["schema_version"] == "script-direction/v1"
  34. assert payload["goals"][1]["parent_goal_id"] == "goal-1"
  35. assert payload["constraints"][0]["constraint_id"] == "constraint-1"
  36. assert payload["preferences"][0]["priority"] == 1
  37. assert "### goal-1: goal statement 1" in direction.legacy_markdown
  38. assert "#### goal-2: goal statement 2" in direction.legacy_markdown
  39. assert "## Constraints" in direction.legacy_markdown
  40. assert "## Preferences" in direction.legacy_markdown
  41. assert EVIDENCE_REF in direction.legacy_markdown
  42. def test_direction_accepts_thirty_goal_nodes_and_rejects_thirty_one() -> None:
  43. DirectionArtifact(
  44. goals=tuple(_goal(index) for index in range(1, 31)), evidence_refs=(EVIDENCE_REF,)
  45. )
  46. with pytest.raises(ValueError, match="between one and thirty"):
  47. DirectionArtifact(
  48. goals=tuple(_goal(index) for index in range(1, 32)),
  49. evidence_refs=(EVIDENCE_REF,),
  50. )
  51. @pytest.mark.parametrize(
  52. ("goals", "message"),
  53. [
  54. ((_goal(1, parent="missing"),), "parent must exist"),
  55. (
  56. (
  57. _goal(1),
  58. _goal(2, parent="goal-1"),
  59. _goal(3, parent="goal-2"),
  60. ),
  61. "only two levels",
  62. ),
  63. (
  64. (DirectionGoal("goal-1", "statement", "rationale", success_criteria=()),),
  65. "requires success criteria",
  66. ),
  67. (
  68. (DirectionGoal("goal-1", "statement", "", success_criteria=("check",)),),
  69. "requires a rationale",
  70. ),
  71. ],
  72. )
  73. def test_direction_rejects_invalid_goal_hierarchy(
  74. goals: tuple[DirectionGoal, ...], message: str
  75. ) -> None:
  76. with pytest.raises(ValueError, match=message):
  77. DirectionArtifact(goals=goals, evidence_refs=(EVIDENCE_REF,))
  78. def test_direction_tool_contract_keeps_schema_name_and_removes_model_markdown() -> None:
  79. parameters = _save_direction_candidate_schema()["function"]["parameters"]
  80. assert parameters["properties"]["goals"]["maxItems"] == 30
  81. assert "constraints" in parameters["required"]
  82. assert "preferences" in parameters["required"]
  83. assert "legacy_markdown" not in parameters["properties"]
  84. assert "criteria" not in parameters["properties"]
  85. def _client_goal(key: str, parent: str | None = None) -> dict[str, object]:
  86. return {
  87. "client_key": key,
  88. "parent_client_key": parent,
  89. "statement": f"statement for {key}",
  90. "rationale": f"rationale for {key}",
  91. "success_criteria": [f"criterion for {key}"],
  92. }
  93. def test_direction_client_keys_generate_stable_order_independent_goal_ids() -> None:
  94. first, first_map = _direction_goals(
  95. "task-a", [_client_goal("Parent"), _client_goal("Child", "Parent")]
  96. )
  97. second, second_map = _direction_goals(
  98. "task-a", [_client_goal(" child ", " parent "), _client_goal("parent")]
  99. )
  100. assert first_map == second_map
  101. assert {item.goal_id for item in first} == {item.goal_id for item in second}
  102. child = next(item for item in second if item.parent_goal_id)
  103. assert child.parent_goal_id == first_map["parent"]
  104. @pytest.mark.parametrize(
  105. ("goals", "message"),
  106. [
  107. ([_client_goal("same"), _client_goal("SAME")], "must be unique"),
  108. ([_client_goal("child", "missing")], "does not exist"),
  109. ([_client_goal("self", "self")], "cannot parent itself"),
  110. (
  111. [
  112. _client_goal("root"),
  113. _client_goal("child", "root"),
  114. _client_goal("grandchild", "child"),
  115. ],
  116. "only two levels",
  117. ),
  118. ([_client_goal(str(index)) for index in range(31)], "between one and thirty"),
  119. ],
  120. )
  121. def test_direction_client_keys_reject_invalid_batches(
  122. goals: list[dict[str, object]], message: str
  123. ) -> None:
  124. with pytest.raises(ValueError, match=message):
  125. _direction_goals("task-a", goals)
  126. def test_direction_hydration_rejects_historical_structure_without_compatibility() -> None:
  127. historical_payload = {
  128. "schema_version": "script-direction/v1",
  129. "goals": [{"goal_id": "goal-1", "statement": "old goal", "rationale": "old rationale"}],
  130. "criteria": [{"criterion_id": "criterion-1", "description": "old criterion"}],
  131. "domain_criteria": [],
  132. "evidence_refs": [EVIDENCE_REF],
  133. "legacy_markdown": "# old projection",
  134. }
  135. with pytest.raises(ProtocolViolation, match="current structured contract"):
  136. _hydrate_artifact(ArtifactKind.DIRECTION, historical_payload, "sha256:" + "a" * 64)