"""Unit tests for timmy.tools — coverage gaps. Tests _make_smart_read_file, _safe_eval edge cases, consult_grok, _create_stub_toolkit, get_tools_for_agent, and AiderTool edge cases. """ from __future__ import annotations import ast import math from unittest.mock import MagicMock, patch import pytest from timmy.tools import ( _create_stub_toolkit, _safe_eval, consult_grok, create_aider_tool, get_tools_for_agent, ) # ── _safe_eval edge cases ───────────────────────────────────────────────────── class TestSafeEval: """Edge cases for the AST-based safe evaluator.""" def _eval(self, expr: str): allowed = {k: getattr(math, k) for k in dir(math) if not k.startswith("_")} allowed["math"] = math allowed["abs"] = abs allowed["round"] = round allowed["min"] = min allowed["max"] = max tree = ast.parse(expr, mode="eval") return _safe_eval(tree, allowed) def test_unsupported_constant_type(self): """String constants should be rejected.""" with pytest.raises(ValueError, match="Unsupported constant"): self._eval("'hello'") def test_unsupported_binary_op(self): """Bitwise ops are not in the allowlist.""" with pytest.raises(ValueError, match="Unsupported"): self._eval("3 & 5") def test_unsupported_unary_op(self): """Bitwise inversion is not supported.""" with pytest.raises(ValueError, match="Unsupported"): self._eval("~5") def test_unknown_name(self): with pytest.raises(ValueError, match="Unknown name"): self._eval("foo") def test_attribute_on_non_math(self): """Attribute access on anything except the math module is blocked.""" with pytest.raises(ValueError, match="Attribute access not allowed"): self._eval("abs.__class__") def test_call_non_callable(self): """Calling a non-callable (like a number) should fail.""" with pytest.raises((ValueError, TypeError)): self._eval("(42)()") def test_unsupported_syntax_subscript(self): """Subscript syntax (a[0]) is not supported.""" with pytest.raises(ValueError, match="Unsupported syntax"): self._eval("[1, 2][0]") def test_kwargs_in_call(self): """math.log with keyword arg should work through the evaluator.""" result = self._eval("round(3.14159)") assert result == 3 def test_math_attr_valid(self): """Accessing a valid math attribute should work.""" result = self._eval("math.pi") assert result == math.pi def test_math_attr_invalid(self): """Accessing a nonexistent math attribute should fail.""" with pytest.raises(ValueError, match="Attribute access not allowed"): self._eval("math.__builtins__") # ── _make_smart_read_file ───────────────────────────────────────────────────── class TestMakeSmartReadFile: """Test the smart_read_file wrapper for directory detection.""" def test_directory_returns_listing(self, tmp_path): """When given a directory, should list its contents.""" (tmp_path / "alpha.txt").touch() (tmp_path / "beta.py").touch() (tmp_path / ".hidden").touch() # should be excluded from timmy.tools import _make_smart_read_file file_tools = MagicMock() file_tools.check_escape.return_value = (True, tmp_path) smart_read = _make_smart_read_file(file_tools) result = smart_read(file_name=str(tmp_path)) assert "is a directory" in result assert "alpha.txt" in result assert "beta.py" in result assert ".hidden" not in result def test_empty_directory(self, tmp_path): """Empty directory should show placeholder.""" from timmy.tools import _make_smart_read_file empty_dir = tmp_path / "empty" empty_dir.mkdir() file_tools = MagicMock() file_tools.check_escape.return_value = (True, empty_dir) smart_read = _make_smart_read_file(file_tools) result = smart_read(file_name=str(empty_dir)) assert "empty directory" in result def test_no_file_name_uses_path_kwarg(self): """When file_name is empty, should fall back to path= kwarg.""" from timmy.tools import _make_smart_read_file file_tools = MagicMock() file_tools.check_escape.return_value = (True, MagicMock(is_dir=lambda: False)) file_tools.read_file.return_value = "file content" smart_read = _make_smart_read_file(file_tools) smart_read(path="/some/file.txt") file_tools.read_file.assert_called_once() def test_no_file_name_no_path(self): """When neither file_name nor path is given, return error.""" from timmy.tools import _make_smart_read_file file_tools = MagicMock() smart_read = _make_smart_read_file(file_tools) result = smart_read() assert "Error" in result def test_file_delegates_to_original(self, tmp_path): """Regular files should delegate to original read_file.""" from timmy.tools import _make_smart_read_file f = tmp_path / "hello.txt" f.write_text("hello world") file_tools = MagicMock() file_tools.check_escape.return_value = (True, f) file_tools.read_file.return_value = "hello world" smart_read = _make_smart_read_file(file_tools) result = smart_read(file_name=str(f)) assert result == "hello world" def test_preserves_docstring(self): """smart_read_file should copy the original's docstring.""" from timmy.tools import _make_smart_read_file file_tools = MagicMock() file_tools.read_file.__doc__ = "Original docstring." smart_read = _make_smart_read_file(file_tools) assert smart_read.__doc__ == "Original docstring." # ── consult_grok ────────────────────────────────────────────────────────────── class TestConsultGrok: """Test the Grok consultation tool.""" @patch("timmy.tools.settings") def test_grok_unavailable(self, mock_settings): """When Grok is disabled, should return a helpful message.""" with patch("timmy.backends.grok_available", return_value=False): result = consult_grok("What is 2+2?") assert "not available" in result.lower() # ── _create_stub_toolkit ────────────────────────────────────────────────────── class TestCreateStubToolkit: """Test stub toolkit creation for creative agents.""" def test_stub_has_correct_name(self): toolkit = _create_stub_toolkit("pixel") if toolkit is None: pytest.skip("Agno tools not available") assert toolkit.name == "pixel" def test_stub_for_different_agent(self): toolkit = _create_stub_toolkit("lyra") if toolkit is None: pytest.skip("Agno tools not available") assert toolkit.name == "lyra" # ── get_tools_for_agent ─────────────────────────────────────────────────────── class TestGetToolsForAgent: """Test get_tools_for_agent (not just the alias).""" def test_known_agent_returns_toolkit(self): result = get_tools_for_agent("echo") assert result is not None def test_unknown_agent_returns_none(self): result = get_tools_for_agent("nonexistent") assert result is None def test_custom_base_dir(self, tmp_path): result = get_tools_for_agent("echo", base_dir=tmp_path) assert result is not None # ── AiderTool edge cases ───────────────────────────────────────────────────── class TestAiderToolEdgeCases: """Additional edge cases for the AiderTool.""" @patch("subprocess.run") def test_aider_success_empty_stdout(self, mock_run, tmp_path): """When stdout is empty, should return fallback message.""" mock_run.return_value = MagicMock(returncode=0, stdout="") tool = create_aider_tool(tmp_path) result = tool.run_aider("do something") assert "successfully" in result.lower() @patch("subprocess.run") def test_aider_custom_model(self, mock_run, tmp_path): """Custom model parameter should be passed to subprocess.""" mock_run.return_value = MagicMock(returncode=0, stdout="done") tool = create_aider_tool(tmp_path) tool.run_aider("task", model="deepseek-coder:6.7b") args = mock_run.call_args[0][0] assert "ollama/deepseek-coder:6.7b" in args @patch("subprocess.run") def test_aider_os_error(self, mock_run, tmp_path): """OSError should be caught gracefully.""" mock_run.side_effect = OSError("Permission denied") tool = create_aider_tool(tmp_path) result = tool.run_aider("task") assert "error" in result.lower()