| from __future__ import annotations |
|
|
| import json |
|
|
| import pytest |
|
|
| from jarvis.tools.openai_tooling import build_function_tool, tool_result_text |
|
|
|
|
| def test_tool_result_text_prefers_text_content_and_serializes_remainder() -> None: |
| result = { |
| "content": [ |
| {"type": "text", "text": "first"}, |
| {"type": "input_image", "image_url": "https://example.com/x.png"}, |
| {"type": "text", "text": "second"}, |
| ], |
| "meta": {"k": "v"}, |
| } |
| text = tool_result_text(result) |
| assert text.startswith("first\nsecond\n") |
| assert json.loads(text.splitlines()[-1]) == {"meta": {"k": "v"}} |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_build_function_tool_invokes_handler_and_normalizes_schema() -> None: |
| calls: list[dict[str, object]] = [] |
|
|
| async def _handler(args: dict[str, object]) -> dict[str, object]: |
| calls.append(dict(args)) |
| return {"content": [{"type": "text", "text": "ok"}]} |
|
|
| tool = build_function_tool( |
| name="demo_tool", |
| description="demo", |
| schema={"properties": {"x": {"type": "string"}}, "required": ["x"]}, |
| handler=_handler, |
| ) |
|
|
| text = await tool.on_invoke_tool(None, '{"x":"value"}') |
| assert text == "ok" |
| assert calls == [{"x": "value"}] |
| assert tool.params_json_schema["type"] == "object" |
| assert tool.params_json_schema["required"] == ["x"] |
| assert tool.params_json_schema["properties"]["x"]["type"] == "string" |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_build_function_tool_rejects_non_object_args() -> None: |
| async def _handler(args: dict[str, object]) -> dict[str, object]: |
| return {"args": args} |
|
|
| tool = build_function_tool( |
| name="demo_tool", |
| description="demo", |
| schema={"type": "object"}, |
| handler=_handler, |
| ) |
|
|
| assert json.loads(await tool.on_invoke_tool(None, "")) == {"args": {}} |
| assert "expected a JSON object" in (await tool.on_invoke_tool(None, "[]")) |
| assert "expected a JSON object" in (await tool.on_invoke_tool(None, "{not-json")) |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_build_function_tool_validates_required_types_and_unknown_fields() -> None: |
| async def _handler(args: dict[str, object]) -> dict[str, object]: |
| return {"args": args} |
|
|
| tool = build_function_tool( |
| name="typed_tool", |
| description="demo", |
| schema={ |
| "type": "object", |
| "properties": { |
| "count": {"type": "integer"}, |
| "flag": {"type": "boolean"}, |
| }, |
| "required": ["count"], |
| }, |
| handler=_handler, |
| ) |
|
|
| missing = await tool.on_invoke_tool(None, "{}") |
| assert "missing required field args.count" in missing |
| bad_type = await tool.on_invoke_tool(None, '{"count":"x"}') |
| assert "field args.count must be integer" in bad_type |
| unknown = await tool.on_invoke_tool(None, '{"count":1,"extra":true}') |
| assert "unexpected field args.extra" in unknown |
| ok = await tool.on_invoke_tool(None, '{"count":2,"flag":false}') |
| assert json.loads(ok) == {"args": {"count": 2, "flag": False}} |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_build_function_tool_hides_internal_exception_details() -> None: |
| async def _handler(_args: dict[str, object]) -> dict[str, object]: |
| raise RuntimeError("secret payload should not leak") |
|
|
| tool = build_function_tool( |
| name="sensitive_tool", |
| description="demo", |
| schema={"type": "object"}, |
| handler=_handler, |
| ) |
|
|
| text = await tool.on_invoke_tool(None, "{}") |
| assert text == "Tool sensitive_tool failed." |
| assert "secret payload" not in text |
|
|