|
@@ -0,0 +1,114 @@
|
|
|
|
|
+from __future__ import annotations
|
|
|
|
|
+
|
|
|
|
|
+import pytest
|
|
|
|
|
+
|
|
|
|
|
+from script_build_host.domain.artifacts import (
|
|
|
|
|
+ ArtifactKind,
|
|
|
|
|
+ DirectionArtifact,
|
|
|
|
|
+ DirectionConstraint,
|
|
|
|
|
+ DirectionGoal,
|
|
|
|
|
+ DirectionPreference,
|
|
|
|
|
+)
|
|
|
|
|
+from script_build_host.domain.errors import ProtocolViolation
|
|
|
|
|
+from script_build_host.repositories.sqlalchemy import _hydrate_artifact
|
|
|
|
|
+from script_build_host.tools.registry import _save_direction_candidate_schema
|
|
|
|
|
+
|
|
|
|
|
+EVIDENCE_REF = "script-build://artifact-versions/1"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _goal(index: int, *, parent: str | None = None) -> DirectionGoal:
|
|
|
|
|
+ return DirectionGoal(
|
|
|
|
|
+ goal_id=f"goal-{index}",
|
|
|
|
|
+ parent_goal_id=parent,
|
|
|
|
|
+ statement=f"goal statement {index}",
|
|
|
|
|
+ rationale=f"goal rationale {index}",
|
|
|
|
|
+ success_criteria=(f"goal criterion {index}",),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_direction_supports_hierarchy_rules_and_deterministic_projection() -> None:
|
|
|
|
|
+ direction = DirectionArtifact(
|
|
|
|
|
+ goals=(_goal(1), _goal(2, parent="goal-1"), _goal(3)),
|
|
|
|
|
+ constraints=(
|
|
|
|
|
+ DirectionConstraint("constraint-1", "facts remain traceable", "hard boundary"),
|
|
|
|
|
+ ),
|
|
|
|
|
+ preferences=(DirectionPreference("preference-1", "prefer a concise opening", priority=1),),
|
|
|
|
|
+ evidence_refs=(EVIDENCE_REF,),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ payload = direction.content_payload()
|
|
|
|
|
+
|
|
|
|
|
+ assert payload["schema_version"] == "script-direction/v1"
|
|
|
|
|
+ assert payload["goals"][1]["parent_goal_id"] == "goal-1"
|
|
|
|
|
+ assert payload["constraints"][0]["constraint_id"] == "constraint-1"
|
|
|
|
|
+ assert payload["preferences"][0]["priority"] == 1
|
|
|
|
|
+ assert "### goal-1: goal statement 1" in direction.legacy_markdown
|
|
|
|
|
+ assert "#### goal-2: goal statement 2" in direction.legacy_markdown
|
|
|
|
|
+ assert "## Constraints" in direction.legacy_markdown
|
|
|
|
|
+ assert "## Preferences" in direction.legacy_markdown
|
|
|
|
|
+ assert EVIDENCE_REF in direction.legacy_markdown
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_direction_accepts_thirty_goal_nodes_and_rejects_thirty_one() -> None:
|
|
|
|
|
+ DirectionArtifact(
|
|
|
|
|
+ goals=tuple(_goal(index) for index in range(1, 31)), evidence_refs=(EVIDENCE_REF,)
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ with pytest.raises(ValueError, match="between one and thirty"):
|
|
|
|
|
+ DirectionArtifact(
|
|
|
|
|
+ goals=tuple(_goal(index) for index in range(1, 32)),
|
|
|
|
|
+ evidence_refs=(EVIDENCE_REF,),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@pytest.mark.parametrize(
|
|
|
|
|
+ ("goals", "message"),
|
|
|
|
|
+ [
|
|
|
|
|
+ ((_goal(1, parent="missing"),), "parent must exist"),
|
|
|
|
|
+ (
|
|
|
|
|
+ (
|
|
|
|
|
+ _goal(1),
|
|
|
|
|
+ _goal(2, parent="goal-1"),
|
|
|
|
|
+ _goal(3, parent="goal-2"),
|
|
|
|
|
+ ),
|
|
|
|
|
+ "only two levels",
|
|
|
|
|
+ ),
|
|
|
|
|
+ (
|
|
|
|
|
+ (DirectionGoal("goal-1", "statement", "rationale", success_criteria=()),),
|
|
|
|
|
+ "requires success criteria",
|
|
|
|
|
+ ),
|
|
|
|
|
+ (
|
|
|
|
|
+ (DirectionGoal("goal-1", "statement", "", success_criteria=("check",)),),
|
|
|
|
|
+ "requires a rationale",
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+)
|
|
|
|
|
+def test_direction_rejects_invalid_goal_hierarchy(
|
|
|
|
|
+ goals: tuple[DirectionGoal, ...], message: str
|
|
|
|
|
+) -> None:
|
|
|
|
|
+ with pytest.raises(ValueError, match=message):
|
|
|
|
|
+ DirectionArtifact(goals=goals, evidence_refs=(EVIDENCE_REF,))
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_direction_tool_contract_keeps_schema_name_and_removes_model_markdown() -> None:
|
|
|
|
|
+ parameters = _save_direction_candidate_schema()["function"]["parameters"]
|
|
|
|
|
+
|
|
|
|
|
+ assert parameters["properties"]["goals"]["maxItems"] == 30
|
|
|
|
|
+ assert "constraints" in parameters["required"]
|
|
|
|
|
+ assert "preferences" in parameters["required"]
|
|
|
|
|
+ assert "legacy_markdown" not in parameters["properties"]
|
|
|
|
|
+ assert "criteria" not in parameters["properties"]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_direction_hydration_rejects_historical_structure_without_compatibility() -> None:
|
|
|
|
|
+ historical_payload = {
|
|
|
|
|
+ "schema_version": "script-direction/v1",
|
|
|
|
|
+ "goals": [{"goal_id": "goal-1", "statement": "old goal", "rationale": "old rationale"}],
|
|
|
|
|
+ "criteria": [{"criterion_id": "criterion-1", "description": "old criterion"}],
|
|
|
|
|
+ "domain_criteria": [],
|
|
|
|
|
+ "evidence_refs": [EVIDENCE_REF],
|
|
|
|
|
+ "legacy_markdown": "# old projection",
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ with pytest.raises(ProtocolViolation, match="current structured contract"):
|
|
|
|
|
+ _hydrate_artifact(ArtifactKind.DIRECTION, historical_payload, "sha256:" + "a" * 64)
|