"""Tests for the shell tool adapters in the agent factory.""" from __future__ import annotations import json import threading from types import SimpleNamespace from typing import Any, cast import pytest from agents.tool import CustomTool, FunctionTool from strix.agents import factory from strix.config import load_settings, models def _capturing_exec_tool(captured: dict[str, str]) -> FunctionTool: async def invoke(_ctx: Any, raw_input: str) -> str: captured["raw_input"] = raw_input return "ok" return FunctionTool( name="exec_command", description="test tool", params_json_schema={"type": "object", "properties": {}}, on_invoke_tool=invoke, ) @pytest.mark.asyncio async def test_wrap_exec_command_defaults_shell_to_bash() -> None: captured: dict[str, str] = {} wrapped = factory._wrap_exec_command(_capturing_exec_tool(captured)) result = await wrapped.on_invoke_tool(cast("Any", None), json.dumps({"cmd": "source /tmp/env"})) assert result == "ok" parsed = json.loads(captured["raw_input"]) assert parsed["cmd"] == "source /tmp/env" assert parsed["shell"] == "bash" expected_cap = load_settings().context.tool_output_max_tokens assert parsed["max_output_tokens"] == expected_cap @pytest.mark.asyncio async def test_wrap_exec_command_preserves_smaller_explicit_output_cap() -> None: captured: dict[str, str] = {} wrapped = factory._wrap_exec_command(_capturing_exec_tool(captured)) await wrapped.on_invoke_tool( cast("Any", None), json.dumps({"cmd": "echo hi", "max_output_tokens": 42}) ) assert json.loads(captured["raw_input"])["max_output_tokens"] == 42 @pytest.mark.asyncio async def test_wrap_exec_command_clamps_oversized_explicit_output_cap() -> None: captured: dict[str, str] = {} wrapped = factory._wrap_exec_command(_capturing_exec_tool(captured)) ceiling = load_settings().context.tool_output_max_tokens await wrapped.on_invoke_tool( cast("Any", None), json.dumps({"cmd": "echo hi", "max_output_tokens": ceiling * 100}), ) assert json.loads(captured["raw_input"])["max_output_tokens"] == ceiling @pytest.mark.asyncio @pytest.mark.parametrize("shell", ["/bin/zsh", ""]) async def test_wrap_exec_command_preserves_explicit_shell(shell: str) -> None: captured: dict[str, str] = {} wrapped = factory._wrap_exec_command(_capturing_exec_tool(captured)) await wrapped.on_invoke_tool( cast("Any", None), json.dumps({"cmd": "echo test", "shell": shell}) ) assert json.loads(captured["raw_input"])["shell"] == shell @pytest.mark.asyncio async def test_responses_filesystem_custom_tool_output_is_bounded() -> None: async def invoke(_ctx: Any, _inp: str) -> str: return "line\n" * 50_000 toolset = SimpleNamespace( read_file=CustomTool(name="read_file", description="read", on_invoke_tool=invoke) ) factory._configure_filesystem_tools(toolset, chat_completions=False) assert isinstance(toolset.read_file, CustomTool) result = await toolset.read_file.on_invoke_tool(cast("Any", None), "{}") assert "truncated" in result assert len(result) < len("line\n" * 50_000) @pytest.mark.asyncio async def test_chat_completions_filesystem_custom_tool_becomes_function_tool() -> None: async def invoke(_ctx: Any, _inp: str) -> str: return "ok" toolset = SimpleNamespace( read_file=CustomTool(name="read_file", description="read", on_invoke_tool=invoke) ) factory._configure_filesystem_tools(toolset, chat_completions=True) assert isinstance(toolset.read_file, FunctionTool) def test_function_tools_are_result_bounded() -> None: agent = factory.build_strix_agent(is_root=True) by_name = {t.name: t for t in agent.tools} assert getattr(by_name["think"], "_strix_bounded", False) is True def test_view_image_is_left_out_once_images_are_not_allowed( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr(models, "_IMAGES_REJECTED", threading.Event()) filesystem = factory.build_strix_agent(is_root=True).capabilities[0] filesystem.bind(cast("Any", object())) view_image = next(tool for tool in filesystem.tools() if tool.name == "view_image") is_enabled = cast("Any", cast("FunctionTool", view_image).is_enabled) assert is_enabled(SimpleNamespace(context={"supports_images": True}), None) is True assert is_enabled(SimpleNamespace(context={"supports_images": False}), None) is False models.mark_images_rejected() assert is_enabled(SimpleNamespace(context={"supports_images": True}), None) is False