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)