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.gateway import _direction_goals 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 _client_goal(key: str, parent: str | None = None) -> dict[str, object]: return { "client_key": key, "parent_client_key": parent, "statement": f"statement for {key}", "rationale": f"rationale for {key}", "success_criteria": [f"criterion for {key}"], } def test_direction_client_keys_generate_stable_order_independent_goal_ids() -> None: first, first_map = _direction_goals( "task-a", [_client_goal("Parent"), _client_goal("Child", "Parent")] ) second, second_map = _direction_goals( "task-a", [_client_goal(" child ", " parent "), _client_goal("parent")] ) assert first_map == second_map assert {item.goal_id for item in first} == {item.goal_id for item in second} child = next(item for item in second if item.parent_goal_id) assert child.parent_goal_id == first_map["parent"] @pytest.mark.parametrize( ("goals", "message"), [ ([_client_goal("same"), _client_goal("SAME")], "must be unique"), ([_client_goal("child", "missing")], "does not exist"), ([_client_goal("self", "self")], "cannot parent itself"), ( [ _client_goal("root"), _client_goal("child", "root"), _client_goal("grandchild", "child"), ], "only two levels", ), ([_client_goal(str(index)) for index in range(31)], "between one and thirty"), ], ) def test_direction_client_keys_reject_invalid_batches( goals: list[dict[str, object]], message: str ) -> None: with pytest.raises(ValueError, match=message): _direction_goals("task-a", goals) 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)