test_tool_argument_normalization.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. from __future__ import annotations
  2. from typing import Any
  3. import pytest
  4. from agent import ToolRegistry
  5. from script_build_host.tools.registry import (
  6. _structured_object,
  7. _structured_object_list,
  8. )
  9. def test_structured_arguments_accept_native_and_provider_encoded_json() -> None:
  10. assert _structured_object({"name": "native"}, "payload") == {"name": "native"}
  11. assert _structured_object('{"name":"encoded"}', "payload") == {"name": "encoded"}
  12. assert _structured_object_list('[{"index":1},{"index":2}]', "items") == [
  13. {"index": 1},
  14. {"index": 2},
  15. ]
  16. assert _structured_object_list(['{"index":1}'], "items") == [{"index": 1}]
  17. @pytest.mark.parametrize("value", ["not-json", "123", '{"not":"an-array"}'])
  18. def test_structured_object_list_rejects_non_object_arrays(value: str) -> None:
  19. with pytest.raises(ValueError):
  20. _structured_object_list(value, "items")
  21. def test_structured_arguments_enforce_item_and_byte_limits() -> None:
  22. with pytest.raises(ValueError, match="item limit"):
  23. _structured_object_list([{}, {}], "items", max_items=1)
  24. with pytest.raises(ValueError, match="byte limit"):
  25. _structured_object({"value": "x" * (2 * 1024 * 1024)}, "payload")
  26. def test_planner_schema_exposes_only_semantic_inputs_and_decision_ids() -> None:
  27. from script_build_host.tools.registry import register_script_tools
  28. registry = ToolRegistry()
  29. register_script_tools(registry, object()) # type: ignore[arg-type]
  30. schema = registry.get_schemas(["plan_script_tasks"])[0]
  31. contracts = schema["function"]["parameters"]["properties"]["contracts"]
  32. contract = contracts["items"]
  33. assert contracts["type"] == "array"
  34. assert "schema_version" not in contract["properties"]
  35. assert "input_decision_ids" in contract["required"]
  36. for derived in (
  37. "input_decision_refs",
  38. "base_artifact_ref",
  39. "write_scope",
  40. "output_schema",
  41. "budget",
  42. "candidate_closure_decision_refs",
  43. "compose_order",
  44. ):
  45. assert derived not in contract["properties"]
  46. assert contract["properties"]["task_kind"]["enum"] == [
  47. "direction",
  48. "pattern-retrieval",
  49. "decode-retrieval",
  50. "external-retrieval",
  51. "knowledge-retrieval",
  52. "structure",
  53. "paragraph",
  54. "element-set",
  55. "compare",
  56. "compose",
  57. "candidate-portfolio",
  58. ]
  59. assert contract["additionalProperties"] is False
  60. @pytest.mark.asyncio
  61. async def test_retrieval_tool_freezes_and_submits_in_one_terminal_call() -> None:
  62. class Gateway:
  63. def __init__(self) -> None:
  64. self.calls: list[tuple[str, Any]] = []
  65. async def retrieve(self, source: str, tool: str, query: Any, context: Any) -> Any:
  66. self.calls.append(("retrieve", (source, tool, query, context)))
  67. return {"artifact_ref": {"uri": "script-build://artifact-versions/1"}}
  68. async def submit_current_attempt(self, context: Any) -> Any:
  69. self.calls.append(("submit", context))
  70. return {"attempt_id": "attempt-1", "status": "awaiting_validation"}
  71. from script_build_host.tools.registry import register_script_tools
  72. gateway = Gateway()
  73. registry = ToolRegistry()
  74. register_script_tools(registry, gateway) # type: ignore[arg-type]
  75. tool = registry._tools["search_knowledge"]["func"]
  76. result = await tool(keyword="hard tech", max_count=3, context={"task_id": "task-1"})
  77. assert result.terminate_run is True
  78. assert [name for name, _ in gateway.calls] == ["retrieve", "submit"]
  79. assert "attempt-1" in result.output